206 Commits
Author SHA1 Message Date
Maciej Lisiewski 7062229090 chore(js): switch the workspaces from npm to pnpm
pnpm 12.15.0 (native executable) installs every JS workspace from one
pnpm-lock.yaml, imported from package-lock.json with the same resolved
versions. website/, the WhatsApp bridge and the photon sidecar stay separate
npm projects.

Configuration moves from .npmrc and the root manifest into
pnpm-workspace.yaml: the 14-day minimumReleaseAge and its excludes,
overrides, allowBuilds (the old allowScripts set, with strictDepBuilds
failing any unlisted build script), engineStrict, sideEffectsCache: false
(in-place rebuilds must not write into the store) and
verifyDepsBeforeRun: false (a `pnpm run` never installs).

The pnpm version is pinned in pm/lock.json and in root devEngines.
pmOnFail: ignore keeps that pin out of pnpm-lock.yaml: a leading pin
document makes osv-scanner read zero packages. scripts/build/node-deps.mjs
enforces the exact version instead, and every lock reader refuses a
multi-document lock. A root preinstall refuses `npm install`; devEngines
onFail is warn so npx (MCP servers, the tsc linter) keeps working inside
the checkout.

User-machine installs run --frozen-lockfile --trust-lockfile (release age is
checked when locking, in CI, not per user). node-deps removes npm-era
node_modules trees before the first pnpm install. Nix builds the store with
fetchPnpmDeps and a hash file that CI checks.
2026-11-06 04:08:24 +01:00
Maciej Lisiewski 215aa7624b fix(ci): end the gate's timing at its verdict, not at the report's steps
close_running_job() counted every finished step of the report's own job. On a PR the PR-only baseline restore finishes before collection, on main it never runs, so a slow cache download inflated the PR's gate time against main's baseline. ci.yaml now passes --own-job-last-step "Evaluate job results" and the gate's time ends there; steps after it are report work.
2026-11-06 03:52:52 +01:00
Maciej LisiewskiandBearHuddleston 7979dc05af fix(ci): skip the running gate's log download; drop an unseeable test
The timing report's own job has no log until it finishes, so fetching it listed the gate under "Unavailable job logs" in every report. Its timing entry stays; only the log download skips it.

test_status_functions_are_wrapped_so_yaml_keeps_the_negation could not see the case it described: ruamel reads an unquoted `if: !cancelled()` as an empty tagged scalar, which test_every_check_runs_whatever_an_earlier_check_did already catches.

Co-authored-by: BearHuddleston <BearHuddleston@users.noreply.github.com>
2026-11-06 03:42:39 +01:00
Maciej LisiewskiandBearHuddleston d2c422f294 fix(ci): name each red step of a failed job in the CI review comment
repo-guards.yml and lint.yml run many checks as steps of one job, so the comment's "Job **X** failed" item no longer said which check went red. split_failed_steps() reports each failed step of a failed job as its own entry, for the comment only: the poller's job counts and transitions stay per job, and a failed job with no failed step (a timeout) keeps its entry. The dependency-bounds failure step carries "supply-chain" in its name so the supply-chain review status still covers it, as the old job name did.

Patch by @BearHuddleston (review of #149786).

Co-authored-by: BearHuddleston <BearHuddleston@users.noreply.github.com>
2026-11-06 03:42:39 +01:00
Maciej Lisiewski 603387951c fix(ci): count the gate job in the timing report it now writes
The timing report moved into the all-checks-pass job, which the jobs API
still lists as in_progress while the report collects. collect_timings
dropped every unfinished job, so the gate vanished from every report and
from main's baseline, and wall time stopped at the last worker.

The job the report runs in (matched by RUNNER_NAME) now counts up to its
last finished step: the verdict, without the report's own steps, as when
the report was a job of its own. Other unfinished jobs stay out.
2026-11-06 02:38:46 +01:00
Maciej Lisiewski d7244125e9 ci: run every cheap repository guard as steps of one job
Thirteen guard workflows (fifteen jobs) ran on the same 4-vCPU Ubuntu
runner, all started right after detect, each paying a checkout and a full
billed minute for 0-13s of work: infographics, profile archives, case
collisions, blob size, lazy_deps imports, stale icons, unrelated
histories, contributor attribution, uv.lock, prompt-surface and
package-lock diffs, the supply-chain scan and dependency bounds, and the
Docker lint.

repo-guards.yml runs them as steps of one job. Each step keeps its old
lane gate in its `if:`, and runs whatever an earlier step did
(`!cancelled()`), so one red guard never hides another; any failed step
fails the job, which all-checks-pass requires. The review-status-*
artifacts the live PR comment reads keep their names. Files the steps
write go under $RUNNER_TEMP, and the guards that need no toolchain run
before setup-pm, whose dependency phase also checks the lock.

The four PR-only jobs the strict release gate tolerated as skips are now
steps of a job that runs on every event, so the only tolerated release
skip left is the deferred Desktop E2E.
2026-11-06 02:15:36 +01:00
Teknium 5819226468 feat(plugins): one switch and one action for unattended plugin updates
main already ships `hermes plugins update` (no name = update all) and the
opt-in `plugins.auto_apply` cadence. This makes both reachable from every
surface and closes the gaps reported on the original PR:

- Catalog installs now ride `plugins.auto_apply` too (it moved only git
  rows, so catalog plugins stayed on their install-day pin). The update
  path still refuses a widened pin or a too-new requires_hermes unattended.
- One updater per plugin: a cross-process lock around update_one and the
  dashboard/Desktop update, so the gateway's auto-apply tick and a manual
  update never run on the same plugin at once; the loser changes nothing.
- `hermes update` names, per profile, the catalog installs its new catalog
  pins left behind and re-arms that profile's plugin check, so an opted-in
  gateway applies them on the next tick.
- New CLI verb `hermes plugins auto-update [on|off]`; `/plugins update` and
  `/plugins auto-update [on|off]` in the classic CLI and the TUI (slash
  worker).
- RPC `plugins.manage` actions `update_all` and `auto_update` (contract +
  regenerated apps/shared outputs) for Desktop.

Off by default (legacy behaviour); all writes are profile-scoped through
get_hermes_home()/the scoped RPC.
2026-11-04 21:58:12 -08:00
kshitijk4poor 23eaee2f17 ci(tests-os): say so when the PowerShell warm-up could not save its cache
Review: if the cache file was never rewritten within the 20s wait, the step
still printed 'module analysis cache ready'. It now prints a warning with the
cache path instead; still non-fatal.
2026-11-04 02:01:54 +05:30
kshitijk4poor ddd63dde0d refactor(runner): both ways to start a file live in run_tests_launch.py
Review: the fork mode spread its server state, lock, process creation and
process-group rules through the runner, so _run_one_file_once had to know that
a forked child calls setsid() while a subprocess needs getpgid(). The runner
now calls start_pytest() and gets the process and its group back; the fork
server, its lock and the Popen shim live with it. Behaviour is unchanged.

classify_changes lists the launch module and the fork child as test harness
next to run_tests_parallel.py, so editing either starts the lanes a runner
edit starts (the fork child was missing since it was added).
2026-11-04 02:01:54 +05:30
kshitijk4poor bf1de8beff ci(tests-os): warm PowerShell's module analysis cache before the Windows tests
Forwarding PSModuleAnalysisCachePath only helps when something already warmed
the cache. Whether the ARM64 build-deps step did depended on how long its
PowerShell stayed up: PowerShell writes the analysis from a timer ~10s after it
runs (AnalysisCache.QueueSerialization), so a process that exits right after
its first cmdlet never saves it. On runs where build-deps finished fast the
cache stayed cold and every PowerShell child a test spawned paid ~22s again
(arm64 test step 206-259s instead of ~130-150s).

Warm it explicitly from a cmd step and stay up until the cache file is
rewritten: ~39s when cold, ~1s when warm. Forced-cold check: a child launched
afterwards started in 0.4s and the test steps ran in 137/146s.
2026-11-04 02:01:54 +05:30
kshitijk4poor 17db2a8e33 ci(setup-pm): keep the cached Git entry valid on Windows
Git for Windows ships etc/mtab as a Cygwin system-file symlink, and PM's digest
covers those 38 bytes. @actions/cache restores the PM store with Git's MSYS tar
under MSYS=winsymlinks:nativestrict, which writes a symlink to a non-Windows
target (/proc/mounts) as a WSL reparse point (tag 0xa000001d). Python cannot
open that, tree_digest raised, PM's verification of the cached Git entry
failed, and every Windows job re-extracted the 56 MiB PortableGit archive.

Before PM verifies the store, recreate each WSL symlink with Git's own ln under
winsymlinks:sys. PM's check still decides whether the entry is intact.
2026-11-04 02:01:54 +05:30
Teknium df128c13bb ci: run the WhatsApp bridge node tests in the JS & TS job
scripts/whatsapp-bridge/*.test.mjs (allowlist, delivery_status,
bridge.native, ...) ran in no workflow: the bridge is a standalone npm
package outside the root workspaces, so run-workspace-checks never sees it.

js-tests.yml now installs the bridge's own lockfile (npm ci
--ignore-scripts; two suites import baileys) and runs
`node --test scripts/whatsapp-bridge/*.test.mjs`. The classifier selects
the frontend lane for scripts/whatsapp-bridge/ and
plugins/platforms/whatsapp/, so a bridge-only or adapter-only PR runs it.
No E2E trigger.
2026-10-28 21:06:25 -07:00
unsupportedpastels 26ba38a098 fix(webapp): report the commit an inspection read, and share the detection cases
The gateway inspection reported which halves a source ships but not the
commit it read, so for an unpinned Install from Git the reviewed tree and
the installed tree could differ if the source moved in between. Return
the inspected HEAD as `sha` (a catalog entry's is its reviewed pin) and
carry it through inspectAgentPlugin, so the install can be held to it the
way the native Desktop probe's will be. A cached answer keeps the sha it
read.

The component-detection rules now live in two inspectors (Python and
Electron). tests/fixtures/plugin_component_detection.json holds one set
of cases both run, registered as a shared fixture so editing it runs the
pytest and vitest consumers.
2026-10-27 21:32:38 -04:00
Teknium ca69d857c3 fix(update): a proxy the user set in .env beats the injected system proxy
The Desktop hands `hermes update` the system proxy in both spellings
(HTTPS_PROXY and https_proxy). The .env load replaces only the spelling
the file uses, and urllib and curl prefer the lower-case one, so the
update still went to the system proxy and the .env NO_PROXY stopped
applying. After the .env load, `hermes update` now undoes everything the
Desktop added when .env sets a proxy (the Desktop injects nothing when it
sees one). A .env NO_PROXY alone replaces the user own entries in every
injected NO_PROXY variant and keeps the bypass the Desktop added; a `*`
there drops the injection.

Review S1 (kshitijk4poor) on #144894.
2026-10-27 13:23:31 -07:00
Teknium df8a320f2a ci: route hermes_cli/env_loader.py to the update suites
update_proxy_env now reads which proxy values a .env load wrote, so the
update path imports env_loader and its changes must start the upgrade lane.
2026-10-27 13:23:31 -07:00
Teknium dc8eccf62d ci: route desktop_release_holders.py to the update suites
main_desktop.py (an update entry point) now imports it to name and close a
process holding release/ on a failed swap, so tests/ci/test_update_ci_routing.py
requires it in _UPDATE_DEPENDENCIES.
2026-10-27 11:51:03 -07:00
Teknium 2ff5f4aa2b fix(ci): route desktop_build_reuse to the update and desktop-update suites
source_build imports hermes_cli/desktop_build_reuse.py on the update path, so
tests/ci/test_update_ci_routing.py requires it routed beside desktop_build_lock.py
(its desktop-reuse.mjs script is already covered by the scripts/build/ prefix).
2026-10-27 07:53:07 -07:00
IAvecilla 11a75355a4 fix(docker): hermes webapp reuses the baked bundle, and local Desktop files stay out of the image 2026-10-27 08:41:33 -04:00
Ben Barclay d96591fbc8 fix(config): route config_backend to the upgrade lane and fault-inject the seam's parser in the MoA read test
update_channel now writes through hermes_cli.config_backend, so the CI
classifier lists it as an update dependency. The MoA transient-read test
patched hermes_cli.config.fast_safe_load, which the seam no longer calls;
it now patches utils.fast_safe_load, the parser the FileBackend uses.
2026-10-25 18:54:09 -07:00
Teknium a80b413cd6 fix(git): graph refresh sweeps only object-store debris and lets git clean up
Review round on #144647 (head d016f04694d):

- CI routing: the refresh (slot claim, bounded runner, stale check, detached spawn) moves to
  hermes_cli/git_commit_graph.py, routed to e2e_upgrade with the update pipeline. The update path
  imports it instead of all of worktree_ops; worktree_ops keeps its repack seams and imports the
  shared slot claim and runner from it (no cycle).
- W1: the -w pass runs in whatever repo it was launched from, so its refresh sweeps only the
  object-store locks (clear_stale_git_locks(names=OBJECT_STORE_LOCK_NAMES)); index.lock / HEAD.lock
  there belong to the user's git. The install checkout's update sweep is unchanged.
- W2: timeout/exit reaping SIGTERMs the git tree first and SIGKILLs only after a 5 s grace
  (kill_process_tree(grace=), default unchanged for every other caller), so git deletes
  commit-graph-chain.lock and tmp_graph_*. The age-gated tmp sweep now also takes
  objects/info/commit-graphs/tmp_graph_*.
- N1: a breakaway-forbidding Windows job gets the no-breakaway retry, and a spawn that fails gives
  the 6 h slot back.
- N2: the detached child logs a failed write (git's fatal line) to the home's agent.log/errors.log.
- core.commitGraph=false in the target repo skips the write.
- N3: a successful no-op write stamps hermes-commit-graph.fresh, so an idle repo is not rewritten
  every slot.
- N4: test_update_finish's completion child and every in-process hermes_cli test stub the
  detached spawn; no writer outlives a test run.
- Greptile: the refresh test now runs on a treeless clone over a Bloom graph and asserts the
  write adds no pack (red when --no-changed-paths is dropped).
2026-10-25 17:51:21 -07:00
emozilla 443732a58b fix(update): hermes update rebuilds the Webapp bundle a home serves
hermes update rebuilt the TUI, the web UI and the desktop app but never apps/desktop/dist-webapp.
A gateway with webapp.enabled kept serving the bundle built before the update, which then talked
to the updated backend with an outdated contract.

The update now rebuilds that bundle when any home sharing this install has webapp.enabled, with
prepare_webapp_renderer, the function hermes webapp uses (a current bundle is reused). Like the
other products it is attempted on its own: a failure is reported with the rest at the end and
never skips another product.
2026-10-25 03:22:27 -07:00
Bear Huddlestonandemozilla b5eb5ee7a8 feat(webapp): carry the browser-hosted Desktop's server pieces from #93508
Carried from BearHuddleston's #93508 onto main. The Webapp relay and the
profile-incarnation fence stay out: carried code follows main's
named-profile rule and refuses a missing or deleted profile home.

- Webapp routes: one-use launch tickets, window handoff, and gateway
  WebSocket tickets minted for the signed-in browser
- Browser surface policy and the SPA document policy
- A host terminal for the browser that survives reconnects
- Staged browser uploads and file tickets; file.attach takes a staged upload
- hermes sessions adopt
- Webapp bundle build helpers and the renderer freshness check
- Regenerated gateway contracts

Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-10-25 03:22:14 -07:00
Teknium f05df166b3 ci: route config_version_stamp to the upgrade suite
post_update now imports hermes_cli/config_version_stamp.py (it converts legacy
AGENT_BROWSER_* settings before stamping), so a change to it must start the
e2e_upgrade lane like every other module the update path imports.
2026-10-23 01:43:17 -07:00
Teknium 0f8bbed1ed fix(config): drop browser.extension_control (default and v52 migration)
Nothing reads the key since browser-extension control was removed; the existing v52 step (a
legacy-key step, so unstamped configs get it too) now strips it from user config.
2026-10-23 01:42:29 -07:00
ahrazzle a9376424e5 fix(desktop): prepare the locked Electron runtime explicitly before use
Electron 42 dropped the postinstall that unpacked its binary into
node_modules/electron/dist. The binary now appears on the first
require('electron'), so parallel vitest workers race to unpack it
("Text file busy" in #128267's CI), and `hermes desktop --source` finds
no path.txt to launch.

electron-runtime.mjs runs Electron's own install.js once: after npm ci
for the desktop workspace, and in CI before the parallel checks. It first
checks the installed package against the desktop pin and the lockfile,
and does nothing when the runtime is already in place. On the current
40.10.2 pin, whose postinstall still unpacks the binary, it is a no-op.

Salvaged from #137648 (the runtime-preparation subset only).
2026-10-22 16:17:57 -04:00
ethernet 0153a9e460 refactor(ci): simplify cache report state and rendering
Use one escaped HTML fragment for the report and review comment instead
of maintaining separate Markdown and HTML table builders. GitHub's
renderer preserves the same table text and all 39 real job/step links.

Remove the obsolete phase-generic accounting helper, redundant attempt
list and save-evidence flag, and unused collected-job counter. Append
main's saved column in one place.

Use urllib's unredirected Authorization header instead of a custom
redirect handler. The real HTTP redirect test still proves that only
the API request receives the token. Exercise PM formatting directly,
while retaining the actual stdlib-only CLI invocation test.

Verification: 696 CI tests pass, one native skip, and all 12 checks pass.
Saved real-log parser records and rendered values remain unchanged.
2026-10-22 11:55:58 -04:00
ethernet 4f53b89a47 ci: summarize cache hits with main-aware job details
put cache hits/attempts and restored/missed archive bytes above an expander.
show hit and miss job/step links in both Markdown and HTML. off main,
compare hits with main's latest measured archive and hide write figures;
on main, show saved bytes without self-comparison.

retain unknown outcomes and sizes instead of treating them as misses or
zero. miss-byte totals use main archive references off main, and newly
saved archive sizes on main. preserve the real run mode and step numbers
in the snapshot so artifact-only rendering keeps the same behavior.
2026-10-22 11:55:58 -04:00
ethernet 19ed6e147c ci: compare cache archive transfers in the review comment
Collect ordinary actions/cache job logs alongside the existing timing
snapshot and retain per-invocation restore/write receipts and archive bytes.
Compare each cache with main and show saved-minus-restored archive changes
in the review comment, Actions summary and complete HTML report.

Count successful writes only after their save receipt; transfer progress
supplies a size once, never a sum. Preserve unknown sizes, failed or missing
logs, lookup-only results and exact-hit save skips. Match content-keyed
writes to their restored paths, without inspecting cache contents or using
the GitHub inventory as a proxy for a runner provider's cache store.
2026-10-22 11:55:58 -04:00
ethernet f3ea0290e2 ci: show PM cache restore results and write policy
Print a compact tools/uv/npm summary from setup-pm, including the selected
restore outcome, exact-hit output, cache path, ref scope and save policy.
Keep actual archive sizes and upload results in the native cache logs.
Do not scan cache trees or add logging wrappers to non-PM cache actions.
2026-10-22 11:55:58 -04:00
yoniebans 3e6589fc34 ci: run the upgrade suite when hermes_state_fts.py changes
The hermes update search-index notice now reads the stored FTS layout through hermes_state_fts.fts_layout_drift, so the module is on the update path.
2026-10-21 04:14:08 -07:00
ethernet 58801c565b perf(ci): stop transporting node_modules in the desktop dependency snapshot
node-deps runs npm ci on every prepare (the receipt never matched across
runs in any sampled job), and npm ci deletes node_modules first. The
restored trees were GBs of tar/untar for nothing; npm's content cache
(npm/_cacache) is what lets npm ci skip downloads. The workspace path
validation only existed to guard those paths, so it goes too.
2026-10-19 12:30:42 -04:00
Teknium d2c6ca2300 test(desktop): E2E guard for turn state after a lost connection
apps/desktop/e2e/core/reconnect-turn-state.spec.ts, real Electron + real
gateway, only the LLM scripted:

1. primary socket blocked mid-turn (loopback proxy that resets and refuses);
   the turn finishes on the gateway while disconnected; after the redial the
   reply renders exactly once (transcript oracle) and no Stop/spinner stays.
2. socket drop while a held turn runs, then a Control+Enter follow-up right
   after the redial: both turns run in order, oracle holds, nothing spins.
3. gateway SIGKILL mid-turn: supervisor brings a new owner up, recovery
   fences the turn `unknown`; the lost-turn card + Discard are on screen
   with "1 Queued — paused"; a follow-up is queued ("2 Queued — paused")
   and does not run ahead; Discard lets it run; oracle holds.

The classifier now routes the queue panel to the core lane, since the
lost-turn card lives there.

Refs #134889 (NF2)
Refs #140337
2026-10-17 11:33:13 -07:00
Teknium 861a0299ed refactor(gateway): no re-export shims for the split status/base helpers; the lock split is one-way
The round-1 health offset moved safe_url_for_log and the scoped-lock
release pair into siblings but re-exported them from gateway.status /
gateway.platforms.base, and status_scoped_locks read gateway.status
privates at call time while gateway.status re-exported the sibling's
privates (_unusable_lock_dir, _warned_unusable_lock_dirs). AGENTS.md:
no re-export shims for internal moves.

- gateway/status_lock_dir.py: the unusable-lock-dir report
  (ScopedLockDirUnusable + public unusable_lock_dir / _recovered /
  reset / reported). gateway.status imports it; it imports nothing back.
- gateway/status_scoped_locks.py: only the release pair, reading
  gateway.status at call time; gateway.status no longer imports it.
- Every importer points at the real home: base.py, run_bootstrap.py,
  the Feishu and Slack adapters, and the tests that patch them
  (gateway.status_scoped_locks.release_*). safe_url_for_log comes from
  gateway.platforms.base_log_safety; base.py imports it only for its
  own use.
- classify_changes: status_lock_dir.py rides with gateway/status.py on
  the e2e_upgrade lane.

code_health: 0 blocking (status.py stays under its main value).

Review finding by @unsupportedpastels on #140490 (minor 4) and by
@alt-glitch on #140490 (suggestion 4)
Refs #134889 (W2)
Refs #140337
2026-10-17 08:03:53 -07:00
Teknium 11380ac90a ci(routing): route hermes_dir_acl.py to the e2e_upgrade suite
hermes_cli/update_cmd_maint.py and update_host_obligation.py import the new
root module, so the update path runs it; list it beside its root siblings
(hermes_bootstrap.py, hermes_constants.py) in the update owners.

Refs #134889 (W8)
Refs #140337
2026-10-17 08:03:52 -07:00
Teknium 275b15fc35 ci: catalog-only PRs run the catalog tests instead of the full suite
A PR that changes only plugin-catalog entry YAML (plus contributor email
maps) ran all 23k Python tests on Linux, macOS and Windows, ~25 min, and
unrelated core flakes kept blocking one-file catalog additions.

The classifier now has a `catalog` lane. A diff made only of catalog entry
data turns `python` off and `catalog` on; ci.yaml then runs
catalog-tests.yml (every test file that reads the catalog or the email
maps, discovered by scripts/ci/catalog_test_files.py), plus the contributor
check. The lane is required by all-checks-pass whenever detect selects it.
plugin-catalog/README.md keeps the full suite (a docs-page test mirrors
it), and any non-catalog file in the diff brings the suite back. The
structural and pinned-source admission gates in plugin-catalog-ci.yml are
unchanged.
2026-10-16 04:37:07 -07:00
Teknium 2cd0ec8686 Merge origin/main into feat/unified-gateway-runtime
215 commits from main (c70332f888). One textual conflict: the generated
apps/shared/src/gateway-contract.generated.ts, regenerated with scripts/gen_gateway_contracts.py
rather than hand-merged. Brings main's fix for the dated shared-metrics install-receipt test
(783637e370).
2026-10-13 05:15:48 -07:00
calvinnwq ec82a5643b ci: open an issue when Hermes Desktop cannot read the plugin catalog
Desktop's hermes://plugin/install?catalog= links fetch /docs/api/plugins.json cross-origin. The docs host answers with a 301 to GitHub Pages, and a browser fails the fetch at that hop unless it carries Access-Control-Allow-Origin. The skills-index watchdog probes with curl -L, which never checks CORS, so it stayed green while every catalog link failed.

scripts/ci/check_catalog_cors.py fetches the catalog hop by hop with Origin: null (what a packaged Desktop renderer sends) and fails when any response lacks the header. A new check-catalog-cors job in skills-index-freshness.yml runs it every 4 hours and files its own [plugin-catalog-cors-watchdog] issue, so its reports never land on a skills-index issue.

Refs #136483
2026-10-13 03:49:29 -07:00
Teknium a89f374d7c Merge origin/main (#45: 255 commits to 3b63f7b0a4) into feat/unified-gateway-runtime
Conflicts (2 files, 1 hunk each); every main-side change is kept, ported to
where the branch moved the code:

hermes_cli/kanban_db.py (build_worker_context helpers _ctx_cap .. _ctx_comments)
  main:   f5a6651231c/d56e357556f coerce foreign/text timestamps on read; in
          _ctx_role_history the "Recent work" stamp became
          _ctx_stamp(_to_epoch(row['ended_at']) or 0, now) instead of int().
  branch: moved the whole _ctx_* helper block into hermes_cli/kanban_db_context.py
          (kanban_db.py imports it back), deleting it from kanban_db.py.
  merged: kept the branch's deletion in kanban_db.py and applied main's change in
          kanban_db_context.py::_ctx_role_history as
          _ctx_stamp(_kb._to_epoch(row['ended_at']) or 0, now). Main's other
          timestamp coercions in kanban_db.py (Task/Run/Comment/Attachment/Event
          from_row, _TASK_EPOCH_COLUMNS, release_stale_claims,
          _extend_live_stale_claim, task_age) auto-merged unchanged.

hermes_cli/web_server.py (_lifespan)
  main:   d7830c6f42 reworded one comment in the free-tier bootstrap block
          ("the ONE place its identity is created" -> "the one boot-time creator
          of its identity"; guests are now also created on first connector use).
  branch: moved _lifespan's body into hermes_cli/web_server_app.py::app_lifespan.
  merged: kept the branch's relocation and applied main's comment rewording in
          web_server_app.py.

Merge-induced health fix (no conflict marker; the combination tripped the ratchet):
apps/desktop/src/app/session/hooks/use-cwd-actions.ts::changeSessionCwd
  main:   63ed3caa4ba/dbc538547fc grew the function (statusbar cwd change for the
          focused chat / tile runtime ids) to CC 20.
  branch: added the session_busy refusal mapping (sidebar.projects.moveBusy) in
          its catch, +1 branch -> CC 21 > target 20 (scripts/check health blocking).
  merged: both behaviours kept; the busy-error mapping moved verbatim into a
          module-level cwdChangeError(err) helper. Health back to 0 blocking.

Check: every line main added between 1fd1827726 and 3b63f7b0a4 (317 files) is
present in the merged tree.
2026-10-12 16:55:20 -07:00
Teknium a0a49926f4 fix(check): blob-size exempts untracked copies and reads --head's allowlist (review)
- An untracked file whose bytes the base tree already holds (git hash-object
  matches a base blob) is no longer an offender, matching what staging the
  same copy already did.
- With an explicit --head, the allowlist comes from that revision
  (git cat-file <head>:.github/blob-size-allowlist.txt; none = no
  exemptions), so `--base A --head B` judges B by B's own exceptions.
  Working-tree mode still reads the file from disk.
- Tests for both: untracked copies of a base binary and base text file pass
  while an edited one is still caught; --head honours head's list over a
  conflicting checkout, a head without the list exempts nothing, and
  working-tree mode reads the disk list.
2026-10-11 17:16:57 -07:00
Teknium db5e618205 feat(ci): reject PRs that add binaries or new files over 500 KiB
Every blob a merge adds stays in every clone forever. In the last 90
days main received a 2 MB PR screenshot, a 2 MB default.tar.gz, three
PR infographics, a Windows cache database written by a test and a 1 MB
fuzz corpus at the repo root; nothing looked at file size.

scripts/ci/check_blob_size.py judges each path changed base -> head:
a binary (git's own --numstat detection) whose new content is over
500 KiB, added or modified, and any newly added file over 500 KiB.
Edits to existing text files stay with the code-health FILE_LINES
ratchet. Content the base tree already holds (a pure move, a copy)
adds no bytes and is not judged. Exact-path exceptions live in
.github/blob-size-allowlist.txt (CODEOWNERS-reviewed), seeded with the
one generated asset over the limit that legitimately changes.

It runs as its own unconditional workflow, like the case-collision
check, because asset-only PRs skip the Python and frontend lanes.
2026-10-11 17:16:57 -07:00
Teknium cc5928bd73 Merge origin/main (#44: 15 commits to 1fd1827726) into feat/unified-gateway-runtime 2026-10-11 14:02:25 -07:00
Teknium 8f05f890c5 ci(remote-backends): debian 13 for the test sshd, container setup as a mounted script
Review nits from #136602: the sshd and docker-backend image moves to the
current stable debian:13-slim (digest-pinned index), and the inline bash -c
setup becomes scripts/ci/test_sshd_container_setup.sh, mounted read-only and
run in the container. The new script joins the remote_backends path gate.
2026-10-11 11:55:56 -07:00
Teknium 438d10200c ci: run the SSH and Docker terminal backends against a real sshd and daemon
The SSH backend's real-host tests skipped on every CI run (nothing set
TERMINAL_SSH_HOST), and every Docker backend test mocked subprocess or the
docker CLI, so no CI run had executed either backend end to end.

- remote-backends.yml: starts a throwaway sshd container on 127.0.0.1 with a
  per-run keypair (scripts/ci/start_test_sshd.sh) and runs
  test_ssh_environment.py plus the new test_docker_backend_real.py against it
  and the runner's Docker daemon with a digest-pinned debian:12-slim. A file
  that skips fails the job, so the lane cannot go green over zero coverage.
- New classifier lane `remote_backends` (tools/environments/, tools/terminal_tool*,
  the real tests, the sshd harness, run_tests.sh). ci.yaml calls the workflow
  only when it fires, plus every push to main; required_results makes a selected
  lane mandatory, like the update consumers.
- The real tests read HERMES_TEST_SSH_* / HERMES_TEST_DOCKER_IMAGE (forwarded
  by run_tests.sh), because the hermetic conftest blanks TERMINAL_SSH_KEY as a
  credential before any test can read it.
- New real SSH cases: exit code/stderr, cwd persistence with spaces vs per-call
  workdir, quoting across the ssh hop, SendEnv passthrough through a real
  AcceptEnv, file tools write/read on the remote.
- New real Docker cases: exec + exit codes, cwd vs workdir, exported env
  persistence, docker_env/docker_forward_env reach the container and
  nothing else does, quoting, file tools write/read, /root surviving a
  container replacement.
- The existing one-shot test is xfail(strict): TERMINAL_SSH_PERSISTENT=false
  is read into ssh_config["persistent"] but SSHEnvironment never consumes it,
  so every SSH session persists exported env. Its _cleanup also cleaned the
  raw task id, not the resolved env key, so one test's env leaked into the next.
2026-10-11 11:55:56 -07:00
Teknium 549e53fd72 Merge origin/main (#43: 73 commits to c0ef5af520) into feat/unified-gateway-runtime
Conflicts (5 files):
- gateway/run_notifications.py: main lifted completion ownership into a new
  GatewayNotificationOwnershipMixin (run_notifications_ownership.py); the branch had lifted the
  update watcher into GatewayUpdateNotificationsMixin. The mixin inherits both. Kept both sides
  of the completion-event metadata (the branch's automation identities + local display card,
  main's proven_session_id / completion_ids), and in the async-delegation group the branch's
  authority admission runs first, then main's terminal-parent drop. Neither function main moved
  was changed on the branch.
- gateway/run_turn.py: the branch moved _hmwa_resolve_session into run_turn_prepare.py; main's
  one changed line there (the resolver receives the proven session id and completion ids) is
  ported into run_turn_prepare.py.
- hermes_cli/subcommands/sessions.py: keep both main's lazy cmd_sessions and the branch's
  rename/discard parser helper.
- ui-tui: keep both canonicalEn and connectorsEn catalogs; keep the branch's interfaces import
  plus main's findSlashCommand import.
2026-10-11 11:24:19 -07:00
Teknium 7bc05085f0 fix(ci): docs drift handles repeated placeholders, scopes its warning filter
Review follow-ups: `providers.<id>.models.<model>.timeout_seconds` now
matches (a second <...> segment no longer ends the pattern), the YAML
warning filter is scoped with catch_warnings, and the slash-command list
drops a `hidden` filter CommandDef never had.
2026-10-11 05:09:46 -07:00
Teknium 4e8fe569e4 feat(ci): new config keys and slash commands must ship with docs
DEFAULT_CONFIG changed 183 times in 30 days and nothing checked that a new
key reached the docs; /pause (the global emergency stop) went undocumented
for two months.

scripts/ci/check_docs_drift.py lists every DEFAULT_CONFIG leaf and every
registered slash command and finds the ones the docs never mention. A key is
documented by its dotted path, a placeholder path in inline code
(`auxiliary.<task>.timeout`, `display.pet.*`), or a YAML example nesting it
under its parents; a slash command by a `/name` row in
reference/slash-commands.md.

The 132 keys undocumented today are baselined in docs_drift_baseline.json;
tests/ci/test_docs_drift.py fails only on NEW gaps, so it never blocks an
unrelated PR, and documenting a baselined key never fails. Deterministic,
stdlib + repo imports, ~1s.

/pause is documented here, so the slash-command baseline starts empty.
contributing.md says what to do when the test fails.
2026-10-11 05:09:46 -07:00
Teknium 82a0d2dc88 Merge origin/main (#42: 9 commits to 79b45d1647) into feat/unified-gateway-runtime
No conflicts. Brings in main's fixes for the reds release run 38120936161 inherited:
79b45d1647 (signed reasoning replays after resume on Anthropic/Bedrock Converse),
7831de8999 + d3bdf761b0 (network upgrade E2E follow the new main-channel default),
0aa764fedd (Desktop hand-off test expects no channel-record request on main).
2026-10-11 03:09:45 -07:00
Teknium c152ba5a82 docs(ci): prompt-surface changes need one core approval, not two
The prompt-surface-review ruleset was meant to require one other reviewer;
it is now set to one hermes-agent-core approval. CODEOWNERS, the CI comment,
the staleness-test message and the developer docs said two.
2026-10-11 02:26:46 -07:00
Teknium ae3598c7cd feat(ci): reviewers see every change to the system prompt and tool schemas
A one-line code change can rewrite what every session sends to the model -- a
guidance constant moving tiers, a toolset gaining a tool, a dynamic rewriter
editing a description -- without the diff showing it. The rendered prompt
surface is now committed and reviewed like code.

- scripts/ci/prompt_surface.py renders the system prompt
  (build_system_prompt_parts) and the agent's tools[] through the production
  code for nine surfaces (cli x claude/gpt/gemini/hermes, cli in a code
  workspace, desktop, telegram, discord, cron) into
  tests/fixtures/prompt_surface/. A fresh home in a child process; clock,
  host OS, network, tool availability, env probe and machine paths pinned,
  so a laptop and CI produce the same bytes. Each tool schema is stored once,
  with a per-surface copy only where a surface serves a different schema.
- tests/ci/test_prompt_surface.py fails when the committed snapshot is stale,
  so a surface change must carry the regenerated snapshot in the same PR.
- prompt-surface-diff.yml (advisory, PR-only, stdlib-only, no PR code runs)
  posts per-surface token deltas, prompt hunks and per-tool
  description/param changes into the unified review comment.
- CODEOWNERS requests hermes-agent-core on the snapshot; a new
  prompt-surface-review ruleset requires two of their approvals.
- Docs: prompt-assembly.md (how it works), contributing.md (what to do when
  the test fails).
2026-10-11 01:21:18 -07:00
Teknium b1ccce898b Merge origin/main (#38: 70 commits to c57331677d) into feat/unified-gateway-runtime
Conflicts: tests/hermes_cli/test_finite_chat_delegation.py and
tests/tools/test_completed_process_results.py, where main's lint sweep (0006a5e229) added
check=False to subprocess.run calls this branch had rewritten. Kept the branch's test bodies
and carried check=False onto their subprocess.run calls.
2026-10-10 17:47:35 -07:00