Skip to content

convert install.ps1 to new bootstrap protocol#27224

Merged
teknium1 merged 4 commits into
mainfrom
jq/install-ps1-stage-protocol
May 17, 2026
Merged

convert install.ps1 to new bootstrap protocol#27224
teknium1 merged 4 commits into
mainfrom
jq/install-ps1-stage-protocol

Conversation

@jquesnelle

@jquesnelle jquesnelle commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Adds an opt-in stage protocol that lets programmatic drivers (the desktop GUI's onboarding wizard, CI, future install.sh parity) drive install.ps1 one step at a time with structured JSON results. Default invocation (irm | iex one-liner) behaves unchanged.

Entry points

    | Flag             | Purpose                                                             |
    |------------------|---------------------------------------------------------------------|
    | install.ps1      | Today's interactive install (unchanged)                             |
    | -ProtocolVersion | Emit protocol version integer                                       |
    | -Manifest        | Emit JSON manifest of available stages                              |
    | -Stage <name>    | Run one stage, emit JSON result                                     |
    | -NonInteractive  | Suppress Read-Host prompts (skips setup wizard + gateway autostart) |
    | -Json            | Machine-readable completion frame                                   |

Manifest exposes 14 stages across prereqs/install/finalize/post-install categories. Two stages (configure, gateway) flag needs_user_input=true so GUI drivers can skip them and handle the equivalent UX themselves.

@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard labels May 17, 2026
Adds an opt-in stage protocol that lets programmatic drivers (the
desktop GUI's onboarding wizard, CI, future install.sh parity) drive
install.ps1 one step at a time with structured JSON results. Default
invocation (`irm | iex` one-liner) behaves unchanged.

Entry points:
  install.ps1                  Today's interactive install (unchanged)
  install.ps1 -ProtocolVersion Emit protocol version integer
  install.ps1 -Manifest        Emit JSON manifest of available stages
  install.ps1 -Stage <name>    Run one stage, emit JSON result
  install.ps1 -NonInteractive  Suppress Read-Host prompts (skips the
                               setup wizard and gateway autostart)
  install.ps1 -Json            Machine-readable completion frame

Manifest exposes 14 stages across prereqs/install/finalize/post-install
categories, with 2 (configure, gateway) flagged needs_user_input=true
so GUI drivers can skip them and handle the equivalent UX themselves.

Along the way, clean-VM testing on stock Windows 10/11 surfaced a
series of latent install.ps1 bugs that were never exercised by
developer machines. Fixed in the same commit:

* Encoding: file is now pure ASCII with no BOM. Windows PowerShell
  5.1 reads BOM-less files as Windows-1252 and chokes on em-dashes
  (and other UTF-8 sequences), while iex chokes on a leading U+FEFF.
  Pure-ASCII satisfies both invocation paths.

* EAP=Stop + native `2>&1` captures: PowerShell wraps stderr lines
  from native commands as ErrorRecord objects under EAP=Stop and
  throws even when the command exits 0. Relaxed to EAP=Continue
  around the astral.sh uv installer, `uv python install`, `npm
  install`, `npx playwright install`, the venv import probes, and
  the Node winget fallback. Check $LASTEXITCODE for the real signal.

* Cross-process state: each `-Stage <name>` invocation spawns a
  fresh powershell child. $script:UvCmd set by Stage-Uv was invisible
  to Stage-Python; PATH updated by Stage-Git/Stage-Node was invisible
  to subsequent stages spawned by the driver shell. Added Resolve-UvCmd
  helper called at the top of every stage that needs uv, and a
  Sync-EnvPath helper called at the top of Invoke-Stage to refresh
  PATH from the registry.

* UAC avoidance: `winget install OpenJS.NodeJS.LTS` triggers a UAC
  prompt that often appears minimized in the taskbar -- looks like a
  hang. Switched Test-Node to prefer the official portable Node zip
  dropped into %LOCALAPPDATA%\hermes\node\ (mirrors the PortableGit
  pattern Install-Git already uses). winget kept as fallback.

* npx hangs on confirmation: `npx playwright install chromium` blocks
  on stdin waiting for "Need to install [email protected] (y/N)" when
  playwright isn't in local node_modules. Tee-Object pipelines
  disconnect stdin from the user's TTY so the install hangs forever.
  Pass `--yes` to auto-accept.

* Silent long-running installs: `*> $logPath` redirected every stream
  to disk and left the user staring at a frozen "Installing..." line
  for the 5-10 minutes Playwright Chromium takes to download. Switched
  to `2>&1 | ForEach-Object { "$_" } | Tee-Object -FilePath $log` so
  output streams live to the console AND captures to log for failure
  diagnostics. ForEach-Object coercion strips PowerShell's red
  NativeCommandError formatter from stderr items.

* Console encoding: forced [Console]::OutputEncoding to UTF-8 so
  playwright/git/npm progress bars, box-drawing, and check marks render
  correctly instead of as IBM437/Windows-1252 mojibake.

* Performance: set $ProgressPreference = "SilentlyContinue" so
  Invoke-WebRequest doesn't paint its per-chunk progress bar. The
  PS 5.1 progress UI throttles downloads by 10-100x (a 57MB PortableGit
  grab takes 5 minutes with the bar on vs ~20 seconds with it off,
  same network). Affects PortableGit, Node portable zip, and the
  Hermes repo zip fallback.

Tests: scripts/tests/test-install-ps1-stage-protocol.ps1 provides 19
metadata-only assertions covering -ProtocolVersion, -Manifest schema,
and unknown -Stage error frame. No install side effects.

End-to-end validated on a clean Windows 10 VM via:
  1. `irm <branch>/scripts/install.ps1 | iex` (canonical CLI path)
  2. `powershell -File install.ps1 -Stage X` iterated through every
     stage (GUI driver path, exercises cross-process fixes)
@jquesnelle
jquesnelle force-pushed the jq/install-ps1-stage-protocol branch from 60c881e to 5165819 Compare May 17, 2026 04:47
@jquesnelle
jquesnelle marked this pull request as ready for review May 17, 2026 05:05
@OutThisLife
OutThisLife requested a review from Copilot May 17, 2026 05:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in "stage protocol" to scripts/install.ps1 so programmatic drivers (the desktop GUI's onboarding wizard, CI, future install.sh) can drive the Windows installer one step at a time and receive structured JSON results, while the default interactive irm | iex flow is unchanged. The PR also replaces non-ASCII glyphs in console output with ASCII equivalents (for PS 5.1 parser/codepage robustness), forces UTF-8 console output for child commands, silences Invoke-WebRequest progress bars, demotes the winget Node install behind the portable-zip path, and adds several $ErrorActionPreference relaxations around native commands whose stderr would otherwise be wrapped as terminating errors.

Changes:

  • New stage protocol surface in install.ps1: -ProtocolVersion, -Manifest, -Stage <name>, -NonInteractive, -Json, plus a 14-stage $InstallStages table, per-stage workers, Invoke-Stage/Invoke-AllStages, and Resolve-UvCmd/Sync-EnvPath helpers for cross-process driver mode.
  • Operational hardening: UTF-8 console encoding, $ProgressPreference = SilentlyContinue, EAP relaxations around uv, winget, python -c, npm install, and playwright install, Tee-Object live output for npm/playwright, persisted User-PATH entry for the portable Node install.
  • New PowerShell smoke test (scripts/tests/test-install-ps1-stage-protocol.ps1) covering -ProtocolVersion, -Manifest shape and required stage names, and unknown-stage error framing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
scripts/install.ps1 Adds the stage-protocol parameters, dispatch block, stage table/workers, helper functions, and various EAP/UTF-8/Tee operational changes; replaces non-ASCII banner/log glyphs with ASCII.
scripts/tests/test-install-ps1-stage-protocol.ps1 New metadata-only smoke test that runs -ProtocolVersion, -Manifest, and an unknown -Stage and asserts exit codes plus JSON shape.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/install.ps1
Comment on lines +1977 to +1989
} catch {
$result.ok = $false
$result.reason = "$_"
throw
} finally {
$result.duration_ms = [int]([DateTime]::UtcNow - $start).TotalMilliseconds
if ($Json -or $Stage) {
# In stage-driver mode every stage emits a JSON line so the
# caller can stream progress. In default interactive mode we
# stay silent here (the worker already wrote human output).
$result | ConvertTo-Json -Compress | Write-Output
}
}
Comment thread scripts/install.ps1
Comment on lines +2063 to +2074
if ($Json -or $Stage) {
# Stage-driver mode: caller wants JSON they can parse. Emit a
# structured error frame and exit non-zero.
$err = @{
ok = $false
stage = if ($Stage) { $Stage } else { $null }
reason = "$_"
}
$err | ConvertTo-Json -Compress | Write-Output
exit 1
}

Comment thread scripts/install.ps1
Comment on lines +183 to +185
# Restore EAP in case the try block threw before the assignment
if ($prevEAP) { $ErrorActionPreference = $prevEAP }
Write-Err "Failed to install uv: $_"
alt-glitch

This comment was marked as resolved.

@alt-glitch alt-glitch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Inline comments on the two bugs found during Windows E2E testing.

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.

@alt-glitch alt-glitch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🐛 Request Changes: Double JSON on failed stages

Tested on Windows 11 PS 5.1.26100 via SSH. The stage protocol metadata surface is clean (19/19 smoke tests pass, cross-process driver works for uv/python/git/node), but there's a protocol contract bug that will bite programmatic drivers:

When a stage fails, the caller gets two JSON lines instead of one:

{"skipped":false,"ok":false,"reason":"Cannot find path ...","duration_ms":46,"stage":"venv"}
{"ok":false,"reason":"Cannot find path ...","stage":"venv"}

Root cause: Invoke-Stage emits JSON in its finally block (line 1987), then re-throws (line 1980). The outer catch at line 2062 also emits a JSON error frame. Both fire for the same failure.

Suggested fix — suppress the re-throw when running a single stage, since the JSON frame from finally is already the structured result:

# In Invoke-Stage, line 1980:
    } catch {
        $result.ok = $false
        $result.reason = "$_"
        if (-not $Stage) { throw }  # Only re-throw in full-install mode
    } finally {

The full-install path (MainInvoke-AllStages) still needs the re-throw so the outer catch can surface the error to the user. Single-stage mode (-Stage venv) doesn't — the JSON frame with ok=false is the contract, and the exit code from the finallycatch chain already propagates correctly.

Also two cosmetic nits (not blocking):

  • Completion banner line 1756 is 62 chars wide vs 59-char borders ([OK] is 3 wider than , trailing spaces not adjusted)
  • Test file line 8 has an em-dash () — only non-ASCII byte across both files

@alt-glitch alt-glitch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adversarial review round 1 — two new bugs found (in addition to the double-JSON already flagged).

@NousResearch NousResearch deleted a comment from github-actions Bot May 17, 2026
@NousResearch NousResearch deleted a comment from cardtest15-coder May 17, 2026
@NousResearch NousResearch deleted a comment from OutThisLife May 17, 2026
@alt-glitch
alt-glitch dismissed their stale review May 17, 2026 05:36

Consolidating into single review

@alt-glitch alt-glitch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review — PR #27224: install.ps1 stage protocol + Windows clean-VM hardening

Tested on real Windows 11 PS 5.1.26100 via SSH (sidbin@vespyr), plus two parallel adversarial Claude Code Sonnet reviewers with independent focus areas.

Testing performed

Test Result
Smoke tests (19 assertions: -ProtocolVersion, -Manifest schema, unknown -Stage) Pass
Cross-process stage driver (uv/python/git/node as separate PS processes) Pass
-NonInteractive suppresses Read-Host (configure stage returns immediately) Pass
Encoding: pure ASCII, no BOM (main had 1,409 non-ASCII bytes) Pass
Existing pytest suite (test_windows_native_support.py, 58 tests) Pass
Failed stage JSON framing Bug
Stage-Node success reporting Bug
Empty -Stage "" dispatch Bug

What's good

The stage protocol design is clean — single source of truth, thin stage workers, Resolve-UvCmd + Sync-EnvPath for cross-process state. The hardening fixes (EAP guards, ProgressPreference 10-100x speedup, portable Node over winget UAC, npx --yes, Tee-Object streaming, console encoding) are all correct and well-documented. The commit message is excellent — every fix traced to a specific clean-VM failure.

Bugs — see inline comments

  1. Double JSON on failed stages (HIGH) — line 1980
  2. Stage-Node reports ok=true when Node install fails (MEDIUM) — line 1916
  3. -Stage "" runs full install instead of erroring (MEDIUM) — line 2043
  4. Completion banner misalignment (LOW) — line 1756

Adversarial findings rejected (verified false on PS 5.1)

  • ConvertTo-Json -Compress multi-line → single-line confirmed
  • Move-Item fails with spaces → works fine
  • PATH duplication in Sync-EnvPath → cosmetic, safe
  • Concurrent stage PATH race → stages are sequential by design

Ready to ship once bugs 1-3 are addressed.

Comment thread scripts/install.ps1
} catch {
$result.ok = $false
$result.reason = "$_"
throw

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug 1: Double JSON on failed stages.

Invoke-Stage emits JSON in the finally block (line 1987), then re-throws here. The outer catch at line 2062 also emits a JSON error frame. Drivers get two lines for one failure:

{"skipped":false,"ok":false,"reason":"...","duration_ms":46,"stage":"venv"}
{"ok":false,"reason":"...","stage":"venv"}

Reproduced on PS 5.1.26100 with -Stage venv (no repo at default path).

Fix — suppress re-throw in single-stage mode:

if (-not $Stage) { throw }  # Only re-throw in full-install mode

Comment thread scripts/install.ps1 Outdated
function Stage-Uv { if (-not (Install-Uv)) { throw "uv installation failed" } }
function Stage-Python { Resolve-UvCmd; if (-not (Test-Python)) { throw "Python $PythonVersion not available" } }
function Stage-Git { if (-not (Install-Git)) { throw "Git not available and auto-install failed -- install from https://git-scm.com/download/win then re-run" } }
function Stage-Node { [void](Test-Node) }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug 2: Stage-Node always reports ok=true even when Node install fails.

Test-Node returns $true unconditionally (line 684). Stage-Node does [void](Test-Node) — never throws. In stage-driver mode the JSON says ok=true when Node isn't actually installed.

The default-install path works because $script:HasNode gates downstream behavior, but the stage protocol contract lies to the GUI driver.

Fix — surface the failure:

function Stage-Node { [void](Test-Node); if (-not $script:HasNode) { throw "Node.js not available (optional — browser tools disabled)" } }

Or if the GUI should treat this as a soft skip, set $result.skipped = $true instead of throwing.

Comment thread scripts/install.ps1 Outdated
exit 0
}

if ($Stage) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug 3: -Stage "" silently runs a full install.

PS treats "" as falsy, so if ($Stage) is false and dispatch falls through to Main. A GUI driver passing empty string gets an entire interactive install instead of an error.

Verified on PS 5.1.26100: "" = falsy, " " = truthy.

Fix — use $PSBoundParameters instead of truthy test:

if ($PSBoundParameters.ContainsKey('Stage')) {
    if ([string]::IsNullOrWhiteSpace($Stage)) {
        @{ ok = $false; stage = $Stage; reason = "Stage name cannot be empty." } | ConvertTo-Json -Compress | Write-Output
        exit 2
    }
    # ... existing dispatch
}

Comment thread scripts/install.ps1 Outdated
Write-Host " Installation Complete! " -ForegroundColor Green
Write-Host "└─────────────────────────────────────────────────────────┘" -ForegroundColor Green
Write-Host "+---------------------------------------------------------+" -ForegroundColor Green
Write-Host "| [OK] Installation Complete! |" -ForegroundColor Green

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: banner misalignment. [OK] is 3 chars wider than but trailing spaces weren't reduced. This line is 62 chars, borders are 59.

+---------------------------------------------------------+   (59)
|              [OK] Installation Complete!                   |   (62)
+---------------------------------------------------------+   (59)

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.
@teknium1
teknium1 dismissed alt-glitch’s stale review May 17, 2026 05:52

Bug #1 (double JSON) was already addressed in this PR via the $script:_StageEmittedErrorFrame guard at lines 2005-2007 and 2091 — reviewer was looking at an older revision. Bug #2 (banner width) and Nit #1 (em-dash in test) just fixed in 9eb9bee.

@github-actions

github-actions Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: jq/install-ps1-stage-protocol vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8350 on HEAD, 8350 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 4366 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

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.
@teknium1
teknium1 merged commit fb138d9 into main May 17, 2026
10 checks passed
teknium1 pushed a commit that referenced this pull request May 17, 2026
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.
@teknium1
teknium1 deleted the jq/install-ps1-stage-protocol branch May 17, 2026 05:55
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#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.
Seven74AI pushed a commit to Seven74AI/hermes-agent that referenced this pull request Jun 13, 2026
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.
Seven74AI pushed a commit to Seven74AI/hermes-agent that referenced this pull request Jun 13, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#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.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
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.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
… 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 dec2556 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.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
Three issues flagged by the Copilot review on this PR:

1. Double JSON emit on stage failure (Copilot NousResearch#1, NousResearch#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 NousResearch#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.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 2089537 re-introduced a UTF-8 BOM that PR NousResearch#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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 dfc218d re-introduced a UTF-8 BOM that PR NousResearch#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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jul 3, 2026
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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jul 3, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 dfc218d re-introduced a UTF-8 BOM that PR NousResearch#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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jul 3, 2026
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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jul 3, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#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.
donbowman pushed a commit to donbowman/hermes-agent that referenced this pull request Jul 13, 2026
Three issues flagged by the Copilot review on this PR:

1. Double JSON emit on stage failure (Copilot NousResearch#1, NousResearch#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 NousResearch#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.
donbowman pushed a commit to donbowman/hermes-agent that referenced this pull request Jul 13, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#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.
kulikman pushed a commit to kulikman/hermes-agent that referenced this pull request Jul 16, 2026
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.
kulikman pushed a commit to kulikman/hermes-agent that referenced this pull request Jul 16, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#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.
LiteSoul added a commit to LiteSoul/hermes-agent that referenced this pull request Jul 20, 2026
…nstalls agent-browser

PR NousResearch#65701 fixed a runtime symptom (zombie agent-browser daemon holding a
TCP port after Hermes restart on Windows). While validating NousResearch#65701's fix
on a Windows + NVM + Node 24 environment, a separate sibling install-flow
bug was traced that explains why the Windows desktop population commonly
lands on [email protected] (the version with the zombie bug) instead of
agent-browser@^0.26.0 (where the daemon idle-timeout was wired in commit
284e084). This PR fixes the install-flow bug -- the sibling, not a
duplicate of NousResearch#65701.

Symptom / evidence trail (reproduced on a Windows + NVM + Node 24 env):

* `browser_navigate` returned "Daemon failed to start (port: 127.0.0.1:55478)"
  with a live zombie `daemon.js` (agent-browser v0.17.1 from NVM) on PATH.
* The Hermes process's PATH includes `$HERMES_HOME\node\bin` (the bundled
  prefix `Install-AgentBrowser` populates via `npm install -g --prefix
  $HERMES_HOME\node "agent-browser@^0.26.0"`), but that directory does NOT
  exist on disk for desktop-driven installs.
* So bare-PATH wins and resolves to NVM's stale v0.17.1.

Root cause:

* The desktop Update button in apps/desktop/electron/bootstrap-runner.ts
  (L779) drives install.ps1 per-stage via `install.ps1 -Stage <name>
  -NonInteractive -Json`, iterating stages fetched from `install.ps1
  -Manifest`.
* install.ps1's auto-driven stage list `$InstallStages` (L3501-L3527) had
  13 entries: uv, python, git, node, system-packages, repository, venv,
  dependencies, node-deps, [desktop if -IncludeDesktop], path,
  config-templates, platform-sdks, bootstrap-marker, configure, gateway.
  There was NO `browser` / `agent-browser` stage.
* `Install-AgentBrowser` (L355, the only function that runs `npm install -g
  --prefix`) was reachable only via `install.ps1 -PostInstall` or
  `install.ps1 -Ensure browser` -- neither path is in the auto-driven
  stage sequence.
* Result: a Windows user installing or updating Hermes purely via the
  desktop GUI gets `agent-browser` from whatever global PATH provides
  (stale NVM 0.17.1), not from the ^0.26.0 the install.ps1 code intends.

The Linux/macOS equivalent -- scripts/install.sh's ensure_browser() -- IS
in the install flow via ensure_mode (L2550-2600). The Windows gap is
asymmetric (introduced in NousResearch#27224 when the stage-manifest API was added;
install.sh's ensure_browser predates that).

The fix:

* Add a `browser` stage to $InstallStages, placed AFTER node (the worker
  calls Resolve-NpmCmd which throws if npm is missing) and AFTER desktop
  (if -IncludeDesktop, so a freshly-built Hermes.exe picks up the freshly-
  installed agent-browser on its first relaunch instead of inheriting a
  stale bare-PATH binary). Orderings documented in-line at install.ps1
  L3518-L3531.
* Add a thin `Stage-Browser` worker (L3577-L3609) that delegates to the
  existing `Install-AgentBrowser` function -- extend, don't duplicate (per
  AGENTS.md). The worker:
    - Soft-skips when Node is unavailable (mirrors Stage-Node at L3548-3558):
      browser tools are optional, the install flow MUST NOT abort. The
      skipped distinction is surfaced to the JSON frame via the
      $script:_StageSkippedReason channel Invoke-Stage resets each stage
      (L3609-L3616).
    - Forwards -SkipChromium to Install-AgentBrowser so any future top-level
      param and the existing Find-SystemBrowser internal short-circuit both
      line up.

Idempotency / the stage contract (L3486-L3492):

* npm install -g --prefix is idempotent against a satisfied version range,
  so a second invocation with agent-browser already at ^0.26.0 is a near-
  no-op. Cold installs measured ~5-15s on Windows; warm reinstalls are
  much faster because npm's local cache satisfies the version. The
  protocol version is NOT bumped -- stages are additive (drivers iterate
  the manifest dynamically, per L3492).

Existing-manual-install note (for reviewer consideration):

A user with a manually-installed agent-browser 0.17.1 in their NVM/Node
global continues to have that v0.17.1 in their NVM. After this PR lands,
Hermes-spawned subprocesses get 0.26.0 from the bundled prefix (which is
prepended to PATH). The user's OLD 0.17.1 remains installed in NVM but is
no longer spawned by Hermes -- the bundled-prefix prepend order is what
wins. This is the same ordering install.sh relies on for POSIX.

Tests:

* Python source-level: tests/test_install_ps1_browser_stage.py (11 new
  tests) pins the stage definition at the source-text level so Linux CI
  can verify the structural contract without running PowerShell. Covers:
  stage is declared with the right shape/NeedsUserInput=false/Category;
  Stage-Browser worker delegates to Install-AgentBrowser (not duplicate);
  soft-skip when Node is missing; -SkipChromium flag forwarding; ordering
  after node/node-deps and before configure; ASCII invariant; the worker
  does NOT introduce a process-kill pathway (defense against the same
  Blocking scoping concern tonydwb raised on NousResearch#65701).
* Pester smoke: scripts/tests/test-install-ps1-stage-protocol.ps1 asserts
  'browser' appears in `install.ps1 -Manifest` output, after 'node', and
  before 'configure', with category='install' / needs_user_input=false.

Verified on Windows (PowerShell 7 + Python 3.11.15): all 5 new Pester
assertions PASS, all pre-existing Pester smoke assertions PASS (29 OK /
0 FAIL); 19/19 source-level install.ps1 Python tests pass (1 ascii +
3 node-path + 4 native-stderr + 11 new browser-stage). Linux/macOS not
directly verified; source-level Python tests use only stdlib regex +
Path.read_bytes() so no host-specific behavior is expected, but is not
asserted here. Pester suite is Windows-only by nature (shells out to
powershell.exe).

Coordination with NousResearch#58687 (fix(update): honor configured bootstrap state):
that PR adds the POSIX-side `disabled_toolsets`-aware browser-install
skip. The Windows `Stage-Browser` worker here does NOT currently consult
agent.disabled_toolsets / platform_toolsets -- a Windows desktop user who
ran `hermes tools disable browser` will still get the bundled 0.26.0
install on the next Update, which is exactly the unconditional-install
behavior NousResearch#58687 was filed to stop on Linux. A symmetric Stage-Browser
guard reading $HERMES_HOME\config.yaml's agent.disabled_toolsets field is
the natural follow-up; not bundled here because NousResearch#58687 is itself still
open and that PR is the consumer of the config-awareness infrastructure.

Refs: NousResearch#65701 (sibling), NousResearch#58687 (related -- disabled_toolsets config-
awareness), NousResearch#27224 (manifest API origin), 284e084 (upstream
idle-timeout that 0.26.0 wires).
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
Three issues flagged by the Copilot review on this PR:

1. Double JSON emit on stage failure (Copilot NousResearch#1, NousResearch#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 NousResearch#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.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#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.
waym0reom3ga added a commit to waym0reom3ga/autolycus-agent that referenced this pull request Jul 21, 2026
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.
waym0reom3ga added a commit to waym0reom3ga/autolycus-agent that referenced this pull request Jul 21, 2026
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#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 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants