Commit Graph

93 Commits

Author SHA1 Message Date
teknium1 97d3d75fdc fix: ignore every cron SQLite store and per-launch marker on flat installs
The flat-install block only ignored the bare cron/executions.db. All three
cron stores (executions, deliveries, notepad) are opened through
sqlite_util.open_db in WAL mode, so executions.db-wal/-shm exist whenever
the scheduler is live, and deliveries.db / notepad.db were not ignored at
all. `git stash push --include-untracked` therefore still unlinked the
live WAL/SHM and the whole deliveries/notepad stores under the running
gateway - the same mechanism this PR closes for the root state.db.

Switch the cron entries to the by-class shape already used at the root
(/cron/*.db plus -wal/-shm/-journal/retired-wal sidecars) and also ignore
the cron lock/heartbeat/output files and the per-launch root markers
(.update_check, gateway-starts.log, .clean_shutdown, active_profile,
.hermes_history, slack_tokens.json) that otherwise force every update
into the stash step. `git ls-files -i -c --exclude-standard` is unchanged
(no tracked file newly hidden). The test's FLAT_INSTALL_RUNTIME_STATE
gains one representative per new class; red before, green after.

Review finding: cron/executions.db-wal, cron/deliveries.db and cron/notepad.db were unignored and swept by the flat-install autostash.
2026-09-15 06:29:09 -07:00
teknium1 a172d76f2d fix(update): also ignore backups/ and the vault on flat installs; document the flat-install rule
`backups/` is where the pre-update snapshot the updater restores a swept
state.db FROM lives — leaving it unignored means the recovery copy is
swept together with the live database. `vault.key`/`vault.json.enc` are
the local secret vault.

Docs: website/docs/getting-started/updating.md explains that on a flat
install (checkout root == $HERMES_HOME) runtime state is git-ignored and
never enters the autostash.
2026-09-15 06:29:09 -07:00
kshitijk4poor 49b8f06abe fix(update): ignore cron/executions.db sidecars, not just the base file
`/*.db-wal` / `/*.db-shm` only match at the checkout root, so on a flat
install `git stash push --include-untracked` still swept
`cron/executions.db-wal` / `-shm` while the scheduler held the WAL-mode
database open (cron.executions._connect opens it via open_db in WAL mode).
The base file stayed put but its WAL vanished under a live writer, so the
next `_connect()` failed with `disk I/O error` (review finding on #111175).

Use `/cron/executions.db*` like the gateway recovery db rule already does,
covering -wal/-shm/-journal and retired-WAL dirs. Every other non-root db
rule in the block already uses the glob. The regression tuple gains the two
sidecars, and the stash test gets the reviewer's repro against the real
`_stash_local_changes_if_needed`: an open WAL connection with one committed
row must still be readable from a fresh connection afterwards.
2026-09-15 11:03:12 +05:30
kshitijk4poor 3ce06055d6 fix(update): ignore flat-install runtime state by class, not by file name
The flat-install block listed state.db and kanban.db sidecars one by one,
so any other root-level SQLite store (response_store.db, a future ledger)
and its -wal/-shm/-journal sidecars would still be swept by the updater's
`git stash push --include-untracked` and unlinked under the running
gateway (#110648). Replace the per-file lines with root-anchored globs
(`/*.db`, `/*.db-wal`, ...). No tracked root-level *.db exists, and
`git ls-files -ci --exclude-standard` is unchanged before/after, so the
globs newly ignore nothing that is committed.

Also add the rest of the flat-install runtime roots the previous fold
missed: the credential siblings from
gateway/platforms/base.py::_ROOT_CREDENTIAL_PATHS (.anthropic_oauth.json,
google_token.json, google_oauth_pending.json, auth/,
webhook_subscriptions.json), the active pairing location platforms/
(gateway/pairing.py), kanban/, gateway_state.json, processes.json,
cron.pid, the channel directory/alias and feishu pairing stores,
pending_messages/, checkpoints/, plugin-data/, hooks/, and the Discord
message-recovery db under gateway/ (gateway/ itself is tracked, so only
that file pattern is ignored). `/.credentials/` had no producer -- the
real dir is `credentials/` (web_routers/files.py, _ROOT_CREDENTIAL_PATHS)
-- so it is replaced. `/state-snapshots/` is dropped: the existing
unanchored `*-snapshots/` rule already matches it.

The test tuple now carries one representative per ignored class and its
comment no longer claims _ROOT_CREDENTIAL_PATHS enumerates the sidecar
set (that is `_sqlite_files`).
2026-09-15 11:03:12 +05:30
kshitijk4poor 34a45b35e5 fix(update): also ignore the flat-install config/credential/profile roots
The same `git stash push --include-untracked` sweep that took state.db
on a flat install (#110648) also takes every other untracked file at the
$HERMES_HOME root: config.yaml, auth.json/auth.lock, memories/,
profiles/, .credentials/, mcp-tokens/ and pairing/. Losing those on a
declined or failed restore strands the user's credentials and profile
config just as badly as losing the session store.

Extend the root-anchored block with those paths (none are tracked or
already ignored on main) and append them to the test's
FLAT_INSTALL_RUNTIME_STATE list so the existing stash invariant covers
them without a new test.
2026-09-15 11:03:12 +05:30
liuhao1024 eef60cc1ff fix(update): also ignore the cron job store on flat installs
The per-profile job store lives at HERMES_HOME/cron/jobs.json, so a
flat install keeps it beside executions.db inside the checkout-root
stash domain. Without an ignore rule the untracked autostash of
hermes update sweeps it away with the rest of the runtime state.
Absorb the path (noted in #110670) and its regression assertion into
the runtime-state carrier.

(cherry picked from commit 66282e6dc3ac5f276cdee5b92a22856f146c9a45)
2026-09-15 11:03:12 +05:30
liuhao1024 8f34499851 fix(update): ignore flat-install runtime state so autostash cannot sweep state.db
On a flat install (checkout root == $HERMES_HOME) the untracked autostash of
`hermes update` sweeps the live state.db/-wal, snapshots, cron ledger and
lock/pid files into the stash and unlinks them under the running gateway; the
restart recreates an empty store at the same path and the declined restore
leaves the profile with no transcripts (#110648).

Root-anchor the runtime state set in .gitignore, mirroring the
.hermes-bootstrap-complete (#38529) and /.install_method (#66189) precedent
and the $HERMES_HOME-root enumeration in the platforms base module, so the
stash step is never entered for runtime state alone. Regression test runs the
exact stash command against a real repo carrying the tracked .gitignore.

(cherry picked from commit 6c74c24e6131af189801e54f1e11b260a529b74a)
2026-09-15 11:03:12 +05:30
teknium1 3e2e2c50eb feat(plugin-catalog): rank entries by GitHub stars, probed at most once a day
Catalog entries sort official → stars desc → name, both in browse shelves and
filtered grids, with a ★ pill on each card linking to the repo's stargazers.

Rate-limit discipline is the design constraint: the docs site deploys many
times a day and shares one GitHub App API budget with every other workflow
(tonight's merge train got rate-limited on unrelated uploads). So
website/scripts/fetch-plugin-stars.py first fetches the live site's own
plugin-stars.json (a CDN GET, not the API); if that cache is under 24h old it
is reused verbatim and GitHub is never called. Only a stale cache triggers one
GET /repos/{owner}/{repo} per unique catalog repo, and a 403/429 mid-run keeps
the previous counts instead of zeroing them. extract-plugins.py merges the
cache into plugins.json (`stars`) and plugins-meta.json (`starsFetchedAt`), and
the page footnote says when the ranking was last refreshed.
2026-09-14 21:24:33 -07:00
teknium1 b980495847 refactor: move live-model benchmark harnesses from scripts/ into evals/
scripts/ is for repo tooling (tests runner, release, installers, CI
checks). The tool-search live tests, the toolperf A/B eval and the browser
eval benchmark are offline benchmarks that spend model budget, which is
exactly what evals/ holds; evals/browser_use already cited
scripts/toolperf_abeval as "the same pattern".

- scripts/tool_search_livetest*.py + analyze_livetest.py + LIVETEST_README
  -> evals/tool_search/ (README.md); repo-root sys.path hop adjusted for
  the extra directory level; gitignore now covers evals/tool_search/out*/
- scripts/toolperf_abeval/ -> evals/toolperf_abeval/
- scripts/benchmark_browser_eval.py -> evals/browser_use/
2026-09-13 06:06:46 -07:00
ethernet 31d0a2428e fix: add MagicMock to gitignore 2026-09-11 16:11:50 -04:00
Teknium 41b4555ed9 feat(plugins): catalog is the sole discovery system — re-port onto main's layout
- hermes_cli/plugins_cmd_catalog.py: new sibling owning resolution, the
  .hermes-catalog.json provenance sidecar, search/info/validate, re-pin on
  update, and the dashboard/TUI payload builders. plugins_cmd.py only
  gains the hooks (cmd_install catalog branch, cmd_update / dashboard
  update re-pin, dashboard_install_plugin catalog_name + kill list,
  dispatch entries); the community index (plugin_index.py) is gone.
- hermes_cli/plugin_catalog.py: catalog_dir parameter replaces the
  test-only HERMES_PLUGIN_CATALOG_DIR env var; live refresh reads ONE
  published document (/docs/api/plugin-catalog.json, 6h cache, in-tree
  fallback) instead of the unauthenticated GitHub contents API (60 req/h,
  1 request per entry); in-tree and live removals are unioned so a stale
  cache can never un-block.
- Catalog route lives in web_routers/dashboard_ui.py (the facade is off
  limits); _plugin_runtime_status shared from web_server_dashboard.py;
  hub rows carry removed_reason. TUI plugins.manage gains catalog_name
  install, catalog row fields and an update action.
- plugin_validate: the probe context honours ctx.get_config defaults
  (real plugins do int(ctx.get_config("timeout", 180)) in register()).
- Installed-state merge matches through the sidecar's catalog_name
  first — catalog names rarely equal manifest names.
- extract-plugins.py emits plugin-catalog.json; deploy-site triggers on
  plugin-catalog/** so entry merges republish it.
2026-09-09 04:38:01 -07:00
Teknium d47adec28f Merge origin/main into feat/plugin-catalog
Python plugin CLI/loader/web/tui files taken from main wholesale; the
catalog layer is re-ported onto main's decomposed shapes in the
following commits. plugin_index.py removed (catalog is the sole
discovery system).
2026-09-09 04:15:27 -07:00
Teknium 1398c0f5ca fix(update): stage-and-swap the Desktop rebuild so a failed pack never removes the working app
`hermes update` → `hermes desktop --build-only` → `npm run pack` packed
electron-builder's output IN PLACE: before-pack.mjs wipes
`release/<platform>-unpacked` (or the mac `Hermes.app`) before the Electron
unpack/asar/rename, so any failure after that point — corrupt cached zip,
blocked download, missing dep, disk full — left the user with NO app and the
update reporting "partially complete" over an empty release/ (#86443).

Fix the class, not the predicate: cmd_gui now passes
`-c.directories.output=apps/desktop/.staging-<pid>-<ts>` to the pack, runs
the existing verification (packaged-exe probe, macOS re-sign, Windows PE
integrity gate) against the STAGED tree, and only then promotes it:
`release/<unpacked>` → `.previous`, `<staging>/<unpacked>` → `release/<unpacked>`,
drop `.previous`. A rename failure between the two steps restores `.previous`.
On any failure the staging dir is removed and the live app is untouched.

- `_purge_electron_build_cache` / `_ensure_desktop_exe_launchable` /
  `_desktop_macos_relaunchable_fixup` take the output dir so the corrupt-zip
  retry purge and the integrity self-heal only ever clear the staging tree,
  never `release/*-unpacked`.
- `.gitignore` the staging dir so a killed build cannot dirty the checkout.
- Docs: updating.md describes the stage-and-swap Desktop rebuild step.

Live repro (real `_rebuild_desktop_after_update` → real `hermes desktop
--build-only` subprocess, fake npm whose pack wipes appOutDir then fails):
before — `release/linux-unpacked/hermes` gone after the failed rebuild;
after — marker intact, no `.staging-*` left, rebuild returns False; a
passing pack swaps the new app into `release/`.

Closes #86443

Co-authored-by: AIalliAI <285906080+AIalliAI@users.noreply.github.com>
Co-authored-by: deathxdefeat <deathxdefeat@users.noreply.github.com>
2026-09-02 06:53:20 -07:00
joaomarcos c26f75baab fix(security): keep profile exports out of source and image contexts
Route automatic profile exports to a managed store instead of the current checkout, and enforce a CI/Docker boundary that rejects archive files before they can be published.
2026-09-01 01:00:23 -07:00
Futahua 58c1876d37 test(sessions): falsify the per-session fence across real processes
The unit tests share one interpreter, so they cannot exercise the failure the
fence exists to prevent: two SEPARATE gateway processes, each holding its own
snapshot of a conversation, both writing to it. That is how the defect was found
and it is the only way to show it is closed.

This drives two real `python -m tui_gateway.entry` processes over stdio and
checks the whole sequence, including the parts that are easy to get wrong:

  session.create claims nothing        an idle composer must not hold a session
  the lease keys on the STORED id      a lease keyed on the runtime handle would
                                       fence nothing, since two processes
                                       resuming one conversation have different
                                       runtime ids by construction
  B may still RESUME                   reading is never fenced; only writing is
  B's submit -> SESSION_NOT_OWNED      typed, and the registry is unchanged
  A killed, B retries -> accepted      a dead owner is pruned, not permanent

No provider is needed. The fence is checked before the agent is built, so a
submit that later fails for want of a model still proves who owns the session --
which keeps the probe free of credentials and of inference cost.

Against the parent commit it stops at the second check with an empty registry,
which is the defect stated exactly: with no cap configured, nothing was recorded
and therefore nothing could be refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:36:33 -07:00
Teknium 46ab5aa365 fix(catalog): remove the parallel community plugin index — the catalog is the only discovery system
The interim index (#84919) merged while this PR was open, leaving two
side-by-side discovery systems. Rip the index out, keep only what the
catalog track uses:

- delete hermes_cli/plugin_index.py, the bundled seed JSON, its test
  file, the .gitignore exception, and the plugins.index_url surface
- cmd_search is catalog-only (term + --json); --capability/--refresh die
- cmd_install: unknown bare catalog-like names hard-error again instead
  of falling through the removed index fallback
- plugin packs bare-name resolution rewired onto plugin_catalog
  (load_catalog_live), the only shipped index consumer
- docs (features/plugins.md, cli-commands.md) rewritten for the catalog
2026-08-31 11:54:54 -07:00
Teknium c49fa88b80 refactor(skills): shipped-set slim — 15 to optional, github 6-way merge, pdf absorbs OCR, channel-gated teams pipeline (index −26%) (#98539)
* refactor(skills): shipped-set slim — 15 skills to optional, github six-way merge, pdf absorbs OCR+nano-pdf, channel-gated teams pipeline

Maintainer-directed shipped-skills curation (skills index 1,900 -> ~1,400
tok/call on desktop; every session pays the index, so this is a per-call
diet on all installs):

- optional-skills moves (installable via skills hub, history preserved):
  creative comfyui/ascii-art/excalidraw/pretext/sketch/touchdesigner-mcp;
  ALL of mlops (huggingface-hub, llama-cpp, serving-llms-vllm,
  weights-and-biases, evaluating-llms-harness — subcategory structure
  kept); research-paper-writing (55 supporting files, 17.3K-tok load);
  openhue; blogwatcher (first taught the cronjob monitor-field watch
  pattern + web_extract instead of pre-cron manual workflows)
- DELETED session-librarian (Aug-12 'inspired by Perplexity Computer'
  port, never maintainer-intended; session_search covers discovery)
- github: six skills (auth, issues, pr-workflow, issue-to-pr,
  code-review, repo-management) merged into ONE software-development/
  github skill — routing body + complete per-workflow references;
  benbarclay authorship credited; codebase-inspection rides along;
  discipline pins from test_github_issue_to_pr_skill.py preserved
  against the reference body in the new test_github_skill.py
- pdf absorbs ocr-and-documents + nano-pdf as references/ + scripts
  (extract_pymupdf, extract_marker converted to the argparse house
  standard its contract test enforces)
- NEW session_platforms frontmatter gate (metadata.hermes): hides a
  skill from the index on gateway channels it is not for; fail-open on
  unknown platform; teams-meeting-pipeline gated to [teams, cron]
- blocked-page-recovery: research -> new web category; trigger-first
  description ('Use when a fetch fails: 403/429, paywall, WAF, bot
  wall.') so the model actually reaches for it on blocked fetches
- docs regenerated via generate-skill-docs.py (195 pages); related_skills
  swept repo-wide; tests: 1672 passed (2 openclaw failures pre-existing
  on clean main, Windows-local)

* chore: ignore .skills_prompt_snapshot.json (local index cache, accidentally committed)
2026-08-30 04:53:39 -07:00
Teknium 7e17ff0ab3 Merge origin/main into feat/plugin-catalog — reconcile with landed index/manifest-v2 tracks 2026-08-27 21:29:42 -07:00
emozilla fe95ed3930 Merge origin/main: reconcile with PR #92092 (in-checkout launcher restore)
PR #92092 fixed the same vanished-launcher bug by restoring copies into
the legacy in-checkout hermes-agent\bin from the update tail. That
location is what this branch removes: untracked files there are swept
by the update autostash on every cycle (restore/sweep treadmill, plus a
parked stash entry per update under --keep-stash), and unconditional
exe copies break on relocatable venvs ('uv trampoline failed to
canonicalize script path'). This branch's managed-binary-dir layout
supersedes both mechanisms, so the merge resolves to it:

- drop _sync_windows_cli_launchers and its _ensure_acp_launcher call
  (Windows staging/repair lives in ensure_windows_bin_launchers at
  process start and migrate_windows_bin_path in the update tail);
  _ensure_acp_launcher is a Windows no-op again
- keep #92092's genuinely better installer semantics: staging stays in
  a dedicated Install-HermesCommandLaunchers function that throws
  BEFORE any PATH mutation when the required launcher cannot be staged
  and verified -- previously Set-PathVariable could put an empty dir on
  PATH and still print 'hermes command ready'. Reworked for this
  branch's layout: caller passes the destination ($HermesHome\bin),
  launcher form follows the venv (exe copy vs .cmd delegator), and the
  verify step accepts either form
- rework #92092's AST-lifted PowerShell test for the new function
  signature, keeping its fail-before-PATH-mutation assertions and
  adding relocatable-venv form-selection coverage
- drop tests/hermes_cli/test_windows_cli_launcher_repair.py (pinned the
  superseded in-checkout mechanism; equivalent and broader coverage
  lives in tests/hermes_cli/test_ensure_windows_bin_launchers.py)
2026-08-22 22:16:38 -04:00
emozilla 679e9cd294 fix(windows): stage hermes launchers in the managed binary dir, not the git checkout
The installer staged the hermes/hermes-acp launcher copies at
hermes-agent\bin -- inside the git working tree -- and put that dir on
the user PATH (#84452). The update command's pre-pull autostash
(git stash push --include-untracked) swept those untracked, unignored
copies off disk, and once the desktop updater stopped re-applying
stashes (--keep-stash, 5dd221d442) nothing restored them: `hermes`
stopped resolving in every new terminal on every desktop-updated
install.

Move the canonical launcher home to the managed binary dir
(%LOCALAPPDATA%\hermes\bin, next to the managed uv) -- outside the
checkout, where no git operation can ever touch it. The dir is
per-machine and shared by every profile, so all anchoring uses
get_default_hermes_root(), never HERMES_HOME (which points inside
profiles\<name> under `hermes -p`).

The copy design also had a second latent break: managed-uv rebuilds
create relocatable venvs, and a relocatable venv's exe trampoline
resolves relative to its own location -- a copy outside venv\Scripts
dies with 'uv trampoline failed to canonicalize script path'. Launcher
form now depends on the venv (lockstep in install.ps1 and
_install_repair.py): exe copy for normal venvs, a .cmd delegator
invoking the in-venv exe by absolute path for relocatable ones. Either
form counts as present, so pre-rebuild exe copies are left alone.

Delivery to the existing fleet, per cohort:

- already-broken installs cannot run the CLI, so an import-time heal in
  hermes_cli.main (ensure_windows_bin_launchers) re-stages missing
  launchers when the desktop app spawns its backend -- the one channel
  that still reaches them. Gates fail toward inaction: canonical dir
  only for the managed clone, legacy hermes-agent\bin only while the
  user PATH still resolves through it (some pre-managed-uv installs
  have no hermes\bin PATH entry; the legacy re-stage is what fixes
  those). Staging-name + os.replace keeps concurrent process starts
  from tearing a launcher; the helper never raises.
- healthy old-layout installs migrate in the update tail
  (migrate_windows_bin_path): stage canonical launchers, verify them
  BEFORE touching the registry, prepend hermes\bin to the user PATH,
  strip the legacy entries (hermes-agent\bin and venv\Scripts, #83797),
  preserving REG_EXPAND_SZ and raw %VARS%. The legacy dir's files stay
  on purpose -- configs holding absolute launcher paths keep working;
  only the sweepable PATH resolution route goes.
- fresh installs get the new layout from install.ps1 directly.

/bin/ is gitignored so the one update that DELIVERS this fix cannot
sweep pre-migration launchers a final time under the old rules; the
gitignore line, the legacy re-stage branch, and the update-tail call
are transition machinery with a named expiry once the fleet has
migrated.

Also rewrites _ensure_acp_launcher's stale Windows paragraph to match
(raw docstring fixes its invalid \S escape) and updates the Windows
native docs to the new layout, with a docs<->installer parity test.
2026-08-22 13:38:56 -04:00
Teknium db7dda468f chore: anchor artifact ignore rules to repo root; block them from Docker image layers
Follow-up to the cherry-picked cleanup: the default.tar.gz profile export
was also carried into published container images by the Dockerfile's
'COPY . .' layer because .dockerignore had no matching pattern. Anchor
the .gitignore rules to repo root (per review feedback on #91712) and
add the same set + /*.tar.gz to .dockerignore so root archives can never
reach an image layer again.
2026-08-22 10:38:27 -07:00
EAbaracus 0af2858a3a chore: remove committed root artifacts (log.txt, sqlite_leak_fix.png, default.tar.gz)
These were committed to the repo root but are build/debug byproducts:
- log.txt: empty 0-byte file
- sqlite_leak_fix.png: unreferenced 832KB image
- default.tar.gz: 1.96MB, only used as a test fixture OUTPUT (tests write it
  to a temp dir, never read from repo root)

Add ignore rules so they cannot be re-committed. Part of audit cleanup
(HA-D11-001 / HA-D3-001).
2026-08-22 10:38:27 -07:00
Teknium d516496cef fix: track bundled plugin.js sources past the tsc-artifact gitignore
apps/desktop/src/**/*.js is gitignored (stale tsc output shadows .tsx),
which silently dropped the hermes-bots plugin.js from the adoption
commit — tests shipped, source didn't, CI ENOENT'd. Negate the pattern
for src/plugins/*/plugin.js: adopted plain-ESM plugins have no .tsx
sibling, so the shadow hazard cannot apply.
2026-08-16 18:30:53 -07:00
Teknium 2e0183169c feat(plugins): community plugin index + hermes plugins search (#64181)
Static machine-readable community plugin index with fuzzy search and
index-resolved installs, mirroring the Skills Hub catalog pattern
(fetch → HERMES_HOME/cache with 24h TTL → bundled seed fallback).

- hermes_cli/plugin_index.py: index fetch/cache/seed chain, fuzzy
  search (name/description/tags/author + typo tolerance), capability
  filter, bare-name resolution. Canonical URL overridable via
  plugins.index_url config key.
- hermes_cli/data/plugin_index.json: bundled seed (offline fallback +
  format reference) with 5 real ecosystem plugins, each pinned to an
  exact commit SHA.
- hermes plugins search [term] [--json] [--capability] [--refresh]:
  Rich table or JSON output, offline-safe, with an explicit
  'indexed ≠ audited' footer.
- hermes plugins install <name>: bare names (no slash, no URL scheme)
  resolve through the index to owner/repo[/subdir] @ pinned ref and
  hand off to the existing install path (ref wired through the #82029
  exact-ref support). Ambiguous names list candidates and exit;
  explicit owner/repo and Git URL installs are untouched, and an
  explicit --ref always beats the index pin.
- Docs: discovery section in user-guide plugins.md (format, submission
  workflow via PR to hermes-plugin-index, security framing) and
  reference/cli-commands.md rows.
- Tests: tests/hermes_cli/test_plugin_index_search.py (38 tests, no
  live network) covering parsing, search, remote→cache→seed fallback,
  TTL, install resolution/ambiguity/passthrough, and --json output.
2026-08-12 18:52:08 -07:00
Chen Jin fae0309c00 chore: drop accidental .omc/ gitignore change from PR 2026-08-10 19:00:19 -07:00
Chen Jin 11f920b812 fix(desktop): attach renderer-lifecycle diagnostics to all BrowserWindow instances (#81290) 2026-08-10 19:00:19 -07:00
ethernet 3d9ec4d62e test(install): prove updating from a release reaches this commit
Nothing covered the update path, which is the worst thing to break: a broken
updater strands users on the version that cannot fix itself. `hermes update`
alone is ~2000 lines (hermes_cli/update_cmd.py) and had no end-to-end test.

tests/install/install-update-e2e.sh installs a genuine earlier Hermes through
the real one-liner (curl -fsSL https://…/install.sh | bash, served by
dev-sandbox's MITM proxy at the canonical URL, cloning "github.com" through the
upload-pack shim), which really installs uv, a managed Python, Node and the
venv. It then applies ONE update route and requires the checkout to land on this
commit with `hermes --version` still working -- so a pass means the venv and
entry point survived, not merely that git moved.

One route per run, each on a sandbox built from scratch. Sharing one install
across routes -- or rewinding with `git reset --hard` between them -- leaves the
second route running against a tree the first already updated (same venv, same
console script, same __pycache__), which is not the state any real user is in: a
route could pass only because its predecessor did the work, and a failure in the
first left the second exercising something undefined.

--install-ref chooses what to install first, so this covers "update from an
older release", not just from the tip. Installer flags are probed against the
target rather than assumed, because releases from months back predate flags
current Hermes takes for granted: --skip-browser is read out of that ref's own
install.sh, and `--yes` is asked of the installed `hermes update --help` (the
update subcommand has lived in main.py, subcommands/update.py and update_cmd.py
across the tags we sample, so a static parse rots silently -- and did). Without
those probes, old releases die on "Unknown option: --skip-browser" and
"unrecognized arguments: --yes" before doing any work.

Installer output is streamed through tee rather than captured: a real install of
uv, Python, Node and the venv IS the substance of this test, so it belongs in
the job log, not only in an artifact. pipefail keeps the installer's exit status
rather than tee's, so a failed install cannot look like a pass. The sandbox's
own proxy log is printed in full on failure, since a rejected TLS handshake
explains a failure that otherwise reads as a bare `curl: (35)`.

Deliberately reuses dev-sandbox rather than adding a second harness. An earlier
draft rewrote install.sh's hardcoded URLs with insteadOf and ran it against the
host; that tested the installer LESS faithfully (bash install.sh instead of the
real one-liner, host libs instead of a clean machine, ssh disabled to keep a
failed rewrite from reaching real GitHub) while duplicating a fake Internet we
already have.

Shell, not pytest, so scripts/run_tests.sh and run_tests_parallel.py stay
untouched: a pytest version needed an entry in the former's `env -i` credential
allowlist and a _SKIP_PARTS exclusion in the latter, and every meaningful line
was a command run inside the sandbox anyway.

Two guards, both earned during bring-up. It prefers the `sandbox` wrapper and
falls back to the raw script only when bwrap is on PATH (under Nix the wrapper
supplies the PATH and DEV_SANDBOX_* vars, so the bare script exits 127). And it
refuses to run on a dirty worktree: every dev-sandbox invocation re-derives fake
main from the working copy, so uncommitted changes move the update target
between the call that installs and the call that verifies -- a failure that
looks like a broken updater but is a moving reference.
2026-08-04 17:36:26 -04:00
Teknium 0fe6a36e6e chore: gitignore the .lazy-refresh-incomplete runtime marker
Companion to the #72002 salvage: the marker was accidentally committed
once (3a69e34702) and the guard now prevents test runs from writing it;
ignoring it prevents any future accidental re-commit.
2026-07-29 21:30:53 -07:00
teknium1 ea2499bcfe fix(update): close the stale-bytecode class with a launch-time checkout-fingerprint sweep
The stale-.pyc bug class (#6207, #60242, live WhatsApp report: gateway
ImportError 'cannot import name parse_model_flags_detailed' after /update)
has one shared shape: the checkout's .py files change while __pycache__
retains bytecode from the previous revision, and a later process trusts
the stale .pyc.

Update-time clears can never fully close this class: 'hermes update'
always executes the PRE-pull updater code, so hardening added to it takes
effect one update late — and a manual 'git pull' never runs the updater
at all.

Class fix:
- Launch-time guard in main(): compare the checkout fingerprint (cheap
  file reads via _read_git_revision_fingerprint, no git subprocess)
  against a .bytecode-fingerprint stamp; sweep __pycache__ once when they
  diverge. Covers manual pulls, old updaters, ZIP restores — every entry
  point (CLI, gateway service, desktop backend) passes through main().
- Record the stamp at all three update-time clear sites (git path pre-
  install, git path post-install, ZIP path) so a normal update never
  triggers a redundant launch sweep.
- Sibling site: 'hermes plugins update' + dashboard plugin update now
  clear __pycache__ under the plugin dir after git pull (plugin trees
  live outside the repo guard).

E2E validated: reproduced the stale-pyc shadowing (same-size same-mtime
source swap → old symbol wins in a fresh process), confirmed the launch
sweep restores the new symbol, no-ops when the checkout is unchanged,
and re-sweeps on the next pull. Sabotage-tested: reverting the sweep
fails 4 of the new tests.
2026-07-27 11:33:40 -07:00
teknium1 085c7da111 chore: untrack committed PR infographics and enforce the rule in CI
PR infographics belong in the PR description, referenced from the
image-provider URL. The binary never enters git history.

This rule has been established twice and leaked twice. #48261 removed the
first batch. #54564 removed a second batch and added `infographic/` to
.gitignore — but .gitignore only stops an accidental `git add`. It does
nothing against `git add -f`, and nothing for a directory that does not
literally match the pattern. In the four weeks after that rule landed,
nine more PNGs were force-added, and an `infograficos/` directory
(#70552's loophole, never actually closed) slipped a tenth past the
pattern entirely.

Removes 11 tracked images (~14MB) with `git rm --cached`, so local copies
survive. Adds an infographic-check CI job that matches on the IMAGE rather
than on one directory spelling, so a localized or typo'd path cannot
sidestep it, and extends the .gitignore pattern list as the first line of
defence.

Verified the guard both ways against synthetic repos: it fires on
`git add -f` into `infographic/`, on the `infograficos/` spelling, and on
nested `docs/pr/infographics/*.jpg`; it does not fire on legitimate
product imagery under `docs/assets/` or `website/`, nor on non-image
files.
2026-07-26 11:54:03 -07:00
Justin Bennington 17bf3c8283 fix(runtime): repair vulnerable managed SQLite builds (E-949) 2026-07-24 16:00:03 -07:00
Bartok c07973aa81 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.
2026-07-22 14:40:44 -04:00
Teknium 1c3d6e59dd Merge remote-tracking branch 'origin/feat/plugcat-docs' into feat/plugin-catalog 2026-07-22 08:15:38 -07:00
Teknium f13f845116 feat(state): messages_fts_cjk — CJK-bigram index on the v23 external-content layout
Integration layer for the cjk_unicode61 tokenizer, rebuilt on the v23
schema (the contributed integration in PR #65544 predated it):

- messages_fts_cjk: external-content FTS5 over a tool-row-excluding view
  (same v23 storage discipline as the trigram index it supersedes — zero
  inline text copies). Serves EVERY CJK query shape the legacy routing
  split between trigram (>=3 chars/token) and LIKE full scans (1-2 char
  tokens). Lone 1-char CJK runs and role_filter=['tool'] queries keep
  their legacy routes.
- Dedicated marker pair (fts_cjk_rebuild_high_water/progress) gates the
  id-scoped triggers, so a cjk-only backfill never gates the complete
  messages_fts/trigram triggers.
- Transitions ride  (the existing
  throttled/resumable chunk engine): fresh DBs are born with the index;
  legacy v22 DBs land on v23+cjk in one run; already-optimized v23 DBs
  gaining the tokenizer get a marker-gated backfill; live writes are
  indexed immediately in every case.
- Tokenizer-loss self-heal: a process that can't load the extension drops
  the cjk triggers (writes keep working), leaves a stale breadcrumb, and
  the index is rebuilt from scratch on the next optimize run — triggers
  are never reinstalled over a gap (external-content 'delete' on an
  unindexed rowid is the FTS5 corruption hazard the marker gating exists
  to prevent).
- Capability classification: 'no such tokenizer: cjk_unicode61' joins the
  degraded-runtime error class everywhere (read probe, write probe,
  repair) so tokenizer absence is never misclassified as corruption.
- Config: sessions.cjk_fts (default on, inert without the .so) and
  sessions.search_slow_ms in config.yaml, bridged to env by CLI + gateway
  (startup + per-turn reload). build.sh falls back to vendored SQLite
  headers so no libsqlite3-dev is needed.

Slow-query log path attribution updated: fts_cjk / fts5 / trigram /
like_scan. Tests: 14 lifecycle tests (fresh/legacy/stale/backfill paths,
tokenizer-loss round-trip) + 5 config-bridge tests + slow-log suite.
2026-07-22 07:56:47 -07:00
Teknium fb40a768fc feat(docs): add /docs/plugins catalog page fed by plugin-catalog/ extractor
- website/scripts/extract-plugins.py: reads plugin-catalog/*.yaml (+removed.yaml),
  emits static/api/plugins.json + plugins-meta.json; degrades to an empty
  catalog with exit 0 when plugin-catalog/ does not exist yet
- website/src/pages/plugins/: catalog page with search, tier tabs
  (All/Official/Community), capability chips, pinned-SHA repo links,
  copyable install commands, and an empty-state submission CTA
- cross-nav between Skills Hub and Plugin Catalog pages + navbar item
- user docs: user-guide/features/plugin-catalog.md (trust model, install,
  submission checklist, custom git-URL contrast), registered in sidebars.ts
- wired into deploy-site.yml and prebuild.mjs; artifacts gitignored
- tests: tests/website/test_extract_plugins.py
2026-07-22 07:28:53 -07:00
Brooklyn Nicholson 502939e19e chore: gitignore node_modules symlinks, not just directories
Worktrees symlink node_modules to the main checkout; the dir-only
node_modules/ pattern doesn't match symlinks, so one slipped into a
commit and broke npm ci on CI (ENOTDIR). Dropping the trailing slash
matches both.
2026-07-21 20:29:48 -05:00
Teknium cca2276115 chore(git): ignore the web UI build lock file
The cross-process build flock (.web_ui_build.lock at the repo root) is
an empty coordination file created on first dashboard boot; it must
never be tracked or show up as untracked noise. Also keeps it out of
the content-hash staleness digest, which skips gitignored paths.
2026-07-21 12:42:07 -07:00
ethernet 0860ee4e5a feat(desktop/e2e): Playwright E2E suite with visual regression diffs
Adds a full desktop Playwright E2E suite that launches the Electron app
against a mock inference server, exercising the full boot chain:

  electron -> hermes serve -> mock provider -> renderer

Includes:
- Mock OpenAI-compatible inference server (mock-server.ts)
- Shared fixtures with sandbox isolation (credentials, HERMES_HOME,
  userData, fixed window-state.json for reproducible screenshots)
- Test specs: boot, boot-failure, onboarding, mock-backend-setup, chat,
  and packaged-app launch
- Visual regression: expectVisualSnapshot() wraps toHaveScreenshot in
  try/catch so diffs are reported without failing the test suite
- CI workflow: xvfb at 1280x1024, baseline cache from main
  (--update-snapshots on main, compare on PRs), step summary table with
  diff/actual/expected image links, dedicated visual-diffs artifact
- dev:mock script for local fake-provider development
- test:e2e:visual + test:e2e:update-snapshots scripts using cage
- .gitignore: *-snapshots/ (baselines cached in CI, not committed)
2026-07-20 11:44:40 -04:00
Brooklyn Nicholson d14bf23fa3 chore(desktop): build config — keep tsc emit out of src, gitignore artifacts 2026-07-13 17:57:54 -04:00
ethernet 1c557bb98a cleanup(ci): make all tsbuildinfo gitignored 2026-07-13 17:22:17 -04:00
ethernet 5e849942c3 feat(dev): add isolated sandbox script for local dev
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.

Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
  survives restarts for repeat testing

In the Nix devShell the script is available as 'sandbox'.

Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
2026-07-10 18:51:49 -04:00
Taylor H. Perkins 8a76de962f fix(secrets): make 1Password bootstrap token reliable outside systemd
The 1Password secret source resolves op:// references using
OP_SERVICE_ACCOUNT_TOKEN read from os.environ. Under systemd the gateway
gets that token via EnvironmentFile, but cron jobs, subprocesses, CLI
runs, macOS launchd, and Docker containers spawn fresh interpreters with
no inherited shell state — so they silently failed to resolve any
reference and fell back to empty strings.

Two patches close the gap, matching Bitwarden's reliability guarantees:

1. env_loader: auto-load ~/.hermes/.op.env after .env so the gitignored
   bootstrap token is available everywhere. override=False plus an
   explicit guard ensure it never clobbers a token already in env (e.g.
   from a systemd EnvironmentFile, which keeps precedence).

2. credential_pool: _get_env_prefer_dotenv() now prefers the resolved
   value in os.environ when .env still holds a raw op:// reference,
   instead of handing a URL to provider auth. Non-op:// values keep the
   existing .env-takes-precedence behaviour.

Also gitignore .op.env, document the three bootstrap-token options, and
add tests covering auto-load, no-override, and the resolved-vs-raw
precedence (plus regression guards).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 04:58:07 -07:00
Teknium 43eaf79ae6 chore: remove committed PR infographics and gitignore the path (#54564)
PR infographics are rendered locally and embedded in PR descriptions via
the image-provider (fal.media) URL — they were never meant to live in the
repo. The intended .gitignore enforcement (documented as added back in May
2026) was never actually committed, so 35 PNGs (~54MB) accumulated under
infographic/ via 'docs: add PR infographic for X' commits.

- Remove all 35 tracked infographic/*.png files.
- Add infographic/ to .gitignore so git add on the path is now a no-op.

The PR body remains the archive for these images.
2026-06-28 20:46:35 -07:00
Ben Barclay fcf6cb3d73 fix(docker): supervised gateway uses --replace to take over stale holder (NS-505) (#47555)
* fix(docker): supervised gateway uses --replace to take over stale holder

Inside the s6 container image the per-profile gateway service rendered a
bare `hermes gateway run` (no --replace). When a gateway is started
OUTSIDE s6 — a stray shell `hermes gateway run`, an agent action, or the
Open WebUI helper (scripts/setup_open_webui.sh) — it grabs the
per-HERMES_HOME PID lock first. The supervised slot then execs the bare
`gateway run`, hits the "Another gateway instance is already running"
guard, exits non-zero, and s6 restarts it: a restart loop that floods the
log every ~12s and never binds. The container looks up but the gateway is
permanently down, and dashboard-only users (no shell) cannot recover.

Render the supervised run script as `gateway run --replace` so s6 is
authoritative for its slot: it reaps the stale holder via the hardened
takeover path (takeover marker + SIGTERM->SIGKILL-with-confirmation +
scoped-lock cleanup in gateway/run.py) and binds. This matches the
systemd service path, which already builds its argv with --replace
(_build_gateway_argv / 'nohup hermes gateway run --replace'), and the
intent already documented in _maybe_redirect_run_to_s6_supervision. The
existing HERMES_S6_SUPERVISED_CHILD sentinel still prevents the
run->start->run redirect recursion. Each profile is scoped to its own
HERMES_HOME and s6 guarantees one supervised instance per slot, so there
is no legitimate supervised sibling for --replace to clobber.

Reported via beta (NS-505): gateway.log showed PID 17907 'running
(manual process)' with the guard error repeating every ~12s on
v2026.6.5.

Adds a regression test asserting every gateway-run exec line in the
rendered script (default + named profile, both privilege branches)
carries --replace, and updates the existing render-script assertion.

* fix(ci): remove stray .venv symlink committed into repo

The PR's commit accidentally tracked a .venv symlink pointing at the
developer's local venv (mode 120000 -> /home/ben/nous/hermes-agent/.venv).
The CI test/e2e/build jobs run `uv venv` to create .venv and failed with
`failed to create directory .venv: File exists (os error 17)` because the
checkout already contained the symlink. All test shards aborted in <15s
during setup, before any test ran.

Untrack the symlink and add a bare `.venv` entry to .gitignore (the
existing `.venv/` rule only matches a directory, so a symlink slipped
through).
2026-06-18 10:49:02 +10:00
brooklyn! d62979a6f3 feat(desktop): composer status stack, live subagent windows, editable prompts (#44630)
* feat(desktop): session-scoped status stack + kill new-window theme flash

Stack subagents, background tasks, and the queue into one collapsible
"sink" above the composer, reusing the queue's chrome so every status
reads as one piece. Extracts shared StatusSection / StatusRow /
TerminalOutput primitives and a unified $statusItemsBySession store
(subagents mirrored, background owned here, merged + grouped for render).
Renames BrailleSpinner → GlyphSpinner now that it drives more than braille.

Separately, fix the white flash on every new/cmd-clicked window: macOS
`vibrancy` paints an NSVisualEffectView that follows the OS appearance and
ignores `backgroundColor`, so a dark app on a light-mode Mac flashed white
until the renderer painted over it. Pin `nativeTheme.themeSource` to the
app theme (persisted to userData so cold launches paint right before the
renderer loads), hold windows with `show:false` until `ready-to-show`, and
pre-paint the themed background via an inline script before the bundle runs.

* feat(desktop): dock the slash popover to the composer via one shared fill var

The slash·@ popover (and ? help) now docks onto the composer's edge with the
same chrome as the queue/status stack — rounded outer corners, fused borderless
edge, no shadow — but keeps its own narrow width.

Surface + drawer paint a single --composer-fill var; the state ladder
(rest / scrolled / focused / drawer-open) lives once in styles.css on
[data-slot='composer-root']. The :has() drawer-open rule is last and forces an
opaque fill, since translucent glass sampling different backdrops (thread vs
fade gradient) can never match. This replaces the focus-within !important
override that repainted the surface behind every previous matching attempt.

Also drop the chevron column from the project file tree — the folder open/closed
icon already carries the expand state.

* feat(desktop): base inset for file tree rows (post-chevron alignment)

* feat(desktop): wire the status stack's background tasks to the real process registry

The background group was UI-only (dev-mock seeded). Now it's live e2e:

- tui_gateway: new session-scoped `process.list` (registry snapshot filtered
  by the session's session_key, plus a 4KB output tail for the inline
  terminal viewer) and `process.kill` (single process, ownership-checked —
  unlike process.stop's kill_all).
- Renderer: `reconcileBackgroundProcesses` syncs snapshots into the store
  layout-stably — rows keep their position when state flips (never re-sort),
  new processes append, unchanged rows keep object identity so memoised rows
  skip re-rendering, and a dismissed-set stops the registry's retained
  finished procs from resurrecting X-ed rows.
- Refresh triggers: session open, terminal/process tool.complete,
  status.update(kind=process) from the gateway's notification poller, and a
  5s poll armed only while a running row is visible (catches silent exits).
- Stop = real `process.kill` + optimistic dismiss; Dismiss = client-side
  with resurrection guard.
- Re-keyed the stack to the RUNTIME session id: it was keyed by the stored
  session id, where neither subagent events nor process.list would ever land.
- Deleted dev-status-mocks.ts (__hermesStatusMocks) — no more seed shit.

Reconcile invariants covered in store/composer-status.test.ts.

* feat(desktop): todos + openable subagents in the status stack, self-healing file tree

- todo lists move out of the inline chat panel into the composer status stack
  (checklist icon, dashed ring = pending, spinner = in progress, check = done),
  fed live from todo tool events and seeded from history on session open
- subagent rows carry the child's real session id end-to-end
  (delegate_tool → gateway → renderer) so clicking one opens ITS session window
- status stack publishes its measured height so the thread's bottom clearance
  grows with it; card paints the shared --composer-fill so focused/scrolled
  states match the composer exactly
- file tree self-heals: ENOENT roots retry on a 3s cadence + Try again button,
  and the main process expands ~ in IPC paths (gateway cwds arrive as ~/...)
- composer drag-drop of tree entries inserts inline refs instead of attachments

* fix(desktop): file tree falls back to the workspace dir when a session's cwd is gone

Sessions record their launch cwd; deleted worktrees leave that path dead,
so opening such a session swapped the tree from the default workspace to a
directory that ENOENTs forever — the 3s retry just spun on it. On a root
read error the tree now asks main to sanitize the cwd (prefers the
configured default project dir), displays that fallback, and quietly
re-probes the original path so it switches back if the dir reappears.

* feat(desktop): working restore-checkpoint button on past user prompts

The discard icon on hover of a past user bubble was decorative — clicking
did nothing. It's now a real control: a confirmation dialog explains that
everything after the prompt is removed, then the session rewinds to that
turn and reruns the same prompt (prompt.submit with
truncate_before_user_ordinal, the same mechanism the edit composer uses).
Failures rethrow into the dialog's inline error instead of toasting.

* fix(desktop): show the restore-checkpoint button on the latest user prompt too

Restoring the most recent prompt is just 'retry this turn' — no reason to
exclude it. Stop still takes the slot while the turn is running.

* fix(desktop): finished todo lists clear themselves out of the status stack

A list whose every item is completed/cancelled lingers ~4s so the final
checkmark is visible, then the todo group drops out of the stack. A fresh
active list arriving within the linger cancels the scheduled clear.

* chore(desktop): drop dead editableCheckpoint copy, terser restore confirm

* fix(desktop): rewind clears the abandoned timeline's todos + background

Restoring to (or editing) an earlier prompt rewinds the conversation, but
the todos and background processes spawned by the now-discarded turns kept
showing in the status stack — and the real background processes kept
running. Both rewind paths now clear the session's todo rows and kill +
drop its background processes before the fresh run repopulates them. Also
drops the click-to-edit clamp transition, which flashed a half-expanded
bubble on the way into the edit composer.

* feat(desktop): user messages are always editable; edit/restore revert mid-stream

The bubble is now always click-to-edit — even while a turn streams — instead
of going inert during a run. Sending an edit acts like restore: it rewinds to
that prompt and re-runs with the new text. Both edit and restore can fire
mid-stream now; the gateway refuses prompt.submit while a turn runs (4009
"session busy"), so they interrupt the live turn first and retry the submit
until the cooperative interrupt winds it down. Restore (re-run as-is) shows on
every prompt except the latest running one, which keeps the Stop button.

* fix(desktop): label preview-pane ⌘L selections with the filename, not "zsh"

The terminal owns a global ⌘/Ctrl+L "send selection to composer" shortcut, so
selecting text in the file preview pane and hitting it fell through to the
terminal handler — which imported the right text but labelled the composer ref
"zsh:N lines" off the shell name. When the selection isn't an xterm selection,
label it with the previewed file instead.

* fix(desktop): ⌘L on a preview line selection inserts the @line ref, like dragging

The source preview lets you select lines in the gutter and drag them into the
composer as an @line:path:start-end ref. ⌘/Ctrl+L now does the same when a line
selection is active — it drops the identical ref instead of falling through to
the terminal's global handler (which grabbed the native text selection and sent
a bogus terminal block). Capture-phase + stopPropagation so it wins; with a line
selection there's no native selection, so the terminal handler stays out of it.

* chore: gitignore apps/desktop/demo/ scratch output

The desktop demo prompt writes demo/*.txt during recorded walkthroughs; it's
throwaway, never part of the app. Ignore it so it stops cluttering git status.

* feat(desktop): subagent watch windows, hard stop, sidebar hygiene

Child-session mirror for live subagent windows, delegate sessions tagged
and excluded from the sidebar, composer focus/stop polish, and WS stall
resilience on the gateway transport.

* refactor: DRY delegate SQL + trim status-stack noise

Extract shared listable-child and delegate-delete helpers in hermes_state,
collapse cancelRun busy release, and cut comment bloat in resume/status paths.

* fix(desktop): hide orphaned subagent sessions in sidebar

Cascade-delete all ephemeral children on parent delete (not just tagged rows),
run v16 backfill to tag legacy orphans, and record new delegates as source=subagent.

* fix: restore orphan contract for untagged children + lazy session eviction

Cascade-delete only _delegate_from-tagged rows (v16 backfill covers legacy),
walk marker chains recursively with FK-safe orphaning, gate lazy watch
sessions out of the still-starting eviction exemption via an explicit flag,
pass session_id to _make_agent only when resuming, and hide source=subagent
from session search.

* fix(gateway): gate child mirror off upgraded sessions + age out stale run entries

Review findings: the mirror could interleave synthetic events with a real
native stream once a watch window upgrades (prompt.submit builds an agent),
and a lost subagent.complete left _active_child_runs pinning running=true
forever. Mirror now stops when the live session owns an agent; liveness
reads ignore entries older than an hour.

* fix(gateway): reject prompt.submit into a watch session while its child runs

A lazy watch session's running flag is False (the run lives in the parent
turn), so typing mid-run sailed past the busy guard and built a second agent
racing the in-flight child on the same stored session. Busy error until the
run completes; afterwards the submit upgrades into a normal conversation.

* refactor(gateway): DRY watch-resume payload + compose listable-child SQL

Fold the duplicated child-run busy overlay into one _reuse_live_payload
helper across both resume reuse paths, collapse the twin mirror early-returns,
and build _LISTABLE_CHILD_SQL from _BRANCH_CHILD_SQL instead of restating it.

* fix(desktop): clip horizontal overflow on sidebar scroll areas

Add overflow-x-hidden alongside overflow-y-auto on session list scrollers
and the shared SidebarContent primitive — vertical scroll unchanged.
2026-06-12 08:30:06 -05:00
Teknium cb29e8a82e refactor(cron): rebrand Cron Recipes -> Automation Blueprints
Product rename across every surface: module/file names (blueprint_catalog,
tools/blueprints, blueprint_cmd), slash command /cron-recipe -> /blueprint
(alias /bp), dashboard API /api/cron/blueprints, desktop deep-link
hermes://blueprint/<key>, docs catalog page + extract script, and the
skill frontmatter block metadata.hermes.blueprint. No behavior change.
2026-06-11 10:49:47 -07:00
teknium1 1593ca5406 feat(cron): Cron Recipes — parameterized automation templates across every surface
A 'recipe' is a one-place definition of an automation that every surface
renders natively. The slot schema (cron/recipe_catalog.py) is the single
source of truth; four renderers consume it, and all paths end at the same
cron.jobs.create_job — no second job engine.

Form where there's a screen, conversation where there's a chat line:
- Dashboard / GUI app: a Recipes sub-tab on the Cron page renders each
  recipe's typed slots as a form (time-picker, enum dropdown, free-text);
  submit POSTs /api/cron/recipes/instantiate which fills + creates the job.
- CLI / TUI / messengers: /cron-recipe lists the catalog, shows a recipe's
  fields, or fills + creates from a pasted 'key slot=val' command. The shared
  handler (hermes_cli/cron_recipe_cmd.py) names any missing/invalid slot so
  the agent can ask a targeted follow-up.
- Docs: a generated Cron Recipes catalog page (website, .mdx + React cards)
  shows each recipe with a copy-paste command and a 'Send to App' button.
- Desktop: a hermes:// URL scheme (Electron single-instance lock +
  setAsDefaultProtocolClient + open-url/second-instance) routes
  hermes://cron-recipe/<key>?slot=val into the chat composer pre-filled.

Typed slots (time/enum/text/weekdays) with defaults: users never type raw
cron — recipes parameterize time-of-day and weekday sets and translate to
cron expressions; a free-text 'schedule' slot is the full-flexibility escape
hatch. Consent-first throughout: nothing schedules without an explicit submit
or send.

Core:
- cron/recipe_catalog.py — CronRecipe + RecipeSlot, 5 curated recipes,
  recipe_form_schema / recipe_slash_command / recipe_deeplink /
  recipe_catalog_entry renderers, fill_recipe (validate + translate to
  create_job kwargs).
- hermes_cli/cron_recipe_cmd.py — shared /cron-recipe handler (CLI + TUI +
  gateway never drift). CommandDef + dispatch in commands.py / cli.py /
  gateway/run.py.

Dashboard: GET /api/cron/recipes + POST /api/cron/recipes/instantiate
(web_server.py), CronRecipes.tsx gallery+form, Segmented sub-tab on CronPage,
api.ts methods + types.

Desktop: hermes:// scheme end to end (main.cjs deep-link router + ready-queue,
preload onDeepLink/signalDeepLinkReady, global.d.ts types, desktop-controller
composer prefill, electron-builder protocols key).

Docs: extract-cron-recipes.py generator wired into prebuild.mjs,
cron-recipes-catalog.mdx + CronRecipesCatalog React component, sidebar entry.
Generated index json gitignored like skills.json.

Tests: 23 core (catalog/slots/schedule-resolution/validation/renderers/command
handler/generator) + 5 web_server endpoint tests. E2E verified end to end:
slot fill -> create_job -> persisted job with correct schedule/deliver/origin.
2026-06-11 10:49:47 -07:00
Teknium a5c32cdf30 fix(update): self-heal a venv left half-built by an interrupted install (#42172)
* fix(update): self-heal a venv left half-built by an interrupted install

An update killed mid dependency-install (Ctrl-C, terminal close, WSL OOM)
could leave the venv with pip wiped and core deps (e.g. Pillow) missing,
with no automatic recovery — the user had to manually run ensurepip +
reinstall.

Drop an install-scoped .update-incomplete breadcrumb right before the dep
install and clear it only after core-dependency verification passes. On the
next launch (any command except 'update' itself), if the marker is present,
unconditionally bootstrap pip via ensurepip then re-run the .[all] install +
verification, then clear the marker. Failure leaves the marker for retry and
prints the manual recovery command. Never raises — recovery cannot block
launch.

* fix(update): address review — stderr-only recovery output, single-flight lock, gitignore marker

- Route all recovery output (status lines + streamed pip/uv install via
  fd-level dup2) to stderr so protocol-on-stdout launches (hermes acp)
  never get install noise on the JSON-RPC stream.
- Single-flight O_EXCL lockfile (.update-incomplete.lock) so a gateway
  start + CLI launch (or two profiles) can't run concurrent installs
  into the shared venv; stale locks (>1h) are broken for the next launch.
- gitignore .update-incomplete + lock so source-tree installs keep a
  clean git status and update's autostash skips them.
- Document why the loose 'update' argv substring match is intentional
  (over-match defers one launch; under-match would race the real update).
- 4 new tests: lock held → skip, stale lock broken, lock released,
  output lands on stderr only.
2026-06-10 02:57:05 -07:00
harjoth b459bac02c fix(cli): gitignore Desktop bootstrap marker so hermes update stops autostashing it
The Desktop bootstrap installer writes `.hermes-bootstrap-complete` into the
managed git checkout root. Because it wasn't gitignored, `hermes update`'s
`git stash push --include-untracked` treated it as a local change and created an
autostash on every run — prompting the user to restore "local changes" that were
really Hermes-managed runtime state (and risking the marker getting stranded in a
stash, which re-triggers Desktop bootstrap).

Add the marker to .gitignore; `git stash -u` and `git status --porcelain` both
skip ignored files, so the updater now sees a clean tree.

Fixes #38529
2026-06-05 02:54:32 -07:00
Jeff 1f347ee543 fix(uv): move venv aside instead of gutting it in place on Windows rebuild
hermes update can brick a Windows install. When 'hermes update --force' runs
past the concurrent-process guard, rebuild_venv runs while the venv is still in
use: shutil.rmtree(ignore_errors=True) deletes site-packages + certifi's cert
bundle but can't remove the locked python.exe, leaving a half-gutted venv that
uv venv then refuses to overwrite. Every later HTTPS call dies with
FileNotFoundError for the missing cacert and there is no recovery.

--clear alone (the c136eb4de retry path) does not fix the real lock case: when
the locked interpreter is *inside* the venv being rebuilt, neither rmtree nor
uv venv --clear can delete it. os.replace of the parent directory *is* allowed
on Windows (a running .exe is tracked by handle, not path), so we move the old
venv aside atomically to <venv>.old, rebuild with --clear in its place, and the
still-running gateway/desktop keep using the moved-aside copy until they
restart. If the venv genuinely can't be moved, we abort cleanly and leave it
fully intact; if the rebuild fails, we restore the moved-aside copy.

Folds in the call-site guards from #38511 (@f3rs3n):
- rebuild_venv() returns False (and restores the backup) if uv exits 0 without
  producing an interpreter.
- both hermes update venv-rebuild call sites abort with RuntimeError instead of
  continuing into dependency install when rebuild_venv() returns False.

Also gitignore /venv.old/ so the update autostash (git stash --include-untracked)
doesn't sweep the moved-aside venv on every run.

Root-cause fix for #37881. Supersedes the --clear-only retry from c136eb4de.

Co-authored-by: f3rs3n <32328813+f3rs3n@users.noreply.github.com>
2026-06-04 12:18:38 -04:00