Skip to content

rip out brew + pip/PyPI wheel support - #68217

Merged
ethernet8023 merged 1 commit into
mainfrom
ethie/rip-out-pip-brew
Jul 22, 2026
Merged

rip out brew + pip/PyPI wheel support#68217
ethernet8023 merged 1 commit into
mainfrom
ethie/rip-out-pip-brew

Conversation

@ethernet8023

@ethernet8023 ethernet8023 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

summary

removes Homebrew and PyPI wheel/sdist as Hermes distribution paths while preserving the supported source, Docker, and Nix workflows.

changes

  • removes the Homebrew formula, PyPI publish workflow, sdist manifest (MANIFEST.in), and wheel/sdist release-attachment logic from scripts/release.py.
  • keeps setuptools metadata and entry points required by editable installs and Docker/Nix builds, but adds a setup.py guard that rejects wheel/sdist builds outside a sealed Nix derivation (HERMES_NIX_BUILD=1).
  • removes pip/Homebrew install detection, PyPI update checks (check_via_pypi), the pip self-update path (_cmd_update_pip, is_uv_tool_install), the deprecation-banner state, the postinstall subcommand, wheel data-directory fallbacks in agent/i18n.py and hermes_constants.py, and the ACP Registry manifest/version-lockstep release logic.
  • adds /nix/store/ path detection so nix run / nix profile install installs (which don't set HERMES_MANAGED) are correctly identified as "nix" rather than falling through to "git"/"unknown".
  • retired install-method values ("pip", "homebrew") in existing .install_method stamps (both code-scoped and home-scoped) are ignored by the allowlist reader and fall through to "unknown" instead of resurrecting a retired enum value.
  • HERMES_MANAGED=brew|homebrew is explicitly ignored — it falls through to git/unknown detection instead of blocking config writes as a managed system.
  • update-apply gating is complete across every entry point: cmd_update, _cmd_update_check, dashboard POST /api/hermes/update (with a no-spawn test), and can_apply is narrowed to git-only.
  • updates Nix packaging to ship bare runtime data (locales, optional-mcps) through store symlinks and wrapper env vars (HERMES_BUNDLED_LOCALES, HERMES_OPTIONAL_MCPS) instead of wheel data-files.
  • removes the ACP Registry manifest/icon and their version-lockstep tests.
  • deletes or rewrites packaging, pip-update, Homebrew, and ACP Registry tests; adds parametrized coverage for the packaging build guard covering BOTH sdist and wheel paths — the guards live in separate cmdclass entries (the bdist_wheel one behind a try/except ImportError), so a passing sdist test alone proves nothing about the wheel path.
  • updates installation/platform documentation and related user-facing copy.
  • adjusts the supply-chain scan so deleted install-hook files do not trigger a finding, while additions or modifications still require the existing ci-reviewed label gate.

supported installation paths

  • git installer (install.sh)
  • Docker
  • Nix/NixOS
  • editable development installs (uv sync, uv pip install -e ., pip install -e .)

validation

  • nix develop -c scripts/run_tests.sh tests/test_packaging_build_guard.py tests/test_project_metadata.py tests/hermes_cli/test_pip_install_detection.py tests/hermes_cli/test_managed_installs.py tests/hermes_cli/test_update_check.py tests/cli/test_update_command.py tests/gateway/test_update_command.py
    • 490 passed (1 pre-existing flake in test_web_server.py::TestPtyWebSocket::test_client_input_reaches_child_stdin, confirmed on origin/main)
  • nix develop -c npm run typecheck && npm test (apps/desktop, focused on reverted files)
    • passed: typecheck + desktop-git.test.ts (4/4)
  • nix build .#checks.x86_64-linux.bundled-locales .#checks.x86_64-linux.bundled-mcps .#checks.x86_64-linux.entry-points-sync .#checks.x86_64-linux.package-contents
    • all pass
  • manual: uv build --wheel rejected with block message; HERMES_NIX_BUILD=1 uv build --wheel builds (811 files, no locales//optional-mcps//skills/ — all env-var resolved, correct)
  • git diff --check origin/main..HEAD — clean

related issue

compatibility-matrix change; no issue number.

@ethernet8023
ethernet8023 requested a review from a team July 20, 2026 20:23
@ethernet8023 ethernet8023 added the ci-reviewed applied to manually approve dangerous changes label Jul 20, 2026
@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard area/install-update Installer, updater, packaging, wheels, doctor sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation needs-decision Awaiting maintainer decision before any implementation labels Jul 20, 2026
@ethernet8023
ethernet8023 force-pushed the ethie/rip-out-pip-brew branch 2 times, most recently from 151a046 to 310284d Compare July 20, 2026 23:49
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on 5dcfcef

CI-sensitive file review

CI-sensitive file review · View job

ci-reviewed label is present.

CI timings

CI timings · View job

Wall time 9m35s vs 10m20s (-7.3%). 10 job(s) slower, 13 faster,

  • Python tests / Run tests slice 8/8: +44.0s
  • Build&Test Docker image / build (amd64, ubuntu-latest, linux/amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=do...: -40.0s
  • Python tests / Run tests slice 1/8: -25.0s
  • Python tests / e2e: -24.0s
  • Python tests / Run tests slice 3/8: -21.0s

@ethernet8023
ethernet8023 force-pushed the ethie/rip-out-pip-brew branch 2 times, most recently from d3ea94e to 76c2f0a Compare July 21, 2026 20:16
@NousResearch NousResearch deleted a comment from nousbot-eng Jul 21, 2026
@NousResearch NousResearch deleted a comment from nousbot-eng Jul 21, 2026
@NousResearch NousResearch deleted a comment from nousbot-eng Jul 21, 2026
@NousResearch NousResearch deleted a comment from nousbot-eng Jul 21, 2026
@NousResearch NousResearch deleted a comment from nousbot-eng Jul 21, 2026
@NousResearch NousResearch deleted a comment from nousbot-eng Jul 21, 2026
@ethernet8023
ethernet8023 force-pushed the ethie/rip-out-pip-brew branch 4 times, most recently from 5f7b833 to 79786a4 Compare July 22, 2026 19:42

@OutThisLife OutThisLife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intended change — dropping Homebrew + pip/PyPI wheel/sdist as distribution paths — is large and, for that scope, competently executed: the i18n locale fallback, banner PyPI-check removal, config managed-system handling (with the nice /nix/store/ detection + retired-enum fall-through), the setup.py wheel/sdist build guard, and the docs all look coherent, and the pip→hermes setup / uv sync install-hint rewrites across the platform adapters and tools are correct and in-scope.

But the branch also silently reverts two unrelated fixes that are live on main, neither mentioned in the description, and CI only stays green because the PR deletes their tests too. These need to come out before this can merge.

1. Reverts #69494 — Codex upstream quota-reset detection

origin/main has this feature (commit fea838c9f, "fix(auth): detect upstream Codex quota resets and lift stale pool cooldowns", #69494 / #43747). This PR removes it wholesale:

  • hermes_cli/auth.py — deletes _probe_codex_quota_restored, clear_codex_pool_quota_cooldowns, _is_codex_rate_limit_shaped, and the re-probe call in resolve_codex_runtime_credentials, plus the access_token/base_url fields on _codex_pool_rate_limit_status.
  • agent/account_usage.py — deletes the clear_codex_pool_quota_cooldowns() call after a redeemed reset.
  • agent/credential_pool.py — deletes _codex_quota_restored_upstream.
  • tests/hermes_cli/test_auth_codex_quota_probe.pydeleted entirely (389 lines on main).

Verification:

git grep -c _probe_codex_quota_restored origin/main -- hermes_cli/auth.py   # → 2 (present)
git grep -c _probe_codex_quota_restored <pr-head> -- hermes_cli/auth.py      # → 0 (removed)

2. Reverts #54242 — pure-Latin search matches inside CJK text

origin/main has the FTS trigram logic from commit 96560ee60 ("fix(agent): recover pure-Latin search matches embedded in CJK text", #54242). This PR removes it:

  • hermes_state.py — deletes _trigram_eligible_tokens and the related FTS5 trigram handling.
  • tests/test_fts_cjk_bigram.py — modified/removed alongside it.
git grep -c _trigram_eligible_tokens origin/main -- hermes_state.py   # → 2 (present)
git grep -c _trigram_eligible_tokens <pr-head> -- hermes_state.py      # → 0 (removed)

Why this happened

Both features' commits are ancestors of this branch and main didn't touch these files after the branch's merge-base, so this isn't main-drift the merge would auto-resolve — the deletions live in the branch's single squashed commit. This is the classic stale-branch/bad-rebase footgun from AGENTS.md: "A stale branch's version of an unrelated file will silently overwrite recent fixes on main when squashed." The branch is 19 commits behind main; a rebase that resolved auth.py / hermes_state.py / account_usage.py / credential_pool.py conflicts by keeping the branch's older copies would produce exactly this.

Ask

Rebase onto current origin/main and re-resolve so these four+ files (and tests/hermes_cli/test_auth_codex_quota_probe.py, tests/test_fts_cjk_bigram.py) carry main's versions — i.e. this PR should touch auth.py / hermes_state.py / account_usage.py / credential_pool.py only where a line genuinely references pip/Homebrew/PyPI (e.g. the format_unsupported_install_warning usage). After rebasing, git diff against main for those files should show no Codex-quota or FTS-trigram deletions. If reverting #69494 is actually intended, it belongs in its own described PR, not bundled here — and the #54242 (CJK search) revert looks purely accidental regardless.

Everything else in the distribution-removal is reviewable and looks good; happy to re-review once the unrelated reverts are dropped.

Removes Homebrew and PyPI wheel/sdist as Hermes distribution paths while
preserving the supported source, Docker, and Nix workflows.

Changes:
- Removes the Homebrew formula, PyPI publish workflow, sdist manifest
  (MANIFEST.in), and wheel/sdist release-attachment logic from scripts/release.py.
- Keeps setuptools metadata and entry points required by editable installs
  and Docker/Nix builds, but adds a setup.py guard that rejects wheel/sdist
  builds outside a sealed Nix derivation (HERMES_NIX_BUILD=1).
- Removes pip/Homebrew install detection, PyPI update checks, the pip
  self-update path, the deprecation-banner state, the postinstall subcommand,
  wheel data-directory fallbacks in agent/i18n.py and hermes_constants.py,
  and the ACP Registry manifest/version-lockstep release logic.
- Adds /nix/store/ path detection so `nix run` / `nix profile install`
  installs (which don't set HERMES_MANAGED) are correctly identified as
  "nix" rather than falling through to "git"/"unknown".
- Retired install-method values ("pip", "homebrew") in existing
  .install_method stamps (both code-scoped and home-scoped) are ignored by
  the allowlist reader and fall through to "unknown" instead of resurrecting
  a retired enum value.
- Updates Nix packaging to ship bare runtime data (locales, optional-mcps)
  through store symlinks and wrapper env vars instead of wheel data-files.
- Removes the ACP Registry manifest/icon and their version-lockstep tests.
- Deletes or rewrites packaging, pip-update, Homebrew, and ACP Registry
  tests; adds parametrized coverage for the packaging build guard covering
  BOTH sdist and wheel paths (the guards live in separate cmdclass entries
  — a passing sdist test proves nothing about the wheel path).
- Updates installation/platform documentation and related user-facing copy.
- Adjusts the supply-chain scan so deleted install-hook files do not trigger
  a finding, while additions or modifications still require the existing
  ci-reviewed label gate.

Supported installation paths (unchanged):
- git installer (install.sh)
- Docker
- Nix/NixOS
- editable development installs (uv sync, uv pip install -e ., pip install -e .)
@ethernet8023
ethernet8023 force-pushed the ethie/rip-out-pip-brew branch from 79786a4 to 5dcfcef Compare July 22, 2026 20:42

@OutThisLife OutThisLife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the rebase — the blocking issue is fully resolved. Thanks for the quick turnaround.

Verified against current origin/main:

  • #69494 (Codex quota-reset detection) restored. hermes_cli/auth.py, agent/account_usage.py, and agent/credential_pool.py now diff zero lines against main, and tests/hermes_cli/test_auth_codex_quota_probe.py is back.
  • #54242 (CJK/trigram search) restored. hermes_state.py diffs zero against main and tests/test_fts_cjk_bigram.py is back.
  • Distribution removal intact. Homebrew formula + PyPI workflow still deleted; the hermes setup / uv sync install-hint rewrites, the config nix/brew handling (_IGNORED_MANAGED_VALUES, /nix/store detection), and the setup.py HERMES_NIX_BUILD build guard are all still present.
  • Branch is now current (merge-base is recent, 2 commits behind main); the diff dropped from 90 → 85 files, exactly the four source files + their tests falling out.

The remaining diff is now cleanly scoped to the pip/PyPI/Homebrew removal, which read well in the first pass. Approving — only nit is that CI is re-running on the rebased head; assuming it lands green (it was green before the rebase and this only restored main's file versions), this is good to go.

@ethernet8023
ethernet8023 merged commit d84e11a into main Jul 22, 2026
42 checks passed
@ethernet8023
ethernet8023 deleted the ethie/rip-out-pip-brew branch July 22, 2026 20:51
zapabob added a commit to zapabob/hermes-agent that referenced this pull request Jul 23, 2026
Upstream removed hermes postinstall (NousResearch#68217) but the merge left a dangling
import that broke every hermes_cli.main import path. Re-wire fork Desktop
navigation/stdio/web-dist helpers so typecheck and E2E can launch again.

Co-authored-by: Cursor <cursoragent@cursor.com>
vashkartik added a commit to vashkartik/hermes-agent that referenced this pull request Jul 24, 2026
* feat(agent): add active-turn redirect core primitive

A follow-up sent while the model is still generating previously ended the
turn: Hermes kept only the visible partial text (reasoning was display-only),
cleared the loop, and replayed the message as a fresh next turn. If the
correction referred to something that only appeared in the thinking stream,
the model no longer had that context.

Add `AIAgent.redirect(text)`: a corrective interrupt distinct from a hard
stop. It cancels only the in-flight model request (not tool workers or child
agents), stashes the correction under a lock shared with `interrupt()` so a
concurrent `/stop` always wins, and lets the loop rebuild the same logical
iteration. `_apply_active_turn_redirect()` checkpoints the reasoning that was
actually shown to the user plus any visible partial text as an ordinary
assistant message, then appends the correction as a real user turn — never
replaying incomplete signed/encrypted provider reasoning, and keeping strict
role alternation and prompt-cache stability intact. During tool execution it
degrades to `steer()` so a running tool finishes at a safe boundary.

`_fire_reasoning_delta` now only records reasoning that a display callback
actually consumed, so `show_reasoning: false` never leaks hidden provider
thinking into the persisted transcript.

* feat(codex): honor redirect and hard stop in the app-server runtime

The Codex app-server runtime bypasses the main conversation loop and drives
its own subprocess turn, so it needs first-class hooks rather than the
OpenAI-loop interrupt path.

- `AIAgent.interrupt()` now forwards a hard stop to
  `CodexAppServerSession.request_interrupt()`, and `redirect()` uses Codex's
  native `turn/steer` protocol instead of cancelling the subprocess.
- `run_turn()` no longer clears an interrupt that arrived during
  `ensure_started()`: a stop landing mid-startup is honored before `turn/start`,
  and the interrupt event is cleared on every exit path.
- `run_codex_app_server_turn()` mirrors the loop finalizer's interrupt handoff
  (surface `interrupted` / `interrupt_message`, then `clear_interrupt()`) on
  both the normal and exception early-return paths, so a hard stop can't leave
  `_interrupt_requested` stale for the next turn.

* feat(desktop): let the agent open the preview pane

Add a desktop-gated open_preview tool so 'open cnn.com in the preview
pane' works. The tool (check_fn on HERMES_DESKTOP, zero footprint
elsewhere) emits a preview.open event through a gateway-injected emitter,
mirroring the close_terminal -> terminal.close bridge. The desktop
handles it in usePreviewRouting, normalizing the target and opening the
pane for the active session only -- a background turn never hijacks it.

Bare domains and localhost are coaxed into fetchable URLs (www.cnn.com ->
https://, localhost:3000 -> http://); file paths and schemes pass through
to the renderer's normalizer.

* flatten assistant message actions into an inline icon row

Drop the kebab overflow so age, branch, copy, read aloud, and refresh are always one hover away.

* feat(surfaces): route busy-input corrections through active-turn redirect

The default `busy_input_mode: interrupt` now redirects the live turn instead
of hard-stopping it and re-queuing a fresh turn, wired consistently across
every first-party surface via the shared core primitive.

- CLI, gateway (busy + PRIORITY paths), TUI (`_handle_busy_submit`), desktop
  (`session.redirect` RPC), and ACP call `redirect()` when the agent advertises
  `_supports_active_turn_redirect`, and fall back to the proven interrupt +
  next-turn queue for older runtimes.
- Redirect is gated to plain text with no attachments: captioned or
  attachment-bearing events (including adapters that classify unknown media as
  `TEXT`) stay queued so media is never dropped.
- ACP `cancel()` records the interrupted prompt, sets its cancel event, and
  hard-stops the agent while holding `runtime_lock`, closing the
  cancel-then-correct ordering gap; connection I/O happens after the lock is
  released.
- Desktop appends the correction as a real user transcript message so the live
  view matches the durable history after reload.
- `/busy` help, onboarding hints, and the new `session.redirect` RPC describe
  the redirect behavior; `/stop` remains the hard stop.

* docs: describe active-turn redirect busy-input behavior

Update the CLI and messaging guides so the default `interrupt` mode reflects
the new behavior: a follow-up redirects the active turn (preserving displayed
reasoning and completed work, letting running tools finish at a safe boundary)
rather than hard-stopping it, with `/stop` still the explicit hard stop.

* feat(desktop): Cursor-style stop-and-correct on the composer

Plain Enter (and the primary send button) while a turn is running now redirects
the live turn with the typed correction instead of queueing it — matching
Cursor's stop-and-correct. Removes the now-redundant steering-wheel button
(redirect is the default gesture) and teaches the primary button the `steer`
action. Attachments still queue; slash commands still run inline.

* feat(desktop): agent can focus panes + shared desktop-UI event bridge

Extract the open_preview emitter into a shared tools/desktop_ui bridge
(one gateway-injected sink, routed by HERMES_UI_SESSION_ID) and add a
second desktop-gated tool on top of it:

- focus_pane(chat|files|terminal|review|sessions) -> pane.reveal event.
  The desktop runs each pane's own reveal path (revealDesktopPane table)
  and only acts on the active window -- a background turn never moves the
  user's focus (desktop AGENTS.md: offer, don't hijack).

open_preview now emits through the same bridge. Both tools are check_fn
on HERMES_DESKTOP (zero footprint elsewhere), sitting beside
read_terminal/close_terminal in _HERMES_CORE_TOOLS.

Deliberately not adding run_slash: letting the agent fire slash commands
mid-turn (/model, /new, /clear) fights prompt-cache + conversation
invariants.

* refine inline message actions: fork icon, sine-wave read-aloud, shared age util

- Use the fork glyph for branch and a sine wave for read aloud (all one lib now)
- Extract compact "2h ago" into formatAgo() in lib/time.ts (+ ageDays locale string)
- Cover formatAgo with a unit test

* fix(desktop): tsc clean before dev / build

A stray tsc run can emit foo.js next to foo.ts under apps/shared/src or
apps/desktop/src. .gitignore hides the artifact from git status, but Vite
resolves extensionless imports .js-before-.ts, so the renderer silently runs
the stale compiled copy.

tsc -b . --clean already knows the emit graph and deletes
matching outputs. Run it before vite in all dev scripts.

This bit for real: a Jul 16 artifact of websocket-url.js predated the #68250
getGatewayWsUrl contract change ({ ok, wsUrl } IPC result), so its old
'if (fresh) return fresh' handed the whole result object to new WebSocket(),
dialing ws://127.0.0.1:5174/[object%20Object] on every boot. The desktop app
could never connect, and the failure survived reboots and cache wipes because
the poison lived in src/.

JsonRpcGatewayClient.connect() now rejects non-ws:// URLs with a readable
error instead of letting new WebSocket() coerce an object into
[object%20Object], so any future contract skew fails diagnosably.

* fix(agent): recover pure-Latin search matches embedded in CJK text (#54242)

A pure-Latin query (no CJK characters) routes to the unicode61
`messages_fts` table, whose tokenizer does not insert a boundary between
Latin letters and adjacent CJK characters. Content like "修改youer服务端" is
indexed as a single token, so `search_messages("youer")` returned zero
results even though the substring is present, and the Latin path had no
fallback.

Add a zero-result trigram fallback to the pure-Latin path: when the
unicode61 search misses, retry against the existing `messages_fts_trigram`
table, which matches substrings regardless of word boundaries. The fallback
is gated on `_trigram_available` and on every token being >=3 chars (the
trigram minimum), and only fires on a zero-result miss, so successful Latin
searches keep their unicode61 ranking unchanged.

The trigram query construction shared with the CJK path is extracted into a
`_run_trigram_search()` helper; the CJK branch is refactored to use it with
no behavior change.

Adds regression tests in tests/test_hermes_state.py::TestCJKSearchFallback.

* test(state): cover pure-Latin embedded-in-CJK recovery via the cjk index

The zero-result fallback prefers messages_fts_cjk when built: exact
ranked token match for Latin runs unicode61 fused onto CJK, including
<3-char tokens the trigram leg can't recover.

* fix(redirect): cover the build-window and post-reconnect correction races

Two narrow timing windows (reported by null-runner) silently downgraded a
mid-turn correction to a plain next-turn message on the desktop client:

- Turn-build window: a fresh turn flips running=True and builds the agent
  asynchronously, so session["agent"] is briefly None. session.redirect
  answered 4010 "unsupported", which the renderer's catch swallowed into a
  lost follow-up. Queue the correction server-side instead and return
  status="queued" — lossless, and honest about what happened.

- Stale runtime id after reconnect: session.redirect 404s on a sid the
  gateway no longer maps. redirectPrompt now resumes the stored session and
  retries once, mirroring stopPrompt, so a correction fired right after a
  reconnect isn't dropped.

The desktop treats "queued" like "redirected": the correction reaches the
model either way, so it's recorded once as a real user message.

* fix(themes): broadcast live skin.changed to WS surfaces (desktop/dashboard), not just stdio

The cross-surface theme SDK's live-repaint relies on a gateway skin watcher
that polls config and emits skin.changed on any move. But that emit is
session-less and fires from a background thread, so write_json fell through
its (session-transport -> contextvar -> stdio) ladder to the module stdio
transport — which only reaches the stdio TUI (tee'd to the dashboard WS
publisher). WS clients (the desktop app, dashboard chat) never got it, so
'Hermes themes itself' repainted the CLI/TUI but not the GUI.

Add a live-transport registry (one entry per connected WS peer, maintained by
handle_ws) and a _broadcast_global_event primitive that fans session-less
announcements out to every connected client, falling back to write_json when
none are registered (stdio path unchanged). Route both skin.changed emits
(watcher + the /skin RPC) through it, so a skin switch from any surface
repaints all of them.

Backend-only; desktop already handles skin.changed and does not drop
session-less events.

* fix(auth): detect upstream Codex quota resets and lift stale pool cooldowns (#69494)

When Codex returns 429 usage_limit_reached, Hermes persists the provider's
reset_at on the pool entry and freezes the credential until it elapses --
which can be days out for weekly windows. But the upstream window can
reopen EARLY: the user redeems a banked rate-limit reset (Codex CLI /
ChatGPT UI), upgrades their plan, or OpenAI resets the window. Hermes
never re-checked, so it kept erroring with 'Codex provider quota
exhausted (429); retry after Ns' until a manual re-auth rewrote the
tokens (issue #43747, externally-reset variant).

- hermes_cli/auth.py: add _probe_codex_quota_restored() -- a throttled
  (5 min/token) GET of the Codex /usage endpoint; quota counts as
  restored when every reported window is <100% used. Add
  clear_codex_pool_quota_cooldowns() to lift 429/quota-shaped cooldowns
  from persisted pool entries (DEAD and auth-shaped entries untouched).
- resolve_codex_runtime_credentials(): before surfacing a pool-only
  cooldown as 'quota exhausted', probe upstream; on a positive probe
  clear the cooldown and return the pool credential.
- agent/credential_pool.py: _available_entries() probes frozen
  openai-codex entries (clear_expired path only) and unfreezes them when
  upstream confirms the reset.
- agent/account_usage.py: a successful /usage reset redemption now
  clears persisted pool cooldowns immediately.

Negative paths preserved: probe 429/exhausted/indeterminate keeps the
cooldown; read-only enumeration never probes; non-JWT tokens never
probe (no network in hermetic tests).

* fix(themes): re-affirming the active skin repaints surfaces that missed the activation

Real-world failure from dogfooding the live-theme flow: display.skin was
already 'synthwave' in config, but the desktop never visibly applied it (the
activation event predated the WS transport fix / the connect). The desktop's
gateway.ready seed records the baseline WITHOUT painting (by design — never
stomp the persisted desktop theme on connect), so it believed it was synced.
Re-running 'hermes config set display.skin synthwave' then did nothing twice
over: the watcher signature (name, skin-file mtime) hadn't moved, so no
skin.changed fired; and even on an event, the desktop's name-equality guard
blocked the apply against the seeded baseline.

Two halves:

- hermes_cli: setting display.skin touches the named skin file so the
  watcher signature always moves on an explicit set — a same-name re-affirm
  now broadcasts skin.changed like any real move. Built-ins (no file) are
  unaffected; a name switch already moves their signature.

- desktop: track whether the synced baseline was actually APPLIED vs merely
  seeded at connect. A skin.changed matching a seed-only baseline is an
  intentional apply and repaints; once applied, repeat same-name events stay
  no-ops (protects a manual desktop-side theme switch from snap-back, incl.
  across a reconnect re-seed).

* fix(desktop): validate gateway WebSocket URLs

* chore(gitignore): ignore installer .install_method stamp (salvage of #54855) (#67364)

* chore(gitignore): ignore installer .install_method stamp

Salvage of #54855 by @drissman — rebased onto current main with root-scoped
rule and sister-marker comments alongside .update-incomplete.

Closes #66189

Root cause: scripts/install.sh writes <install>/.install_method but git
did not ignore it, so managed checkouts show ?? .install_method and
hermes update may autostash the untracked marker.

Fix: add /.install_method to .gitignore (repo-root only).

Verification: git check-ignore -v .install_method

* test(update): assert .install_method survives update autostash (#66189)

Add hermetic regression mirroring the .hermes-bootstrap-complete test:
adopt the real .gitignore, drop the installer .install_method stamp, run
the exact 'git stash push --include-untracked' the updater uses, and assert
the marker is neither swept nor reported dirty. Requested by hermes-sweeper
review on #67364.

* fmt(js): `npm run fix` on merge (#69562)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* refactor(themes): DRY the event frame, type the transport registry, tighten comments

_emit and _broadcast_global_event were each building the JSON-RPC event
envelope — extract _event_frame and use it from both. Type the registry as
set[Transport] (protocol already imported), and cut comment bloat at the
call sites. No behavior change; suites stay green.

* fix(themes): repaint the desktop on an in-place edit of the active skin

Live theme authoring's core loop — Hermes recolors the skin file it just
activated — repainted the TUI but not the GUI. The event path was fine
(post-#69533 the WS broadcast lands and ingestBackendSkin refreshes the
$backendThemes registry); the same-name apply guard also no-ops correctly
(it's what protects a manual desktop theme pick). The repaint was supposed
to come from the registry: the active theme IS that skin, its palette just
changed. But ThemeProvider memoized deriveTheme on [themeName, resolvedMode]
only, while deriveTheme reads the registry non-reactively via resolveTheme —
so the store update re-rendered the provider and handed back the stale
palette. Name switches repainted (themeName moves); recolors never did.

Add the theme stores (user/backend/registry) to the memo's deps — they are
deriveTheme's actual reactivity, same as the availableThemes memo directly
above. applyTheme is idempotent, and $backendThemes only publishes on a
real palette change, so no spurious repaints.

Tests: render ThemeProvider for real — activation applies; a same-name
recolor repaints (fails without the fix); an inactive-skin seed doesn't
touch the painted theme.

* fix(desktop): scope submit drift guard to genuine session switches (#69578)

The submit "session context drift" guard (regression 7acaff5ef / #54527,
partially fixed by 8c288760d and da52ffea1) aborted a prompt submission
whenever the selected stored id OR the route token changed mid-submit. Both
signals churn programmatically on a busy gateway, so on machines with
background streaming sessions, per-minute cron sessions, the Telegram surface,
or gateway-profile switches, essentially every send from a second chat aborted
silently: the optimistic message was dropped, the draft was left in the
composer, no error was shown, and prompt.submit never fired.

The false-positive churn sources were:
  - selection null-resets — gateway-switch's setSelectedStoredSessionId(null)
    on a gateway/profile switch or reconnect read as a switch away;
  - search/hash-only route-token changes — overlays and side panels park state
    in location.search/hash, so the pathname (the only part that selects a
    chat) was unchanged yet the raw token differed;
  - background-event active-ref retargets — createBackendSessionForSend's
    3-prong check also watched activeSessionIdRef, which gateway events retarget
    while other sessions stream (#47709 class), during a seconds-long
    session.create round-trip.

New shared helper session-context-drift.ts reduces a route token to the chat it
targets (pathname only; the new-chat route is '__new__', non-chat routes null)
and reports drift only when selection or the routed chat moves to a DIFFERENT,
non-null chat that is not the submit's own target. Selection null-resets,
search/hash-only churn, and moves onto the submit target are no longer drift;
genuine user switches (click another chat, click New Session mid-submit) still
abort. Site A (submit.ts) routes all five guard points through the helper and
logs '[submit-drift-abort]' with a per-site phase; the post-create active-ref
check and baseline re-pin from 8c288760d are kept intact. Site B
(createBackendSessionForSend) drops the active-ref prong entirely — every real
switch retargets selection and route synchronously — and logs before closing
the orphaned session.


(cherry picked from commit b390e3a22ee0dacc94283cda1748e3bafae15313)

Co-authored-by: Kennedy Umege <kenmege@yahoo.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(desktop): salvage /compress cluster — session.compress RPC + dedicated RPC routing (#68229)

* fix(desktop): route /compress through session.compress RPC with transcript replacement

Salvages #44462, #53755, and #68218 into a single canonical fix for the
desktop /compress cluster.

The desktop routed /compress through slash.exec, which sends it to the
_SlashWorker subprocess. Compressing a large session outlives both the
desktop's 30s WS timeout and the worker's 45s pipe timeout — the client
gives up, runExec's blanket catch swallows the error, and command.dispatch
surfaces a misleading "not a quick/plugin/skill command: compress" (#44456).
Even when compression succeeded via the _mirror_slash_side_effects path,
the desktop never received the post-compress message list, so summarized
bubbles stayed on screen forever — /compress looked like a no-op.

This change routes /compress to the dedicated session.compress RPC (the TUI's
path), combining the best of all three PRs:

- 120s client timeout matching the TUI's HERMES_TUI_RPC_TIMEOUT_MS (#44462)
- Transcript replacement from the response `messages` via toChatMessages,
  the same converter session.resume uses (#68218, teknium1 review on #44462)
- Session-isolation guard: updateSessionState only publishes for the active
  runtime, so a late result after a session switch can't clobber the
  foreground transcript (#53755, teknium1 review on #53755)
- Coalescing: dedup concurrent compress requests per session (#53755)
- Progress toast ("compressing context...") outside the transcript (#53755)
- Error unmasking in runExec: when slash.exec fails and command.dispatch only
  adds "not a quick/plugin/skill command" routing noise, surface the original
  worker error instead (#44462)
- /compact alias + focus_topic forwarding

Co-authored-by: AlliDev <AIalliAI@users.noreply.github.com>
Co-authored-by: PinkEVO <PINKIIILQWQ@users.noreply.github.com>

* feat(desktop): route slash commands with dedicated RPCs to those RPCs

Salvages #63513 — introduces a new `rpc` kind on DesktopCommandSurface so
commands with a first-class gateway @method handler bypass slash.exec /
command.dispatch entirely, and a `renderRpcResult` utility that shapes
each RPC's structured reply into readable transcript text.

Migrates 6 commands from exec() to rpc(...):
  /agents → agents.list
  /save   → session.save
  /status → session.status
  /steer  → session.steer
  /stop   → process.stop
  /usage  → session.usage

/compress stays as action('compress') — it needs transcript replacement
from the response `messages`, which the generic rpc path can't do (per
teknium1 review on #44462/#63513).

Also includes the json-rpc-gateway timeout message improvement: the error
now includes the configured timeout duration ("request timed out after 120s:
session.compress") so a user can tell whether the default 30s fired or a
per-call override.

Co-authored-by: Jelvin <SmallNew2003@users.noreply.github.com>

* fix(desktop): preserve provider choice during config initialization

* fix(desktop): preserve slash command and host compression semantics

Keep commands whose CLI behavior exceeds their current RPC contracts on slash.exec.
Propagate the full compression timeout through compute-host control, return structured
host compression outcomes with metadata, and retain successful compression feedback
in the desktop transcript.

Add regressions for timeout forwarding, host aborts and metadata sync, structured host
control responses, command routing parity, and numeric stop counts.

* fix(desktop): harden compression state handling

Preserve the invoking stored-session binding for delayed compression results,
normalize replacement histories, and serialize provider selection. Stabilize
gateway platform tests and guard the desktop Git facade during renderer teardown.

---------

Co-authored-by: AlliDev <AIalliAI@users.noreply.github.com>
Co-authored-by: PinkEVO <PINKIIILQWQ@users.noreply.github.com>
Co-authored-by: Jelvin <SmallNew2003@users.noreply.github.com>

* test(desktop): cover submit drift through e2e (#69599)

Exercise the full Electron, gateway, and mock-provider submit path while
same-chat route query tokens churn during session creation. Assert the mock
provider receives the prompt and its streamed response reaches the transcript.

* Nous portal model pricing (#69579)

* nous portal model pricing

* update top message

* rip out brew + pip/PyPI wheel support (#68217)

Removes Homebrew and PyPI wheel/sdist as Hermes distribution paths while
preserving the supported source, Docker, and Nix workflows.

Changes:
- Removes the Homebrew formula, PyPI publish workflow, sdist manifest
  (MANIFEST.in), and wheel/sdist release-attachment logic from scripts/release.py.
- Keeps setuptools metadata and entry points required by editable installs
  and Docker/Nix builds, but adds a setup.py guard that rejects wheel/sdist
  builds outside a sealed Nix derivation (HERMES_NIX_BUILD=1).
- Removes pip/Homebrew install detection, PyPI update checks, the pip
  self-update path, the deprecation-banner state, the postinstall subcommand,
  wheel data-directory fallbacks in agent/i18n.py and hermes_constants.py,
  and the ACP Registry manifest/version-lockstep release logic.
- Adds /nix/store/ path detection so `nix run` / `nix profile install`
  installs (which don't set HERMES_MANAGED) are correctly identified as
  "nix" rather than falling through to "git"/"unknown".
- Retired install-method values ("pip", "homebrew") in existing
  .install_method stamps (both code-scoped and home-scoped) are ignored by
  the allowlist reader and fall through to "unknown" instead of resurrecting
  a retired enum value.
- Updates Nix packaging to ship bare runtime data (locales, optional-mcps)
  through store symlinks and wrapper env vars instead of wheel data-files.
- Removes the ACP Registry manifest/icon and their version-lockstep tests.
- Deletes or rewrites packaging, pip-update, Homebrew, and ACP Registry
  tests; adds parametrized coverage for the packaging build guard covering
  BOTH sdist and wheel paths (the guards live in separate cmdclass entries
  — a passing sdist test proves nothing about the wheel path).
- Updates installation/platform documentation and related user-facing copy.
- Adjusts the supply-chain scan so deleted install-hook files do not trigger
  a finding, while additions or modifications still require the existing
  ci-reviewed label gate.

Supported installation paths (unchanged):
- git installer (install.sh)
- Docker
- Nix/NixOS
- editable development installs (uv sync, uv pip install -e ., pip install -e .)

* fix(desktop): keep first response layout stable

* fix(ui-tui): themed input text — typed text tracked the HOST terminal's fg, not the skin's

Live-repaint's composer gap: flip a light terminal to a dark skin and the
input goes black-on-black. The placeholder was already explicit truecolor
(theme muted), but TYPED text rendered with no color at all — the terminal's
default foreground — in both paint paths:

- the Ink render (<Text wrap="wrap">{rendered}</Text>, no color), and
- the fast-echo bypass, which writes raw cells straight to stdout.

The skin owns the background (OSC-11) but the default fg still belongs to
the host terminal's polarity, so any skin/terminal polarity mismatch made
input invisible. Every other transcript line already paints
theme.color.text (the completed inputBuf rows directly above the composer).

Give TextInput a color prop and paint both paths with it: the Ink <Text>
(chalk re-opens the outer color after the placeholder chips' embedded [39m
closes; INV cursor/selection cells never touch fg) and the fast-echo write
via colorizeEcho — same explicit-truecolor-only rule as colorizeHint, so
the bypass cell can't flash terminal-default before the next frame. All six
TextInput sites (composer, prompts, masked, billing ×2, session switcher)
pass theme text; no color ⇒ passthrough, unthemed inputs keep the terminal
default.

Tests: colorizeEcho SGR wrap + passthrough contracts; full ui-tui suite
1338✓; typecheck clean.

* feat(devshell): add terminal UI capture tools (#69629)

* fix(ui-tui): skin owns BOTH terminal defaults — OSC-10 foreground beside the OSC-11 background

The input fix's sibling, hit immediately after: the composer was themed but
AGENT text went black-on-black the same way. Root cause is the class, not
the call site — markdown body, borders, and every token rendered without an
explicit color falls back to the terminal's DEFAULT foreground, which
belongs to the HOST profile's polarity, not the skin's. A dark skin on a
light terminal repaints the backdrop via OSC-11 while thousands of
default-fg cells stay near-black.

Chasing every <Text> is unwinnable. Instead own the default itself: when a
skin authors a background (the existing opt-in), paint the default
foreground from the resolved theme's text color via OSC-10. Every unthemed
token — present and future — re-bases onto the skin atomically, exactly
like the background.

terminalModes: the OSC-11 slot generalizes to defaultColorSlot(10|11) —
same paint/clear/exit-restore contract, tracked per slot, so a skinless
session still never touches the terminal. reapplyTheme repaints the fg too:
polarity flips swap paired palettes, moving the text tone while the
background stays.

Tests: slot contract runs table-driven over both OSC codes; handler test
pins the invariant (default fg == theme text; dropping the background
releases both defaults). Suite 1344✓, typecheck/lint/prettier clean.

* fix(desktop): warm-route resume jitter from double setMessages (#69635)

* test(desktop): e2e for warm-route resume render jitter

Pre-seeds a 32-message session into state.db, boots the app, does a
cold resume (populates warm cache), navigates away, then clicks back
(warm resume). A MutationObserver + innerHTML-length poll detects
whether the transcript is re-rendered after the initial warm-cache
paint — the jitter bug where syncSessionStateToView fires twice
(warm cache paint, then session.activate RPC reconcile).

* fix(desktop): warm-route resume jitter from double setMessages

The warm resume path in resumeSession() calls syncSessionStateToView
twice: once for the warm cache paint, then again after the
session.activate RPC reconciles messages. The second call created new
message objects via toChatMessages (different references, same content),
and flushPendingViewState's sameMessageList guard used reference
equality per slot — so it always failed and setMessages fired a second
time, causing a visible transcript re-render.

Replace sameMessageList (reference equality) with chatMessageArraysEquivalent
(deep content comparison: id, role, parts, pending, error, etc.). This
was already used by the cold path's fast-path guard; the flush guard
was the last holdout using shallow reference equality.

Also updates the e2e test to use textContent polling on the first
message element (instead of innerHTML on the full viewport) to avoid
false positives from metadata-only DOM changes.

* test(desktop): e2e for warm resume after background inference

Extract the render counter (MutationObserver + text-content poll) and
assertion into reusable helpers. Add a second test that sends a message,
waits for the mock response to complete, navigates away, then warm-
resumes — verifying the warm cache already has the completed turn from
message.complete events and no second paint occurs.

* polish(desktop/billing): auto-poll, no-card notice, grouped card layout

Post-merge review polish of #68722 + #68761:
- Usage: drop the manual refresh + 'Updated Xm ago'; billing queries now
  refetchInterval-poll while the page is mounted, like every other data view.
- No card: lead the page with a warn notice naming the blocker + 'Add card ↗',
  so the silently-disabled buy/auto-refill controls have an obvious cause.
- Layout: unify on a shared SettingsCard/SettingsSection primitive; collapse the
  three floating one-row sections (Payment / One-time top-up / Automatic refill)
  into a single divide-y 'Payment & credits' card.
- Dev fixture switcher: relabel as a wrench + 'preview' dashed control so it
  reads as the DEV-only tool it is (compiled out of production).

Removed formatUsageUpdatedAgo/oldestUpdatedAt/UsageRefreshRow + their tests;
added no-card-notice and no-manual-refresh tests. vitest 107 green, tsc + eslint clean.

* fmt(js): `npm run fix` on merge (#69644)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(desktop): avoid duplicate inflight user rows on resume (#69649)

Keep a live session projection from adding its user turn when the latest
persisted row already represents that same inflight prompt. Add real Electron
coverage for fast and cold resume with idle and background-inference sessions.

* fmt(js): `npm run fix` on merge (#69651)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* feat(tts): provider-agnostic streaming core with a shared sentence chunker

StreamingTTSProvider ABC + registry + resolve_streaming_provider() —
ElevenLabs (pcm_24000) and OpenAI (response_format=pcm) stream chunked
PCM; every other provider keeps its configured voice and falls back to
per-sentence sync synthesis. SentenceChunker is the one incremental
cutter every surface shares: sentence-boundary cuts on the delta
stream, <think> blocks stripped even when split across deltas, short
fragments merged forward so they never stall as tiny clips.

* feat(tts): route the speaker pipeline through the streaming core

stream_tts_to_speaker() drops its hardcoded ElevenLabs client for
resolve_streaming_provider() + SentenceChunker, so any provider speaks
sentence-by-sentence while the model is still generating. Markdown
stripping also drops emoji — providers stall on them or read them out
loud.

* fix(stt): flag empty transcripts as no_speech

An STT provider that hears no words is reporting silence, not failing.
ElevenLabs/xAI empty-transcript errors now carry no_speech so live
voice loops can re-listen quietly instead of surfacing an error on
every pause.

* feat(voice): barge-in detector with pre-roll utterance capture

listen_for_speech(): sustained-RMS speech detection with the noise
floor calibrated against audible playback (speaker bleed doesn't
self-trigger). With capture=True it keeps a rolling pre-roll buffer and
records through to a silence endpoint, so the interruption is
transcribed from its first syllable — detection alone loses the opening
words to the detector's sustain window plus the mic re-open.
transcribe_recording() maps no_speech provider results to a successful
empty transcript (silence, not an error).

* feat(voice): stream any provider in the CLI, with barge-in capture

The streaming gate broadens from ElevenLabs-only to any provider that
passes check_tts_requirements(). In continuous voice mode a mic monitor
runs during playback: talking over the agent cuts TTS at detection
while the monitor keeps recording, then the captured interruption is
transcribed and queued as the next turn (process_loop's auto-restart
stands down while the capture owns the mic). New voice.barge_in config
key, default true.

* feat(voice): stream turn deltas through the TUI gateway, with barge-in

Turn deltas feed a per-turn TTS pipeline; the post-complete speak_text
call survives only as a fallback. session.interrupt, /voice toggles,
and new turns cut in-flight speech. VAD barge-in emits
voice.interrupted at detection, then the captured interruption goes out
as voice.transcript — the same event the TUI already submits as a
spoken turn.

* feat(voice): desktop speech-stream sessions with barge-in capture

/api/audio/speak-stream WebSocket: one socket + one Web Audio clock per
reply. The renderer feeds raw LLM deltas as they arrive; the server
cuts sentences with the shared chunker and streams int16 PCM back while
generation continues — speech overlaps generation with no per-sentence
connection or synthesis gaps. Falls back to the POST data-URL path for
old backends / non-chunked providers.

Barge-in runs a MediaRecorder on the monitor's stream the whole time
playback is live (rotated while quiet to bound pre-roll); talking over
the agent cuts playback and the complete utterance goes straight to
transcription and submit. /api/audio/transcribe returns 200/"" for
no-speech results so quiet turns re-listen instead of toasting a 400.

* docs(voice): streaming TTS + barge-in across surfaces

* feat(voice): speech-interrupted latch in the TTS streaming core

mark_speech_interrupted() / take_speech_interrupted(): a one-shot,
TTL'd (120s) latch plus SPEECH_INTERRUPTED_NOTE. Barge-in paths mark
it when they cut live speech; the next turn's submit path pops it and
prepends the note to the model-bound message — API-call local, never
persisted, so history and prompt caching are untouched.

* feat(voice): CLI + TUI tell the model when its spoken reply was cut

CLI: the VAD monitor's playback cut and the record-key interrupt both
mark the latch; the chat path prepends the note via the existing
_prepend_note_to_message channel. TUI: _tts_stream_stop() grows a
user_barge flag (False for /voice off — a mode change isn't an
interruption; no-op when speech already finished), the VAD monitor
marks on cut, prompt.submit accepts interrupted:true from clients, and
_run_prompt_submit pops the latch into the run message.

* feat(voice): desktop flags interrupted submits

markVoicePlaybackInterrupted() / takeVoicePlaybackInterrupted() mirror
the backend latch in the renderer (the barge happens client-side, where
the audio plays). VAD barges and typing over playback mark it; the next
prompt.submit carries interrupted:true, which the TUI gateway latches
into the model note.

* test(desktop): add E2E coverage for session lifecycle (#69580)

* test(desktop): e2e test for interim assistant message preservation (#65919)

Adds a Playwright E2E test that reproduces the fix from PR #65919 across
all three layers (agent core → tui_gateway → desktop renderer). The mock
inference server is upgraded with a multi-turn scripted response that
exercises several interleaved patterns:

  1. text + tool_call  → should produce an interim message
  2. text + tool_call  → another interim message
  3. no text + tool_call → NO interim (no visible text alongside tools)
  4. text + tool_call  → another interim message
  5. final answer (stop) → message.complete, different from all interims

Two describe blocks exercise display.interim_assistant_messages both on
(default) and off:
  - ON:  all interim texts + the final answer visible in the transcript
  - OFF: only the final answer visible, all interim texts wiped

Also fixes a footgun: test:e2e now runs `npm run build` as a pretest
hook so the renderer dist/ is always fresh. Previously, running
`npx playwright test` locally would silently load a stale dist/ that
predated renderer fixes — the python backend ran from source (had the
fix) but the renderer was frozen in an old bundle. CI already built
fresh, so the explicit build step there is removed to avoid duplication.

* test(desktop): e2e sidebar states — background dot, subagent, cross-session

Add sidebar-states.spec.ts with three E2E tests exercising the desktop
sidebar's session dot states driven by real gateway events:

1. Background process dot appears during a terminal(background=true)
   call and disappears after auto-dismiss; subagent (delegate_task)
   runs concurrently; final answer is visible in the transcript.

2. Background dot remains visible while a subagent runs concurrently
   (longer sleep 5 background process so the dot is catchable).

3. Cross-session dot transition: start a turn with a background process,
   wait for the turn to complete, open a new session, then verify the
   original session's dot transitions from 'background running' to
   'finished — unread' when the background process exits.

The mock server gains SIDEBAR_SCRIPT and SIDEBAR_CROSS_SCRIPT trigger
keywords that return tool_calls for terminal(background=true) and
delegate_task — the agent executes these for real (real background
process, real subagent), so the tests assert against genuine gateway
events rather than mocked UI state.

Verified: 3 passed (1.2m) under cage headless wlroots.

* test(desktop): e2e tests for tile-unread bug (tab passes, split fails)

Two scenarios for the tile-unread bug where a session that finishes
while visible on-screen gets the green 'finished unread' dot even
though the user is looking right at it.

The unread check in handleTransition (session-states.ts:174) only
compares against $selectedStoredSessionId and ignores $sessionTiles,
so a session visible in a tile gets marked unread even though it's
on screen.

1. TAB (hidden, PASSES): ⌃-click opens the session as a stacked tab
   that is NOT visible on screen. The unread dot IS correct here —
   the user isn't looking at it.

2. SPLIT (visible, FAILS): drag the session row to the workspace's
   right edge to create a side-by-side split tile. Both sessions are
   visible on screen. The unread dot is WRONG — the session is visible
   in the split tile, so it should not be marked 'unread'. This test
   is RED until the fix lands.

Also adds explicit page.screenshot() calls at key assertion points in
sidebar-states.spec.ts so the trace viewer has full-res captures of the
sidebar dot states during the test.

* test(desktop): cover compression and queued stop lifecycle

Add real desktop E2E coverage for session compression continuation and
queue parking after an explicit Stop. Extend the mock server with a
blocking scripted turn and submitted-prompt assertions.

* test(desktop): cover busy composer submit routing

Replace the invalid queued-stop E2E scenario: plain text redirects a busy
turn rather than entering the queue. Add focused submit-routing coverage for
plain text, slash commands, attachments, explicit Stop, and idle submission.

* fmt(js): `npm run fix` on merge (#69669)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* feat(billing): shared cross-surface out-of-credits signal

Detect a billing wall once (agent/error_classifier → FailoverReason.billing) and
map it to a recovery link + label in one place, then carry that structured
BillingBlock to every surface instead of re-parsing free-form error text per
surface.

- agent/billing_links.py: provider-agnostic slug/host → (label, billing URL)
  table (single source of truth), Nous-aware (is_nous routes to the in-app flow);
  unknown providers degrade to a readable label with no invented URL.
- conversation_loop: both billing exit paths return a billing_block through one
  helper; the guidance message carries the derived URL for every provider.
- gateway forwards billing_block on message.complete (it was dropped).
- @hermes/shared: BillingBlock type shared by desktop + TUI.

* feat(desktop): billing toast + in-chat status-row banner with smart CTA

On a billing wall, raise a sticky, billing-specific toast (never the generic
error toast) and a persistent in-composer banner for the active session — both
with one recovery action: Nous → in-app Settings → Billing, other providers →
their billing page (deep-linked). The banner reuses the shared StatusRow chrome
(no bordered alert, Codicon glyph, shared buttons), and the composer stays usable
so slash commands keep working.

* feat(tui): billing wall opens a confirm dialog

Open the shared ConfirmPrompt (full-width, themed, same lane as approval/clarify)
rather than a truncating status-bar notice. One recovery action: Nous → /topup
(opens the rich billing overlay), other providers → their billing page, or /model
to switch when there's no URL. The transcript keeps the full guidance; the dialog
is the concise actionable layer.

* feat(cli): out-of-credits panel below the response

Pin a provider-agnostic "Out of credits" panel after a billing-classified turn so
the one recovery action (Nous → /topup, other providers → their billing page)
stays visible instead of scrolling away as prose.

* fix(ui-tui): a skin that authors a background owns its polarity

The theme engine picked light/dark adaptation from the HOST terminal
(detectLightMode) even when the skin authors its own background — which
the TUI then paints onto the terminal via OSC-11. On a light-mode Apple
Terminal without truecolor, a pure-black skin (e.g. Bloomberg) got its
foregrounds ansi256-bucketed *for a light background that no longer
exists*: theme.color.text became 'ansi256(214)', which also fails the
OSC-10 hex gate, so the terminal default fg stayed the light profile's
near-black — markdown body text rendered black-on-black.

fromSkin now resolves polarity from the skin's authored background when
present (skinIsLight), uses that background as the reference canvas for
the derived tone ladder and contrast adaptation, and only falls back to
host detection for skins without a background (they render on the
terminal's own surface). The paired light_colors/dark_colors pick in
themeForSkin follows the same rule.

* fix(desktop): refresh composer branch after worktree creation (#69657)

* fix(desktop): refresh composer branch after worktree creation

Route new worktree sessions through the shared workspace target handoff so
composer git status follows the created worktree instead of remaining on the
main checkout. Add an Electron E2E regression test for Ctrl+Shift+B.

* fix(desktop): fixme flaky test

* feat(desktop): add prefix/suffix adornments to Input primitive; use $ prefix in billing top-up

* ci: surface E2E screenshots in review comment (#69631)

* ci: surface E2E screenshots in review comment

* ci: mark completed review commits in past tense

* ci: surface approved sensitive-file reviews

* ci: link sensitive files to reviewed changes

* ci: stage desktop E2E visual evidence

Track screenshots newly introduced against main and package visual diffs for a trusted publisher.

* fix(ci): pass E2E evidence output paths

Supply the manifest and staging-directory arguments required by the screenshot status helper.

* fix(ci): download the OSV SARIF artifact

Match the artifact name and result filename emitted by the pinned upstream reusable workflow.

* fix(desktop): show steer glyph in busy composer (#69612)

* fix(desktop): show steer glyph in busy composer

Restore the steering-wheel glyph when a typed draft redirects a live turn.
Add a real Electron E2E regression test for stop, steer, attachment queueing,
and a paused queue after Stop.

* feat(desktop): add queue action beside steer

Restore an explicit queue action in the former steering slot while a typed
correction is ready. Keep the primary steering-wheel action for redirecting
the active turn.

* fix(desktop): retain dictation beside queued drafts

Keep the dictation microphone visible when a typed busy draft exposes the
separate queue action. Cover the combined controls in the real Electron flow.

* fix(desktop): place queue beside the busy action

Keep the Queue message control after the read-aloud toggle and directly before
the shared Stop/Steer action. Cover the rendered control order in Electron E2E.

* fix(desktop): narrow billing custom top-up input (w-20 -> w-16)

* fix(desktop): prevent stale optimistic tails after compression (#69682)

* refactor(desktop): extract shared Progress primitive; billing uses it

Add components/ui/progress.tsx — one rounded track + animated fill that owns
role="progressbar" + aria. Migrate the hand-rolled bars (updates overlay,
onboarding, install overlay, billing usage, pet hatch) onto it.

- Pet's bar was never a color variant (--primary and --ui-accent are the same
  brand color); its only real difference is the sliding indeterminate, now an
  `animated` prop. Color stays a normal `fillClassName` override.
- Billing usage now uses the plain primitive — dropped the bespoke dither
  track, inset shadow, and danger nub; tone rides `destructive`/`fillClassName`.
- Drop the redundant emoji "no saved card" row description; the page-level
  warn banner is the single explainer.
- Rename pet CSS .pet-progress* -> generic .progress-slide.

* fix(acp): flag replayed compaction summaries via _meta

A context-compaction handoff is persisted as an ordinary history message
but is not a real turn. The ACP history replay streamed it as a bare
user/agent message chunk, dropping the in-process _compressed_summary
marker, so ACP frontends (editors, vscode-hermes) rendered the entire
handoff as a regular message.

Tag replayed summary chunks under _meta.hermes (ACP's extensibility
channel), covering all three persistence shapes the compressor emits:

- standalone role="user" handoff -> compactionSummary: true
- standalone role="assistant" handoff (alternation-driven role pick)
  -> compactionSummary: true
- merge-into-tail message (preserved tail content + appended summary)
  -> containsCompactionSummary: true, a distinct key so clients that
  collapse standalone summaries cannot hide the preserved real content

Detection honors the in-process metadata flag and falls back to a new
ContextCompressor.classify_summary_content() content classifier
(standalone/merged/None), so it also works for a DB-reloaded session
that lost the in-memory flag. _is_context_summary_content is now a thin
wrapper over the classifier, keeping existing callers unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(compression): pin classify_summary_content agreement with summary predicates on live emissions

Hardening for the #59114 salvage: generate real standalone and merged
handoffs with the current compressor and assert classify_summary_content
agrees with _is_context_summary_content and is_compaction_summary_message
on every emitted shape (behavior contract, not a format snapshot), incl.
the flag-stripped DB-reload copy. Also adds the contributor email mapping
for @israellot.

* fix(cron): finalize compressed session tips (86e2darn2)

* test(cron): assert no-rotation tip resolution is a finalize no-op

With compression.in_place defaulting True the cron session id never
rotates; get_compression_tip returns the input id. Pin that the
salvaged #67188 fix titles/ends the ORIGINAL cron session in that path
(and for falsy tip returns), i.e. zero behavior change when no
compression rotation happened. Also maps colingreig's contributor
email for attribution CI.

* fix(desktop): synchronize context usage and compaction status

* fix(search): surface compaction-archived and compression-parent sessions in discovery

After context compaction, pre-compaction content was invisible to
session_search — a memory black hole. The _discover() skip logic
filtered both same-session and same-lineage hits unconditionally,
without distinguishing compression-summarised content (gone from live
context) from delegation children (still visible to the parent agent).

Reworked _resolve_to_parent to return (root_id, has_compression_hop),
checking end_reason='compression' on every hop during the same
db.get_session() traversal — zero extra queries.

_discover() now has three compression-aware paths:
- In-place compaction: FTS hits on active=0 (compacted=1) rows pass
  through even when raw_sid == current_session_id
- Legacy rotation: lineage hits pass through when has_compression_hop
  is true on either side of the chain
- Delegation children: still excluded (no compression edge)

18 new tests covering all three scenarios + unit tests for the helpers.

Addresses Teknium's review feedback on #6256.
Closes #13840, #13841.

* fix(search): require compacted=1 not just active=0, scope delegation-under-compression

Addresses @teknium1's second review round on #63144:

1. _is_compacted_message now checks both active=0 AND compacted=1.
   Previously checked active=0 alone, which also matched rewind/undo rows
   (active=0, compacted=0) that must stay hidden.

2. Added _is_compression_ended() — checks only the session's own
   end_reason, not the lineage-wide has_compression_hop flag. This prevents
   delegation children living under a compression continuation from leaking
   through the lineage filter.

3. _discover lineage skip now uses is_ended_session (session-level check)
   instead of has_compression (lineage-level flag).

New tests:
- TestRewindExclusion: rewind rows stay hidden alongside compacted rows
- TestCompressionEndedHelper: session-level end_reason checks
- TestLegacyContinuationPlusDelegation: delegate child excluded while
  compression ancestors surface

78 passed (71 + 7 new).

* test(search): prove discovery scope and bookend bounding work together on compacted sessions

Integration test for the two compaction layers landing this sweep:
#63144's discovery-scope fix (archived rows surface from the current
session) and #69334's bookend bounding (summary exclusion + content
caps). A single compacted session exercises both: the archived FTS hit
must surface while the compaction handoff at the session tail stays out
of bookend_end and long messages stay capped.

* fix(compress): allow manual /compress when auto-compaction is disabled [overflow error directs users there]

compression.enabled: false is documented (agent/conversation_loop.py
overflow path) as disabling *automatic* compaction only — the terminal
context-overflow error explicitly tells users to run /compress manually,
and the gateway handler has never gated on the flag. But the classic CLI
(_manual_compress) and the ACP adapter (/compact) refused with
'Compression is disabled', leaving users at a full context with no
manual escape hatch on those surfaces.

Remove the stale gates (they predate the overflow-path design; the CLI
gate came from the original /compress commit's boilerplate) and unify
force=True across all manual-compaction call sites: ACP /compact and the
TUI's _compress_session_history (manual-only helper) now bypass the
summary-failure cooldown exactly like the CLI and gateway already did.
Also reword the ACP /context status line so a disabled-compression agent
no longer implies /compact is unavailable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(acp): align salvaged /compact wording and test with current /compress command name

Main renamed the ACP slash command from /compact to /compress after #63630
was written; update the salvaged status line, comment, and behavioral test
to dispatch the command that actually exists.

* test(compress): pin manual-compress-allowed-when-auto-disabled on every surface

Surface audit for #64438 consistency: cli.py and acp_adapter had their
gates removed by the salvaged #63630 commit (each with a behavioral
test). The remaining manual-compress surfaces never gated on
compression.enabled — pin that contract so a gate can't regress in:

- gateway/slash_commands.py _handle_compress_command (new behavioral
  test: compression_enabled=False still compresses, force=True).
- tui_gateway/server.py — all three manual routes (session.compress
  RPC, command.dispatch compress branch, slash.exec mirror) plus the
  compute-host slash.compress/session.compress controls converge on
  _compress_session_history; new test pins the helper ignores
  agent.compression_enabled and passes force=True.

* fix(desktop): skip bootstrap for explicit Nix backend (#69688)

Treat HERMES_DESKTOP_HERMES as an authoritative deployment override.
This keeps the Nix desktop package on its matching immutable Hermes CLI
instead of falling through to install.sh when a best-effort version probe
fails or times out.

* fix(gateway): suppress pre-API compression chatter

* chore: map matt-strawbridge contributor email for attribution CI

* fix(gateway): widen compression noise filter to all routine status lines

Post-sweep audit of every compression status emission (conversation_loop,
turn_context, conversation_compression): the buffered overflow/attempt-cap
retry chatter (🗜️ 'Context too large…', 'Compressed X → Y, retrying…',
'Context reduced to…'), the #69332-reworded auto-lower notice
("Auto-lowered this session's threshold…"), the aux-provider-unavailable
notice, and the concurrent-compression skip all leaked past
_TELEGRAM_NOISY_STATUS_RE on chat platforms. Add anchored alternatives for
each; the ', retrying'/'— compressing' anchors keep manual /compress
feedback ('Compressed: 30 → 12 messages') and failure/abort notices
visible per the deliberate carve-outs.

Also extract every routine compression status string into importable
template constants in agent/conversation_compression.py (single source of
truth shared by all emission sites), so tests can iterate the actual
emitted wording instead of hand-copied literals.

* test(gateway): pin routine-suppression + visible-carve-out contracts

- Iterate ROUTINE_COMPRESSION_STATUS_SAMPLES (formatted from the source
  constants the emit sites use) through _prepare_gateway_status_message on
  every chat platform — emission-wording drift now fails the suite without
  re-copying literals.
- Extend the pinned NOISY_STATUS_MESSAGES with the buffered retry chatter
  and post-#69332 wordings.
- New VISIBLE_COMPRESSION_MESSAGES negative suite: manual /compress
  headlines and abort/failure notices must never be swallowed by the
  widened regex.

* feat(desktop/settings): DOM-shaped skeleton loaders on every settings page

Model settings was the only page that kept its shape while loading; the rest
flashed a centered spinner (LoadingState) or empty placeholders. Standardize on
skeletons that mirror the settings rhythm.

- Add shared SectionHeadingSkeleton / ListRowSkeleton / SettingsSkeleton to
  settings/primitives.tsx (mirror SectionHeading + ListRow).
- Convert keys, providers, sessions, gateway, custom-endpoints, and config
  (non-model) from LoadingState to SettingsSkeleton; remove now-dead LoadingState.
- billing: BillingSkeleton (summary cards + sections) on first load instead of
  "—" placeholder cards.
- pet: skeleton grid on first load instead of a premature "unreachable" message.

* fix(desktop): show composer action shortcuts (#69707)

Expose the existing platform-aware keybind hints on Send, Steer, and Queue
composer control tooltips. Add focused tooltip coverage for each action.

* test(desktop): cover correction resume without duplicate prompts (#69708)

* test(desktop): cover correction resume without duplicate prompts

Exercise a live composer correction, switch away and back before the
response settles, and assert both user turns retain their order and occur
exactly once.

* test(desktop): cover correction warm resume during tool run

Exercise a correction accepted at a foreground-tool boundary, a switch through a persisted session, and the warm resume back. Assert the original prompt and correction remain singular and ordered.

* feat(relay): egress typing indicators through the connector (op="typing") (#69721)

The base class spawns _keep_typing for every adapter — a 2s refresh loop
that runs for the whole turn, including while the stream consumer sends
the response — but RelayAdapter inherited BasePlatformAdapter's no-op
send_typing. The loop ran every turn and emitted nothing, so relay-fronted
chats (hosted Discord/Telegram/Signal/Slack) never showed 'is typing…'.

The rest of the pipe already existed on both sides: OutboundOp "typing"
is in the wire contract (contract_version 1), and every connector-side
sender implements it (Discord POST /channels/{id}/typing, Telegram
sendChatAction, Signal sendTyping, Slack assistant status).

This bridges the tick onto the existing outbound frame, mirroring send():
- _with_scope re-attaches the tenant discriminator (metadata.scope_id, or
  user_id for DMs) — the connector's routedEgressGuard wraps ALL ops, so
  an undiscriminated typing frame is declined like a bare send would be.
- the Phase 1.5 per-frame platform tag routes typing through the platform
  the chat lives on for multi-platform gateways.

Best-effort by design: transport errors are swallowed (typing is cosmetic;
the next tick retries), no transport is a silent no-op, and stop_typing
stays the base no-op (platform indicators self-expire). Additive within
contract_version 1 — an older connector returns an unsupported-op result
we ignore. Contract doc §4 updated (typing now carries metadata?).

* test(desktop): cover queued prompt turn boundary (#69729)

* fix(desktop): preserve active correction on warm resume (#69725)

Update the gateway's live turn projection after an accepted correction so a warm session resume does not add the stale original prompt beside the persisted correction.

Cover the inference-time correction path end to end and assert both redirect entry points refresh the live user text.

* fix(desktop): place steer messages before redirected replies (#69739)

* test(desktop): reproduce steer transcript placement

* fix(desktop): place steer messages before redirected replies

* fix(desktop): open branched chat in a new tab and switch to it

Branching/forking a chat replaced the primary chat (setActiveSessionId +
setSelectedStoredSessionId + navigate). Instead, open the branch as its own
session tile tab in the center zone and reveal it, leaving the parent chat
exactly where it is — mirroring openNewSessionTile. All branch entry points
(message GitFork button, /branch and /fork, sidebar Branch, tile Branch) go
through forkBranch, so this covers every surface.

* fix(ci): publish inline E2E evidence (#69699)

* fix(ci): publish inline E2E evidence

Upload bounded screenshot evidence from E2E, then publish validated images
from a trusted workflow_run job to commit-pinned branches in the evidence repo.

Wait briefly for the live CI review comment marker before publishing, so
GitHub's read-after-write delay cannot leave an orphaned evidence branch.

* fix(ci): isolate privileged credentials from PR jobs

Keep App private keys and Docker Hub credentials out of PR-controlled
workflows. Use protected environments for trusted publishing and a public
repository variable for the App client ID.

* fix(ci): attach E2E evidence with restricted bot session

Replace the App-backed evidence repository publisher with gh-image uploads
from a dedicated bot session in the gh-image environment.

* fix(ci): publish validated E2E evidence from forks

Let the trusted default-branch publisher handle bounded, validated evidence
artifacts from fork PR CI without checking out or executing fork code.

* fix(desktop): use fork icon for branch in session context menu

The row/tab context-menu 'Branch from here' item used the git-branch
codicon while the inline message action uses a GitFork icon. Switch the
menu item to the git-fork codicon so branching looks consistent across
surfaces.

* fix(desktop): use repo-forked codicon (git-fork has no glyph)

The bundled @vscode/codicons font has no `git-fork` glyph (only
`git-fork-private`), so the Branch menu item rendered blank. Use
`repo-forked`, the actual fork icon, to match the inline GitFork action.

* feat(desktop): skipped clarify keeps its choices visible and answerable

When a clarify prompt times out, the settled card collapsed to just
'Skipped' — the options were unrecoverable (the args carry them, the
renderer dropped them) and there was no way to answer late.

The skipped card now:
- renders the original choices, letter-badged like the live card
- clicking one drafts a quoted follow-up ('Re: "<question>" — my
  answer: <choice>') into the composer via the insert bus. Enter sends
  it; if the agent is mid-turn it queues like any other prompt.
- a hint line explains the question timed out and what picking does

No retroactive resolution of the expired request: the tool already
returned empty and the turn moved on — injecting into past context
would break prompt-cache and role-alternation invariants, and
clarify.respond on an expired id hard-errors (#56558). The follow-up
message path needs no backend change and works against old backends.

Answered clarifies and free-text (no-choice) skips are unchanged.

Interim UX for #44845 (durable ID-addressable clarify decisions).

* refactor(desktop): share the clarify choice row and fix skip copy

Builds on the skipped-clarify card so it holds up beyond the timeout case:

- Extract a shared `ChoiceButton` (letter badge + label + row chrome) used
  by both the live pending card and the settled skip card. The two blocks
  had drifted into duplicated markup; now they can't diverge.
- Fix the hint copy. An empty `user_response` is emitted for BOTH a
  server-side timeout AND a manual Skip (tools/clarify_tool.py) — there is
  no field on the result that tells them apart — so asserting "This question
  timed out" was wrong half the time. Neutral wording ("This prompt is no
  longer waiting…") is correct for either, and the recover-your-answer path
  now also helps someone who mis-clicked Skip. Updated en/ja/zh/zh-hant.

No behavior change to the live prompt or the follow-up-message flow.

Co-authored-by: SHL0MS <SHL0MS@users.noreply.github.com>

* fix(ci): retain delayed and composite-action jobs in review (#69768)

Keep the live PR review comment polling for a short grace period after
visible jobs complete. Preserve composite-action jobs in timing and live
status collection, and split the timing HTML from its linked review-status
artifact.

* fix(desktop): separate workspace defaults from live cwd (#69765)

* fix(desktop): resolve tab title and project color for sessions outside the recents page

Opening a session in a new tab left the tab titled 'Session' with no
project-color dot until new activity landed the row in the paginated
recents list. Two gaps:

1. tileTitle/tileAccent/tileDragPayload only looked the stored row up in
   $sessions (the recents page). Sessions opened from a project group are
   often older than that page, so the lookup missed entirely. Resolve
   through the project tree as a fallback (tileStoredRow) and re-sync pane
   titles/accents when $projectTree loads.

2. Even with the row present, liveSessionProjectId returned null for a
   session whose cwd sits outside its recorded git_repo_root (mid-session
   relocation / sibling worktree), because the cwd-under-root guard ran
   before the explicit-project folder match. The backend tree groups such
   rows under the project; the client now agrees — an explicit folder
   match is authoritative, only the auto-project (repo root) fall…
ccomcorp added a commit to ccomcorp/hermes-agent that referenced this pull request Jul 27, 2026
…ing)

Bringing main current after 2,928 commits of hermes-agent work since the last
promotion (2026-07-16). Highlights: the 158-commit upstream/main absorption,
the D:\HeicH chassis cutover, the native composite memory/experience-store
vendoring (M0-B1 through M0-B3), and this session's fixes:
  - restored the chassis venv's editable install (was blocking every CLI/
    desktop launch with ModuleNotFoundError: hermes_cli)
  - vendored the remaining AIOS-hosted runtime code (experience health view,
    skill-librarian, session-continuity primitives) so hermes-agent no
    longer depends on I:/PROJECTS/AIOS/AIOS at runtime
  - fixed a real bug in the ported session-continuity code: session-end
    reconciliation was writing into AIOS's own journal instead of the
    active project's

Verified via `git diff --stat` against the merge-base that main holds no
content hermes-agent lacks: every file unique to main traces to a
deliberate, already-documented removal/refactor on hermes-agent (PyPI/
Homebrew ripout NousResearch#68217, desktop-controller retirement, infographic
untracking policy, etc.), not a regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@iMichka

iMichka commented Aug 3, 2026

Copy link
Copy Markdown

Hello. Homebrew maintainer here.

Just to let you know that we acknowledged this change, and will put the formula on deprecation mode for 6 months.
See Homebrew/homebrew-core#296811
Users can still install the old version but will get a deprecation notice.

After 6 months the formula will be disabled and will not be installable anymore.

We are tacking extra measures here, using a slow deprecation cycle (per our policy https://docs.brew.sh/Deprecating-Disabling-and-Removing), as there were more than 7000 downloads in the last 90 days (>2000 in the last 30), so we want to give our users some extra heads up.

By experience, with this high number of downloads, there is a risk that many users will complain about this change, so we will handle the fallout as much as we can our side.

@Nek-12

Nek-12 commented Aug 6, 2026

Copy link
Copy Markdown

I'll be the first to complain then.

What the hell?
Please revert this and support brew again.
What is the justification and reasoning for the change?

Brew is the only install path that updates consistently on my machine with other packages, doesn't crash, doesn't explode after updates, doesn't break because of dependencies or dev environment.

@MikeMcQuaid

Copy link
Copy Markdown

@Nek-12 Homebrew Project Leader here 👋🏻. We are reverting the deprecation and will continue to attempt to package this in Homebrew.

@Nek-12

Nek-12 commented Aug 6, 2026

Copy link
Copy Markdown

Thanks Mike, I really appreciate that, but I'm asking the maintainers here. The removal of brew is entirely unjustified in my eyes, that's why i'm asking for the reasoning behind the choice. I do not think Brew team should try to maintain the package alone with no help or support from the maintainers.

@MikeMcQuaid

Copy link
Copy Markdown

@Nek-12 We will try either way but I agree ideally we'd have help and support from upstream.

@dot-agi

dot-agi commented Aug 6, 2026

Copy link
Copy Markdown

@Nek-12 maintainer of the Homebrew formula here 👋🏻 . I will ensure this software is packaged properly via Homebrew. Thanks to @MikeMcQuaid, the formula is undeprecated.

I have a draft PR for maintaining upstream installation parity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor ci-reviewed applied to manually approve dangerous changes comp/cli CLI entry point, hermes_cli/, setup wizard needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants