Skip to content

fix(installers): make a failed install say what went wrong - #158

Open
Aiml3ss wants to merge 3 commits into
mainfrom
claude/nice-ptolemy-47ce54
Open

Aiml3ss wants to merge 3 commits into
mainfrom
claude/nice-ptolemy-47ce54

Conversation

@Aiml3ss

@Aiml3ss Aiml3ss commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

A Windows install failed and left nothing to read. This makes a failing install
legible on both platforms, and fixes two Windows runtime faults found while
auditing that path.

Why the error was invisible. Neither installer wrote a log. Before either can
fail it emits package-manager output, a multi-gigabyte model download, and long
readiness waits -- far more than a console or Terminal window keeps -- so the one
line explaining the failure had scrolled out of the buffer by the time the window
paused. Both shims also paused only when no arguments were passed, so an
argument-bearing run closed outright.

Both installers now record the whole run to install.log, created private to the
current user (0600 on macOS, a single-ACE ACL on Windows) because it records this
machine's paths and, on Windows, its account name. setup.ps1 adds a trap that
reprints the failure and the log path last, after everything that scrolls;
setup.sh names the log from fail(). Install.cmd and Install.command hold
the window open on any failure and name the log -- or say plainly that setup
stopped before one existed, which on Windows is what a Group Policy execution
policy overriding -ExecutionPolicy Bypass produces.

Opening the log is deliberately non-fatal on both. The likeliest reason it fails
is a read-only checkout, and the existing writable-checkout probe has a far
better message a few steps later. An uninstall is excluded on both: it prints a
short plan that never scrolls, and an open log would be one more file it then had
to remove.

Two Windows runtime faults

winget's non-zero success codes. Install-WingetPackage treated any non-zero
exit as fatal, but winget answers "already installed" and "no applicable update"
that way. A machine that already had ffmpeg or Ollama aborted the whole install
over a dependency that was present. The code is now recorded and reported, and
the executable probe that already followed every call is what decides.

Native stderr under $ErrorActionPreference = "Stop". ollama show writes to
stderr for a model that is merely absent -- the expected answer during a first
install. The call redirected its streams with *> $null, and Windows PowerShell
turns redirected native stderr into an error record that Stop makes
terminating. That sat immediately before the step which downloads the model.
Those calls now go through Invoke-Native, which reads the exit code instead.

Why it shipped

windows-smoke.yml only parsed setup.ps1, and parsed it under pwsh while
users run Windows PowerShell 5.1, where StrictMode and native-command error
handling both differ. Not one line of the installer had ever been executed
anywhere. It now runs the paths that change nothing under 5.1 and reports each:

ok: --help
ok: --uninstall dry run
ok: bad arguments rejected
ok: failure banner and install.log

That last line is the fix proving itself on a real Windows host.

What this does not do

It does not identify the specific failure on the reporting machine. Without the
error text that is not knowable, so the two faults above are credible causes, not
confirmed ones. The logging is what makes the next attempt diagnosable.

Ruled out while auditing: all 154 files setup.ps1 requires are present and ship
in the bundle; dictate.py.lock carries correct sys_platform == 'win32'
markers and uv sync --locked passes on a Windows runner; every dictate.py
flag the installer invokes exists; the script parses cleanly.

Grant of Copyright and Patent Rights

Read the Whisper Face Contributor License Agreement
before submitting outside contributions.

  • I own this contribution or have authority to submit it, and I disclosed
    any third-party material and restrictions.
  • I have read and agree to the Whisper Face Contributor License Agreement version 1.0.

Installer parity

  • I classified the Mac and Windows installer impact using
    docs/installer-release-process.md.
  • I updated every affected installer, service template, lockfile, asset,
    default, test, and instruction-or explained why no installer edit is
    required.
  • I did not duplicate runtime source inside an installer.
  • Private state remains preserved on reinstall.

Installer parity: updated both installers and both shims -- setup.ps1,
Install.cmd, setup.sh, Install.command -- because the missing install log
was identical on each. The winget exit-code handling and the native-stderr
hazard are Windows-only; neither has a macOS analogue. Also updated
scripts/windows_bundle.py (forbidden-entry list), .gitignore,
.github/workflows/windows-smoke.yml, and tests/test_installers.py, whose
PERSONAL_FILES constant now covers install.log and so enforces on both
platforms that it cannot be removed without --remove-personal-data.

No runtime source is duplicated; both installers still execute the checkout.
Private state is untouched -- install.log is additive and kept on uninstall
unless --remove-personal-data is passed.

Verification

All of the following pass on the committed tree:

  • uv lock --check --script dictate.py
  • uv run tests/test_parrot_core.py
  • uv run tests/test_voice_compiler.py
  • uv run tests/test_consequence_routing.py
  • uv run tests/test_cleanup_circuit_breaker.py
  • uv run tests/test_benchmark_voice_compiler.py
  • uv run tests/test_benchmark_consequence_routing.py
  • uv run tests/test_benchmark_cleanup_latency.py
  • uv run tests/test_cleanup_proof_recovery.py
  • uv run tests/test_quality_gate.py
  • uv run quality_gate.py
  • uv run tests/test_benchmark_cleanup_proof_recovery.py
  • uv run tests/test_benchmark_asr.py
  • uv run tests/test_benchmark_macos_asr_warm_path.py
  • uv run tests/test_performance_lab.py
  • uv run tests/test_dictate.py
  • uv run tests/test_gui_settings_runtime.py
  • uv run tests/test_insertion_integrity.py
  • uv run tests/test_benchmark_insertion_reliability.py
  • uv run tests/test_compatibility_fingerprint.py
  • uv run tests/test_voice_input_protocol.py
  • uv run tests/test_acoustic_keyword_memory.py
  • uv run tests/test_acoustic_keyword_bias_evaluation.py
  • uv run tests/test_acoustic_keyword_activation.py
  • uv run tests/test_acoustic_calibration.py
  • uv run tests/test_acoustic_calibration_activation.py
  • uv run tests/test_measurement_mode.py
  • uv run tests/test_benchmark_acoustic_calibration.py
  • uv run tests/test_delayed_cleanup_merge.py
  • uv run tests/test_macos_delayed_cleanup_destination.py
  • uv run tests/test_model_wallet.py
  • uv run tests/test_model_wallet_shadow.py
  • uv run tests/test_model_readiness_evidence.py
  • uv run tests/test_point_and_speak_resolver.py
  • uv run tests/test_drop_to_target.py
  • uv run tests/test_macos_drop_to_target_snapshot.py
  • uv run tests/test_voice_objects.py
  • uv run tests/test_spoken_edit_commands_runtime.py
  • uv run tests/test_voice_inbox.py
  • uv run tests/test_demonstration_drafts.py
  • uv run tests/test_competitor_benchmark.py
  • uv run tests/test_public_scorecard.py
  • uv run tests/test_personal_regression.py
  • uv run tests/test_support_bundle.py
  • uv run tests/test_whisper_face_gui.py
  • uv run tests/test_whisper_face_characters.py
  • uv run --locked --script dictate.py --native-gui-smoke-test
  • uv run tests/test_network_egress.py
  • uv run tests/test_supply_chain_integrity.py
  • uv run tests/test_installers.py
  • uv run tests/test_repository_governance.py
  • uv run tests/test_macos_distribution.py
  • uv run tests/test_windows_distribution.py
  • uv run tests/test_safe_update_advisor.py
  • uv run tests/test_side_by_side_update.py
  • uv run tests/test_self_update.py
  • Live Mac or Windows installer verification was run, or the unavailable
    platform is explicitly disclosed.

Live verification. ./setup.sh --verify was run on macOS against these
changes and exercised the new path end to end: it wrote a 0600 install.log
holding the whole run, named that log on failure, and preserved its exit code
through the tee. ./setup.sh --uninstall correctly wrote no log and listed
install.log as an inventory item. The verify itself reports a pre-existing
Parakeet ASR helper is missing on that machine, unrelated to this change.

