Commit graph

835 commits

Author SHA1 Message Date
Teknium
032d4cafc4 chore(release): map @booker1207 for PR #25132 salvage 2026-05-18 22:35:28 -07:00
Teknium
efc37409aa test+release: fix test fixture for forum_commands; map @chromalinx 2026-05-18 22:34:48 -07:00
Teknium
38356cc98b chore(release): map @kunci115 for PR #27098 salvage 2026-05-18 22:32:00 -07:00
Teknium
fc42bb918b chore(release): map @karthikeyann for PR #26609 salvage 2026-05-18 22:30:28 -07:00
Teknium
470edfa901 chore(release): map @aqilaziz for PR #26406 salvage 2026-05-18 22:29:45 -07:00
Teknium
e19f4c1730 chore(release): map @samahn0601 for PR #27887 salvage 2026-05-18 22:29:03 -07:00
Teknium
bf6a2870a7 chore(release): map @nftpoetrist for PR #25856 salvage 2026-05-18 22:28:21 -07:00
Teknium
35781bab90 chore(release): map @Zyrixtrex for PR #26754 salvage 2026-05-18 22:27:40 -07:00
Teknium
b46ef2ef7a chore(release): map @eliteworkstation94-ai for PR #28157 salvage 2026-05-18 22:25:53 -07:00
Teknium
9a444a9355 test+release: align send_message mocks for MessageEntity import; map @fonhal 2026-05-18 22:19:50 -07:00
Teknium
e7a3e9934f test+release: align stale sticky-IP test for #24511; map @falconexe 2026-05-18 22:14:45 -07:00
Teknium
2994bf494d chore(release): map @fabiosiqueira for PR #27212 salvage 2026-05-18 22:03:12 -07:00
Teknium
17f3254ede fix(test+release): update conflict retry count for MAX=5; map @CryptoByz 2026-05-18 22:01:31 -07:00
Teknium
32435dfad8 chore(release): map @erhnysr for PR #25198 salvage 2026-05-18 21:58:47 -07:00
Teknium
b58b4188f6 chore(release): map @pepelax for PR #25419 salvage 2026-05-18 21:54:47 -07:00
Teknium
785993bcae chore(release): map bartok9 noreply for PR #24879 salvage 2026-05-18 21:53:57 -07:00
Teknium
ab11d0998c chore(release): map @asdlem for PR #27852 salvage
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Docker Build and Publish / move-main (push) Blocked by required conditions
Docker Build and Publish / move-latest (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix Lockfile Fix / auto-fix-main (push) Waiting to run
Nix Lockfile Fix / fix (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
OSV-Scanner / Scan lockfiles (push) Waiting to run
Tests / test (push) Waiting to run
Tests / e2e (push) Waiting to run
uv.lock check / uv lock --check (push) Waiting to run
2026-05-18 21:49:19 -07:00
eloklam
9d9f3161ae chore(release): map contributor email for attribution check 2026-05-18 21:02:17 -07:00
Teknium
2064a3976c chore(release): map @yannsunn for PR #28064 xai proxy adapter salvage 2026-05-18 20:09:32 -07:00
iqdoctor
4229facc01 docs(windows): avoid piping installer directly into iex 2026-05-18 20:05:47 -07:00
Teknium
effdebb65e
chore(release): alias stale-ID salvage commit for @Grogger (#28334)
PR #28330 was salvaged with a wrong noreply numeric ID (18091625 vs
the correct 7065068). The commit on main is correctly authored to
Grogger by username, but neither noreply form was in AUTHOR_MAP.
Adds both so release-notes generation maps them to @Grogger.
2026-05-18 20:01:12 -07:00
Teknium
7267c38695
chore(release): pre-stage AUTHOR_MAP for May 2026 LHF batch group 8 (#28328)
Pre-stages AUTHOR_MAP entries for 10 new contributors whose PRs are being
salvaged in the May 2026 low-hanging-fruit batch (group 8). Lands ahead
of the per-PR salvage PRs so they don't get blocked by AUTHOR_MAP CI.

Contributors:
- AceWattGit (#28159 — _pool_may_recover_from_rate_limit NameError)
- YuanHanzhong (#28032 — x.com/status fallbacks link-like)
- colin-chang (#28245, #28249, #28251 — gateway + mattermost fixes)
- felix-windsor (#28019 — preserve cron asterisks in strip mode)
- houenyang-momo (#28205 — charizard completion menu contrast)
- iqdoctor (#28095 — windows installer docs)
- joe102084 (#28151 — whitespace-only cron responses)
- jvinals (#27936 — Slack U-IDs → DM channel)
- maxmilian (#28267 — ModelPickerDialog portal)
- samggggflynn (#27952 — dingtalk pre_start)

Per references/batch-pr-salvage-may14-additions.md.
2026-05-18 19:59:23 -07:00
Teknium
a24184f295
chore(release): alias stale-ID salvage commit for @LifeJiggy (#28317)
* fix(process-registry): detach stdin from background subprocesses to prevent keyboard freeze

Background process non-PTY path used stdin=subprocess.PIPE unconditionally,
creating an orphan pipe that was never written to and never closed. Child
processes that read stdin would block indefinitely, competing with the
parent's prompt_toolkit event loop for terminal ownership and causing
complete keyboard lockout.

Change to stdin=subprocess.DEVNULL so children get immediate EOF on stdin
reads instead of blocking forever. For interactive stdin, the PTY path
(which has its own independent PTY via ptyprocess.PtyProcess.spawn) should
be used instead.

Fixes #17959

* chore(release): alias stale-ID salvage commit for LifeJiggy

PR #28315 was salvaged with a wrong noreply numeric ID (192385615 vs
the correct 141562589). The commit on main is correctly authored to
LifeJiggy by username, but the noreply email doesn't match AUTHOR_MAP.
Adds an alias so release-notes generation maps both forms to the same
contributor.

---------

Co-authored-by: LifeJiggy <192385615+LifeJiggy@users.noreply.github.com>
2026-05-18 19:35:21 -07:00
teknium1
e73e487d40 chore(release): pre-stage AUTHOR_MAP for May 2026 LHF batch group 7
Pre-stages AUTHOR_MAP entries for 5 new contributors whose PRs are being
salvaged in the May 2026 low-hanging-fruit batch (group 7). Lands ahead
of the per-PR salvage PRs so they don't get blocked by AUTHOR_MAP CI.

Contributors:
- 02356abc (#28286 — wecom WSMsgType.CLOSING)
- burjorjee (#28201 — inline-shell timeout guard)
- oseftg (#28168 — natural response ending: emoji + caret)
- rudi193-cmd (#28241 — empty credential pool entries)
- sadiksaifi (#27982 — kanban horizontal scroll)

Per references/batch-pr-salvage-may14-additions.md.
2026-05-18 19:31:00 -07:00
0xjackyang
3df699be50 chore(release): map Jack Yang contributor email
Adds the contributor email mapping for Jack Yang (@0xjackyang) so future
release-note generation attributes commits correctly.

Salvage of #27964 by @0xjackyang.
2026-05-18 19:31:00 -07:00
Jeffrey Quesnelle
49c8299798
Merge pull request #28169 from NousResearch/jq/install-ps1-improvements
feat(install.ps1): strip BOM, add -Commit/-Tag pin params, harden git ops
2026-05-18 21:28:40 -04:00
Teknium
378bca1d2f chore(release): add AUTHOR_MAP entry for falasi 2026-05-18 14:31:37 -07:00
emozilla
a53e8ca733 feat(install.ps1): strip BOM, add -Commit/-Tag pin params, harden git ops
Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR #27822) that benefit the canonical CLI install flow on main:

1. Strip UTF-8 BOM from scripts/install.ps1.

   The canonical 'irm <raw URL> | iex' install flow has been broken
   since commit 4279da4db re-introduced a UTF-8 BOM that PR #27224
   had explicitly stripped. PowerShell 5.1's 'irm' returns the
   response body as a string with the BOM surviving as a leading
   \ufeff character; 'iex' then evaluates that string and the parser
   chokes on the invisible character before param(), surfacing as a
   cascade of 'The assignment expression is not valid' errors at
   every param default value.

   File body is verified pure ASCII (no character above byte 127),
   so PS 5.1 with no BOM falls back to Windows-1252 decoding which
   is identical to ASCII for our content. Both install paths work:
     - 'irm ... | iex' (canonical one-liner)
     - 'powershell -File install.ps1' (programmatic / desktop bootstrap)

2. New -Commit and -Tag string params for reproducible pinning.

   Higher-precedence variants of -Branch. When set, the repository
   stage clones $Branch (fast partial fetch) and then 'git checkout's
   the exact ref. Precedence: Commit > Tag > Branch. Honoured by all
   three code paths:
     - Update path (existing valid checkout): fetch + checkout
       --detach <commit|tag> instead of checkout + pull.
     - Fresh clone: clone --branch $Branch, then post-clone
       'git checkout --detach' to the requested ref.
     - ZIP fallback: pick archive URL for the most-specific ref
       (commit -> archive/<sha>.zip, tag -> archive/refs/tags/
       <tag>.zip, else archive/refs/heads/<branch>.zip).

   Used by the Hermes desktop's first-launch bootstrap to pin the
   .exe to the exact commit it was built against, so the cloned
   Hermes Agent tree always matches what the .exe was tested with.
   Also enables release-bundle pinning (e.g. Microsoft Store builds
   pinning to a release tag) and CI reproducibility.

3. EAP=Continue wrap around the new pin-step git invocations.

   'git fetch origin <commit>' writes the routine 'From <url>' info
   line to stderr. Under the script's global $ErrorActionPreference
   = 'Stop' that stderr line is wrapped as an ErrorRecord and
   terminates the script even though fetch+checkout actually succeed.
   Same EAP=Stop + native-stderr footgun we hit during the install.ps1
   hardening pass in Install-Uv, Test-Python, _Run-NpmInstall.

   Wrap both the update-path fetch/checkout block AND the post-clone
   pin block in $ErrorActionPreference = 'Continue' (restored in
   finally). Real failures still caught by $LASTEXITCODE checks.
2026-05-18 15:45:28 -04:00
teknium1
956dd44625 chore(release): add AUTHOR_MAP entry for dskwe 2026-05-18 10:51:15 -07:00
alt-glitch
1f9b2e4d0b chore: add gianfrancopiana to AUTHOR_MAP 2026-05-18 17:39:50 +00:00
teknium1
47bc8e080d chore(release): AUTHOR_MAP noreply entry for Slimydog21 2026-05-18 10:37:35 -07:00
teknium1
bc77f79798 chore(release): AUTHOR_MAP entries for Fewmanism + Slimydog21 2026-05-18 10:23:13 -07:00
teknium
65e0c49b77 chore(release): add AUTHOR_MAP entry for glennc 2026-05-18 10:14:38 -07:00
Siddharth Balyan
d9b6f75c0b
refactor(bootstrap): consolidate ACP browser bootstrap into install.{sh,ps1} (#27851)
* refactor(bootstrap): consolidate ACP browser bootstrap into install.{sh,ps1}

Delete 687 lines of duplicated browser bootstrap code from
acp_adapter/bootstrap/. All browser installation now routes through
dep_ensure -> install.{sh,ps1} --ensure, using agent-browser install
for Chromium. install.sh gains ensure_browser() with macOS app-bundle
detection and per-distro guidance.

Tracking: #27826

* fix(install.sh): add --ignore-scripts to npm install for camofox

@askjo/camofox-browser has a dependency (impit) whose postinstall
script runs `npx only-allow pnpm`, which fails under npm. Adding
--ignore-scripts avoids the spurious failure without affecting
functionality.

Tracking: #27826

* fix: add explicit return in ensure_browser, narrow exception in entry.py

ensure_browser() now returns 0 explicitly on all success paths.
_run_setup_browser() catches OSError instead of broad Exception,
letting ImportError propagate as a real packaging bug.
2026-05-18 16:36:26 +05:30
Siddharth Balyan
e3a254d65b
feat(dep_ensure): complete Windows bootstrap — dep_ensure + install.ps1 + detection (#27845)
* feat(dep_ensure): complete Windows bootstrap — dep_ensure + install.ps1 + detection

dep_ensure.py gains Windows awareness: PowerShell invocation, platform-
specific browser detection, (path, shell) tuple returns.

install.ps1 gains -Ensure/-PostInstall modes using npm -g --prefix
(aligned with install.sh) and agent-browser install for Chromium.

browser_tool.py gains node/ in candidate dirs for Windows .cmd shims.
Both install scripts bundled in pip wheel.

Tracking: #27826

* fix(install.ps1): add --ignore-scripts to npm install for camofox

@askjo/camofox-browser has a dependency (impit) whose postinstall
script runs `npx only-allow pnpm`, which fails under npm. Adding
--ignore-scripts avoids the spurious failure without affecting
functionality.

Tracking: #27826

* fix: remove duplicate install scripts from git

CI already copies scripts/install.{sh,ps1} into hermes_cli/scripts/
during wheel build. No need to commit copies — .gitignore keeps them
out, _find_install_script() falls back to scripts/ for git-clone users.

Tracking: #27826

* fix: address review — remove env_extra, fix ps1 error handling

- Remove unused env_extra parameter from ensure_dependency()
- Invoke-EnsureMode node case now uses Test-Node consistently
- Install-AgentBrowser uses throw instead of exit 1
2026-05-18 16:34:24 +05:30
Siddharth Balyan
6f5ec929a1
feat(config): add install-method stamping + Docker detection (#27843)
* feat(config): add install-method stamping + Docker detection

Dockerfile stamps "docker", install.sh stamps "git", and cmd_postinstall
stamps "pip" into ~/.hermes/.install_method. detect_install_method() reads
the stamp first, then falls back to managed-system / container / .git
heuristics. Adds Docker upgrade guidance.

Tracking: #27826

* fix(stamp): move Docker stamp to entrypoint, install.sh stamp after print_success

The Dockerfile stamp was overwritten by the VOLUME overlay at container
start. Moving it to entrypoint.sh ensures it persists. The install.sh
stamp now writes after print_success so it only lands on full success.
2026-05-18 16:34:10 +05:30
Teknium
abf1af5401
feat(session_search): single-shape tool with discovery, scroll, browse — no LLM (#27590)
* feat(session_search): single-shape tool with discovery, scroll, browse — no LLM

Replaces the LLM-summarized session_search with a single-shape tool that
returns actual messages from the DB. Three calling shapes inferred from
args (no mode parameter):

  1. Discovery — pass query. FTS5 + anchored ±5 window + bookends per hit,
     all in one call. ~20ms on a real DB instead of ~90s for the previous
     three aux-LLM calls.
  2. Scroll — pass session_id + around_message_id. Returns a window
     centered on the anchor. To paginate, re-anchor on the first/last id
     of the returned window. Boundary message appears in both windows
     as the orientation marker. ~1ms per scroll call.
  3. Browse — no args. Recent sessions chronologically.

Bookend_start (first 3 user+assistant msgs) and bookend_end (last 3) give
the agent goal + resolution on every discovery hit, so a single tool call
reconstructs a long session's arc without loading the whole transcript.

The aux-LLM summary path is gone: it cost ~$0.30/call, took ~30s, and
laundered FTS5 hits through a model that could confabulate when the right
session wasn't in the hit list. The merged shape returns byte-for-byte
content from SQLite.

History:
- PR #20238 (JabberELF) seeded the fast/summary dual-mode split.
- PR #26419 (yoniebans) expanded to fast/guided/summary with bookends,
  multi-anchor drill-down, default-mode config, and a teaching skill.

This PR collapses that toolkit into one shape with explicit scroll
support, drops the summary path, drops the mode parameter, drops the
config knob, drops the skill. JabberELF's seed work is acknowledged via
the AUTHOR_MAP entry.

Validation:
- 38/38 tool tests pass (tests/tools/test_session_search.py)
- 12/12 get_messages_around tests pass (tests/hermes_state/)
- 11/11 get_anchored_view tests pass (tests/hermes_state/)
- Full tests/tools/ run: 5168 passing, 2 failures pre-exist on main
  (test ordering in test_delegate.py, unrelated)
- E2E against live state DB: discovery 20ms, scroll 1ms, browse 280ms;
  pagination forward+backward works with boundary-message orientation;
  error paths return clean tool_error responses

Co-authored-by: JabberELF <abcdjmm970703@gmail.com>
Co-authored-by: yoniebans <jonny@nousresearch.com>

* chore(session_search): prune dead LLM-summary config and docs

Companion to the single-shape rewrite. The auxiliary.session_search config
block, max_concurrency / extra_body tunables, and matching docs sections
all referenced the removed LLM summarization path. Removing them so users
don't try to tune knobs that nothing reads.

- hermes_cli/config.py: drop dead auxiliary.session_search block from
  DEFAULT_CONFIG. Leftover keys in user config.yaml are harmless and
  ignored.
- hermes_cli/tips.py: drop two tips referencing the removed
  max_concurrency / extra_body knobs.
- website/docs/user-guide/configuration.md: drop 'Session Search Tuning'
  section and the auxiliary.session_search block from the example.
- website/docs/user-guide/features/fallback-providers.md: drop session_search
  rows from the auxiliary-tasks tables and the dedicated tuning subsection.
- website/docs/reference/tools-reference.md: rewrite the session_search
  entry to describe the new three-shape behaviour.
- CONTRIBUTING.md: update the file-tree description.
- tests/tools/test_llm_content_none_guard.py: remove TestSessionSearchContentNone
  class and test_session_search_tool_guarded — both guard against an
  unguarded .content.strip() call site in _summarize_session() that no
  longer exists.

Validation: 97/97 targeted tests still pass (hermes_state + session_search +
llm_content_none_guard). Config tests 55/55.

---------

Co-authored-by: JabberELF <abcdjmm970703@gmail.com>
Co-authored-by: yoniebans <jonny@nousresearch.com>
2026-05-17 23:28:45 -07:00
teknium1
034110e7ac chore(release): map zccyman noreply email for #26998 2026-05-17 17:15:31 -07:00
teknium1
a2cc30544c chore(release): map vaddisrinivas for #26394 salvage 2026-05-17 11:51:46 -07:00
teknium1
ee7cd10281 chore(release): map hehehe0803 email for #26212 salvage 2026-05-17 11:50:43 -07:00
teknium1
3f01e9493c chore(release): AUTHOR_MAP entries for batch salvage group 6 contributors
Final LHF run group. Adds release-note attribution mappings for:
- @bird (PR #25219)
- @davidcampbelldc (PR #26834)

(zccyman, wesleysimplicio already mapped from prior groups.)
2026-05-17 11:39:37 -07:00
teknium1
37286a5bcd chore(release): map QuenVix, Mind-Dragon, soynchux emails for Tier 4 salvage 2026-05-17 11:37:45 -07:00
teknium1
150b577da5 chore(release): AUTHOR_MAP entries for batch salvage group 5 contributors
Adds release-note attribution mappings for the contributors from group 5:
- @haran2001 (PR #27070, #27068)
- @ms-alan (PR #26443)
- @godlin-gh (PR #26118)
- @wesleysimplicio (PR #25777, ext-email form)
- @Carry00 (PR #26851)
- @alaamohanad169-ship-it (PR #26036)
- @hawknewton (PR #26294)

(YanzhongSu PR #25879 and flamiinngo PR #27231 already mapped.)
2026-05-17 02:31:18 -07:00
teknium1
cc59880ab0 chore(release): map EloquentBrush0x email for #26642 salvage 2026-05-17 02:21:06 -07:00
teknium1
7322816efa chore(release): AUTHOR_MAP entries for batch salvage group 4 contributors
Adds release-note attribution mappings for 9 contributors from group 4:
- @EloquentBrush0x (PR #26657)
- @subtract0 (PR #25658)
- @zwolniony (PR #26961)
- @that-ambuj (PR #26582)
- @zccyman (PR #25294)
- @lidge-jun (PR #26814)
- @phoenixshen (PR #26768)
- @AhmetArif0 (PR #26635)
- (francip already mapped from prior PR #26134 attribution)

#27147 dropped from this batch — already landed on main as 4b17c2411.
2026-05-16 23:11:43 -07:00
teknium1
04bb30730a chore(release): AUTHOR_MAP entries for batch salvage group 3 contributors
Adds release-note attribution mappings for 9 contributors from group 3:
- @darvsum (PR #26766)
- @hueilau (PR #26498)
- @Timur00Kh (PR #27114)
- @Grogger (PR #27061)
- @lemassykoi (PR #27042)
- @draplater (PR #26707)
- @pr7426 (PR #27048)
- @therahul-yo (PR #26215)
- @flamiinngo (PR #27205)

#27154 dropped from this batch — already landed on main as 4e9cedcd4.
2026-05-16 23:05:27 -07:00
flamiinngo
8973b00ff3 fix(scripts): fix UnicodeEncodeError in footgun checker on Windows
The check-windows-footguns.py script outputs a checkmark (U+2713) and
cross (U+2717) to report results. Windows terminals default to cp1252,
which cannot encode these characters, so running the script on Windows
threw a UnicodeEncodeError before any results were printed.

This made the tool completely unusable on the exact platform it exists
to help -- a developer on Windows trying to check their code for
Windows-safety issues would just get a crash instead.

Fix: reconfigure stdout and stderr to UTF-8 at the start of main(),
before any output is produced. Verified on Windows 11 Home with
Python 3.13 (terminal defaulting to cp1252).
2026-05-16 23:05:27 -07:00
teknium1
fb138d91ca fix(install.ps1): Stage-Node honest reporting + reject empty -Stage
Two protocol-correctness gaps from review:

1. Stage-Node used [void](Test-Node) which discarded Test-Node's return
   value, so the JSON frame always reported ok=true even when Node
   install fully failed.  A GUI driver consuming the manifest couldn't
   tell 'node ready' from 'node missing'.  Wire a soft-skip channel
   ($script:_StageSkippedReason) that workers can populate to surface
   'ran, but the thing it was supposed to set up is not available' as
   skipped=true with a reason in the JSON, without aborting the install
   (Node is optional -- browser tools degrade gracefully, matches
   Write-Completion's existing 'Note: Node.js could not be installed'
   behavior).  Reset before each stage so a prior reason can't leak.

2. The -Stage dispatch used 'if ($Stage)' which is falsy for empty
   string, so 'install.ps1 -Stage ""' fell through to Main and silently
   kicked off a full destructive install.  Switch to
   PSBoundParameters.ContainsKey('Stage') so an explicit empty value
   surfaces as unknown-stage exit 2 with a structured JSON frame, the
   way every other bad stage name does.
2026-05-16 22:55:12 -07:00
teknium1
3925be2791 fix(install.ps1): trim completion banner + strip em-dash in test
Address the two cosmetic items from review:

- Completion banner middle line was 62 chars vs 59-char top/bottom borders
  (replacing the 1-char checkmark with [OK] added width that wasn't
  reflected in the trailing whitespace).  Drop 3 trailing spaces.
- Smoke test file had a single em-dash in a comment -- the only
  non-ASCII byte across both files.  Replace with -- for consistency
  with install.ps1's pure-ASCII goal.
2026-05-16 22:55:12 -07:00
emozilla
c0b64f0877 fix(install.ps1): address Copilot review on #27224
Three issues flagged by the Copilot review on this PR:

1. Double JSON emit on stage failure (Copilot #1, #2). When -Stage <name>
   ran a worker that threw, Invoke-Stage's finally emitted a JSON result
   frame AND the entry-point catch emitted a second error frame --
   producing two concatenated JSON objects on stdout and breaking the
   one-line-per-invocation contract that drivers parse against. Same
   issue applied to -Json mode on a full install (every stage's finally
   plus a final error frame missing duration_ms/skipped).

   Fix: Invoke-Stage's finally now sets $script:_StageEmittedErrorFrame
   when it emits a failure frame; the entry-point catch checks the flag
   and skips its own emit, still exit 1.

2. $prevEAP uninitialized on early try-block throw (Copilot #3). In
   Install-Uv, Test-Python, Test-Node's winget fallback,
   _Run-NpmInstall, and the playwright block, '$prevEAP =
   $ErrorActionPreference' lived as the first statement INSIDE the
   try. If anything between 'try {' and that line threw (Write-Info on
   an unusual host, the npx-finding loop, etc.), the catch's
   'if ($prevEAP) { ... }' restore was a no-op and EAP could remain
   relaxed.

   Fix: hoist '$prevEAP = $ErrorActionPreference' to the line
   immediately before 'try {' in all five sites. Catch's restore is
   now always meaningful regardless of where in the try the throw
   originated.

No change to Invoke-Stage's success path or to the four lint-clean EAP
sites (Test-Node was the only winget-related catch). All 19 metadata
smoke tests still pass.
2026-05-16 22:55:12 -07:00