Disclosed as unavailable: no Windows machine was available, so
.\setup.ps1 --verify was not run by hand. The new Windows PowerShell 5.1 job
in windows-smoke.yml executes those paths on a real windows-latest host
instead, and passes.

🤖 Generated with Claude Code

An install that failed left nothing to read. setup.ps1 wrote no log, and
before it can fail it emits winget output, a 3.4 GB model pull, uv sync, two
model downloads, and up to 180 readiness dots -- so the one line explaining
the failure had scrolled out of the console buffer by the time the window
paused. Install.cmd also paused only when no arguments were passed, so an
argument-bearing run closed outright.

setup.ps1 now records a transcript to install.log, ACL-locked like the other
runtime logs because its header names this machine and account, and a trap
reprints the failure and the log path last, after everything that scrolls.
Install.cmd holds the window open on any failure and names the log -- or says
plainly that setup stopped before one existed, which is what a Group Policy
execution policy overriding -ExecutionPolicy Bypass produces.

Auditing that path turned up two runtime faults.

winget answers "already installed" and "no applicable update" with a non-zero
exit code. Install-WingetPackage treated any non-zero code as fatal, so a
machine that already had ffmpeg or Ollama aborted the whole install over a
dependency that was present. The code is now recorded and reported, and the
executable probe that already followed every call is what decides.

`ollama show` writes to stderr for a model that is merely absent -- the
expected answer during a first install. The call redirected its streams with
*> $null, and Windows PowerShell turns redirected native stderr into an error
record that $ErrorActionPreference = "Stop" makes terminating. That sat
immediately before the step which downloads the model. Those calls now go
through Invoke-Native, which reads the exit code instead.

None of this had ever executed. windows-smoke.yml only parsed setup.ps1, and
parsed it under pwsh while users run Windows PowerShell 5.1, where StrictMode
and native-command error handling both differ. It now runs the paths that
change nothing under 5.1: --help, the dry-run --uninstall, three rejection
cases, and a failing --verify asserted to print the banner and write the log.

install.log joins .gitignore, the Windows bundle's forbidden-entry list, and
the uninstaller's personal-file inventory, so it is never packaged and never
removed unless --remove-personal-data asks for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Aiml3ss and others added 2 commits August 13, 2026 18:20
The step ends by proving that a failing --verify prints the banner and
writes install.log, which means the last native call deliberately exits
non-zero. GitHub appends "exit $LASTEXITCODE" to a powershell step, so the
job failed on that leftover code with every assertion having passed and no
message explaining it -- the same class of invisible failure this branch is
about. It now exits 0 explicitly and names each check as it passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setup.sh had the identical gap: no install log at all. Homebrew, a
multi-gigabyte model download, and the readiness waits emit far more output
than a Terminal window keeps, so a failure's explanation had already scrolled
away by the time it stopped.

Everything after the argument parse is now copied to install.log, created 0600
because it records this machine's paths, and fail() names it. Opening it is
deliberately non-fatal: the likeliest reason for that to fail is a read-only
checkout, and confirm_writable_checkout has a far better message waiting a few
steps later. An uninstall is excluded for the same reason it is on Windows --
it prints a short plan that never scrolls, and an open log would be one more
file it then had to remove.

Install.command now holds the window open on any failure rather than only on a
bare double-click, and names the log, or says plainly that setup stopped
before one existed.

install.log joins the uninstaller's personal-file inventory on both platforms,
so PERSONAL_FILES in tests/test_installers.py now covers it and enforces that
neither installer can remove it without --remove-personal-data.

Verified live on macOS: ./setup.sh --verify writes a 0600 install.log holding
the whole run and names it on failure; ./setup.sh --uninstall writes none and
lists it as an inventory item.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Aiml3ss Aiml3ss changed the title fix(windows): make a failed install say what went wrong fix(installers): make a failed install say what went wrong Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant