diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index f481558b..37cc0701 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -95,48 +95,24 @@ jobs: env: HP_CACHE_EXACT_HIT: ${{ steps.conda_cache_restore.outputs.cache-hit }} run: | - $condaMain = 'C:\Users\Public\Documents\Miniconda3\condabin\conda.bat' - $condaAlt = 'C:\Users\Public\Documents\Miniconda3\Scripts\conda.bat' - $condaBat = if (Test-Path $condaMain) { $condaMain } elseif (Test-Path $condaAlt) { $condaAlt } else { $null } - if ($null -eq $condaBat) { - Write-Host "No conda binary found; fresh install will proceed normally." - } else { - $output = & cmd /c "`"$condaBat`" info" 2>&1 - if ($LASTEXITCODE -ne 0) { - if ($env:HP_CACHE_EXACT_HIT -eq 'true') { - # derived requirement: an EXACT primary-key hit that's corrupted can never be - # replaced in place (a GitHub Actions cache entry's blob is immutable once saved - # under a key) -- see docs/agent-closed-backlog.md's Item 19 (cache-lane - # self-perpetuating-corruption) for the full trace. Keep the original - # skip-this-run behavior for this narrow case; fully closing it needs an explicit - # cache-deletion API call, a smaller follow-on not implemented here. - Write-Host "::warning::Conda binary health check failed (exit=$LASTEXITCODE) on an EXACT cache-key hit; cache corrupted, skipping fast-path tests this run." - 'HP_CACHE_CORRUPTED=1' | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append - } else { - # derived requirement: a restore-keys PREFIX match (the common case -- the cache - # key hashes run_setup.bat, which changes on nearly every PR) is not a guarantee - # the restored blob is still valid. Treating this like HP_CACHE_CORRUPTED=1 was - # the actual bug: the ONLY step that can do a fresh install is gated on that same - # flag, so a poisoned blob could never be replaced -- a permanent, self- - # perpetuating loop. Fix: treat it like a genuine cache miss instead -- delete the - # stale directory and let the run fall through to a real fresh install and a real - # fresh save under the current key, breaking the loop. - Write-Host "::warning::Conda binary health check failed (exit=$LASTEXITCODE) on a restore-keys prefix match; deleting stale cache directory and proceeding as a fresh install." - Remove-Item -LiteralPath 'C:\Users\Public\Documents\Miniconda3' -Recurse -Force -ErrorAction SilentlyContinue - if (Test-Path 'C:\Users\Public\Documents\Miniconda3') { - # derived requirement: same AV/indexer file-lock hazard class already documented - # for :try_embed_fallback's own directory swap in run_setup.bat -- if deletion - # didn't fully succeed, do not proceed into an uncertain half-deleted state; - # fall back to the original, safe skip-this-run behavior instead. - Write-Host "::warning::Stale cache directory could not be fully removed (possible file lock); falling back to HP_CACHE_CORRUPTED=1 for this run." - 'HP_CACHE_CORRUPTED=1' | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append - } else { - Write-Host "Stale cache directory removed; fresh install will proceed normally." - } - } - } else { - Write-Host "Conda health OK: $output" - } + # derived requirement: the actual health-check-and-heal logic now lives in + # tools/ci_cache_selfheal.ps1 so tests/test_ci_cache_selfheal.ps1 can exercise it + # deterministically on every CI run (a GATING lane), independent of whether GitHub's + # own cache happens to be organically corrupted this run -- see that file and + # docs/agent-closed-backlog.md's Item 19 entry for why the ambient `cache` lane alone + # (informational, job-level continue-on-error) was not enough signal on its own. + $exactArgs = @() + if ($env:HP_CACHE_EXACT_HIT -eq 'true') { $exactArgs = @('-ExactHit') } + & .\tools\ci_cache_selfheal.ps1 -CondaDir 'C:\Users\Public\Documents\Miniconda3' @exactArgs + $rc = $LASTEXITCODE + if ($rc -eq 1 -or $rc -eq 3) { + 'HP_CACHE_CORRUPTED=1' | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append + } + if ($rc -eq 3) { + 'HP_CACHE_SELFHEAL_FAILED=1' | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append + } + if ($rc -eq 2 -or $rc -eq 3) { + 'HP_CACHE_SELFHEAL_ATTEMPTED=1' | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append } exit 0 # health check is informational; never fail this step @@ -152,6 +128,49 @@ jobs: $row | Add-Content 'tests\~test-results.ndjson' -Encoding Ascii $row | Add-Content 'ci_test_results.ndjson' -Encoding Ascii + - name: "Record cache self-heal outcome (visibility row; never fails)" + # derived requirement: this is the gap the owner flagged directly -- when the self-heal + # SUCCEEDS (the ordinary case), HP_CACHE_CORRUPTED is never set, so no NDJSON row was + # ever emitted saying "this run actually had to self-heal a corrupted cache" -- the run + # just looked like an ordinary fresh install, with the only trace being a ::warning:: + # buried in raw job logs. Emitting this unconditionally whenever the self-heal branch is + # entered (success OR failure) makes both outcomes queryable on the diagnostics site over + # time, instead of requiring a manual raw-log dig to notice either one. + if: ${{ !cancelled() && env.HP_CACHE_SELFHEAL_ATTEMPTED == '1' }} + shell: pwsh + run: | + if (-not (Test-Path 'tests')) { New-Item -ItemType Directory 'tests' | Out-Null } + $healed = ($env:HP_CACHE_SELFHEAL_FAILED -ne '1') + $row = [ordered]@{ + id = 'self.cache.selfheal.fired' + pass = $true + lane = 'cache' + desc = 'Restored cache failed its health check on a restore-keys prefix match; self-heal (delete stale dir, fall through to fresh install) was attempted' + details = [ordered]@{ healed = $healed } + } | ConvertTo-Json -Compress + $row | Add-Content 'tests\~test-results.ndjson' -Encoding Ascii + $row | Add-Content 'ci_test_results.ndjson' -Encoding Ascii + + - name: "Enforce cache self-heal success (Item 19 follow-on)" + # derived requirement: an ordinary corrupted-and-healed cache is routine infra noise and + # stays informational -- run_setup.bat hashes into the cache key on nearly every PR, so + # this fires often and is expected. But the self-heal ITSELF failing to clear the stale + # directory is not routine noise: it means this repo's own Item 19 fix has regressed back + # into the exact pre-fix "always corrupted, never self-heals" trap that item existed to + # close. This step's own failure is still absorbed by the `cache` lane's job-level + # continue-on-error (see CLAUDE.md's CI lane gating maturity notes -- this lane stays + # intentionally non-gating for ordinary organic flakiness), so on its own this does not + # block a PR; it exists so the failure is a loud, explicit ::error:: annotation and a red + # step marker instead of a buried ::warning::. tests/test_ci_cache_selfheal.ps1 (a GATING + # regression test wired into the `real` lane) is what actually catches a regression in + # this logic on every single CI run, independent of whether GitHub's own cache happens to + # be organically corrupted this run. + if: ${{ !cancelled() && env.HP_CACHE_SELFHEAL_FAILED == '1' }} + shell: pwsh + run: | + Write-Host "::error::Cache self-heal failed to clear the stale Miniconda3 directory -- see 'Validate restored conda binary' step output above." + exit 1 + # probe fires in real, conda-full, uv, and contract-uv* lanes; cache lane skips it intentionally - name: Enable Miniconda probe (real/conda-full/uv/contract-uv mode) if: ${{ matrix.mode == 'real' || matrix.mode == 'conda-full' || matrix.mode == 'uv' || matrix.mode == 'contract-uv' || matrix.mode == 'contract-uv-fail' }} @@ -253,6 +272,17 @@ jobs: } } + # derived requirement: wired into `real` specifically (a GATING lane, not in the + # job-level continue-on-error list at the top of this file) so a regression in + # tools/ci_cache_selfheal.ps1 actually fails CI -- see that file and + # docs/agent-closed-backlog.md's Item 19 entry for why the ambient `cache` lane alone + # (informational, only fires on organic corruption) was not sufficient signal on its own. + - name: "Self-test: cache-lane self-heal logic (real lane only, GATING)" + if: ${{ matrix.mode == 'real' }} + shell: pwsh + run: | + & tests\test_ci_cache_selfheal.ps1 + - name: "Pre-bootstrap: ensure Python file exists" shell: pwsh run: | diff --git a/docs/agent-closed-backlog.md b/docs/agent-closed-backlog.md index 901d1d3e..e21addb1 100644 --- a/docs/agent-closed-backlog.md +++ b/docs/agent-closed-backlog.md @@ -13,6 +13,13 @@ CLAUDE.md's ~4200 lines (>60%) despite being pure historical record with no forw action attached to any entry. Nothing here needs re-reading by default; it exists so a specific past decision or fix can be looked up when its details actually matter. +**A `docs/demo-bootstrapper-output.md` "Part N, Scenario N" citation below reflects that doc's +structure AT THE TIME the entry was written, not necessarily its current numbering** -- that file +went through a flow-only reorg pass (2026-08-02) that renumbered every Part and Scenario, and will +likely be reorganized again as it grows. Treat a Part/Scenario citation here as "roughly where to +look," not a precise current coordinate; this file is an append-only historical record, so its own +entries are not retroactively renumbered to track that doc's current structure. + **Two sections below.** "Closed Active Backlog Items" holds items that were promoted out of `CLAUDE.md`'s own "Active Backlog" section once fully resolved (each keeps its original item number for cross-reference stability -- other docs cite these by number). "Closed Backlog" is @@ -755,6 +762,52 @@ this belongs to). opportunistically across this and future PRs' own `cache`-lane runs, not via a dedicated verification loop. + **Follow-on shipped 2026-08-02, prompted by the owner directly asking "is the fix holding" and + then "make it go red so it actually helps something."** The "observed opportunistically" plan + above turned out to have two real gaps, both found while answering the first question + honestly rather than assuming: (1) verification meant manually pulling raw job logs each time + -- nothing queryable recorded whether a run's self-heal had actually fired (a corrupted + restore-keys-prefix-match cache with a successful heal never sets `HP_CACHE_CORRUPTED`, so no + NDJSON row was ever emitted for it -- the only trace was a `::warning::` buried in the log); + (2) even a hypothetical step-level failure inside the `cache` lane could never have surfaced as + a real CI failure, because that whole lane is job-level `continue-on-error` (see the top of + `batch-check.yml`) -- confirmed directly against the first real post-fix run (`30684739923`, + job `91328449387`, commit `40e6187`/PR #409): the corrupted-prefix-match branch fired for real + (`##[warning]Conda binary health check failed (exit=1) on a restore-keys prefix match; deleting + stale cache directory and proceeding as a fresh install.` at 04:55:51Z, `Stale cache directory + removed; fresh install will proceed normally.` at 04:56:21Z, `Cache saved with key: + win-Windows-py311b-conda-...` at 05:16:43Z) -- genuine end-to-end proof the fix works, found + only by pulling the raw log by hand, not by anything CI itself surfaced. + - Extracted the inline health-check-and-heal PowerShell out of `batch-check.yml`'s "Validate + restored conda binary" step into `tools/ci_cache_selfheal.ps1`, a small parameterized script + (`-CondaDir`, `-ExactHit`) with 4 distinct exit codes (0=healthy/no-op, 1=exact-hit-corrupted + [unchanged accepted gap], 2=prefix-corrupted-and-healed, 3=prefix-corrupted-heal-FAILED -- + the new case: the stale directory could not be fully cleared, meaning this fix has regressed + back toward its own pre-fix trap). + - Added `tests/test_ci_cache_selfheal.ps1`: a deterministic regression test exercising all 4 + exit codes against a scratch temp directory with fake `conda.bat` stand-ins (including a + genuine locked-file reproduction of the heal-FAILED case via a held `FileStream` handle) -- + no dependency on GitHub's cache ever being organically corrupted. Wired into the `real` + lane specifically (a GATING lane, not in the job-level `continue-on-error` list), so a future + regression in this logic fails CI for real, unlike the ambient `cache` lane which cannot. + Windows-only (shells out to `conda.bat` via `cmd.exe`; the lock scenario needs Windows + file-locking semantics), matching this repo's usual `$IsWindows`-gated-skip convention. + - Added `self.cache.selfheal.fired`: an always-`pass:true` visibility row emitted by the + ambient `cache` lane itself whenever the self-heal branch is entered (success or failure), + recording the real outcome in `details.healed` -- closes gap (1) above; an organic + occurrence is now queryable on the diagnostics site instead of requiring a raw-log dig. + - Added a loud `::error::` tripwire step (`Enforce cache self-heal success`) for the + heal-FAILED sub-case specifically, mirroring `diag.conda.available.gate`'s established + "default to a loud failure, not a silent one" pattern -- explicitly documented as *not* a + substitute for the gating test above (still absorbed by the `cache` lane's own job-level + `continue-on-error`), only a clearer annotation than the `::warning::` it replaces for that + one case. + - Deliberately did NOT flip the `cache` lane itself to gating, and did NOT touch the still-open + exact-key-hit-corrupted gap (still needs the `gh cache delete`/cache-deletion-API follow-on + noted above) -- both out of scope for this pass; the ask was specifically "make a regression + in the self-heal mechanism visible and blocking," not "make ordinary organic cache flakiness + block PRs." + ### Item 15 (closed 2026-08-01) - **`:exe_smokerun_hints`'s diagnostic re-run of a freshly-failed EXE had no timeout, unlike every diff --git a/docs/agent-ndjson.md b/docs/agent-ndjson.md index 00a1fe30..84df13aa 100644 --- a/docs/agent-ndjson.md +++ b/docs/agent-ndjson.md @@ -27,6 +27,7 @@ self.failfast.probe.fastfail, self.failfast.probe.alive, self.failfast.probe, self.checkpoint.accept, self.checkpoint.decline, self.entry.entry1, self.entry.entryA, self.entry.entryB, self.entry.entryC, self.entry.entryD, self.entry.helper.invoke.absent, self.entry.results, self.entry.spaced-path, self.entry.picker, +self.entry.picker.overflow, self.entry.req011.crossdir, self.entry.req011.sameDir, self.isolation.req010.pythonpath, entry.single.direct, entry.expected, helper.invoke, self.envname.hyphen, self.size.tripwire, @@ -163,7 +164,7 @@ visa.detect, emit.helpers, env.state.write, dep.check.parse_lock, dp.compat, prep.multi.constraint, batch.paren.balance, env.foldername, conda.path, conda.url, env.mode, self.warnfix.platform_filter, self.exe.smokerun, helper.find_entry.syntax, entry.helper.ok, -self.cache.corrupted, self.cache.bootstrap.failed, +self.cache.corrupted, self.cache.bootstrap.failed, self.cache.selfheal.fired, meta.env.mode, workflow.lint, version.metadata, host.env.os, host.env.ps, host.env.python, @@ -557,6 +558,34 @@ substitute for a human's own interactive session). self.interactive.stdin.roundtrip ``` +## selfapps-cache-selfheal NDJSON rows (test_ci_cache_selfheal.ps1, `real` lane only, GATING) + +Item 19 follow-on (docs/agent-closed-backlog.md): the cache-lane self-heal logic +(`tools/ci_cache_selfheal.ps1`) previously had no deterministic CI coverage -- the ambient +`cache` lane only reaches it when GitHub's own cache happens to be organically corrupted, and +that lane is entirely informational (job-level `continue-on-error`) besides, so a regression in +the self-heal logic itself would ship silently. This test exercises all 4 branches of that +script directly against a scratch temp directory with fake `condabin\conda.bat` stand-ins (no +real conda/network dependency), wired into `real` -- a GATING lane -- so a regression actually +fails CI. Windows-only (the script under test shells out to `conda.bat` via `cmd.exe`, and the +locked-directory scenario needs Windows file-locking semantics); skips with `skip=true` on +non-Windows via the bare `self.ci.cache_selfheal` row. + +``` +self.ci.cache_selfheal, +self.ci.cache_selfheal.healthy, self.ci.cache_selfheal.prefix_healed, +self.ci.cache_selfheal.exact_hit_corrupted, self.ci.cache_selfheal.prefix_heal_failed, +self.ci.cache_selfheal.no_binary +``` + +`self.cache.selfheal.fired` (inline `batch-check.yml`, `cache` lane, `HP_CACHE_SELFHEAL_ATTEMPTED`-gated) +is the companion VISIBILITY row -- unlike the deterministic test above, this +fires only when the AMBIENT `cache` lane's own restored cache is organically corrupted on a +restore-keys prefix match, and records whether that real self-heal attempt actually succeeded +(`details.healed`). Always `pass:true` (informational, matching `self.cache.corrupted`'s own +convention) -- its purpose is to make an organic occurrence queryable on the diagnostics site +over time instead of requiring a raw-log dig to notice it happened at all, not to gate. + --- ## Key facts for debugging missing rows diff --git a/docs/demo-bootstrapper-output.md b/docs/demo-bootstrapper-output.md index 6d2e87e6..44df1157 100644 --- a/docs/demo-bootstrapper-output.md +++ b/docs/demo-bootstrapper-output.md @@ -9,31 +9,34 @@ in place; don't keep the old one around for context. Ongoing investigation notes day-to-day refinement-pass checks belong in `docs/agent-scratchlog.md` (internal working notes), not here. Unresolved design questions belong in `docs/open-questions.md`, not here. -**Sourcing convention:** every quoted block is either copied verbatim from a real GitHub Actions -job log (cited with run ID, job ID, lane, and test file) or, where noted, taken directly from -`run_setup.bat`'s current source because no CI run has exercised that exact wording yet -- always -labeled explicitly which case applies, never presented as a real capture when it isn't. - -**Scope:** grouped by feature area, roughly in the order each was reviewed (now eight Parts, not -the original two -- this paragraph covers only the first two below since they were the doc's -starting point; see the table of contents for the full current list). Part I covers the AV-Safe -Build Path work (Tier A Nuitka fallback, its interaction with hidden-import auto-recovery, and the -requirement-9 optimized-build offer). Part II covers the CLI-interactivity plan -(`docs/plan-cli-interactive-verification.md`): live-tee verification, argv passthrough (REQ-026), -and honest ambiguous-exit messaging (REQ-027). Extend with a new Part as new feature areas get -reviewed, rather than growing any existing Part indefinitely. - -**TODO for the next reorg pass (owner request, 2026-08-01, not done yet -- flow only, no content -change):** the current ordering front-loads two fairly narrow/advanced topics (Part I, Part II) -before the reader ever sees the basic happy path. Move Scenario 38 ("No `.py` files at all -- the -graceful `no_python_files` exit," currently the very LAST scenario in the doc, in Part VIII) up to -the front -- it is the simplest, most foundational case (what happens before anything else can -even run) and reads more naturally as an early scenario than a footnote at the end. Push Part I -and Part II further down, or to the end, to make room. This is a pure reordering/flow pass: move -the existing sections, then update the table of contents anchors and any in-doc cross-references -that name a Part by number (e.g. "see Part I" / "see Part III") so they still point at the right -content after the move -- no scenario text itself should change. Sized for its own dedicated pass, -not a drive-by edit alongside unrelated content changes. +**House style, stated once here rather than repeated per-scenario:** this doc describes CURRENT +behavior, not a changelog of how it got that way. Avoid narrating this document's own revision +history inside scenario prose (phrasing like "not yet re-confirmed against a fresh capture," "the +earlier scan of this scenario quoted only X," or hedging a quote as "the CURRENT shipped wording" +as if some other wording still mattered) -- that's bookkeeping for whoever edits this file next, +not something a reader trying to understand the bootstrapper needs. Likewise, don't cite this +repo's own internal backlog-item numbers or "Active Backlog item N" bookkeeping as part of the +user-facing description; point to `docs/agent-lessons-learned.md` / +`docs/agent-interconnect.md` / `docs/agent-closed-backlog.md` directly instead if a pointer is +genuinely useful. **A real historical GOTCHA is a different thing and stays welcome** -- a bug that +shaped current behavior, worth knowing so a reader doesn't rediscover it the hard way, is exactly +the kind of content this doc wants (see Scenario 29, Scenario 39 for two kept in full). The +distinction is between explaining what the reader is looking at right now versus narrating how +this document itself was assembled. + +**Sourcing convention:** every quoted block carries one explicit provenance label -- REAL CI +CAPTURE (copied verbatim from a real GitHub Actions job log, cited with run ID, job ID, lane, and +test file), a source excerpt (taken directly from `run_setup.bat`'s current source because no CI +run has exercised that exact wording yet), or, for the composite walkthroughs in Part VII, +`[Extrapolated Branch]` (assembled by splicing several independently-real fragments together, each +already cited in its own originating scenario). A source excerpt or a composite splice is never +described as a job log or presented as a single real capture. + +**Scope:** grouped by feature area, ordered roughly the way a real user would actually encounter +each area -- the default happy path and its immediate variations first, narrower and more advanced +topics last -- rather than the order each was originally reviewed. Nine Parts total; see the table +of contents for the full current list. Extend with a new Part as new feature areas get reviewed, +rather than growing any existing Part indefinitely. **Console vs. `~setup.log`:** the bootstrapper writes to two different places that are easy to conflate: @@ -47,2466 +50,2746 @@ conflate: ## Table of contents -- [Part I: AV-Safe Build Path (Nuitka fallback)](#part-i-av-safe-build-path-nuitka-fallback) - - [Scenario 1: PyInstaller build fails, Tier A (Nuitka) fallback succeeds](#scenario-1-pyinstaller-build-fails-tier-a-nuitka-fallback-succeeds) - - [Scenario 2: PyInstaller build fails, Tier A fallback ALSO fails (tier exhaustion)](#scenario-2-pyinstaller-build-fails-tier-a-fallback-also-fails-tier-exhaustion) - - [2a. `execfail`](#2a-execfail----the-pyinstaller-build-command-itself-fails) - - [2b. `output_vanish`](#2b-output_vanish----pyinstaller-succeeds-then-the-exe-disappears-immediately) - - [Scenario 3: Tier A + hidden-import auto-recovery skip guard](#scenario-3-tier-a-hidden-import-auto-recovery-skip-guard) - - [Scenario 4: Requirement 9 -- elective "want an optimized build too?" offer](#scenario-4-requirement-9----elective-want-an-optimized-build-too-offer) - - [4a. `accept`](#4a-accept----a-real-optimized-build-succeeds-and-is-swapped-in) - - [4b. `forcefail`](#4b-forcefail----accepted-but-the-build-fails-original-exe-is-left-untouched) - - [4c. `swapfail`](#4c-swapfail----verified-build-but-the-final-swap-step-fails-original-exe-is-left-untouched) - - [4d. `decline`](#4d-decline----defaultci-path-prompt-shown-but-nothing-built) - - [Reactive-only failure hint](#reactive-only-failure-hint-both-tier-a-and-requirement-9s-real-build-failure-paths) -- [Part II: CLI interactivity, argv passthrough & honest messaging](#part-ii-cli-interactivity-argv-passthrough-honest-messaging) - - [Scenario 5: Interactive verification -- live-tee, activity-aware kill, and the quit-prompt hint](#scenario-5-interactive-verification----live-tee-activity-aware-kill-and-the-quit-prompt-hint) - - [Scenario 6: Argv passthrough (REQ-026) -- launch arguments through the bootstrapper](#scenario-6-argv-passthrough-req-026----launch-arguments-through-the-bootstrapper) - - [Scenario 7: Honest ambiguous-exit messaging (REQ-027)](#scenario-7-honest-ambiguous-exit-messaging-req-027) - - [7a. No-EXE path, interpreter also failed](#7a-no-exe-path-interpreter-also-failed) - - [7b. Cached-EXE fast path, kept despite a non-zero exit](#7b-cached-exe-fast-path-kept-despite-a-non-zero-exit) -- [Part III: Default double-click happy path (uv-first, fresh machine, zero flags)](#part-iii-default-double-click-happy-path-uv-first-fresh-machine-zero-flags) - - [Scenario 8: Pre-flight guards, lock acquisition, and entry detection on a clean run](#scenario-8-pre-flight-guards-lock-acquisition-and-entry-detection-on-a-clean-run) - - [Scenario 9: Provider acquisition and dependency install (uv-first)](#scenario-9-provider-acquisition-and-dependency-install-uv-first) - - [Scenario 10: Build, verify, and the final status panel](#scenario-10-build-verify-and-the-final-status-panel) - - [Scenario 11: The two elective prompts a real user faces after every successful run](#scenario-11-the-two-elective-prompts-a-real-user-faces-after-every-successful-run) -- [Part IV: Second run, nothing changed (repeat-run fast paths)](#part-iv-second-run-nothing-changed-repeat-run-fast-paths) - - [Scenario 12: The EXE fast path (nothing changed at all)](#scenario-12-the-exe-fast-path-nothing-changed-at-all) - - [Scenario 13: Source touched just enough to force a rebuild, but the environment is reused](#scenario-13-source-touched-just-enough-to-force-a-rebuild-but-the-environment-is-reused) -- [Part V: User configuration and CLI overrides](#part-v-user-configuration-and-cli-overrides) - - [Scenario 14: `PVW_PYTHON_EXE` / `PVW_UV_EXE` / `PVW_TARGET_PY` / `PVW_WORKSPACE`](#scenario-14-pvw_python_exe--pvw_uv_exe--pvw_target_py--pvw_workspace) - - [Scenario 15: `PVW_CONDA_EXE` and its interaction with the conda self-heal flow](#scenario-15-pvw_conda_exe-and-its-interaction-with-the-conda-self-heal-flow) - - [Scenario 16: Drag-and-drop / CLI entry-file override (REQ-011 same-directory rule + REQ-002 priority)](#scenario-16-drag-and-drop--cli-entry-file-override-req-011-same-directory-rule--req-002-priority) -- [Part VI: Adversarial and recovery branches](#part-vi-adversarial-and-recovery-branches) - - [Scenario 17: Network connectivity check and transient-retry (REQ-013 + REQ-022)](#scenario-17-network-connectivity-check-and-transient-retry-req-013--req-022) - - [Scenario 18: Corrupted-conda self-heal (detect / decline / accept)](#scenario-18-corrupted-conda-self-heal-detect--decline--accept) - - [Scenario 19: Miniconda install chain (AllUsers -> JustMe -> both-failed)](#scenario-19-miniconda-install-chain-allusers---justme---both-failed) - - [Scenario 20: Standalone embed-tier download (REQ-009 Tier 5) -- decline and real success](#scenario-20-standalone-embed-tier-download-req-009-tier-5----decline-and-real-success) - - [Scenario 21: REQ-009 provider cascade -- one real run showing the FULL chain](#scenario-21-req-009-provider-cascade----one-real-run-showing-the-full-chain) - - [Scenario 22: `--hidden-import` auto-recovery (success and exhaustion)](#scenario-22---hidden-import-auto-recovery-success-and-exhaustion) - - [Scenario 23: Warnfix repair loop (success, and the failure that feeds the cascade)](#scenario-23-warnfix-repair-loop-success-and-the-failure-that-feeds-the-cascade) - - [Scenario 24: Pre-flight guards actually firing](#scenario-24-pre-flight-guards-actually-firing) - - [Scenario 25: Concurrent-instance lock contention (REQ-024)](#scenario-25-concurrent-instance-lock-contention-req-024) -- [Part VII: Remaining branches (dependency source precedence, write-back, and misc)](#part-vii-remaining-branches-dependency-source-precedence-write-back-and-misc) - - [Scenario 26: Git config merge (`.gitignore`/`.gitattributes`, REQ-015)](#scenario-26-git-config-merge-gitignoregitattributes-req-015) - - [Scenario 27: Python-version precedence (REQ-004) and dependency-source precedence (`pyproject.toml`)](#scenario-27-python-version-precedence-req-004-and-dependency-source-precedence-pyprojecttoml) - - [Scenario 28: PEP 723 dependency write-back (REQ-005.11) -- the fresh-install trigger](#scenario-28-pep-723-dependency-write-back-req-00511----the-fresh-install-trigger) - - [Scenario 29: `HP_PVW_KNOWN_IDEMPOTENT` execute-mode discovery (REQ-005.13)](#scenario-29-hp_pvw_known_idempotent-execute-mode-discovery-req-00513) - - [Scenario 30: NI-VISA detection and install outcome (REQ-008)](#scenario-30-ni-visa-detection-and-install-outcome-req-008) - - [Scenario 31: pandas/openpyxl heuristic dependency augmentation (REQ-005.8)](#scenario-31-pandasopenpyxl-heuristic-dependency-augmentation-req-0058) - - [Scenario 32: Conda base periodic update](#scenario-32-conda-base-periodic-update) - - [Scenario 33: REQ-014 system-Python consent -- ACCEPT](#scenario-33-req-014-system-python-consent----accept) -- [Part VIII: Additional branches found in a full-file sweep](#part-viii-additional-branches-found-in-a-full-file-sweep) - - [Scenario 34: Interactive entry picker -- multiple `.py` files, no clear winner (REQ-002)](#scenario-34-interactive-entry-picker----multiple-py-files-no-clear-winner-req-002) - - [Scenario 35: Pre-flight syntax-error rejection (REQ-021), and a real bug it exposed](#scenario-35-pre-flight-syntax-error-rejection-req-021-and-a-real-bug-it-exposed) - - [Scenario 36: REQ-007 system-Python build consent, and the resulting no-EXE interpreter path](#scenario-36-req-007-system-python-build-consent-and-the-resulting-no-exe-interpreter-path) - - [Scenario 37: EXE smoke-run diagnostic hints (companion to Scenario 22)](#scenario-37-exe-smoke-run-diagnostic-hints-companion-to-scenario-22) - - [Scenario 38: No `.py` files at all -- the graceful `no_python_files` exit](#scenario-38-no-py-files-at-all----the-graceful-no_python_files-exit) +- [Part I: Default double-click happy path (uv-first, fresh machine, zero flags)](#part-i-default-double-click-happy-path-uv-first-fresh-machine-zero-flags) + - [Scenario 1: No `.py` files at all -- the graceful `no_python_files` exit](#scenario-1-no-py-files-at-all----the-graceful-no_python_files-exit) + - [Scenario 2: Pre-flight guards, lock acquisition, and entry detection on a clean run](#scenario-2-pre-flight-guards-lock-acquisition-and-entry-detection-on-a-clean-run) + - [Scenario 3: Provider acquisition and dependency install (uv-first)](#scenario-3-provider-acquisition-and-dependency-install-uv-first) + - [Scenario 4: Build, verify, and the final status panel](#scenario-4-build-verify-and-the-final-status-panel) + - [Scenario 5: The two elective prompts a real user faces after every successful run](#scenario-5-the-two-elective-prompts-a-real-user-faces-after-every-successful-run) +- [Part II: Second run, nothing changed (repeat-run fast paths)](#part-ii-second-run-nothing-changed-repeat-run-fast-paths) + - [Scenario 6: The EXE fast path (nothing changed at all)](#scenario-6-the-exe-fast-path-nothing-changed-at-all) + - [Scenario 7: Source touched just enough to force a rebuild, but the environment is reused](#scenario-7-source-touched-just-enough-to-force-a-rebuild-but-the-environment-is-reused) +- [Part III: User configuration and CLI overrides](#part-iii-user-configuration-and-cli-overrides) + - [Scenario 8: `PVW_PYTHON_EXE` / `PVW_UV_EXE` / `PVW_TARGET_PY` / `PVW_WORKSPACE`](#scenario-8-pvw_python_exe--pvw_uv_exe--pvw_target_py--pvw_workspace) + - [Scenario 9: `PVW_CONDA_EXE` and its interaction with the conda self-heal flow](#scenario-9-pvw_conda_exe-and-its-interaction-with-the-conda-self-heal-flow) + - [Scenario 10: Drag-and-drop / CLI entry-file override (REQ-011 same-directory rule + REQ-002 priority)](#scenario-10-drag-and-drop--cli-entry-file-override-req-011-same-directory-rule--req-002-priority) +- [Part IV: Adversarial and recovery branches](#part-iv-adversarial-and-recovery-branches) + - [Scenario 11: Network connectivity check and transient-retry (REQ-013 + REQ-022)](#scenario-11-network-connectivity-check-and-transient-retry-req-013--req-022) + - [Scenario 12: Corrupted-conda self-heal (detect / decline / accept)](#scenario-12-corrupted-conda-self-heal-detect--decline--accept) + - [Scenario 13: Miniconda install chain (AllUsers -> JustMe -> both-failed)](#scenario-13-miniconda-install-chain-allusers---justme---both-failed) + - [Scenario 14: Standalone embed-tier download (REQ-009 Tier 5) -- decline and real success](#scenario-14-standalone-embed-tier-download-req-009-tier-5----decline-and-real-success) + - [Scenario 15: REQ-009 provider cascade -- one real run showing the FULL chain](#scenario-15-req-009-provider-cascade----one-real-run-showing-the-full-chain) + - [Scenario 16: `--hidden-import` auto-recovery (success and exhaustion)](#scenario-16---hidden-import-auto-recovery-success-and-exhaustion) + - [Scenario 17: Warnfix repair loop (success, and the failure that feeds the cascade)](#scenario-17-warnfix-repair-loop-success-and-the-failure-that-feeds-the-cascade) + - [Scenario 18: Pre-flight guards actually firing](#scenario-18-pre-flight-guards-actually-firing) + - [Scenario 19: Concurrent-instance lock contention (REQ-024)](#scenario-19-concurrent-instance-lock-contention-req-024) +- [Part V: Remaining branches (dependency source precedence, write-back, and misc)](#part-v-remaining-branches-dependency-source-precedence-write-back-and-misc) + - [Scenario 20: Git config merge (`.gitignore`/`.gitattributes`, REQ-015)](#scenario-20-git-config-merge-gitignoregitattributes-req-015) + - [Scenario 21: Python-version precedence (REQ-004) and dependency-source precedence (`pyproject.toml`)](#scenario-21-python-version-precedence-req-004-and-dependency-source-precedence-pyprojecttoml) + - [Scenario 22: PEP 723 dependency write-back (REQ-005.11) -- the fresh-install trigger](#scenario-22-pep-723-dependency-write-back-req-00511----the-fresh-install-trigger) + - [Scenario 23: `HP_PVW_KNOWN_IDEMPOTENT` execute-mode discovery (REQ-005.13)](#scenario-23-hp_pvw_known_idempotent-execute-mode-discovery-req-00513) + - [Scenario 24: NI-VISA detection and install outcome (REQ-008)](#scenario-24-ni-visa-detection-and-install-outcome-req-008) + - [Scenario 25: pandas/openpyxl heuristic dependency augmentation (REQ-005.8)](#scenario-25-pandasopenpyxl-heuristic-dependency-augmentation-req-0058) + - [Scenario 26: Conda base periodic update](#scenario-26-conda-base-periodic-update) + - [Scenario 27: REQ-014 system-Python consent -- ACCEPT](#scenario-27-req-014-system-python-consent----accept) +- [Part VI: Additional branches found in a full-file sweep](#part-vi-additional-branches-found-in-a-full-file-sweep) + - [Scenario 28: Interactive entry picker -- multiple `.py` files, no clear winner (REQ-002)](#scenario-28-interactive-entry-picker----multiple-py-files-no-clear-winner-req-002) + - [Scenario 29: Pre-flight syntax-error rejection (REQ-021), and a real bug it exposed](#scenario-29-pre-flight-syntax-error-rejection-req-021-and-a-real-bug-it-exposed) + - [Scenario 30: REQ-007 system-Python build consent, and the resulting no-EXE interpreter path](#scenario-30-req-007-system-python-build-consent-and-the-resulting-no-exe-interpreter-path) + - [Scenario 31: EXE smoke-run diagnostic hints (companion to Scenario 16)](#scenario-31-exe-smoke-run-diagnostic-hints-companion-to-scenario-16) +- [Part VII: Full startup-to-shutdown walkthroughs](#part-vii-full-startup-to-shutdown-walkthroughs) + - [Scenario 32: Full walkthrough -- the ordinary happy path, start to shutdown](#scenario-32-full-walkthrough----the-ordinary-happy-path-start-to-shutdown) + - [Scenario 33: Full walkthrough -- uv can't resolve a dependency, cascades to conda, which does](#scenario-33-full-walkthrough----uv-cant-resolve-a-dependency-cascades-to-conda-which-does) + - [Scenario 34: Full walkthrough -- warnfix repair and rebuild, start to finish](#scenario-34-full-walkthrough----warnfix-repair-and-rebuild-start-to-finish) + - [Scenario 35: Full walkthrough -- `--hidden-import` auto-recovery succeeds on the first rebuild](#scenario-35-full-walkthrough------hidden-import-auto-recovery-succeeds-on-the-first-rebuild) + - [Scenario 36: Full walkthrough -- `HP_PVW_KNOWN_IDEMPOTENT`, with the actual input and output files](#scenario-36-full-walkthrough----hp_pvw_known_idempotent-with-the-actual-input-and-output-files) +- [Part VIII: AV-Safe Build Path (Nuitka fallback)](#part-viii-av-safe-build-path-nuitka-fallback) + - [Scenario 37: PyInstaller build fails, Tier A (Nuitka) fallback succeeds](#scenario-37-pyinstaller-build-fails-tier-a-nuitka-fallback-succeeds) + - [Scenario 38: PyInstaller build fails, Tier A fallback ALSO fails (tier exhaustion)](#scenario-38-pyinstaller-build-fails-tier-a-fallback-also-fails-tier-exhaustion) + - [38a. `execfail` -- the PyInstaller build command itself fails](#38a-execfail----the-pyinstaller-build-command-itself-fails) + - [38b. `output_vanish` -- PyInstaller succeeds, then the EXE disappears immediately](#38b-output_vanish----pyinstaller-succeeds-then-the-exe-disappears-immediately) + - [Scenario 39: Tier A + hidden-import auto-recovery skip guard](#scenario-39-tier-a--hidden-import-auto-recovery-skip-guard) + - [Scenario 40: Requirement 9 -- elective "want an optimized build too?" offer](#scenario-40-requirement-9----elective-want-an-optimized-build-too-offer) + - [40a. `accept` -- a real optimized build succeeds and is swapped in](#40a-accept----a-real-optimized-build-succeeds-and-is-swapped-in) + - [40b. `forcefail` -- accepted, but the build fails; original EXE is left untouched](#40b-forcefail----accepted-but-the-build-fails-original-exe-is-left-untouched) + - [40c. `swapfail` -- verified build, but the final swap step fails; original EXE is left untouched](#40c-swapfail----verified-build-but-the-final-swap-step-fails-original-exe-is-left-untouched) + - [40d. `decline` -- default/CI path, prompt shown but nothing built](#40d-decline----defaultci-path-prompt-shown-but-nothing-built) + - [Reactive-only failure hint (both Tier A and requirement 9's real-build-failure paths)](#reactive-only-failure-hint-both-tier-a-and-requirement-9s-real-build-failure-paths) +- [Part IX: CLI interactivity, argv passthrough & honest messaging](#part-ix-cli-interactivity-argv-passthrough--honest-messaging) + - [Scenario 41: Interactive verification -- live-tee, activity-aware kill, and the quit-prompt hint](#scenario-41-interactive-verification----live-tee-activity-aware-kill-and-the-quit-prompt-hint) + - [Scenario 42: Argv passthrough (REQ-026) -- launch arguments through the bootstrapper](#scenario-42-argv-passthrough-req-026----launch-arguments-through-the-bootstrapper) + - [Scenario 43: Honest ambiguous-exit messaging (REQ-027)](#scenario-43-honest-ambiguous-exit-messaging-req-027) + - [43a. No-EXE path, interpreter also failed](#43a-no-exe-path-interpreter-also-failed) + - [43b. Cached-EXE fast path, kept despite a non-zero exit](#43b-cached-exe-fast-path-kept-despite-a-non-zero-exit) --- -## Part I: AV-Safe Build Path (Nuitka fallback) +## Part I: Default double-click happy path (uv-first, fresh machine, zero flags) -### Scenario 1: PyInstaller build fails, Tier A (Nuitka) fallback succeeds +**Scope note:** this Part opens the document with what a completely ordinary run looks like end to +end -- one `.py` file, no test hooks, no prior state, no flags, uv reachable (the REQ-009 default, +`uv -> conda -> embed -> venv -> system`). This is what the large majority of real users actually +see; narrower/advanced feature areas (the AV-Safe Build Path's Nuitka fallback, CLI-interactivity +internals) are covered later, in Part VIII and Part IX. Evidence is pulled from a recent fully-green +run (`30328748330`, commit `5872028`, all lanes +green) rather than any single dedicated "happy path" test -- no such test exists as one file, so +each piece below is sourced from whichever real, non-`HP_CI_SKIP_ENV` sub-bootstrap in that run +exercises it most faithfully (mainly `tests/selfapps_envsmoke.ps1`'s real env-smoke sub-bootstrap +and `tests/selftest.ps1`'s stub-app fast path), cited individually per item. -**What's tested:** `self.exe.build.tiera` (`tests/selfapps_nuitka_tiera.ps1`, uv lane, -non-gating). `HP_TEST_FORCE_PYINSTALLER_FAIL=1` forces the primary build to fail deterministically; -the Nuitka fallback (`:try_nuitka_tier_a`) then runs for real -- a genuine compile, not simulated. +**A structural caveat that applies to every scenario in this Part, stated once here instead of +repeated per-scenario:** every CI lane sets `HP_CI_LANE` at the job level (`batch-check.yml`), +which silently auto-declines every consent prompt in the file the instant it's reached, with no +wait. A genuine double-click has none of `HP_CI_LANE`/`NOINPUT`/`HP_NONINTERACTIVE` set, so any +prompt this Part encounters would, for a real user, actually pause and wait for a keypress (or, +for the one genuinely timed gate, wait up to its timeout) instead of resolving instantly. Scenario +11 below covers this distinction in detail for the two prompts that fire on this exact happy path; +it applies identically to every other consent gate documented elsewhere in this file. -**What appears on screen**, from the moment PyInstaller's build is attempted through to the final -summary. Sourced from real CI (run `29788624195`, job `88506013149`), with the "(fallback build -system)" verification line and the drive-message reassurance line as originally captured, but the -"Verifying the built standalone EXE" line and the "Does your program need launch arguments" -paragraph both updated in place to the CURRENT shipped wording (`docs/plan-cli-interactive- -verification.md` requirement 3's activity-aware kill, and REQ-026's argv passthrough -- both -landed after this specific run) -- **not yet re-confirmed against a fresh CI capture that includes -either addition**: +### Scenario 1: No `.py` files at all -- the graceful `no_python_files` exit + +**What's tested:** `self.empty_repo.msg` (`tests/selftest.ps1`, `real` lane, real, passing). + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). + +Referenced throughout this document (e.g. Scenario 3's note that this repo's own bootstrapper +root, which has no loose `.py` files, exercises this exact path) but never shown directly: when +`PYCOUNT` (a plain `dir /b /a-d *.py` count) is zero, the bootstrapper takes the shortest path in +the entire file -- no provider selection, no dependency install, nothing network-touching at all, +skipping straight to a graceful, successful exit: ``` -[INFO] Building standalone executable -- this may take a minute or two... -[INFO] (A stray one-line Windows message about a missing drive may appear next -- that is a known side effect from an unrelated background process, unrelated to your app; safe to ignore.) -The system cannot find the drive specified. -The system cannot find the drive specified. -[TEST] HP_TEST_FORCE_PYINSTALLER_FAIL: simulating PyInstaller build failure. -[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). -[INFO] Fallback build succeeded: dist\.exe was produced using the fallback build system. -[DEBUG] warnfix: warn file not found -[INFO] PyInstaller build artifacts cleaned up. -[INFO] EXE smokerun: testing dist\.exe -[INFO] Running entry script smoke test via packaged EXE. -[WARN] Verifying the built standalone EXE (fallback build system) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. -[INFO] EXE smokerun: exited 0 (ok) -[INFO] Entry smoke exit=0 -[STATUS] Run Status: SUCCESS (Exit Code: 0) +[INFO] Environment name: _selftest_empty +[INFO] Host OS: Microsoft Windows [Version 10.0.26100.32995] +[INFO] Host PowerShell: 5.1.26100.32995 +[INFO] Python file count: 0 +Python file count: 0 +No Python files detected; skipping environment bootstrap. +[INFO] No Python files detected; skipping environment bootstrap. +``` -*** Verification finished -- see the Run Status above. *** -*** You can run your program again now via the interpreter as an extra diagnostic check. *** -[INFO] REQ-018: post-execution checkpoint (exe): declined (run footprint stays at one execution). +(the last message genuinely appears twice, back to back, in the real captured log -- once as a +plain `echo` straight to console with no timestamp, once through `:log`'s own timestamped form +written to both console and `~setup.log`; the block above shows both, with the second line's +real timestamp prefix, e.g. `Tue 07/28/2026 4:29:15.97`, omitted here since it carries no +information beyond confirming the two lines are adjacent). `~bootstrap.status.json` reads +`{"state":"no_python_files","exitCode":0, +"pyFiles":0}` -- a real user who double-clicks the bootstrapper in an empty folder, or in the +wrong folder entirely, gets a clear, immediate, non-alarming message rather than the bootstrapper +attempting (and inevitably failing) to build an environment for nothing. -============================================================ - SETUP COMPLETE -============================================================ - Your standalone application is ready: - dist\.exe +### Scenario 2: Pre-flight guards, lock acquisition, and entry detection on a clean run - RUNNING YOUR APP - Double-click dist\.exe to run it. +**What's tested:** no single dedicated test asserts the CLEAN (non-firing) pass of these checks -- +`docs/agent-ndjson.md` only registers rows for the *firing* branches (`self.warn.onedrive`, +`self.warn.longpath`, `self.warn.sysdir`, `self.stub.lock_held_decline`, etc.). The evidence below +is the incidental byproduct of `tests/selfapps_envsmoke.ps1`'s full, real bootstrap, which captures +every byte of console output via `cmd /c .\run_setup.bat > '~envsmoke_bootstrap.log'`. - STARTUP MAY BE SLOW: a one-file .exe unpacks itself each time it - starts, so allow 10-15 seconds (longer for big libraries like - numpy/scipy/matplotlib, or when extra packages were bundled to fix - missing imports) before assuming it has hung. +**Source:** REAL CI CAPTURE, run `30328748330`, `tests/~envsmoke/~envsmoke_bootstrap.log` +(published diagnostics site), identical across all 6 lanes checked (`real`, `uv`, `conda-full`, +`justme-test`, `contract-uv`, `contract-uv-fail`): - If the window flashes and closes instantly: that's normal if - your program finished quickly or hit an error before printing - anything. To see what happened, open Command Prompt, cd to - this folder, and run: - dist\.exe - This keeps the window open so you can read any messages. +``` +Tue 07/28/2026 4:29:43.96 [INFO] REQ-015: Appending standard ignores to .gitignore. +``` - A progress indicator that updates in place may appear all at - once instead of live when run as the .exe -- that is a stdout - buffering difference between the .exe and the script, not an error. +Before that line, nothing prints -- `HP_APP_ARGS` capture (REQ-026, pure variable assignment), the +workspace-path-exists check, `cd /d`, `HP_SCRIPT_ROOT` construction, and the top-of-file UNC-path +check (`if "%HP_SCRIPT_LAUNCH_DIR:~0,2%"=="\\"`, which prints a much louder `*** WARNING: +UNC/network paths detected...` banner when it genuinely fires) are all silent on an ordinary, +non-UNC path. - Does your program need launch arguments (e.g. --input file.csv)? Run - this bootstrapper again with them added after the entry file, e.g. - run_setup.bat "" --input file.csv - and they will be forwarded to your program during THIS setup run - (up to 8 extra arguments). This does not change how a plain - double-click of dist\.exe launches it afterward -- for that, - make a Windows shortcut to the .exe and add the arguments to its - Target field, or launch it yourself from a Command Prompt. +**The four REQ-025-family pre-flight guards (path-length, OneDrive, system-directory, disk-space -- +also part of that unlabeled prologue) are completely silent unless they fire.** Confirmed by both +the absence of any related text anywhere in the captured log, AND by reading the source: none of +the four has an `else` branch that prints a success/clean message -- each is a bare `if (...) ( +echo/log ... )` with nothing on the false path. A real user on an ordinary setup (short path, not +under OneDrive, not under `Windows`/`Program Files`, plenty of free disk) sees zero output from +any of these four checks. - KEEP these files with your project: - requirements.txt -- packages your app depends on - runtime.txt -- Python version pin +**`:acquire_lock` is equally silent on an uncontended acquire.** The `mkdir "%HP_LOCK_DIR%"` call +succeeds immediately (no prior lock directory), jumps straight to `:lock_acquired`, writes a +transient `~bootstrap.lock\owner.txt` marker (a file, not console output), and returns -- every +`echo`/`:log` call inside `:acquire_lock` lives inside the "another instance is already running" +branch, only reached on a genuine `mkdir` failure. The lock directory (and its `owner.txt`) is +gone again by the time the run completes (`:release_lock`, called from both `:die` and `:success`, +is equally silent). - SAFE TO DELETE to reclaim disk space: - .*_env\ folders -- environment directories - ~* files -- tilde-prefix work files (e.g. ~setup.log) - build\ -- PyInstaller build cache -============================================================ +**Entry detection for the common single-`.py`-file case** (`:determine_entry` -> +`tools/find_entry.py` -> `:record_chosen_entry`) produces exactly one line, sourced from +`tests/~envsmoke/~setup.log` (same run, the real, non-`HP_CI_SKIP_ENV` `:determine_entry` call +site inside `:after_env_bootstrap` -- not the separate `HP_CI_SKIP_ENV`-only `:ci_skip_entry` +implementation that +`tests/selfapps_entry.ps1`/`selfapps_single.ps1` exercise, which looks textually similar but is a +different code path): + +``` +Chosen entry: app.py ``` -The "Verifying the built standalone EXE" line now correctly says "(fallback build system)" -instead of a hardcoded "(PyInstaller)" when the EXE being verified was actually Nuitka-built -(`:warn_user_code_launch` branches on `HP_NUITKA_FALLBACK_USED`). The postflight briefing's -"PyInstaller build cache" line is left as-is -- Nuitka never creates a `build\\` folder of -its own (its `--remove-output` flag cleans up its own intermediates), so that line stays literally -true regardless of which tool actually built the current EXE: if a `build\` folder exists, it's -PyInstaller's. See Scenario 6 for the argv-passthrough paragraph's own dedicated writeup. +This is identical whether the sole `.py` file has a preferred name (`main.py`/`app.py`/`run.py`/ +`cli.py`) or an arbitrary one (`tools/find_entry.py`'s `len(files) == 1` branch handles both the +same way, with zero stderr diagnostics either way) -- the interactive picker +(`:pick_entry_interactive`) is only ever reached when more than one `.py` file is ambiguous, which +this scenario deliberately doesn't have. `:determine_entry` actually runs twice in a normal +bootstrap (once early, for PEP 723/autopep723 discovery purposes, well before `:after_env_bootstrap`, +and once again at the real entry-selection call site inside `:after_env_bootstrap` that produces +this console line) -- only the second call's result is what a user sees echoed. --- -### Scenario 2: PyInstaller build fails, Tier A fallback ALSO fails (tier exhaustion) +### Scenario 3: Provider acquisition and dependency install (uv-first) -**What's tested:** `self.exe.build.xfail` (`tests/selfapps_pyinstaller_fail.ps1`, real/conda-full -lanes, gating). Three sub-scenarios share one NDJSON row id: `execfail` (the PyInstaller build -command itself fails), `output_vanish` (PyInstaller succeeds, then the output EXE vanishes -immediately -- simulating AV-style post-creation removal), and `execfail_runtimefail` (packaging -fails AND the interpreter fallback that runs next ALSO exits non-zero -- see Scenario 7a for that -one's console text, since it's really a REQ-027 demo). The first two additionally force -`HP_TEST_FORCE_NUITKA_FAIL=1` so the fallback also fails, proving genuine tier exhaustion. +**What's tested:** `tests/selfapps_envsmoke.ps1`'s real sub-bootstrap (`self.env.smoke.*` rows), +uv-first lane, against a stub `app.py` that genuinely does `import colorama` -- a real, if small, +dependency, chosen deliberately over a zero-dependency stub so the dependency-discovery/install +machinery actually has something to do. + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` ("real" lane), +`tests/~envsmoke/~envsmoke_bootstrap.log` (what a real user's terminal shows) and +`tests/~envsmoke/~setup.log` (internal detail file, receives everything the console does PLUS +extra content this repo deliberately never puts on-screen -- see this doc's own "Console vs. +`~setup.log`" note at the top). Both are cited below, labeled. -#### 2a. `execfail` -- the PyInstaller build command itself fails +**IMPORTANT: this exact CI run's job-level "Bootstrap environment (run_setup.bat)" step is NOT +representative evidence for this scenario** -- that step runs `run_setup.bat` against the +bootstrapper repo's OWN root (no loose `.py` files there), so it takes the `no_python_files` +graceful-exit path and produces nothing relevant. The genuinely representative evidence is the +"Self-test: real env smoke (CI-only)" step's own inner sub-bootstrap, which runs `run_setup.bat` +for real against a scratch app directory instead. -Real CI capture, run `29788624195`, job `88506013028` ("real" lane): +**uv acquisition** (console, first-ever run, no cached `~uv_bin` -- every fresh CI scratch dir +starts this way, matching a real user's first-ever double-click): ``` -[INFO] Building standalone executable -- this may take a minute or two... -[TEST] HP_TEST_FORCE_PYINSTALLER_FAIL: simulating PyInstaller build failure. -[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). -[TEST] HP_TEST_FORCE_NUITKA_FAIL: simulating fallback build failure. -[ERROR] PyInstaller execution failed. -[DEBUG] warnfix: warn file not found -[INFO] PyInstaller build artifacts cleaned up. -[WARN] EXE smokerun: dist\.exe not found; skipping -[INFO] Running entry script smoke test via uv interpreter. -[INFO] Entry smoke exit=0 -[STATUS] Run Status: SUCCESS (Exit Code: 0) - -[INFO] REQ-018: post-execution checkpoint (interpreter): declined (run footprint stays at one execution). +[INFO] uv: UV_PYTHON_PREFERENCE=only-managed (orchestration uses managed Python). +[INFO] uv: downloading to ~uv_bin... +[INFO] Downloading uv from https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip... +[INFO] uv: acquired at ~uv_bin\uv.exe +[INFO] uv-first: Miniconda download skipped. ``` -When BOTH the PyInstaller build and the Nuitka fallback fail outright but the interpreter -fallback's own run exits 0 (this trivial stub script does), the final line is -`[STATUS] Run Status: SUCCESS (Exit Code: 0)` and the postflight panel is the plain -"YOUR CODE RAN -- BUT NO STANDALONE .EXE WAS PRODUCED" variant -- see Scenario 7a for the -DIFFERENT (honest, "we can't confirm") panel this same tier-exhaustion path now shows when the -interpreter run ALSO fails, which is the REQ-027 fix that closed the gap this section used to flag -as an open question. - -#### 2b. `output_vanish` -- PyInstaller succeeds, then the EXE disappears immediately +(`~setup.log`-only, never shown on-screen: curl's own progress-bar text between the "downloading" +and "acquired" lines, plus uv's own managed-CPython fetch triggered by the version-detection call +-- `Downloading cpython-3.14.6-windows-x86_64-none (download) (21.5MiB)` / +`Downloaded cpython-3.14.6-windows-x86_64-none (download)`. On a REPEAT run with `~uv_bin` already +populated, the cached-binary branch just before `:uv_acquire_download` instead logs `[INFO] uv: +cached binary found at ~uv_bin\uv.exe` -- not independently captured here since every CI scratch +dir is fresh; see this doc's repeat-run scenarios, added in a later pass, for the dedicated +treatment.) -Real CI capture, same run/job as 2a: +**uv venv creation** (console, no pre-existing version pin -- this app has neither `runtime.txt` +nor a `pyproject.toml` constraint): ``` -[INFO] Building standalone executable -- this may take a minute or two... -[TEST] HP_TEST_FORCE_OUTPUT_VANISH: deleting freshly-built EXE to simulate post-creation removal. -[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). -[TEST] HP_TEST_FORCE_NUITKA_FAIL: simulating fallback build failure. -[ERROR] PyInstaller did not produce dist\.exe -[DEBUG] warnfix: warn file found -[INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. -[INFO] PyInstaller build artifacts cleaned up. -[WARN] EXE smokerun: dist\.exe not found; skipping -[INFO] Running entry script smoke test via uv interpreter. -[INFO] Entry smoke exit=0 -[STATUS] Run Status: SUCCESS (Exit Code: 0) - -*** Verification finished -- see the Run Status above. *** -*** You can run your program again now via the interpreter as an extra diagnostic check. *** -[INFO] REQ-018: post-execution checkpoint (interpreter): declined (run footprint stays at one execution). +[INFO] uv: creating venv at .uv_env... +[INFO] uv: venv created at .uv_env +[INFO] HP_ENV_MODE=uv +[BOOT] REQ-009: Selected Python provider: UV. +[INFO] runtime.txt written: python-3.14.6 ``` ---- - -### Scenario 3: Tier A + hidden-import auto-recovery skip guard +A version-constrained app (e.g. `pyproject.toml` with `requires-python = ">=3.9"`) instead logs +`[INFO] uv: creating venv at .uv_env with Python 3.9 or newer...` -- confirmed via a second real +capture in the same run (`tests/~pyproject_prec/~pyproject_prec_bootstrap.log`), an exact pin like +`python==3.11` templates to `...with Python 3.11...` per the `HP_UV_PY_DISP` derivation just +above `:uv_venv_ready` (not independently captured, but the template is a literal source string, +not inferred). `~setup.log`-only detail: uv's own raw venv output (`Using CPython 3.14.6`, +`Creating virtual environment with seed packages at: .uv_env`, a `Failed to hardlink files; +falling back to full copy` warning, `+ pip==26.1.2`, `Activate with: .uv_env\Scripts\activate`) -- +none of this reaches the console. -**What's tested:** `self.exe.tiera.hidden_skip` (`tests/selfapps_nuitka_tiera_hidden_skip.ps1`, uv -lane, non-gating). Forces Tier A to trigger and succeed for real, then has the stub app fabricate -a `ModuleNotFoundError: No module named 'nuitka'` on stderr and exit 1 -- the exact signature that -used to (before this fix) trigger an incorrect PyInstaller rebuild attempt against a Nuitka-built -EXE. +**Dependency discovery** -- pipreqs and the REQ-005.12 `autopep723 check` Tier 1 merge running +TOGETHER, genuine production behavior (no `HP_SKIP_PIPREQS` test-isolation flag involved, unlike +the dedicated `selfapps_autopep_discovery.ps1`/`selfapps_pvw_idempotent.ps1` tests that deliberately +isolate one mechanism from the other): -**Source:** confirmed in real CI run `29877805447`, uv lane, job `88792048278`: ``` -{"details":{"appStdoutFound":true,"noRepairRebuild":true,"successLogged":true,"skipGuardLogged":true,"exeExists":true,"statusState":"ok","bootstrapExit":0,"smokerunNonzeroLogged":true,"attemptLogged":true,"log":"~nuitka_tiera_hidden_skip_bootstrap.log"},"req":"REQ-AV","pass":true,"desc":"AV-Safe Build Path Tier A: hidden-import auto-recovery correctly skips (never rebuilds via PyInstaller) against a Nuitka-built EXE","id":"self.exe.tiera.hidden_skip","lane":"uv"} +[INFO] pipreqs 0.4.13 installed successfully; using it for dependency discovery. +[INFO] pipreqs (direct) command: pipreqs . --force --mode compat --savepath "...\requirements.auto.txt" --ignore ".git,.github,.venv,venv,env,.uv_env,build,dist,__pycache__,tests" +*** [WARN] Dependencies were auto-detected (pipreqs) +*** [WARN] Auto-detection may be incomplete or incorrect +*** [INFO] Consider adding requirements.txt or PEP 723 metadata for reliability +[INFO] REQ-005.5: dependency source diff computed -- ~pipreqs.diff.txt +[INFO] REQ-005.12: autopep723 discovery merge complete. ``` -The test only dumps a full console log to CI when a scenario fails; since this one passes, the -exact console text below is reconstructed from `run_setup.bat`'s source rather than copied from a -console dump -- the NDJSON row's `skipGuardLogged`/`noRepairRebuild` fields are the test's own -regex-verified confirmation that these exact lines were present/absent in the real captured log: + +**A real quirk worth flagging so it isn't misread**: the bootstrap log around this point also shows +`DEP_FINAL_COUNT=0` even though `colorama` is a genuine, real dependency that gets installed a few +steps later -- this count is taken BEFORE `requirements.auto.txt` is copied into `requirements.txt` +a few lines further down, still inside `:after_pipreqs_run`'s own tail, so `DEP_FINAL_COUNT=0` +here does not mean "pipreqs found nothing"; it's a pre-existing ordering quirk in the log sequence, not a bug in this +run. `~setup.log`-only (pipreqs's raw output is redirected to a SEPARATE file entirely, +`~pipreqs_direct.log`, not even `~setup.log` -- a real user never sees any of the following): ``` -[INFO] Building standalone executable -- this may take a minute or two... -[TEST] HP_TEST_FORCE_PYINSTALLER_FAIL: simulating PyInstaller build failure. -[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). -[INFO] Fallback build succeeded: dist\.exe was produced using the fallback build system. -[INFO] EXE smokerun: testing dist\.exe -[WARN] EXE smokerun: exited 1 (non-zero) -[INFO][HIDDEN_IMPORT] Skipping --hidden-import auto-recovery: dist\.exe was built via the fallback build system (Nuitka), which uses a different missing-import mechanism than PyInstaller's --hidden-import flag. +WARNING: Import named "colorama" not found locally. Trying to resolve it at the PyPI server. +WARNING: Import named "colorama" was resolved to "colorama:0.4.6" package (https://pypi.org/project/colorama/). +Please, verify manually the final list of requirements.txt to avoid possible dependency confusions. +INFO: Successfully saved requirements file in ...\requirements.auto.txt ``` -Without this guard, the OLD behavior would have printed -`[REPAIR][HIDDEN_IMPORT] Adding --hidden-import=nuitka; rebuilding EXE (iter 1/3)` here and -attempted a PyInstaller rebuild against a Nuitka-built EXE. - ---- - -### Scenario 4: Requirement 9 -- elective "want an optimized build too?" offer +plus a handful of cosmetic `SyntaxWarning` lines from pipreqs 0.4.13's own bundled `docopt`/`yarg` +dependencies. `~setup.log` also shows the autopep723 merge's own uvx tool-bootstrap noise +(`Installed 1 package in 26ms`) and its own result line, `no-op: all autopep723 dependencies +already present` -- because pipreqs already found `colorama` first, autopep723's own independent +discovery of the same import is redundant here, a real working example of this repo's "augment, +never replace" design for the two mechanisms. -**What's tested:** `self.optbuild.offer` (`tests/selfapps_optimized_build.ps1`, uv lane, -non-gating), four scenarios sharing one row id. +**Dependency install:** -**Source:** confirmed in real CI run `29877805447`, uv lane, job `88792048278`, all four scenarios -passing: ``` -{"lane":"uv","details":{"log":"~optbuild_accept_bootstrap.log","statusState":"ok","scenario":"accept","successLogged":true,"promptShown":true,"tmpExeGone":true,"exeExists":true,"bootstrapExit":0,"acceptedLogged":true,"appStillRuns":true},"desc":"AV-Safe Build Path requirement 9 (accept): a real optimized build succeeds, verifies, and is swapped into place","req":"REQ-AV","id":"self.optbuild.offer","pass":true} -{"req":"REQ-AV","lane":"uv","desc":"AV-Safe Build Path requirement 9 (forcefail): a failed optimized build leaves the original PyInstaller EXE completely untouched","id":"self.optbuild.offer","details":{"originalStillRuns":true,"bootstrapExit":0,"log":"~optbuild_forcefail_bootstrap.log","tmpExeGone":true,"promptShown":true,"exeExists":true,"testHookFired":true,"scenario":"forcefail","statusState":"ok","noSuccessMsg":true},"pass":true} -{"req":"REQ-AV","id":"self.optbuild.offer","lane":"uv","pass":true,"details":{"bootstrapExit":0,"acceptedLogged":true,"originalStillRuns":true,"log":"~optbuild_swapfail_bootstrap.log","promptShown":true,"exeExists":true,"tmpExeGone":true,"scenario":"swapfail","statusState":"ok","noSuccessMsg":true,"swapFailLogged":true},"desc":"AV-Safe Build Path requirement 9 (swapfail): a verified optimized build whose final swap fails leaves the original PyInstaller EXE completely untouched and cleans up the leftover temp file"} -{"desc":"AV-Safe Build Path requirement 9 (decline): default/CI path shows the prompt but never attempts a build","lane":"uv","details":{"statusState":"ok","noBuildAttempt":true,"tmpExeGone":true,"scenario":"decline","log":"~optbuild_decline_bootstrap.log","bootstrapExit":0,"exeExists":true,"declinedLogged":true,"promptShown":true},"req":"REQ-AV","id":"self.optbuild.offer","pass":true} +[INFO] UV_USED=1 +[INFO] DEP_INSTALLED_CAPTURED=1 +[INFO] Environment snapshot written: ~environment.lock.txt +[INFO] REQ-005.11: PEP 723 header write-back succeeded via uv add --script. ``` -#### 4a. `accept` -- a real optimized build succeeds and is swapped in +`~setup.log`-only (`uv pip install --python ... -r requirements.txt`'s own raw output, never on +console): `Using Python 3.14.6 environment at: .uv_env`, `Resolved 1 package in 257ms`, `Prepared +1 package in 43ms`, `Installed 1 package in 15ms`, ` + colorama==0.4.6`. The real +`~environment.lock.txt` (uv mode copies the pip-freeze output here) contains more than just +`colorama` -- it also lists pipreqs's OWN transitive dependencies (`certifi`, `charset-normalizer`, +`docopt`, `idna`, `pipreqs`, `requests`, `urllib3`, `yarg`), because in `HP_ENV_MODE=uv`, pipreqs +is installed via `uv pip install --python "%HP_PY%"` into the SAME venv as the target app (inside +the pipreqs install step, before `:pipreqs_direct_done`), not an isolated tool venv. This is +genuine, expected production behavior +-- a real user's `.uv_env` always ends up with pipreqs's own dependency footprint mixed in +alongside their actual dependencies, not a CI artifact. -Real, verbatim console dump (`~selftest_optbuild_accept\~optbuild_accept_bootstrap.log`), with the -"Verifying the built standalone EXE" line updated in place to the CURRENT shipped wording -(requirement 3's activity-aware kill plus the quit-prompt hint, both landed after this capture) -- -**not yet re-confirmed against a fresh CI capture**: +--- + +### Scenario 4: Build, verify, and the final status panel + +**What's tested:** `self.stub.fastpath` (`tests/selftest.ps1`, real lane), a genuine first-run +(non-cached) build of a trivial one-line stub app (`hello_stub.py` = `print("hello-from-stub")`). +Chosen over Scenario 3's `colorama`-importing app for this piece specifically because it produces +a single, fully self-consistent, real capture spanning build through the final status panel with +no lines needing a source-level touch-up -- unlike the similar block quoted in Scenario 37 +(Part VIII), where two lines had to be patched to current wording. + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` ("real" lane), +`tests/~selftest_stub/~stub_bootstrap.log`: ``` -[INFO] Building standalone executable -- this may take a minute or two... +Tue 07/28/2026 4:55:47.55 [INFO] Building standalone executable -- this may take a minute or two... +Tue 07/28/2026 4:55:47.55 [INFO] (A stray one-line Windows message about a missing drive may appear next -- that is a known side effect from an unrelated background process, unrelated to your app; safe to ignore.) The system cannot find the drive specified. The system cannot find the drive specified. -[INFO] PyInstaller produced dist\.exe -[INFO] warnfix: Platform-specific modules in the list above are expected on Windows: posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, _frozen_importlib_external. These will be filtered out automatically. -[INFO] PyInstaller build artifacts cleaned up. -[INFO] EXE smokerun: testing dist\.exe -[INFO] Running entry script smoke test via packaged EXE. -[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. -[INFO] EXE smokerun: exited 0 (ok) -[INFO] Entry smoke exit=0 -[STATUS] Run Status: SUCCESS (Exit Code: 0) +Tue 07/28/2026 4:55:58.01 [INFO] PyInstaller produced dist\_selftest_stub.exe +Tue 07/28/2026 4:55:58.02 [DEBUG] warnfix: warn file found +Tue 07/28/2026 4:55:58.02 [INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. +Tue 07/28/2026 4:55:58.39 [INFO] PyInstaller build artifacts cleaned up. +Tue 07/28/2026 4:55:58.40 [INFO] EXE smokerun: testing dist\_selftest_stub.exe +Tue 07/28/2026 4:55:58.41 [INFO] Running entry script smoke test via packaged EXE. +Tue 07/28/2026 4:55:58.43 [WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +[INFO] Process ID 6076. If it seems stuck: Task Manager > Details tab > find this PID > End Task (this window stays open). +hello-from-stub +Tue 07/28/2026 4:55:59.87 [INFO] EXE smokerun: exited 0 (ok) +Tue 07/28/2026 4:55:59.88 [INFO] Entry smoke exit=0 +Tue 07/28/2026 4:55:59.88 [STATUS] Run Status: SUCCESS (Exit Code: 0) +``` +Build took ~10.5s wall-clock here (this run has effectively zero dependencies, so it's near the +fast end of what's possible). This run's PyInstaller build DID trigger the warnfix path (`warn +file found` -- expected, since even a bare stub still needs the platform-module filter pass) but +needed no repair install, so the informational filter messaging is present but purely +informational -- the "no warnfix repair needed" success case this doc previously had no example +of. `hello-from-stub` is the stub program's OWN real stdout, live-teed via +`tools/exe_smokerun.ps1`'s chunk-based `ReadAsync` reader (see Scenario 41's fuller writeup of that +mechanism) -- landing exactly between the PID line and the "exited 0" line, precisely where a real +user's own program output appears. The `[INFO] Process ID . If it seems stuck: Task Manager > +Details tab...` line is the already-shipped stuck-program recovery aid (`tools/exe_smokerun.ps1`, +see `docs/agent-interconnect.md`'s "Process-ID display for stuck-program recovery" section) -- +confirmed here as a genuine, working console line, not just source text. + +Immediately following (elective prompts, both auto-declined by CI -- see Scenario 5 below for +what a real user experiences here instead) and then the final panel: + +``` *** Verification finished -- see the Run Status above. *** *** You can run your program again now via the interpreter as an extra diagnostic check. *** -[INFO] REQ-018: post-execution checkpoint (exe): declined (run footprint stays at one execution). +Tue 07/28/2026 4:55:59.91 [INFO] REQ-018: post-execution checkpoint (exe): declined (run footprint stays at one execution). *** Your app is ready. *** *** Want to build an optimized version too? It takes a bit longer to build right now, *** *** but it starts up more reliably on Windows and runs faster once it is built. *** -[INFO] Optimized build: accepted; building now (this may take a minute or two). -[INFO] Optimized build succeeded and verified: dist\.exe now uses the fallback build system. -``` +Tue 07/28/2026 4:55:59.94 [INFO] Optimized build: declined. -Note the "warnfix: Platform-specific modules..." line above still shows the OLD wording (this -capture predates the messaging fix described in Scenario 2b) -- the NEW wording is the one shown -there. +============================================================ + SETUP COMPLETE +============================================================ + Your standalone application is ready: + dist\_selftest_stub.exe -**The interactive `Build the optimized version now? [Y/N]` prompt line is echoed unconditionally -by design** (same pattern as `:run_postexec_checkpoint`), but does not appear literally in any -CI capture -- CI answers via the `HP_TEST_OPTBUILD_ANSWER` env-var override, not the interactive -`set /p` path, so only the resolution lines (`accepted`/`declined`) show up in these logs. This is -expected (CI is non-interactive by design), not a gap. + RUNNING YOUR APP + Double-click dist\_selftest_stub.exe to run it. -#### 4b. `forcefail` -- accepted, but the build fails; original EXE is left untouched + STARTUP MAY BE SLOW: a one-file .exe unpacks itself each time it + starts, so allow 10-15 seconds (longer for big libraries like + numpy/scipy/matplotlib, or when extra packages were bundled to fix + missing imports) before assuming it has hung. -Real, verbatim console dump (`~selftest_optbuild_forcefail\~optbuild_forcefail_bootstrap.log`): + If the window flashes and closes instantly: that's normal if + your program finished quickly or hit an error before printing + anything. To see what happened, open Command Prompt, cd to + this folder, and run: + dist\_selftest_stub.exe + This keeps the window open so you can read any messages. -``` -*** Your app is ready. *** -*** Want to build an optimized version too? It takes a bit longer to build right now, *** -*** but it starts up more reliably on Windows and runs faster once it is built. *** -[INFO] Optimized build: accepted; building now (this may take a minute or two). -[TEST] HP_TEST_FORCE_OPTBUILD_FAIL: simulating optimized-build failure. -[INFO] REQ-016: Post-flight briefing printed. -``` + A progress indicator that updates in place may appear all at + once instead of live when run as the .exe -- that is a stdout + buffering difference between the .exe and the script, not an error. -No further message prints between the forced-fail log line and the (unrelated, always-present) -post-flight briefing -- the subroutine cleans up the temp file and returns silently. This is a -narrower silence than the wording used on a REAL build-failure branch (which explicitly says -"your app is still ready to use as-is" -- see the reactive hint below); a forced-test-hook failure -and a genuine build failure currently give the user different amounts of reassurance for what is, -from their perspective, the same outcome. + Does your program need launch arguments (e.g. --input file.csv)? Run + this bootstrapper again with them added after the entry file, e.g. + run_setup.bat "hello_stub.py" --input file.csv + and they will be forwarded to your program during THIS setup run + (up to 8 extra arguments). This does not change how a plain + double-click of dist\_selftest_stub.exe launches it afterward -- for that, + make a Windows shortcut to the .exe and add the arguments to its + Target field, or launch it yourself from a Command Prompt. -#### 4c. `swapfail` -- verified build, but the final swap step fails; original EXE is left untouched + KEEP these files with your project: + requirements.txt -- packages your app depends on + runtime.txt -- Python version pin -Regression test for a real bug: the swap-verification check used to test the DESTINATION file -(which already exists before the move, success or failure alike) instead of the SOURCE (which -should be gone only on success) -- a genuinely failed swap would have been silently misreported -as success. Fixed; console text (expected from source, not yet dumped in a CI console capture -since this scenario has passed on every run so far): + SAFE TO DELETE to reclaim disk space: + .*_env\ folders -- environment directories + ~* files -- tilde-prefix work files (e.g. ~setup.log) + build\ -- PyInstaller build cache +============================================================ +Tue 07/28/2026 4:56:00.03 [INFO] REQ-016: Post-flight briefing printed. ``` -*** Your app is ready. *** -*** Want to build an optimized version too? ... *** -[INFO] Optimized build: accepted; building now (this may take a minute or two). -[WARN] Optimized build verified successfully but could not be swapped into place; your app is still ready to use as-is. -``` - -#### 4d. `decline` -- default/CI path, prompt shown but nothing built -Real, verbatim console dump (`~selftest_optbuild_decline\~optbuild_decline_bootstrap.log`): +The companion `tests/~selftest_stub/~bootstrap.status.json` reads: +```json +{"state":"ok","exitCode":0,"pyFiles":1} ``` -*** Your app is ready. *** -*** Want to build an optimized version too? It takes a bit longer to build right now, *** -*** but it starts up more reliably on Windows and runs faster once it is built. *** -[INFO] Optimized build: declined. -``` - -#### Reactive-only failure hint (both Tier A and requirement 9's real-build-failure paths) - -Fires only on a GENUINE Nuitka compiler failure (not the `forcefail` test hook, which bypasses it -entirely). No CI run to date has exercised a real Nuitka compiler failure, so this is sourced from -`run_setup.bat` rather than a console capture: -``` -[WARN] Optimized build did not complete; your app is still ready to use as-is. -[WARN] Hint: if you have Visual Studio 2022 (or newer) with the 'Desktop development with C++' workload installed, this should use it automatically -- no extra setup needed. If not, installing the free Visual Studio Build Tools with that workload can help. -``` +`exitCode` here means "did the bootstrapper's own env/build lifecycle succeed" (a hardcoded `0` +written unconditionally at the success label), NOT the user program's own exit code -- that's +separately surfaced via the console's `[STATUS] Run Status: SUCCESS (Exit Code: 0)` line above. +Both happen to be 0 in this clean run, so the distinction isn't visible here, but it's worth +knowing they're two independent things (see CLAUDE.md's "User-code exit-code semantics" Known +Finding). --- -## Part II: CLI interactivity, argv passthrough & honest messaging +### Scenario 5: The two elective prompts a real user faces after every successful run -Covers `docs/plan-cli-interactive-verification.md` (P0/P1/P2, all shipped): the live-tee -verification redesign that lets an interactive `input()`-driven program's prompts actually reach -the console, the activity-aware 30-second kill, argv passthrough (REQ-026), and the honest -ambiguous-exit messaging panels (REQ-027). +**What's tested:** the framing text of both prompts is confirmed via real CI capture (they're +unconditionally echoed even when CI auto-declines); the actual `[Y/N]` question lines themselves +are never visible in ANY CI log, by construction -- they live inside the branch of an `if/elif` +chain that CI's `HP_CI_LANE` auto-decline always short-circuits past, even in tests that force an +accept via an `HP_TEST_*_ANSWER` override (the override branch assigns the answer variable +directly, bypassing the real `set /p` prompt entirely). Both are therefore genuinely +**`[Extrapolated Branch]`** for the exact prompt-line wording specifically, cited from source, even +though the surrounding framing text is a real capture. -### Scenario 5: Interactive verification -- live-tee, activity-aware kill, and the quit-prompt hint +**Source:** framing lines are REAL CI CAPTURE (run `30328748330`, job `90179708091`, "real" lane, +same log as Scenario 4 above); the two `set /p` prompt lines themselves are +`[Extrapolated Branch]`, cited from `:run_postexec_checkpoint` and `:offer_optimized_build` +respectively. -**What changed, in one sentence:** before this plan, `:run_exe_smokerun`'s verification launch -force-killed at a hard 30 seconds regardless of output, and captured stdout/stderr only to a file -(never live to the console) -- so a program correctly waiting on its first `input()` prompt looked -identical to a genuinely hung one, and the user watching the window saw nothing until the process -either finished or got killed. +**Relationship to README.md's `[REQ-018]` spec, since the two don't map 1:1 in an obvious way:** +`:run_postexec_checkpoint`'s own log line is explicitly tagged `REQ-018` in source (`[INFO] +REQ-018: post-execution checkpoint (...): accepted/declined...`), so it -- not a separate, +undocumented prompt -- is the shipped implementation of README's "After a build, the real run is +offered, not forced" / "Consent before any extra run" bullets. Two nuances worth flagging +explicitly rather than leaving implicit: (1) the offered rerun uses the INTERPRETER, not a second +launch of the packaged EXE -- README's "offers to launch the app untimed for real" phrasing reads +as if it means the EXE specifically, but the actual accepted-path log line says "running a second +time via the interpreter", and a companion note the same subroutine prints for the EXE call site +spells this out too ("this diagnostic run uses the interpreter, not the packaged EXE, so behavior +can differ"); (2) README's separate REQ-018 bullet about the FIRST (mandatory) verification run +being "force-stopped after a short interval even if running fine" is stale relative to the +activity-aware-kill behavior actually shipped later (see Scenario 41's fuller writeup and CLAUDE.md's +Closed Backlog "Activity-aware EXE-smoke kill" entry) -- the real WARN text quoted in Scenario 4 +above says the opposite: any output at all, not just a clean exit, keeps the run alive indefinitely, +only a genuinely SILENT process gets force-stopped. Neither nuance changes what actually ships; +both are flagged here because a reader cross-checking this scenario against README's prose could +otherwise reasonably conclude something was missed. -**What's tested (the plumbing):** `self.interactive.stdin.roundtrip` -(`tests/selfapps_interactive_stdin.ps1`, uv lane, non-gating) builds a real PyInstaller EXE from a -multi-round `input()`-driven stub app and pipes a scripted answer sequence into `cmd.exe`'s own -stdin, exercising the full `cmd.exe -> :run_exe_smokerun -> ~exe_smokerun.ps1 -> the built EXE` -chain and asserting each answer lands in the right round via ordering checks on the captured log --- it proves the plumbing doesn't drop or reorder stdin/stdout, not a live human's own typing -timing (which can't be automated). +Right after a normal successful verification run (Scenario 4's tail), a real double-click user +sees: -**The WARN line a user sees right before the verification launch**, current shipped wording -(source: `run_setup.bat`, `:warn_user_code_launch` -- not a console capture, since CI answers -scripted stdin rather than a human watching the window; Scenario 1 and Scenario 4a above both show -this exact line in situ): +``` +*** Verification finished -- see the Run Status above. *** +*** You can run your program again now via the interpreter as an extra diagnostic check. *** + Run again via the interpreter now? [Y/N] _ +``` + +(cursor sits after `[Y/N] `, waiting indefinitely -- `:run_postexec_checkpoint`, an UNBOUNDED +`set /p`, no timeout of any kind). Any answer other than a leading `Y`/`y` -- including just +pressing Enter -- resolves to decline. If accepted, the entry program runs a second time via the +interpreter (not the packaged EXE) as a diagnostic; if declined, the bootstrap immediately +continues to the second prompt: ``` -[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +*** Your app is ready. *** +*** Want to build an optimized version too? It takes a bit longer to build right now, *** +*** but it starts up more reliably on Windows and runs faster once it is built. *** + Build the optimized version now? [Y/N] _ ``` -Three things this one line is doing: -1. **States the actual kill rule truthfully**: the 30-second cap is a classification checkpoint, - not an unconditional deadline -- `Kill()` only fires if the process has stayed COMPLETELY - silent that whole time. Any output (including a bare prompt with no trailing newline, the exact - shape of Python's own `input("...")`) switches to an unbounded wait. -2. **Actively guides the user toward a clean result**: driving an interactive program to its own - quit/exit option during this pass turns an otherwise-ambiguous exit into a genuine, confirmed - `[STATUS] Run Status: SUCCESS` -- this directly reduces how often a real user ever sees either - of Scenario 7's ambiguous-exit panels. -3. **Still warns it's a throwaway pass, not the user's real, saveable session** -- this verification - EXE is never reused; only the file it's already tested is kept for later double-clicks. +(same shape -- `:offer_optimized_build`, also an unbounded `set /p`, also defaults to decline on +anything but a leading Y). Both prompts genuinely fire on essentially every +successful default run (the checkpoint is called after every clean verification; the optimized- +build offer only skips if the AV-Safe-Build-Path Tier A Nuitka fallback already ran, or the +verification itself failed) -- **this is not an edge case, it's what most real users see twice in +a row at the very end of an otherwise fully successful first run.** -The `hidden_import` recovery loop's own separate, narrower verification check (see -`docs/agent-interconnect.md`'s "Activity-aware EXE-smoke kill" section) deliberately keeps the -OLDER, unconditional 30-second wording -- it's a bounded repair-verification check on an -already-built EXE, not the user's primary run, so it never got the interactive-friendly rewrite. +**CI cannot show either question line, ever, structurally -- not just "doesn't currently show +them."** Both prompts follow this repo's own established CI-safe-gate pattern (see +`docs/agent-interconnect.md`'s "CI-safe interactive gates" section): echo the framing +unconditionally, THEN branch on `HP_TEST_*_ANSWER` override / `HP_CI_LANE` auto-decline / real +`set /p`. Because the actual question text lives inside the `set /p` call itself (not a separate +unconditional `echo`), and CI always takes one of the first two branches, no CI log -- gating or +non-gating, auto-decline or forced-accept -- can ever contain the literal `" Run again via the +interpreter now? [Y/N] "` or `" Build the optimized version now? [Y/N] "` text. This is a genuine +blind spot in what CI evidence alone can show about this bootstrapper's real user-facing behavior, +worth keeping in mind when reading any other scenario in this file that involves a `set /p`-based +consent gate. -### Scenario 6: Argv passthrough (REQ-026) -- launch arguments through the bootstrapper +**For contrast, briefly (full treatment is Pass 4/adversarial-recovery territory, not this +Part):** not every consent gate in this file shares the "blocks forever" shape. The REQ-009 +provider-cascade gate (`:cascade_consent_gate`, only reached if a build succeeds but a dependency +repair genuinely fails) is the one gate that's genuinely TIMED for a real user -- `choice /C YN /N +/T 30 /D N`, defaulting to decline after 30 seconds with no answer, so it structurally cannot hang +forever even for a truly unattended user. The REQ-014 system-Python consent gate +(`:system_python_consent_gate`, only reached as the absolute last-resort Tier 4) is unbounded like +the two documented above, but its full question text (unusually, including the actual `[y/n]` +wording) IS an unconditional `echo` rather than living inside `set /p` -- so, unlike this +scenario's two prompts, CI logs genuinely do show the complete question for that one, e.g. +`Proceed with System Python? (Global pollution risk) [y/n]: y to accept, n to decline.` (REAL CI +CAPTURE, same run, job `90179708091`) -- only its own terse follow-up `"Your choice [y/n]: "` line +is hidden the same way. Neither of these two gates fires on this Part's happy path; both are +documented fully in Pass 4. -Extra arguments after the entry file on `run_setup.bat`'s own command line (up to 8) are forwarded -verbatim to the target program at every real launch site -- the cached-EXE fast path, the fresh EXE -verification, the no-EXE interpreter run, and the post-execution checkpoint's elective second run. -This is a documented, opt-in escape hatch (no detection or heuristics involved) for a program that -needs `--flag value`-style launch arguments to run correctly, on top of this bootstrapper's usual -zero-argument double-click flow. +--- -**The postflight guidance a user sees after a successful EXE build** (Scenario 1's full panel -above shows this in context): +## Part II: Second run, nothing changed (repeat-run fast paths) -``` - Does your program need launch arguments (e.g. --input file.csv)? Run - this bootstrapper again with them added after the entry file, e.g. - run_setup.bat "" --input file.csv - and they will be forwarded to your program during THIS setup run - (up to 8 extra arguments). This does not change how a plain - double-click of dist\.exe launches it afterward -- for that, - make a Windows shortcut to the .exe and add the arguments to its - Target field, or launch it yourself from a Command Prompt. -``` +**Scope note:** this Part documents the success side of running the bootstrapper a SECOND time in +the same folder with nothing changed -- same entry file, same requirements, no test flags. The +FAILURE side of one of these fast paths (a stale cached EXE that's kept and later exits non-zero) +is already documented as Scenario 43b (Part IX); this Part doesn't repeat that. Evidence again +comes from a recent clean green run (`30328748330`, commit `5872028`) rather than one single +dedicated "repeat run" test file -- `tests/selfapps_envsmoke.ps1` re-invokes `run_setup.bat` a +second time in the same scratch directory with nothing changed (the EXE fast path), and +`tests/selftest.ps1`'s stub scenario and `tests/selfapps_depcheck.ps1` go one step further -- +`Run 1` (fresh bootstrap), `Run 2` (identical, EXE fast path), then deliberately touch the source +file and run a THIRD time -- exercising the "source changed just enough to force a rebuild, but +the environment itself doesn't need recreating" fast paths this Part's second scenario covers. -**The equivalent guidance on the no-EXE path** (direct-interpreter-invocation form, part of -Scenario 7a's panel below): +### Scenario 6: The EXE fast path (nothing changed at all) -``` - Need launch arguments? Add them directly after that command, e.g. - "" "" --input file.csv -``` +**What's tested:** `self.fastpath` (`tests/selfapps_envsmoke.ps1`'s second, back-to-back +invocation of `run_setup.bat` in the same scratch directory, zero CLI arguments, nothing touched). -Both are additive to the launch commands already shown in each panel, not a separate prompt -- -matching this bootstrapper's general rule that env-var/CLI flags only ever add an opt-in path or -suppress an optional step, never gate a behavior the Prime Directive needs (see CLAUDE.md's -`[REQ-019]`). +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` ("real" lane), the console +capture of that second invocation (`~envsmoke_fastpath.log`) plus the matching block of +`~setup.log`. -### Scenario 7: Honest ambiguous-exit messaging (REQ-027) +**What fires FIRST, before any provider/entry/dependency logic even starts:** `:try_fast_exe` is +called immediately after environment-name derivation and the Python-file count, right at the top +of the file -- before uv acquisition, before Miniconda, before `:determine_entry`, before +anything else. It runs exactly ONE real check: compare `dist\.exe`'s modification time +against the newest non-infrastructure `.py` file's modification time (via the embedded +`HP_FAST_CHECK` helper). If the EXE is newer-or-equal, the whole rest of the bootstrap short- +circuits straight to `:success`. -Both panels below fire only when a verification run ends AMBIGUOUSLY -- the program exited with an -error, and no automatic repair (`--hidden-import` auto-recovery, the REQ-009 dependency-resolution -cascade) fixed it. Neither panel claims to know WHY: a bug in the program's own code, something -this bootstrapper missed, or an unresolved dependency are all indistinguishable from here, and both -panels say so plainly rather than guessing. This is messaging only -- `~bootstrap.status.json` -semantics, the process exit code, and consent-gate behavior are all unchanged. +**Non-interactive console text (what CI captures -- this is also exactly what a real user would +see if they ran the bootstrapper non-interactively, e.g. from a script):** -Both are new enough (shipped, then refined once more for wording, entirely within this same -session) that no CI run has yet produced a console capture including the current wording -- both -quotes below are sourced directly from `run_setup.bat`, not a job log. +``` +Tue 07/28/2026 4:30:33.81 [INFO] Environment name: _envsmoke +Tue 07/28/2026 4:30:33.83 [INFO] Host OS: Microsoft Windows [Version 10.0.26100.32995] +Tue 07/28/2026 4:30:34.04 [INFO] Host PowerShell: 5.1.26100.32995 +Tue 07/28/2026 4:30:34.10 [INFO] Python file count: 1 +Tue 07/28/2026 4:30:34.98 [INFO] Fast path: reusing dist\_envsmoke.exe +Tue 07/28/2026 4:30:35.96 [INFO] Entry smoke exit=0 +Tue 07/28/2026 4:30:35.97 [STATUS] Run Status: SUCCESS (Exit Code: 0) +Tue 07/28/2026 4:30:35.99 [INFO] Fast path: skipping PyInstaller rebuild for existing dist\_envsmoke.exe +``` -#### 7a. No-EXE path, interpreter also failed +No "SETUP COMPLETE" postflight banner appears -- confirmed both structurally (that dispatch is +gated on the fast path NOT having fired) and in the raw capture, which ends right after the last +line above. `~setup.log` shows one extra line between "Fast path: reusing" and "Entry smoke +exit=0" that never reaches the console (`Fast path command: "dist\_envsmoke.exe" > "~run.out.txt" +2> "~run.err.txt"`) -- a raw log-file-only write, not part of what a user actually sees. -Fires when BOTH PyInstaller and the Nuitka fallback fail to package the app outright, AND the -interpreter fallback that runs next (the only way left to run the program at all) also exits -non-zero -- the scenario Scenario 2a's own `execfail` sub-case would hit if its trivial stub script -didn't happen to exit cleanly. Source: `:print_no_exe_briefing`'s `:noexe_caveat` branch, -`run_setup.bat`: +**Interactive console text (what a genuine double-click end user sees -- differs from CI because +`HP_CI_LANE` is unset, so `:try_fast_exe` takes its OTHER branch, `:try_fast_exe_probe`, which +launches the cached EXE through the same never-kills fail-fast probe mechanism Scenario 41 +documents rather than the plain redirect above).** Assembled from real, independently-confirmed +fragments (the header and PID lines are genuine captured text from a different test that forces +this same interactive branch; their pairing into a clean, fast, successful sequence is +`[Extrapolated Branch]`, grounded directly in source rather than guessed): ``` -============================================================ - NO STANDALONE .EXE -- AND WE CAN'T CONFIRM YOUR CODE RAN CLEANLY -============================================================ - We could not package your app into a double-clickable .exe - (see the ERROR message above for why). We also just ran it - directly via the prepared Python environment, and it exited - with an error (see the [STATUS] line above) -- so we can't - tell whether that's a bug in the Python code we tried to run - or something this bootstrapper missed. Your environment and - dependencies ARE still installed correctly; run it yourself - below to see the full output. - - RUNNING YOUR APP (without an .exe) -- the most direct option - "" "" - Need launch arguments? Add them directly after that command, e.g. - "" "" --input file.csv +Tue 07/28/2026 4:30:33.81 [INFO] Environment name: _envsmoke +Tue 07/28/2026 4:30:33.83 [INFO] Host OS: Microsoft Windows [Version 10.0.26100.32995] +Tue 07/28/2026 4:30:34.04 [INFO] Host PowerShell: 5.1.26100.32995 +Tue 07/28/2026 4:30:34.10 [INFO] Python file count: 1 +Tue 07/28/2026 4:30:34.98 [INFO] Launching your program now via the cached standalone EXE (PyInstaller build): dist\_envsmoke.exe +[INFO] Process ID 7692. If it seems stuck: Task Manager > Details tab > find this PID > End Task (this window stays open). + <- the app's own live stdout/stderr tees here, if any +Tue 07/28/2026 4:30:35.96 [INFO] Entry smoke exit=0 +Tue 07/28/2026 4:30:35.97 [STATUS] Run Status: SUCCESS (Exit Code: 0) +Tue 07/28/2026 4:30:35.99 [INFO] Fast path: skipping PyInstaller rebuild for existing dist\_envsmoke.exe +Press any key to continue . . . +``` - Want to try different arguments through the bootstrapper itself - instead? Your already-installed environment is reused either - way; it will just attempt the .exe build again too: - run_setup.bat "" arg1 arg2 +The "still running after Nms, keep waiting?" WARN line that the fail-fast probe can print is +conditional on the process actually exceeding its short classification window (confirmed absent +here via real CI capture of the same forced-interactive mechanism failing fast in an unrelated +scenario) -- omitted above since "nothing changed, app still runs fine" implies a normal-speed +exit. `Press any key to continue . . .` is `cmd.exe`'s own native output from a `pause` at the +very end of the main line, gated on `HP_CI_LANE` being unset -- real end-user only, never appears +in any CI log. - KEEP these files with your project: - requirements.txt -- packages your app depends on - runtime.txt -- Python version pin +**Why it's fast -- everything this run skips entirely, not just runs faster:** once `:try_fast_exe` +succeeds, the bootstrap jumps to `:success` before any of the following ever execute: the +`HP_CI_SKIP_ENV` dispatch, the entire uv acquisition block (no download, no `uv venv`, no +`UV_PYTHON_PREFERENCE` even gets set), `:select_conda_bat` and all Miniconda install/probe logic, +the env-state fast path (moot -- conda was never considered), `:conda_base_update`, the +`HP_PREP_REQUIREMENTS` heuristic dependency augmentation, `:determine_entry` (the cached EXE's +identity is trusted as-is, no REQ-002 re-selection), pipreqs entirely (no `pipreqs.install`/ +`pipreqs.run`, no `requirements.auto.txt` diff), and -- the single biggest reason this is fast -- +`:run_entry_smoke` never runs, meaning no `py_compile` preflight and no PyInstaller build +invocation of any kind, cached or otherwise. The reused EXE genuinely gets EXECUTED, not merely +detected -- confirmed by the real `Entry smoke exit=0`/`[STATUS]` lines above, which come from an +actual process launch. - SAFE TO DELETE to reclaim disk space: - .*_env\ folders -- environment directories - ~* files -- tilde-prefix work files (e.g. ~setup.log) -============================================================ -``` +--- -The direct-run command stays the visually primary option (matches this panel's own established -preference for running the program directly over going back through the bootstrapper); the -bootstrapper-rerun mention is deliberately secondary and uses the real entry filename (`%HP_ENTRY%` -is reliably set by this point in the pipeline -- unlike Scenario 7b below). +### Scenario 7: Source touched just enough to force a rebuild, but the environment is reused -When the interpreter run instead exits CLEANLY (the common case, and what Scenario 2a's own capture -shows), this panel's header and opening paragraph read differently -- plain "YOUR CODE RAN -- BUT -NO STANDALONE .EXE WAS PRODUCED", with no claim of an unconfirmed run -- but the rest of the panel -(launch commands, KEEP/SAFE TO DELETE lists) is identical either way. +**What's tested:** `tests/selftest.ps1`'s stub scenario and `tests/selfapps_depcheck.ps1`, both of +which do Run 1 (fresh) -> Run 2 (Scenario 6's EXE fast path) -> touch the entry file's content +and modification time -> Run 3, which is the case documented here: the EXE fast-path timestamp +check now fails (source is newer than the cached EXE), so PyInstaller reruns and produces a new +EXE -- but the ENVIRONMENT itself (the uv venv or conda env, and already-satisfied dependencies) +is recognized as still valid and reused rather than recreated from scratch. -#### 7b. Cached-EXE fast path, kept despite a non-zero exit +**Source:** REAL CI CAPTURE, run `30328748330`, jobs `90179708091` ("real" lane, uv-first) and +`90179708094` ("conda-full" lane). -Fires when the fail-fast probe classifies a REUSED `dist\.exe` (the top-of-file fast path, -before any provider/entry-file logic runs) as alive/healthy -- so it's kept, never -discarded-and-rebuilt -- and it later exits non-zero. Before this fix, this exact case had no -postflight signal at all beyond one `[WARN]` log line buried among other console output. Source: -`:print_fastpath_ambiguous_note`, `run_setup.bat`: +**uv-first lane** (`.uv_env\Scripts\python.exe` already exists and its `import pip` canary +succeeds, so venv creation is skipped -- the gate just above `:uv_venv_ready`): ``` -============================================================ - SETUP COMPLETE -- BUT WE CAN'T CONFIRM YOUR LAST RUN WORKED -============================================================ - Your existing standalone application was reused (dist\.exe), - and it exited with an error just now (see the [STATUS] line - above) -- so we can't tell whether that's a bug in the Python - code we tried to run, or something else. Your environment and - dependencies ARE still installed correctly. - - RUNNING YOUR APP - Double-click dist\.exe to run it, or run it from a - Command Prompt to see the full output. +[INFO] uv: reusing existing .uv_env +[INFO] HP_ENV_MODE=uv +[BOOT] REQ-009: Selected Python provider: UV. +``` - WANT TO TRY AGAIN? You do not have to start over from scratch -- - just run this bootstrapper again the same way you did before; - your already-installed environment and built .exe are reused. +**conda-full lane** (`~env.state.json` is valid and the conda env's `python.exe` is present -- +`:env_state_fast_path`; this mechanism is explicitly bypassed in uv mode, since it exists purely +for the conda-specific case): - WANT A FRESH BUILD instead (re-checks all dependencies from scratch)? - Delete dist\.exe and run this bootstrapper again. -============================================================ +``` +[INFO] Env-state fast path: reusing conda env _selftest_stub. +[BOOT] REQ-009: Selected Python provider: Conda (Portable) [fast path]. ``` -This panel is a PLAIN INFORMATIONAL PRINT, never a consent gate -- the cached-EXE fast path is -deliberately zero-friction for prompts (see `docs/agent-interconnect.md`'s "Fast path = ZERO -friction" design requirement), and this doesn't violate that since it never asks a question. +Confirmed firing across every scratch env in that job's log (not a one-off), so this is a broadly +reliable fast path, not a narrow coincidence. `self.stub.state_skip`'s own NDJSON assertion checks +for EITHER phrase, which is why one shared test scenario validates both depending on which lane +it runs under. -**No entry filename appears anywhere in this panel, unlike 7a's rerun mention -- deliberately.** -`HP_ENTRY` is not set yet at the point the top-of-file fast path runs (it fires before -`:determine_entry` ever executes, since the cached EXE is self-contained and doesn't need the -original source filename to relaunch), so naming one here would show blank or stale text. The two -rerun options are worded to distinguish a genuinely different tradeoff instead: rerunning WITHOUT -deleting the EXE reuses it (via the same fast path that got the user here) with no promise about -whether it's actually faster overall, while deleting it first forces a full, slower, from-scratch -dependency check. +**Both lanes then converge on the same dependency-install skip, immediately after dependency +discovery** (pipreqs + the Tier 1 autopep723 merge from Scenario 3 still run normally here -- +neither of the two fast paths above touches dependency DISCOVERY, only environment creation): ---- +``` +[INFO] Dep-check: all pipreqs packages satisfied in lock; skipping conda install. +``` -## Part III: Default double-click happy path (uv-first, fresh machine, zero flags) +This message literally says "skipping conda install" even in uv mode -- confirmed intentional +(a shared log line covering both providers via the same `HP_DEP_SKIP` flag), not a copy-paste +bug, so don't read it as evidence the wrong provider was used. **One nuance worth flagging so it +isn't misread as a second, real install still happening:** in conda mode specifically, an +unconditional "pip gap fill from `requirements.txt`" step still runs immediately after this skip +line, even though nothing was found missing -- it's a fast, harmless no-op safety net (confirmed +completing in well under a second in the real capture), not evidence the skip failed to take +effect. -**Scope note:** Parts I and II each document one feature area's edge cases. This Part documents -the OTHER thing this doc has never shown: what a completely ordinary run looks like end to end -- -one `.py` file, no test hooks, no prior state, no flags, uv reachable (the REQ-009 default, -`uv -> conda -> embed -> venv -> system`). This is what the large majority of real users actually -see. Evidence is pulled from a recent fully-green run (`30328748330`, commit `5872028`, all lanes -green) rather than any single dedicated "happy path" test -- no such test exists as one file, so -each piece below is sourced from whichever real, non-`HP_CI_SKIP_ENV` sub-bootstrap in that run -exercises it most faithfully (mainly `tests/selfapps_envsmoke.ps1`'s real env-smoke sub-bootstrap -and `tests/selftest.ps1`'s stub-app fast path), cited individually per item. +**Net effect for the user:** a rebuild triggered by an ordinary source edit is meaningfully faster +than the very first run -- no fresh uv/conda acquisition, no fresh venv/env creation, and (when +nothing about the dependency set changed) no re-running of the actual install step -- while still +producing a genuinely fresh PyInstaller build and a real verification run of the new EXE. -**A structural caveat that applies to every scenario in this Part, stated once here instead of -repeated per-scenario:** every CI lane sets `HP_CI_LANE` at the job level (`batch-check.yml`), -which silently auto-declines every consent prompt in the file the instant it's reached, with no -wait. A genuine double-click has none of `HP_CI_LANE`/`NOINPUT`/`HP_NONINTERACTIVE` set, so any -prompt this Part encounters would, for a real user, actually pause and wait for a keypress (or, -for the one genuinely timed gate, wait up to its timeout) instead of resolving instantly. Scenario -11 below covers this distinction in detail for the two prompts that fire on this exact happy path; -it applies identically to every other consent gate documented elsewhere in this file. +--- -### Scenario 8: Pre-flight guards, lock acquisition, and entry detection on a clean run +## Part III: User configuration and CLI overrides -**What's tested:** no single dedicated test asserts the CLEAN (non-firing) pass of these checks -- -`docs/agent-ndjson.md` only registers rows for the *firing* branches (`self.warn.onedrive`, -`self.warn.longpath`, `self.warn.sysdir`, `self.stub.lock_held_decline`, etc.). The evidence below -is the incidental byproduct of `tests/selfapps_envsmoke.ps1`'s full, real bootstrap, which captures -every byte of console output via `cmd /c .\run_setup.bat > '~envsmoke_bootstrap.log'`. +**Scope note:** argv passthrough (extra launch arguments forwarded to the user's program, REQ-026) +is already fully covered as Scenario 42 -- cross-reference it rather than re-documenting it here. +This Part covers the remaining configuration surface: the five `PVW_*` super-user override +environment variables (distinct from `HP_TEST_*`, which are CI-only and out of scope for this +doc), and the CLI-argument/drag-and-drop entry-file override. -**Source:** REAL CI CAPTURE, run `30328748330`, `tests/~envsmoke/~envsmoke_bootstrap.log` -(published diagnostics site), identical across all 6 lanes checked (`real`, `uv`, `conda-full`, -`justme-test`, `contract-uv`, `contract-uv-fail`): +**All five `PVW_*` variables share one generic acknowledgment line**, printed the instant the +variable is defined, before any real detection/acquisition work runs and regardless of whether the +value ever turns out to be usable: ``` -[WARN] UNC paths not supported -Tue 07/28/2026 4:29:43.96 [INFO] REQ-015: Appending standard ignores to .gitignore. +[DEBUG] Using super-user override for PVW_: ``` -**The very first console line above (`[WARN] UNC paths not supported`) is historical, from the -captured run, and no longer appears -- fixed since this capture, kept here unedited because it is -a real, timestamped log.** It came from a broken, redundant top-of-file `findstr`-based check (in -the file's unlabeled prologue before the first `:label`): `findstr /C:"\\\\"` was intended to -match a UNC-style double-backslash but did not correctly gate on one -- its exact internal parsing -(`findstr`'s own additional backslash-doubling behavior layered on cmd.exe's own C-runtime -backslash-before-quote argument parsing, documented in `docs/agent-lessons-learned.md`) was never -independently verified. What WAS confirmed is that it fired on 6/6 checked lanes against an -entirely ordinary local CI checkout path (`D:\a\Python_vs_Windows\Python_vs_Windows\tests\ -~envsmoke\`) -- not a UNC path. The companion, independent, correctly-targeted UNC-prefix check a -couple of lines below it (`if "%HP_SCRIPT_LAUNCH_DIR:~0,2%"=="\\"`, which prints the much louder -`*** WARNING: UNC/network paths detected...` banner) already covered the real UNC-detection job on -its own, so the broken check was simply removed rather than repaired -- see -`docs/agent-closed-backlog.md`'s Item 8 for the full trace. - -Between that line and `REQ-015`, nothing else prints -- `HP_APP_ARGS` capture (REQ-026, pure -variable assignment), the workspace-path-exists check, `cd /d`, and `HP_SCRIPT_ROOT` construction -(the rest of that same unlabeled prologue) are all silent by design on a clean pass. +**A real nuance worth stating up front rather than repeating per-variable:** the source's own +header comment describes all five uniformly as bypassing auto-detection, but that's only literally +true for two of them. `PVW_UV_EXE` and `PVW_WORKSPACE` genuinely skip the corresponding +detection/creation branch outright. `PVW_PYTHON_EXE` and `PVW_TARGET_PY` do NOT skip anything -- +the full normal detection logic still runs to completion (including real network/disk work), and +the override simply overwrites the *result* variable afterward. `PVW_CONDA_EXE` (Scenario 9) is +its own case again, discussed separately since it has a unique interaction with the conda +self-heal flow. -**The four REQ-025-family pre-flight guards (path-length, OneDrive, system-directory, disk-space -- -also part of that unlabeled prologue) are completely silent unless they fire.** Confirmed by both -the absence of any related text anywhere in the captured log, AND by reading the source: none of -the four has an `else` branch that prints a success/clean message -- each is a bare `if (...) ( -echo/log ... )` with nothing on the false path. A real user on an ordinary setup (short path, not -under OneDrive, not under `Windows`/`Program Files`, plenty of free disk) sees zero output from -any of these four checks. +### Scenario 8: `PVW_PYTHON_EXE` / `PVW_UV_EXE` / `PVW_TARGET_PY` / `PVW_WORKSPACE` -**`:acquire_lock` is equally silent on an uncontended acquire.** The `mkdir "%HP_LOCK_DIR%"` call -succeeds immediately (no prior lock directory), jumps straight to `:lock_acquired`, writes a -transient `~bootstrap.lock\owner.txt` marker (a file, not console output), and returns -- every -`echo`/`:log` call inside `:acquire_lock` lives inside the "another instance is already running" -branch, only reached on a genuine `mkdir` failure. The lock directory (and its `owner.txt`) is -gone again by the time the run completes (`:release_lock`, called from both `:die` and `:success`, -is equally silent). +**What's tested:** all four now have real, valid-value end-to-end CI coverage; `PVW_PYTHON_EXE` and +`PVW_WORKSPACE` also each have a real, invalid-value CI scenario (`tests/selfapps_pvw_overrides.ps1`, +`uv` lane, non-gating -- confirmed passing in the same fully-green CI run this whole document's +release cycle was verified against: run `30709255610`, job `91393894838`). Neither `PVW_UV_EXE` +nor `PVW_TARGET_PY` has invalid-value coverage of its own; that half of 8b/8c below stays +`[Extrapolated Branch]`, traced from source. -**Entry detection for the common single-`.py`-file case** (`:determine_entry` -> -`tools/find_entry.py` -> `:record_chosen_entry`) produces exactly one line, sourced from -`tests/~envsmoke/~setup.log` (same run, the real, non-`HP_CI_SKIP_ENV` `:determine_entry` call -site inside `:after_env_bootstrap` -- not the separate `HP_CI_SKIP_ENV`-only `:ci_skip_entry` -implementation that -`tests/selfapps_entry.ps1`/`selfapps_single.ps1` exercise, which looks textually similar but is a -different code path): +**8a. `PVW_PYTHON_EXE`** overrides `HP_PY` at the shared convergence point every REQ-009 provider +path (uv, conda, embed, venv, system, and every provider-cascade re-entry) funnels into after +already selecting and setting up a working interpreter -- so it does NOT skip provider +acquisition, it only overwrites the final result: ``` -Chosen entry: app.py +[INFO] Python host: using super-user override PVW_PYTHON_EXE. ``` -This is identical whether the sole `.py` file has a preferred name (`main.py`/`app.py`/`run.py`/ -`cli.py`) or an arbitrary one (`tools/find_entry.py`'s `len(files) == 1` branch handles both the -same way, with zero stderr diagnostics either way) -- the interactive picker -(`:pick_entry_interactive`) is only ever reached when more than one `.py` file is ambiguous, which -this scenario deliberately doesn't have. `:determine_entry` actually runs twice in a normal -bootstrap (once early, for PEP 723/autopep723 discovery purposes, well before `:after_env_bootstrap`, -and once again at the real entry-selection call site inside `:after_env_bootstrap` that produces -this console line) -- only the second call's result is what a user sees echoed. - ---- - -### Scenario 9: Provider acquisition and dependency install (uv-first) - -**What's tested:** `tests/selfapps_envsmoke.ps1`'s real sub-bootstrap (`self.env.smoke.*` rows), -uv-first lane, against a stub `app.py` that genuinely does `import colorama` -- a real, if small, -dependency, chosen deliberately over a zero-dependency stub so the dependency-discovery/install -machinery actually has something to do. - -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` ("real" lane), -`tests/~envsmoke/~envsmoke_bootstrap.log` (what a real user's terminal shows) and -`tests/~envsmoke/~setup.log` (internal detail file, receives everything the console does PLUS -extra content this repo deliberately never puts on-screen -- see this doc's own "Console vs. -`~setup.log`" note at the top). Both are cited below, labeled. - -**IMPORTANT: this exact CI run's job-level "Bootstrap environment (run_setup.bat)" step is NOT -representative evidence for this scenario** -- that step runs `run_setup.bat` against the -bootstrapper repo's OWN root (no loose `.py` files there), so it takes the `no_python_files` -graceful-exit path and produces nothing relevant. The genuinely representative evidence is the -"Self-test: real env smoke (CI-only)" step's own inner sub-bootstrap, which runs -`run_setup.bat` for real against a scratch app directory (matches CLAUDE.md's own documented -finding for Active Backlog item 15/`conda_avail`'s history -- the same distinction applies here). +Real, confirmed via `tests/selfapps_pvw_overrides.ps1`'s `self.pvw.python_exe.valid` scenario +(a two-stage test: stage 1 does an ordinary uv bootstrap to materialize a real interpreter, stage 2 +is a genuinely fresh scratch directory pointing `PVW_PYTHON_EXE` at stage 1's interpreter) -- the +override log line fires and the app runs successfully via the borrowed interpreter. Invalid-value +trace, confirmed by the same file's `self.pvw.python_exe.invalid` scenario (a nonexistent path): no +existence/executability check on the path itself; the first real probe is a non-fatal interpreter +smoke test (`[WARN] Interpreter smoke test failed (continuing).`) that does NOT abort the run -- +every subsequent `pip install` call is similarly wrapped in a WARN-only failure handler, so the +bootstrap proceeds all the way to the PyInstaller build attempt with a broken interpreter before +finally hitting a real failure there (the pre-existing, already-documented `:die`/`state=error` +path). A bad `PVW_PYTHON_EXE` is therefore detected early (one WARN) but not treated as fatal +until several steps downstream, not at the point of misuse. -**uv acquisition** (console, first-ever run, no cached `~uv_bin` -- every fresh CI scratch dir -starts this way, matching a real user's first-ever double-click): +**8b. `PVW_UV_EXE`** overrides `HP_UV_EXE` and genuinely skips the entire uv download/acquire +branch (jumps straight past it): ``` -[INFO] uv: UV_PYTHON_PREFERENCE=only-managed (orchestration uses managed Python). -[INFO] uv: downloading to ~uv_bin... -[INFO] Downloading uv from https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip... -[INFO] uv: acquired at ~uv_bin\uv.exe -[INFO] uv-first: Miniconda download skipped. +[INFO] uv: using super-user override PVW_UV_EXE. ``` -(`~setup.log`-only, never shown on-screen: curl's own progress-bar text between the "downloading" -and "acquired" lines, plus uv's own managed-CPython fetch triggered by the version-detection call --- `Downloading cpython-3.14.6-windows-x86_64-none (download) (21.5MiB)` / -`Downloaded cpython-3.14.6-windows-x86_64-none (download)`. On a REPEAT run with `~uv_bin` already -populated, the cached-binary branch just before `:uv_acquire_download` instead logs `[INFO] uv: -cached binary found at ~uv_bin\uv.exe` -- not independently captured here since every CI scratch -dir is fresh; see this doc's repeat-run scenarios, added in a later pass, for the dedicated -treatment.) - -**uv venv creation** (console, no pre-existing version pin -- this app has neither `runtime.txt` -nor a `pyproject.toml` constraint): +REAL CI CAPTURE, run `30328748330`, job `90179708086` (`contract-uv` lane), +`tests/selfapps_contract_uv.ps1`'s dedicated uv-version-forwarding scenarios (which reuse an +already-downloaded `uv.exe` via this override specifically to avoid re-downloading it for each of +several sub-bootstraps): ``` -[INFO] uv: creating venv at .uv_env... -[INFO] uv: venv created at .uv_env -[INFO] HP_ENV_MODE=uv -[BOOT] REQ-009: Selected Python provider: UV. -[INFO] runtime.txt written: python-3.14.6 +[DEBUG] Using super-user override for PVW_UV_EXE: D:\a\...\~envsmoke\~uv_bin\uv.exe +[INFO] uv: using super-user override PVW_UV_EXE. +[INFO] uv: creating venv at .uv_env with Python 3.12... ``` -A version-constrained app (e.g. `pyproject.toml` with `requires-python = ">=3.9"`) instead logs -`[INFO] uv: creating venv at .uv_env with Python 3.9 or newer...` -- confirmed via a second real -capture in the same run (`tests/~pyproject_prec/~pyproject_prec_bootstrap.log`), an exact pin like -`python==3.11` templates to `...with Python 3.11...` per the `HP_UV_PY_DISP` derivation just -above `:uv_venv_ready` (not independently captured, but the template is a literal source string, -not inferred). `~setup.log`-only detail: uv's own raw venv output (`Using CPython 3.14.6`, -`Creating virtual environment with seed packages at: .uv_env`, a `Failed to hardlink files; -falling back to full copy` warning, `+ pip==26.1.2`, `Activate with: .uv_env\Scripts\activate`) -- -none of this reaches the console. +Invalid-value trace (`[Extrapolated Branch]`, no test forces a bad path): a broken/invalid +`PVW_UV_EXE` is fully absorbed by the existing REQ-009 provider-cascade fallback machinery -- the +uv-first Python-detection probe fails gracefully (WARN, falls toward Miniconda), and even if venv +creation is separately attempted with the same bad binary, an independent, exit-code-agnostic +on-disk check (`if not exist "...\Scripts\python.exe" goto :uv_venv_fail`) catches a binary that +misleadingly reports success without doing real work -- no crash, no silent success, clean +fall-through to conda. -**Dependency discovery** -- pipreqs and the REQ-005.12 `autopep723 check` Tier 1 merge running -TOGETHER, genuine production behavior (no `HP_SKIP_PIPREQS` test-isolation flag involved, unlike -the dedicated `selfapps_autopep_discovery.ps1`/`selfapps_pvw_idempotent.ps1` tests that deliberately -isolate one mechanism from the other): +**8c. `PVW_TARGET_PY`** overrides `PYSPEC` at the shared merge point both the uv-first and +conda-base detection paths converge on -- like `PVW_PYTHON_EXE`, detection still runs to +completion first: ``` -[INFO] pipreqs 0.4.13 installed successfully; using it for dependency discovery. -[INFO] pipreqs (direct) command: pipreqs . --force --mode compat --savepath "...\requirements.auto.txt" --ignore ".git,.github,.venv,venv,env,.uv_env,build,dist,__pycache__,tests" -*** [WARN] Dependencies were auto-detected (pipreqs) -*** [WARN] Auto-detection may be incomplete or incorrect -*** [INFO] Consider adding requirements.txt or PEP 723 metadata for reliability -[INFO] REQ-005.5: dependency source diff computed -- ~pipreqs.diff.txt -[INFO] REQ-005.12: autopep723 discovery merge complete. +[INFO] Python version: using super-user override PVW_TARGET_PY. ``` -**A real quirk worth flagging so it isn't misread**: the bootstrap log around this point also shows -`DEP_FINAL_COUNT=0` even though `colorama` is a genuine, real dependency that gets installed a few -steps later -- this count is taken BEFORE `requirements.auto.txt` is copied into `requirements.txt` -a few lines further down, still inside `:after_pipreqs_run`'s own tail, so `DEP_FINAL_COUNT=0` -here does not mean "pipreqs found nothing"; it's a pre-existing ordering quirk in the log sequence, not a bug in this -run. `~setup.log`-only (pipreqs's raw output is redirected to a SEPARATE file entirely, -`~pipreqs_direct.log`, not even `~setup.log` -- a real user never sees any of the following): +REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane), +`tests/selfapps_pipgap.ps1` (sets this to `python=3.12` to pin conda's Python version so a +specific `opencv-python` wheel is guaranteed available for a different test purpose entirely): ``` -WARNING: Import named "colorama" not found locally. Trying to resolve it at the PyPI server. -WARNING: Import named "colorama" was resolved to "colorama:0.4.6" package (https://pypi.org/project/colorama/). -Please, verify manually the final list of requirements.txt to avoid possible dependency confusions. -INFO: Successfully saved requirements file in ...\requirements.auto.txt +[DEBUG] Using super-user override for PVW_TARGET_PY: python=3.12 +[INFO] Python version: using super-user override PVW_TARGET_PY. ``` -plus a handful of cosmetic `SyntaxWarning` lines from pipreqs 0.4.13's own bundled `docopt`/`yarg` -dependencies. `~setup.log` also shows the autopep723 merge's own uvx tool-bootstrap noise -(`Installed 1 package in 26ms`) and its own result line, `no-op: all autopep723 dependencies -already present` -- because pipreqs already found `colorama` first, autopep723's own independent -discovery of the same import is redundant here, a real working example of this repo's "augment, -never replace" design for the two mechanisms. +Invalid-value trace (`[Extrapolated Branch]`): no format validation; a garbage value becomes an +invalid conda package spec or an invalid `uv venv --python` request, surfacing as a real, +correctly-handled provider failure absorbed by the same fallback/cascade machinery as 8b -- +reaching a graceful `:die` (`state=error`) only if every fallback tier is also exhausted, never an +uncontrolled crash. -**Dependency install:** +**8d. `PVW_WORKSPACE`** overrides `HP_UV_ENV_PATH` (the uv venv's path) with a clean, immediate +override -- the default is assigned and instantly replaced before any use, unlike 8a/8c's +"let it run, override the result" pattern. **Scope limitation worth flagging explicitly: this +variable only takes effect in uv mode.** Conda's own environment path has no corresponding check +at all -- a conda-mode run ignores `PVW_WORKSPACE` entirely. + +Uniquely among the four, **there is no dedicated confirmation log line at its actual point of +use** -- only the generic top-of-file `[DEBUG]` line, which fires purely because the variable is +defined, before it's even known whether uv mode (where this variable matters at all) will be +selected. Real, confirmed via `tests/selfapps_pvw_overrides.ps1`'s own `self.pvw.workspace.valid` +scenario: ``` -[INFO] UV_USED=1 -[INFO] DEP_INSTALLED_CAPTURED=1 -[INFO] Environment snapshot written: ~environment.lock.txt -[INFO] REQ-005.11: PEP 723 header write-back succeeded via uv add --script. +[DEBUG] Using super-user override for PVW_WORKSPACE: ``` -`~setup.log`-only (`uv pip install --python ... -r requirements.txt`'s own raw output, never on -console): `Using Python 3.14.6 environment at: .uv_env`, `Resolved 1 package in 257ms`, `Prepared -1 package in 43ms`, `Installed 1 package in 15ms`, ` + colorama==0.4.6`. The real -`~environment.lock.txt` (uv mode copies the pip-freeze output here) contains more than just -`colorama` -- it also lists pipreqs's OWN transitive dependencies (`certifi`, `charset-normalizer`, -`docopt`, `idna`, `pipreqs`, `requests`, `urllib3`, `yarg`), because in `HP_ENV_MODE=uv`, pipreqs -is installed via `uv pip install --python "%HP_PY%"` into the SAME venv as the target app (inside -the pipreqs install step, before `:pipreqs_direct_done`), not an isolated tool venv. This is -genuine, expected production behavior --- a real user's `.uv_env` always ends up with pipreqs's own dependency footprint mixed in -alongside their actual dependencies, not a CI artifact. +with `Scripts\python.exe` genuinely present at the custom path afterward (and the default +`.uv_env` never created). Invalid-value trace, confirmed by the same test file's +`self.pvw.workspace.invalid` scenario (a path already occupied by a plain file, so uv cannot +create a venv "inside" it): no path validation up front, but the failure lands on exactly the +already-established `:uv_venv_fail` fallback chain 8b/8c also use, reaching the log line `falling +back to conda create` -- no crash, no silent success. --- -### Scenario 10: Build, verify, and the final status panel +### Scenario 9: `PVW_CONDA_EXE` and its interaction with the conda self-heal flow -**What's tested:** `self.stub.fastpath` (`tests/selftest.ps1`, real lane), a genuine first-run -(non-cached) build of a trivial one-line stub app (`hello_stub.py` = `print("hello-from-stub")`). -Chosen over Scenario 9's `colorama`-importing app for this piece specifically because it produces -a single, fully self-consistent, real capture spanning build through the final status panel with -zero staleness caveats -- strictly better evidence than the similar block quoted in Scenario 1 -(Part I), which has two lines marked "not yet re-confirmed against a fresh capture." +**What's tested:** `self.corrupt.conda.override_exit` (`tests/selftest.ps1`), self-contained by +construction (no ordering dependency on Miniconda already being installed elsewhere in the job, +unlike its sibling corrupt-conda scenarios). -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` ("real" lane), -`tests/~selftest_stub/~stub_bootstrap.log`: +**Source:** REAL CI CAPTURE, run `30328748330`, both gating lanes (`real` job `90179708091` and +`conda-full` job `90179708094`), both `pass: true`, `exitCode: 2`. + +`PVW_CONDA_EXE` overrides the resolved conda batch-file path unconditionally, the instant it's +defined -- and because this happens BEFORE the "install Miniconda if missing" block, setting it +also skips the Miniconda download/install entirely: ``` -Tue 07/28/2026 4:55:47.55 [INFO] Building standalone executable -- this may take a minute or two... -Tue 07/28/2026 4:55:47.55 [INFO] (A stray one-line Windows message about a missing drive may appear next -- that is a known side effect from an unrelated background process, unrelated to your app; safe to ignore.) -The system cannot find the drive specified. -The system cannot find the drive specified. -Tue 07/28/2026 4:55:58.01 [INFO] PyInstaller produced dist\_selftest_stub.exe -Tue 07/28/2026 4:55:58.02 [DEBUG] warnfix: warn file found -Tue 07/28/2026 4:55:58.02 [INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. -Tue 07/28/2026 4:55:58.39 [INFO] PyInstaller build artifacts cleaned up. -Tue 07/28/2026 4:55:58.40 [INFO] EXE smokerun: testing dist\_selftest_stub.exe -Tue 07/28/2026 4:55:58.41 [INFO] Running entry script smoke test via packaged EXE. -Tue 07/28/2026 4:55:58.43 [WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. -[INFO] Process ID 6076. If it seems stuck: Task Manager > Details tab > find this PID > End Task (this window stays open). -hello-from-stub -Tue 07/28/2026 4:55:59.87 [INFO] EXE smokerun: exited 0 (ok) -Tue 07/28/2026 4:55:59.88 [INFO] Entry smoke exit=0 -Tue 07/28/2026 4:55:59.88 [STATUS] Run Status: SUCCESS (Exit Code: 0) +[DEBUG] Using super-user override for PVW_CONDA_EXE: ``` -Build took ~10.5s wall-clock here (this run has effectively zero dependencies, so it's near the -fast end of what's possible). This run's PyInstaller build DID trigger the warnfix path (`warn -file found` -- expected, since even a bare stub still needs the platform-module filter pass) but -needed no repair install, so the informational filter messaging is present but purely -informational -- the "no warnfix repair needed" success case this doc previously had no example -of. `hello-from-stub` is the stub program's OWN real stdout, live-teed via -`tools/exe_smokerun.ps1`'s chunk-based `ReadAsync` reader (see Scenario 5's fuller writeup of that -mechanism) -- landing exactly between the PID line and the "exited 0" line, precisely where a real -user's own program output appears. The `[INFO] Process ID . If it seems stuck: Task Manager > -Details tab...` line is the already-shipped stuck-program recovery aid (`tools/exe_smokerun.ps1`, -see `docs/agent-interconnect.md`'s "Process-ID display for stuck-program recovery" section) -- -confirmed here as a genuine, working console line, not just source text. - -Immediately following (elective prompts, both auto-declined by CI -- see Scenario 11 below for -what a real user experiences here instead) and then the final panel: +**The special interaction, and the whole reason this variable gets its own scenario:** normally, +when a health check on the resolved conda binary fails, the bootstrapper offers an interactive +Y/N self-heal prompt that (on accept) deletes and rebuilds the entire Miniconda root. When +`PVW_CONDA_EXE` is set, this self-heal path is skipped outright -- the very FIRST check in the +corruption-handling subroutine, ahead of every other check including the CI auto-decline logic -- +because the bootstrapper will never auto-delete a path it doesn't own: ``` -*** Verification finished -- see the Run Status above. *** -*** You can run your program again now via the interpreter as an extra diagnostic check. *** -Tue 07/28/2026 4:55:59.91 [INFO] REQ-018: post-execution checkpoint (exe): declined (run footprint stays at one execution). +================================================================ + CORRUPTED PYTHON ENVIRONMENT DETECTED +================================================================ -*** Your app is ready. *** -*** Want to build an optimized version too? It takes a bit longer to build right now, *** -*** but it starts up more reliably on Windows and runs faster once it is built. *** -Tue 07/28/2026 4:55:59.94 [INFO] Optimized build: declined. + The local conda installation appears to be broken. + This can happen after a Windows update or OS migration + (example: DLL load error 0xc000007b). -============================================================ - SETUP COMPLETE -============================================================ - Your standalone application is ready: - dist\_selftest_stub.exe + Affected path: - RUNNING YOUR APP - Double-click dist\_selftest_stub.exe to run it. + This binary was specified via PVW_CONDA_EXE: + - STARTUP MAY BE SLOW: a one-file .exe unpacks itself each time it - starts, so allow 10-15 seconds (longer for big libraries like - numpy/scipy/matplotlib, or when extra packages were bundled to fix - missing imports) before assuming it has hung. + Automatic self-healing is not available for user-managed conda. + Please fix or replace the binary at the path above, then re-run. +``` - If the window flashes and closes instantly: that's normal if - your program finished quickly or hit an error before printing - anything. To see what happened, open Command Prompt, cd to - this folder, and run: - dist\_selftest_stub.exe - This keeps the window open so you can read any messages. +followed by the exact error/exit sequence: - A progress indicator that updates in place may appear all at - once instead of live when run as the .exe -- that is a stdout - buffering difference between the .exe and the script, not an error. +``` +[ERROR] Corrupt user-managed conda (PVW_CONDA_EXE); fix manually. +``` - Does your program need launch arguments (e.g. --input file.csv)? Run - this bootstrapper again with them added after the entry file, e.g. - run_setup.bat "hello_stub.py" --input file.csv - and they will be forwarded to your program during THIS setup run - (up to 8 extra arguments). This does not change how a plain - double-click of dist\_selftest_stub.exe launches it afterward -- for that, - make a Windows shortcut to the .exe and add the arguments to its - Target field, or launch it yourself from a Command Prompt. +and the process exits with code **2** -- notably, there is no Y/N prompt at all in this path, even +for a genuinely interactive real user; the override check runs before the interactivity dispatch +even has a chance to matter. - KEEP these files with your project: - requirements.txt -- packages your app depends on - runtime.txt -- Python version pin +--- - SAFE TO DELETE to reclaim disk space: - .*_env\ folders -- environment directories - ~* files -- tilde-prefix work files (e.g. ~setup.log) - build\ -- PyInstaller build cache -============================================================ +### Scenario 10: Drag-and-drop / CLI entry-file override (REQ-011 same-directory rule + REQ-002 priority) -Tue 07/28/2026 4:56:00.03 [INFO] REQ-016: Post-flight briefing printed. +**What's tested:** three real, currently-passing NDJSON rows across two test files -- +`self.entry.req011.crossdir` and `self.entry.req011.sameDir` (`tests/selfapps_isolation.ps1`), and +`self.entry.override` (`tests/selfapps_ux_hardening.ps1`, which specifically proves the override +wins over auto-detection, not merely that dragging works at all). + +**Source:** REAL CI CAPTURE, run `30328748330`, both gating lanes (`real` job `90179708091` and +`conda-full` job `90179708094`), all three rows `pass: true` in both. + +A user can either type a `.py` filename as the first CLI argument to `run_setup.bat`, or literally +drag a `.py` file onto the `.bat` file's icon in Windows Explorer (Windows translates the drop +into the identical `%1` argument). **REQ-011's rule: the file must be in the SAME directory as +`run_setup.bat` itself**, checked twice for defense-in-depth (once as an early pre-flight check, +for instant feedback before any environment work begins, and once again inside the entry-selection +subroutine itself). A cross-directory attempt genuinely terminates the whole process (`exit /b 1`, +not merely a `call`-frame return) with: + +``` +[ERROR] REQ-011: Dragged files must reside in the bootstrapper root folder for environment cleanliness. ``` -The companion `tests/~selftest_stub/~bootstrap.status.json` reads: +(This is the raw, untimestamped console line; the separately-written log-file copy is a shorter +variant without the "for environment cleanliness" clause -- a real, source-confirmed difference, +not a typo, confirmed via real CI capture: `[ERROR] REQ-011: Dragged files must reside in the +bootstrapper root folder.`) + +A same-directory file succeeds and prints the filename (a historical bug that once printed this +line with an EMPTY filename -- see `docs/agent-lessons-learned.md`'s "Provider-cascade dispatch is +goto-based on purpose" entry -- is long fixed; the current, correct text is shown below): -```json -{"state":"ok","exitCode":0,"pyFiles":1} +``` +*** Using drag-and-drop file: ``` -`exitCode` here means "did the bootstrapper's own env/build lifecycle succeed" (a hardcoded `0` -written unconditionally at the success label), NOT the user program's own exit code -- that's -separately surfaced via the console's `[STATUS] Run Status: SUCCESS (Exit Code: 0)` line above. -Both happen to be 0 in this clean run, so the distinction isn't visible here, but it's worth -knowing they're two independent things (see CLAUDE.md's "User-code exit-code semantics" Known -Finding). +**Interaction with the REQ-002 interactive entry picker: fully and structurally skipped.** +Providing a valid same-directory file makes entry selection return immediately, before the +auto-detection block (and therefore the picker, which is only ever invoked from inside that same +block) is even reached -- this is REQ-002's documented "priority 0": a co-located override always +wins over auto-detected names, and can never trigger the ambiguous-case timed picker. Confirmed +positively (not just "dragging works," but that override genuinely beats auto-detection) by +`self.entry.override`'s real capture: a scratch directory staged with BOTH `main.py` (which would +win plain auto-detection by name-priority) and `zzz_override.py`, with `zzz_override.py` passed as +the override -- the real run confirms the drag message names the override file, the entry-selected +log line names `zzz_override.py` (not `main.py`), and the override file's own distinguishing output +is what actually ran. --- -### Scenario 11: The two elective prompts a real user faces after every successful run +## Part IV: Adversarial and recovery branches -**What's tested:** the framing text of both prompts is confirmed via real CI capture (they're -unconditionally echoed even when CI auto-declines); the actual `[Y/N]` question lines themselves -are never visible in ANY CI log, by construction -- they live inside the branch of an `if/elif` -chain that CI's `HP_CI_LANE` auto-decline always short-circuits past, even in tests that force an -accept via an `HP_TEST_*_ANSWER` override (the override branch assigns the answer variable -directly, bypassing the real `set /p` prompt entirely). Both are therefore genuinely -**`[Extrapolated Branch]`** for the exact prompt-line wording specifically, cited from source, even -though the surrounding framing text is a real capture. +**Scope note:** excludes `HP_TEST_*` CI-only flags as the documented subject -- they're the +mechanism a test uses to force a scenario deterministically, but the resulting console text below +is exactly what a real user hits when the same underlying condition occurs for real (a genuine +flaky connection, a genuinely corrupted conda install, a genuinely missing bundled module, and so +on). Evidence for this entire Part is pulled from run `30328748330` (commit `5872028`, all lanes +green): `real` (job `90179708091`), `conda-full` (job `90179708094`), `uv` (job `90179708109`), +`justme-test` (job `90179708103`). -**Source:** framing lines are REAL CI CAPTURE (run `30328748330`, job `90179708091`, "real" lane, -same log as Scenario 10 above); the two `set /p` prompt lines themselves are -`[Extrapolated Branch]`, cited from `:run_postexec_checkpoint` and `:offer_optimized_build` -respectively. +### Scenario 11: Network connectivity check and transient-retry (REQ-013 + REQ-022) -**Relationship to README.md's `[REQ-018]` spec, since the two don't map 1:1 in an obvious way:** -`:run_postexec_checkpoint`'s own log line is explicitly tagged `REQ-018` in source (`[INFO] -REQ-018: post-execution checkpoint (...): accepted/declined...`), so it -- not a separate, -undocumented prompt -- is the shipped implementation of README's "After a build, the real run is -offered, not forced" / "Consent before any extra run" bullets. Two nuances worth flagging -explicitly rather than leaving implicit: (1) the offered rerun uses the INTERPRETER, not a second -launch of the packaged EXE -- README's "offers to launch the app untimed for real" phrasing reads -as if it means the EXE specifically, but the actual accepted-path log line says "running a second -time via the interpreter", and a companion note the same subroutine prints for the EXE call site -spells this out too ("this diagnostic run uses the interpreter, not the packaged EXE, so behavior -can differ"); (2) README's separate REQ-018 bullet about the FIRST (mandatory) verification run -being "force-stopped after a short interval even if running fine" is stale relative to the -activity-aware-kill behavior actually shipped later (see Scenario 5's fuller writeup and CLAUDE.md's -Closed Backlog "Activity-aware EXE-smoke kill" entry) -- the real WARN text quoted in Scenario 10 -above says the opposite: any output at all, not just a clean exit, keeps the run alive indefinitely, -only a genuinely SILENT process gets force-stopped. Neither nuance changes what actually ships; -both are flagged here because a reader cross-checking this scenario against README's prose could -otherwise reasonably conclude something was missed. +**What's tested:** `self.ux.connectivity.*` rows (`tests/selfapps_ux_hardening.ps1`, real lane); +`self.stub.conda_retry`/`self.stub.conda_create_retry`/`self.stub.conda_perpkg` +(`tests/selftest.ps1`, conda-full lane). All real, all passing. + +When a download genuinely fails, `:check_net_after_dl_fail` (REQ-013) pings `8.8.8.8` (2 attempts) +then, if ICMP is blocked, tries an HTTPS reachability check against `conda.anaconda.org` (2 +attempts) before concluding the user is actually offline -- this doubled-attempt design exists +because a single dropped ICMP echo or a momentarily-contended connect on a busy machine is enough +to misclassify a genuinely-online host as offline (this was root-caused from a REAL CI flake, not +a hypothetical). If both checks fail, a real user sees an unbounded prompt: + +``` +[WARN] REQ-013: Connectivity check: no internet detected (ICMP and HTTPS check failed). +WARNING: No internet connection detected. Remote providers may fail. Retry? (Fix connection then press Y) or proceed offline (N): +``` + +Pressing Enter with no answer defaults to offline; `Y` re-tries (2 more ping attempts, then 2 more +HTTPS attempts) before re-prompting on continued failure; `N` proceeds offline (`HP_OFFLINE_MODE=1` +for the rest of the run). + +**Transient-retry for `conda create` and conda's bulk package install (REQ-022)** -- both use the +identical detect-and-retry-once pattern: scan the failure output for `CondaHTTPError`/`Failed to +fetch`/`timed out`/`ConnectionError`, wait 15 seconds, retry exactly once. If the retry ALSO fails, +it falls straight through to the normal (non-transient) failure/fallback chain -- this is not an +infinite-retry loop. -Right after a normal successful verification run (Scenario 10's tail), a real double-click user -sees: +`conda create` retry, REAL CI CAPTURE (`tests/selftest.ps1`'s conda-full sub-bootstrap): ``` -*** Verification finished -- see the Run Status above. *** -*** You can run your program again now via the interpreter as an extra diagnostic check. *** - Run again via the interpreter now? [Y/N] _ +Creating Python environment '_selftest_conda_create_retry' -- this may take several minutes... +Conda environment creation failed -- possible network or repository issue. Retrying once... +[INFO] conda create: transient failure detected; retrying after 15s. +Retrying environment creation... +[INFO] runtime.txt written: python-3.14.6 +[BOOT] REQ-009: Selected Python provider: Conda (Portable). ``` -(cursor sits after `[Y/N] `, waiting indefinitely -- `:run_postexec_checkpoint`, an UNBOUNDED -`set /p`, no timeout of any kind). Any answer other than a leading `Y`/`y` -- including just -pressing Enter -- resolves to decline. If accepted, the entry program runs a second time via the -interpreter (not the packaged EXE) as a diagnostic; if declined, the bootstrap immediately -continues to the second prompt: +Bulk package install retry, REAL CI CAPTURE (same test file, a different scratch env): ``` -*** Your app is ready. *** -*** Want to build an optimized version too? It takes a bit longer to build right now, *** -*** but it starts up more reliably on Windows and runs faster once it is built. *** - Build the optimized version now? [Y/N] _ +Conda install failed -- possible network or repository issue. Retrying once... +[INSTALL] conda bulk: transient failure detected; retrying after 15s. +Retrying package installation... ``` -(same shape -- `:offer_optimized_build`, also an unbounded `set /p`, also defaults to decline on -anything but a leading Y). Both prompts genuinely fire on essentially every -successful default run (the checkpoint is called after every clean verification; the optimized- -build offer only skips if the AV-Safe-Build-Path Tier A Nuitka fallback already ran, or the -verification itself failed) -- **this is not an edge case, it's what most real users see twice in -a row at the very end of an otherwise fully successful first run.** +A NON-transient bulk failure (any error text that doesn't match the transient patterns above) +skips the retry entirely and instead falls back to installing packages one at a time +(`self.stub.conda_perpkg`, REAL CI CAPTURE, `[INSTALL] conda per-pkg fallback`) -- a completely +separate recovery path from the transient-retry one, chosen based on what kind of failure +actually occurred. -**CI cannot show either question line, ever, structurally -- not just "doesn't currently show -them."** Both prompts follow this repo's own established CI-safe-gate pattern (see -`docs/agent-interconnect.md`'s "CI-safe interactive gates" section): echo the framing -unconditionally, THEN branch on `HP_TEST_*_ANSWER` override / `HP_CI_LANE` auto-decline / real -`set /p`. Because the actual question text lives inside the `set /p` call itself (not a separate -unconditional `echo`), and CI always takes one of the first two branches, no CI log -- gating or -non-gating, auto-decline or forced-accept -- can ever contain the literal `" Run again via the -interpreter now? [Y/N] "` or `" Build the optimized version now? [Y/N] "` text. This is a genuine -blind spot in what CI evidence alone can show about this bootstrapper's real user-facing behavior, -worth keeping in mind when reading any other scenario in this file that involves a `set /p`-based -consent gate. +--- -**For contrast, briefly (full treatment is Pass 4/adversarial-recovery territory, not this -Part):** not every consent gate in this file shares the "blocks forever" shape. The REQ-009 -provider-cascade gate (`:cascade_consent_gate`, only reached if a build succeeds but a dependency -repair genuinely fails) is the one gate that's genuinely TIMED for a real user -- `choice /C YN /N -/T 30 /D N`, defaulting to decline after 30 seconds with no answer, so it structurally cannot hang -forever even for a truly unattended user. The REQ-014 system-Python consent gate -(`:system_python_consent_gate`, only reached as the absolute last-resort Tier 4) is unbounded like -the two documented above, but its full question text (unusually, including the actual `[y/n]` -wording) IS an unconditional `echo` rather than living inside `set /p` -- so, unlike this -scenario's two prompts, CI logs genuinely do show the complete question for that one, e.g. -`Proceed with System Python? (Global pollution risk) [y/n]: y to accept, n to decline.` (REAL CI -CAPTURE, same run, job `90179708091`) -- only its own terse follow-up `"Your choice [y/n]: "` line -is hidden the same way. Neither of these two gates fires on this Part's happy path; both are -documented fully in Pass 4. +### Scenario 12: Corrupted-conda self-heal (detect / decline / accept) ---- +**What's tested:** `self.corrupt.conda.detect`/`.heal.decline`/`.heal.accept` +(`tests/selftest.ps1`, conda-full lane, all three real and passing). The `PVW_CONDA_EXE`-override +variant is already covered in Scenario 9 -- this scenario is the DEFAULT case, where the +bootstrapper owns the conda install and can offer to fix it. -## Part IV: Second run, nothing changed (repeat-run fast paths) +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane). -**Scope note:** this Part documents the success side of running the bootstrapper a SECOND time in -the same folder with nothing changed -- same entry file, same requirements, no test flags. The -FAILURE side of one of these fast paths (a stale cached EXE that's kept and later exits non-zero) -is already documented as Scenario 7b (Part II); this Part doesn't repeat that. Evidence again -comes from a recent clean green run (`30328748330`, commit `5872028`) rather than one single -dedicated "repeat run" test file -- `tests/selfapps_envsmoke.ps1` re-invokes `run_setup.bat` a -second time in the same scratch directory with nothing changed (the EXE fast path), and -`tests/selftest.ps1`'s stub scenario and `tests/selfapps_depcheck.ps1` go one step further -- -`Run 1` (fresh bootstrap), `Run 2` (identical, EXE fast path), then deliberately touch the source -file and run a THIRD time -- exercising the "source changed just enough to force a rebuild, but -the environment itself doesn't need recreating" fast paths this Part's second scenario covers. +When a health check on the resolved conda binary fails, the user sees: -### Scenario 12: The EXE fast path (nothing changed at all) +``` +================================================================ + CORRUPTED PYTHON ENVIRONMENT DETECTED +================================================================ -**What's tested:** `self.fastpath` (`tests/selfapps_envsmoke.ps1`'s second, back-to-back -invocation of `run_setup.bat` in the same scratch directory, zero CLI arguments, nothing touched). + The local conda installation appears to be broken. + This can happen after a Windows update or OS migration + (example: DLL load error 0xc000007b). -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` ("real" lane), the console -capture of that second invocation (`~envsmoke_fastpath.log`) plus the matching block of -`~setup.log`. + Affected path: C:\Users\Public\Documents\Miniconda3 +``` -**What fires FIRST, before any provider/entry/dependency logic even starts:** `:try_fast_exe` is -called immediately after environment-name derivation and the Python-file count, right at the top -of the file -- before uv acquisition, before Miniconda, before `:determine_entry`, before -anything else. It runs exactly ONE real check: compare `dist\.exe`'s modification time -against the newest non-infrastructure `.py` file's modification time (via the embedded -`HP_FAST_CHECK` helper). If the EXE is newer-or-equal, the whole rest of the bootstrap short- -circuits straight to `:success`. +followed by an unbounded `[Y/N]` prompt (` Would you like to delete it and rebuild? [Y/N] `, +`[Extrapolated Branch]` for the exact prompt line, same reasoning as Scenario 5's two prompts -- +it lives inside `set /p`, never visible in a CI log even when a test forces the accept branch via +an answer override). -**Non-interactive console text (what CI captures -- this is also exactly what a real user would -see if they ran the bootstrapper non-interactively, e.g. from a script):** +**Decline** (real capture): ``` -[WARN] UNC paths not supported -Tue 07/28/2026 4:30:33.81 [INFO] Environment name: _envsmoke -Tue 07/28/2026 4:30:33.83 [INFO] Host OS: Microsoft Windows [Version 10.0.26100.32995] -Tue 07/28/2026 4:30:34.04 [INFO] Host PowerShell: 5.1.26100.32995 -Tue 07/28/2026 4:30:34.10 [INFO] Python file count: 1 -Tue 07/28/2026 4:30:34.98 [INFO] Fast path: reusing dist\_envsmoke.exe -Tue 07/28/2026 4:30:35.96 [INFO] Entry smoke exit=0 -Tue 07/28/2026 4:30:35.97 [STATUS] Run Status: SUCCESS (Exit Code: 0) -Tue 07/28/2026 4:30:35.99 [INFO] Fast path: skipping PyInstaller rebuild for existing dist\_envsmoke.exe + Exiting without changes. Delete the folder above manually, + then run this setup again. ``` -No "SETUP COMPLETE" postflight banner appears -- confirmed both structurally (that dispatch is -gated on the fast path NOT having fired) and in the raw capture, which ends right after the last -line above. `~setup.log` shows one extra line between "Fast path: reusing" and "Entry smoke -exit=0" that never reaches the console (`Fast path command: "dist\_envsmoke.exe" > "~run.out.txt" -2> "~run.err.txt"`) -- a raw log-file-only write, not part of what a user actually sees. +logged as `[ERROR] Corrupt conda env; user declined rebuild.`, exit code 2. -**Interactive console text (what a genuine double-click end user sees -- differs from CI because -`HP_CI_LANE` is unset, so `:try_fast_exe` takes its OTHER branch, `:try_fast_exe_probe`, which -launches the cached EXE through the same never-kills fail-fast probe mechanism Scenario 5 -documents rather than the plain redirect above).** Assembled from real, independently-confirmed -fragments (the header and PID lines are genuine captured text from a different test that forces -this same interactive branch; their pairing into a clean, fast, successful sequence is -`[Extrapolated Branch]`, grounded directly in source rather than guessed): +**Accept** (real capture; this specific test run additionally forces the actual re-download step +to be skipped for CI-safety reasons, so the "Downloading fresh copy..." line below is +`[Extrapolated Branch]`, cited from source, while everything else is real): ``` -[WARN] UNC paths not supported -Tue 07/28/2026 4:30:33.81 [INFO] Environment name: _envsmoke -Tue 07/28/2026 4:30:33.83 [INFO] Host OS: Microsoft Windows [Version 10.0.26100.32995] -Tue 07/28/2026 4:30:34.04 [INFO] Host PowerShell: 5.1.26100.32995 -Tue 07/28/2026 4:30:34.10 [INFO] Python file count: 1 -Tue 07/28/2026 4:30:34.98 [INFO] Launching your program now via the cached standalone EXE (PyInstaller build): dist\_envsmoke.exe -[INFO] Process ID 7692. If it seems stuck: Task Manager > Details tab > find this PID > End Task (this window stays open). - <- the app's own live stdout/stderr tees here, if any -Tue 07/28/2026 4:30:35.96 [INFO] Entry smoke exit=0 -Tue 07/28/2026 4:30:35.97 [STATUS] Run Status: SUCCESS (Exit Code: 0) -Tue 07/28/2026 4:30:35.99 [INFO] Fast path: skipping PyInstaller rebuild for existing dist\_envsmoke.exe -Press any key to continue . . . + [INFO] Removing corrupt Miniconda installation... + [INFO] Corrupt installation removed. Downloading fresh copy... ``` -The "still running after Nms, keep waiting?" WARN line that the fail-fast probe can print is -conditional on the process actually exceeding its short classification window (confirmed absent -here via real CI capture of the same forced-interactive mechanism failing fast in an unrelated -scenario) -- omitted above since "nothing changed, app still runs fine" implies a normal-speed -exit. `Press any key to continue . . .` is `cmd.exe`'s own native output from a `pause` at the -very end of the main line, gated on `HP_CI_LANE` being unset -- real end-user only, never appears -in any CI log. +``` +[INFO] Self-healing: corrupt conda evicted from C:\Users\Public\Documents\Miniconda3. +[INFO] Workspace: ...\tests\~selftest_heal_accept +[INFO] Env name: _selftest_heal_accept +[INFO] HP_ENV_MODE=conda +[INFO] Creating Python environment '_selftest_heal_accept' -- this may take several minutes... +[INFO] runtime.txt written: python-3.14.6 +[BOOT] REQ-009: Selected Python provider: Conda (Portable). +``` -**Why it's fast -- everything this run skips entirely, not just runs faster:** once `:try_fast_exe` -succeeds, the bootstrap jumps to `:success` before any of the following ever execute: the -`HP_CI_SKIP_ENV` dispatch, the entire uv acquisition block (no download, no `uv venv`, no -`UV_PYTHON_PREFERENCE` even gets set), `:select_conda_bat` and all Miniconda install/probe logic, -the env-state fast path (moot -- conda was never considered), `:conda_base_update`, the -`HP_PREP_REQUIREMENTS` heuristic dependency augmentation, `:determine_entry` (the cached EXE's -identity is trusted as-is, no REQ-002 re-selection), pipreqs entirely (no `pipreqs.install`/ -`pipreqs.run`, no `requirements.auto.txt` diff), and -- the single biggest reason this is fast -- -`:run_entry_smoke` never runs, meaning no `py_compile` preflight and no PyInstaller build -invocation of any kind, cached or otherwise. The reused EXE genuinely gets EXECUTED, not merely -detected -- confirmed by the real `Entry smoke exit=0`/`[STATUS]` lines above, which come from an -actual process launch. +A real user's accept path re-downloads and reinstalls Miniconda from scratch (the full +acquisition sequence from Scenario 3), then proceeds exactly as a fresh first run would. --- -### Scenario 13: Source touched just enough to force a rebuild, but the environment is reused +### Scenario 13: Miniconda install chain (AllUsers -> JustMe -> both-failed) -**What's tested:** `tests/selftest.ps1`'s stub scenario and `tests/selfapps_depcheck.ps1`, both of -which do Run 1 (fresh) -> Run 2 (Scenario 12's EXE fast path) -> touch the entry file's content -and modification time -> Run 3, which is the case documented here: the EXE fast-path timestamp -check now fails (source is newer than the cached EXE), so PyInstaller reruns and produces a new -EXE -- but the ENVIRONMENT itself (the uv venv or conda env, and already-satisfied dependencies) -is recognized as still valid and reused rather than recreated from scratch. +**What's tested:** `conda.install.justme` (`tests/selftest.ps1`, `justme-test` lane, real, +passing) and `self.conda.bothfail` (`tests/selfapps_conda_bothfail.ps1`, `uv` lane, real, +passing). -**Source:** REAL CI CAPTURE, run `30328748330`, jobs `90179708091` ("real" lane, uv-first) and -`90179708094` ("conda-full" lane). +**Source:** REAL CI CAPTURE, run `30328748330`, jobs `90179708103` (`justme-test`) and +`90179708109` (`uv`). -**uv-first lane** (`.uv_env\Scripts\python.exe` already exists and its `import pip` canary -succeeds, so venv creation is skipped -- the gate just above `:uv_venv_ready`): +Miniconda install first attempts an AllUsers (machine-wide) install; if UAC rejects elevation (or +the process simply isn't elevated), it skips straight to a JustMe (per-user) install instead, no +wasted attempt. **Both the "skip, never attempted" path and a genuine post-attempt AllUsers +failure fall through to the same shared `:tci_justme` label** (`run_setup.bat`) -- the label checks +a flag set only right before the real AllUsers install attempt, so the two paths get distinct +wording instead of both unconditionally claiming AllUsers "failed." On the common non-elevated +machine (skip path, `[Extrapolated Branch]`, cited from source): ``` -[INFO] uv: reusing existing .uv_env -[INFO] HP_ENV_MODE=uv -[BOOT] REQ-009: Selected Python provider: UV. +[INFO] Not elevated; skipping AllUsers Miniconda install. +[INFO] Miniconda AllUsers install skipped (not elevated); trying JustMe install instead. +[INFO] Miniconda installed (JustMe fallback). ``` -**conda-full lane** (`~env.state.json` is valid and the conda env's `python.exe` is present -- -`:env_state_fast_path`; this mechanism is explicitly bypassed in uv mode, since it exists purely -for the conda-specific case): +A genuine, post-attempt AllUsers failure still gets the original WARN wording, now also carrying +the installer's own exit code and a reason token (`[Extrapolated Branch]`, cited from source -- +this branch requires a real elevated process whose AllUsers installer genuinely fails, which no +current CI hook forces without also forcing the skip path): ``` -[INFO] Env-state fast path: reusing conda env _selftest_stub. -[BOOT] REQ-009: Selected Python provider: Conda (Portable) [fast path]. +[WARN] Miniconda AllUsers install failed (exitCode=1, reason=installer_failed); retrying with JustMe. ``` -Confirmed firing across every scratch env in that job's log (not a one-off), so this is a broadly -reliable fast path, not a narrow coincidence. `self.stub.state_skip`'s own NDJSON assertion checks -for EITHER phrase, which is why one shared test scenario validates both depending on which lane -it runs under. - -**Both lanes then converge on the same dependency-install skip, immediately after dependency -discovery** (pipreqs + the Tier 1 autopep723 merge from Scenario 9 still run normally here -- -neither of the two fast paths above touches dependency DISCOVERY, only environment creation): +If the installer instead hits `:run_installer_timeout`'s own 60-minute ceiling (see that +subroutine's header comment), the exit code is a hardcoded sentinel, not the installer's real +exit code -- reported as `reason=timeout` with no fabricated `exitCode` field instead +(`[Extrapolated Branch]`, an even rarer sub-case of the one above, never observed in CI): ``` -[INFO] Dep-check: all pipreqs packages satisfied in lock; skipping conda install. +[WARN] Miniconda AllUsers install failed (reason=timeout); retrying with JustMe. ``` -This message literally says "skipping conda install" even in uv mode -- confirmed intentional -(a shared log line covering both providers via the same `HP_DEP_SKIP` flag), not a copy-paste -bug, so don't read it as evidence the wrong provider was used. **One nuance worth flagging so it -isn't misread as a second, real install still happening:** in conda mode specifically, an -unconditional "pip gap fill from `requirements.txt`" step still runs immediately after this skip -line, even though nothing was found missing -- it's a fast, harmless no-op safety net (confirmed -completing in well under a second in the real capture), not evidence the skip failed to take -effect. +**If JustMe ALSO fails** (both installation options exhausted; REAL CI CAPTURE for the skip-path +lines, `[Extrapolated Branch]` for the now-corrected wording): -**Net effect for the user:** a rebuild triggered by an ordinary source edit is meaningfully faster -than the very first run -- no fresh uv/conda acquisition, no fresh venv/env creation, and (when -nothing about the dependency set changed) no re-running of the actual install step -- while still -producing a genuinely fresh PyInstaller build and a real verification run of the new EXE. +``` +[INFO] Not elevated; skipping AllUsers Miniconda install. +[INFO] Miniconda AllUsers install skipped (not elevated); trying JustMe install instead. +[ERROR] Miniconda install failed (AllUsers skipped -- not elevated; JustMe also failed). +``` + +This is a genuine `:die` (`state=error`), not a graceful degrade -- with no Python interpreter +acquirable at all through conda, there's nothing left for this tier to hand back. Both installer +launches are bounded by a generous 60-minute timeout (not unbounded), based on real-world reports +of Miniconda's silent installer hanging indefinitely on some machines. --- -## Part V: User configuration and CLI overrides +### Scenario 14: Standalone embed-tier download (REQ-009 Tier 5) -- decline and real success -**Scope note:** argv passthrough (extra launch arguments forwarded to the user's program, REQ-026) -is already fully covered as Scenario 6 -- cross-reference it rather than re-documenting it here. -This Part covers the remaining configuration surface: the five `PVW_*` super-user override -environment variables (distinct from `HP_TEST_*`, which are CI-only and out of scope for this -doc), and the CLI-argument/drag-and-drop entry-file override. +**What's tested:** `self.embed.fallback.decline`/`self.embed.fallback.real` +(`tests/selfapps_ux_hardening.ps1`, `uv` lane, both real and passing). -**All five `PVW_*` variables share one generic acknowledgment line**, printed the instant the -variable is defined, before any real detection/acquisition work runs and regardless of whether the -value ever turns out to be usable: +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708109` (`uv` lane). + +When uv and conda have both failed (or been declined/exhausted), the bootstrapper tries +downloading a private, checksummed Python interpreter directly from python.org -- no consent +prompt (unlike system Python), since this is a bootstrapper-controlled, disposable extraction: ``` -[DEBUG] Using super-user override for PVW_: +[WARN] Attempting embedded Python download (REQ-009 Tier 5)... ``` -**A real nuance worth stating up front rather than repeating per-variable:** the source's own -header comment describes all five uniformly as bypassing auto-detection, but that's only literally -true for two of them. `PVW_UV_EXE` and `PVW_WORKSPACE` genuinely skip the corresponding -detection/creation branch outright. `PVW_PYTHON_EXE` and `PVW_TARGET_PY` do NOT skip anything -- -the full normal detection logic still runs to completion (including real network/disk work), and -the override simply overwrites the *result* variable afterward. `PVW_CONDA_EXE` (Scenario 15) is -its own case again, discussed separately since it has a unique interaction with the conda -self-heal flow. +**Real success** (a genuine end-to-end download/verify/extract/patch/pip-bootstrap/canary/build/ +run, REAL CI CAPTURE): + +``` +[INFO] Downloading embedded Python 3.14.6 from https://www.python.org/ftp/python/3.14.6/python-3.14.6-embed-amd64.zip... +[INFO] embed fallback: 3.14.6 extracted and verified. +``` -### Scenario 14: `PVW_PYTHON_EXE` / `PVW_UV_EXE` / `PVW_TARGET_PY` / `PVW_WORKSPACE` +**Tier exhaustion** (this specific tier also fails, e.g. offline -- REAL CI CAPTURE): -**What's tested:** `PVW_UV_EXE` and `PVW_TARGET_PY` each have real, valid-value end-to-end CI -coverage (details below). `PVW_PYTHON_EXE` and `PVW_WORKSPACE` have **zero** test coverage of any -kind -- not even a valid-value smoke test -- confirmed via a repo-wide search across every test -file and every lane of a recent clean run. **No test anywhere exercises an INVALID value for any -of the four** -- all invalid-value behavior below is `[Extrapolated Branch]`, traced from source. +``` +[WARN] embed fallback: offline mode; cannot download embedded Python. +``` -**14a. `PVW_PYTHON_EXE`** overrides `HP_PY` at the shared convergence point every REQ-009 provider -path (uv, conda, embed, venv, system, and every provider-cascade re-entry) funnels into after -already selecting and setting up a working interpreter -- so it does NOT skip provider -acquisition, it only overwrites the final result: +A genuine download failure (not offline, an actual failed transfer) retries the WHOLE +download+verify cycle once before giving up. Real, confirmed via `self.embed.dl.retry` +(`tests/selfapps_ux_hardening.ps1`, `uv` lane, non-gating -- `HP_TEST_FORCE_EMBED_DL_FAIL_ONCE=1` +deterministically fails only the first attempt, no network touched, then a real second attempt +succeeds): ``` -[INFO] Python host: using super-user override PVW_PYTHON_EXE. +[TEST] HP_TEST_FORCE_EMBED_DL_FAIL_ONCE: simulating download failure on attempt 1 (no network touched). +[WARN] embed fallback: download failed; retrying once. ``` -`[Extrapolated Branch]`, genuinely untested. Invalid-value trace: no existence/executability check -on the path itself; the first real probe is a non-fatal interpreter smoke test -(`[WARN] Interpreter smoke test failed (continuing).`) that does NOT abort the run -- every -subsequent `pip install` call is similarly wrapped in a WARN-only failure handler, so the -bootstrap proceeds all the way to the PyInstaller build attempt with a broken interpreter before -finally hitting a real failure there (the pre-existing, already-documented `:die`/`state=error` -path). A bad `PVW_PYTHON_EXE` is therefore detected early (one WARN) but not treated as fatal -until several steps downstream, not at the point of misuse. +followed by the tier succeeding end to end on the second attempt. A checksum mismatch is treated +the same way (redownload, not just re-verify, since a mismatch can mean a truncated download +rather than a bad pin) -- that specific trigger has no dedicated test of its own, `[Extrapolated +Branch]` for that one detail. -**14b. `PVW_UV_EXE`** overrides `HP_UV_EXE` and genuinely skips the entire uv download/acquire -branch (jumps straight past it): +--- + +### Scenario 15: REQ-009 provider cascade -- one real run showing the FULL chain + +**What's tested:** `self.cascade.exec` (`tests/selfapps_cascade.ps1`, `uv` lane, real, passing). +This single test happens to exercise BOTH mid-exhaustion (uv/conda/embed all cascade past) AND +full exhaustion (system Python, the final tier, declines) in one coherent real capture -- an +unusually complete real-world illustration of this mechanism. + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708109` (`uv` lane). + +After a build succeeds but a dependency repair genuinely fails (Scenario 17 covers what triggers +this), the user is offered (Scenario 5's timed `:cascade_consent_gate`, up to 30s, default +decline) a chance to try the next Python provider. On acceptance, the REAL capture below shows the +cascade working through every tier in order -- each hop re-attempts the full dependency phase +under the new provider, and each hop that ALSO can't fully resolve dependencies triggers another +verification failure and another cascade offer: ``` -[INFO] uv: using super-user override PVW_UV_EXE. +[INFO] REQ-009: cascading provider uv to conda; re-attempting dependencies. +*** [INFO] Trying the next Python provider (conda) to resolve dependencies... ``` -REAL CI CAPTURE, run `30328748330`, job `90179708086` (`contract-uv` lane), -`tests/selfapps_contract_uv.ps1`'s dedicated uv-version-forwarding scenarios (which reuse an -already-downloaded `uv.exe` via this override specifically to avoid re-downloading it for each of -several sub-bootstraps): +``` +[INFO] REQ-009: cascading provider conda to embed; re-attempting dependencies. +*** [INFO] Trying the next Python provider (embed) to resolve dependencies... +``` ``` -[DEBUG] Using super-user override for PVW_UV_EXE: D:\a\...\~envsmoke\~uv_bin\uv.exe -[INFO] uv: using super-user override PVW_UV_EXE. -[INFO] uv: creating venv at .uv_env with Python 3.12... +[INFO] REQ-009: cascading provider embed to venv; re-attempting dependencies. +*** [INFO] Trying the next Python provider (venv) to resolve dependencies... ``` -Invalid-value trace (`[Extrapolated Branch]`, no test forces a bad path): a broken/invalid -`PVW_UV_EXE` is fully absorbed by the existing REQ-009 provider-cascade fallback machinery -- the -uv-first Python-detection probe fails gracefully (WARN, falls toward Miniconda), and even if venv -creation is separately attempted with the same bad binary, an independent, exit-code-agnostic -on-disk check (`if not exist "...\Scripts\python.exe" goto :uv_venv_fail`) catches a binary that -misleadingly reports success without doing real work -- no crash, no silent success, clean -fall-through to conda. +**Note the log wording says "uv to conda", never "uv -> conda"** -- deliberate: `:log` echoes +unquoted, so a literal `>` would be parsed as shell redirection and silently eat the line. -**14c. `PVW_TARGET_PY`** overrides `PYSPEC` at the shared merge point both the uv-first and -conda-base detection paths converge on -- like `PVW_PYTHON_EXE`, detection still runs to -completion first: +Right before each cascade offer, the elective postexec-checkpoint/optimized-build prompts from +Scenario 5 are SKIPPED (not just auto-declined) once cascade is approved -- confirmed in this +same real capture: ``` -[INFO] Python version: using super-user override PVW_TARGET_PY. +[INFO] Entry smoke exit=1 +[STATUS] Run Status: FAILED (Exit Code: 1) +[INFO] REQ-009: cascade approved; skipping the post-verification offers for this build. ``` -REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane), -`tests/selfapps_pipgap.ps1` (sets this to `python=3.12` to pin conda's Python version so a -specific `opencv-python` wheel is guaranteed available for a different test purpose entirely): +**Full exhaustion**, reached when venv ALSO can't fully resolve dependencies and the cascade +offers the absolute last resort, system Python (REQ-014's consent gate, Scenario 5's other +cross-referenced gate) -- declined here (CI auto-decline; a real interactive user sees the full +timed/untimed prompt sequence documented in Scenario 5): ``` -[DEBUG] Using super-user override for PVW_TARGET_PY: python=3.12 -[INFO] Python version: using super-user override PVW_TARGET_PY. +[INFO] REQ-009: cascading provider venv to system; re-attempting dependencies. +[WARN] Attempting system Python fallback (degraded)... + +*** WARNING: System Python Execution *** +*** Using global system Python may pollute shared packages. *** + +Proceed with System Python? (Global pollution risk) [y/n]: y to accept, n to decline. +[INFO] REQ-014: System Python consent: user declined. +[INFO] REQ-014: System Python fallback aborted: consent not granted. +[WARN] REQ-009: cascade target system Python unavailable; keeping current build. ``` -Invalid-value trace (`[Extrapolated Branch]`): no format validation; a garbage value becomes an -invalid conda package spec or an invalid `uv venv --python` request, surfacing as a real, -correctly-handled provider failure absorbed by the same fallback/cascade machinery as 14b -- -reaching a graceful `:die` (`state=error`) only if every fallback tier is also exhausted, never an -uncontrolled crash. +With every tier now exhausted, the bootstrapper keeps whatever build it had (venv, in this +capture) and prints the honest caveat panel (Scenario 43's REQ-027 messaging) instead of "SETUP +COMPLETE": -**14d. `PVW_WORKSPACE`** overrides `HP_UV_ENV_PATH` (the uv venv's path) with a clean, immediate -override -- the default is assigned and instantly replaced before any use, unlike 14a/14c's -"let it run, override the result" pattern. **Scope limitation worth flagging explicitly: this -variable only takes effect in uv mode.** Conda's own environment path has no corresponding check -at all -- a conda-mode run ignores `PVW_WORKSPACE` entirely. +``` +============================================================ + SETUP COMPLETE -- WITH A CAVEAT +============================================================ + We packaged your app, but couldn't fully verify it runs as a + standalone program. Your environment and dependencies ARE + installed correctly -- you can always run your app directly: + "" "app.py" + + RUNNING YOUR APP + Double-click dist\_selftest_cascade_exec.exe to run it. +``` -`[Extrapolated Branch]`, genuinely untested, and uniquely among the four, **there is no dedicated -confirmation log line at its actual point of use** -- only the generic top-of-file `[DEBUG]` line, -which fires purely because the variable is defined, before it's even known whether uv mode (where -this variable matters at all) will be selected. Invalid-value trace: no path validation; if a -`Scripts\python.exe` already happens to exist at the given path, a real functional canary -(`import pip`) guards whether it's actually reused; a creation failure at a bad path is absorbed -by the same `:uv_venv_fail` fallback chain as 14b/14c. +**Each tier is tried at most once as a cascade source** (`HP_CASCADE_TRIED_` guards), +`HP_ENV_MODE` only ever advances (`uv -> conda -> embed -> venv -> system`), so the cascade +structurally cannot loop -- it either lands on a working tier or exhausts and stops, exactly as +shown here. --- -### Scenario 15: `PVW_CONDA_EXE` and its interaction with the conda self-heal flow +### Scenario 16: `--hidden-import` auto-recovery (success and exhaustion) -**What's tested:** `self.corrupt.conda.override_exit` (`tests/selftest.ps1`), self-contained by -construction (no ordering dependency on Miniconda already being installed elsewhere in the job, -unlike its sibling corrupt-conda scenarios). +**What's tested:** `self.exe.hidden_import` (success, `tests/selfapps_hidden_import.ps1`) and +`self.exe.hidden_import.exhaust` (`tests/selfapps_hidden_import_exhaust.ps1`), both real/conda-full +lanes, real, passing. -**Source:** REAL CI CAPTURE, run `30328748330`, both gating lanes (`real` job `90179708091` and -`conda-full` job `90179708094`), both `pass: true`, `exitCode: 2`. +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). -`PVW_CONDA_EXE` overrides the resolved conda batch-file path unconditionally, the instant it's -defined -- and because this happens BEFORE the "install Miniconda if missing" block, setting it -also skips the Miniconda download/install entirely: +When a frozen EXE fails at runtime with `ModuleNotFoundError` for a module that IS installed in +the build interpreter, the bootstrapper rebuilds with `--hidden-import=` added, bounded to +3 attempts. This is strict by design -- an uninstalled module (a real missing dependency) or a +plain `ImportError` (not a missing module at all) never triggers a rebuild, since the fix is not +mechanically derivable. + +**Exhaustion** (three DIFFERENT modules missing across three rebuilds, still never fully +resolving -- REAL CI CAPTURE): ``` -[DEBUG] Using super-user override for PVW_CONDA_EXE: +[WARN][HIDDEN_IMPORT] Auto-recovery exhausted after 3 attempts; module(s) still missing. ``` -**The special interaction, and the whole reason this variable gets its own scenario:** normally, -when a health check on the resolved conda binary fails, the bootstrapper offers an interactive -Y/N self-heal prompt that (on accept) deletes and rebuilds the entire Miniconda root. When -`PVW_CONDA_EXE` is set, this self-heal path is skipped outright -- the very FIRST check in the -corruption-handling subroutine, ahead of every other check including the CI auto-decline logic -- -because the bootstrapper will never auto-delete a path it doesn't own: +`~bootstrap.status.json` still reads `state: ok` in this case -- the user's own program repeatedly +failing to import something is not a bootstrapper failure (the environment and build lifecycle +both succeeded); see CLAUDE.md's "User-code exit-code semantics" Known Finding. -``` -================================================================ - CORRUPTED PYTHON ENVIRONMENT DETECTED -================================================================ +--- - The local conda installation appears to be broken. - This can happen after a Windows update or OS migration - (example: DLL load error 0xc000007b). +### Scenario 17: Warnfix repair loop (success, and the failure that feeds the cascade) - Affected path: +**What's tested:** `self.exe.warnfix.install`/`.pass` (success) and `.xfail` (a module install +genuinely fails), `tests/selfapps_warnfix.ps1`, real/conda-full lanes, all real, all passing. - This binary was specified via PVW_CONDA_EXE: - +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). - Automatic self-healing is not available for user-managed conda. - Please fix or replace the binary at the path above, then re-run. +Unlike Scenario 16 (runs AFTER a launch fails), warnfix runs BEFORE the EXE is ever launched -- +PyInstaller's own warn file lists modules it couldn't bundle statically, and the bootstrapper +tries to install and rebuild: + +**Success** (real capture, a clean repair with no failures): + +``` +[INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. +[REPAIR] missing modules detected; installing and rebuilding. +[REPAIR] rebuild complete after warnfix. ``` -followed by the exact error/exit sequence: +**A module that genuinely can't be installed** (real capture -- `StringIO`, a Python-2-only +module some Python-3 code still conditionally imports, which no Python-3 environment can satisfy): ``` -[ERROR] Corrupt user-managed conda (PVW_CONDA_EXE); fix manually. +[REPAIR] missing modules detected; installing and rebuilding. +[WARN] Repair failed: StringIO +[WARN] One or more repair attempts failed +[REPAIR] rebuild complete after warnfix. ``` -and the process exits with code **2** -- notably, there is no Y/N prompt at all in this path, even -for a genuinely interactive real user; the override check runs before the interactivity dispatch -even has a chance to matter. +The rebuild still happens (bundling whatever WAS successfully installed), but this specific +unresolved-after-rebuild situation is exactly what feeds `HP_CASCADE_CANDIDATE` -- setting up the +provider-cascade offer documented fully in Scenario 15. --- -### Scenario 16: Drag-and-drop / CLI entry-file override (REQ-011 same-directory rule + REQ-002 priority) +### Scenario 18: Pre-flight guards actually firing -**What's tested:** three real, currently-passing NDJSON rows across two test files -- -`self.entry.req011.crossdir` and `self.entry.req011.sameDir` (`tests/selfapps_isolation.ps1`), and -`self.entry.override` (`tests/selfapps_ux_hardening.ps1`, which specifically proves the override -wins over auto-detection, not merely that dragging works at all). +**What's tested:** `self.warn.onedrive`, `self.warn.sysdir`, `self.stub.low_disk_warn` +(`tests/selftest.ps1`, real lane, all real and passing). Contrast with Scenario 2, which showed +these same four guards' CLEAN (silent) pass. -**Source:** REAL CI CAPTURE, run `30328748330`, both gating lanes (`real` job `90179708091` and -`conda-full` job `90179708094`), all three rows `pass: true` in both. +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). -A user can either type a `.py` filename as the first CLI argument to `run_setup.bat`, or literally -drag a `.py` file onto the `.bat` file's icon in Windows Explorer (Windows translates the drop -into the identical `%1` argument). **REQ-011's rule: the file must be in the SAME directory as -`run_setup.bat` itself**, checked twice for defense-in-depth (once as an early pre-flight check, -for instant feedback before any environment work begins, and once again inside the entry-selection -subroutine itself). A cross-directory attempt genuinely terminates the whole process (`exit /b 1`, -not merely a `call`-frame return) with: +**OneDrive** (real capture): ``` -[ERROR] REQ-011: Dragged files must reside in the bootstrapper root folder for environment cleanliness. +*** WARNING: Script appears to be in a OneDrive folder. File locking may cause failures. +[WARN] OneDrive path detected; file locking may cause failures. ``` -(This is the raw, untimestamped console line; the separately-written log-file copy is a shorter -variant without the "for environment cleanliness" clause -- a real, source-confirmed difference, -not a typo, confirmed via real CI capture: `[ERROR] REQ-011: Dragged files must reside in the -bootstrapper root folder.`) - -A same-directory file succeeds and prints the filename (a historical bug that once printed this -line with an EMPTY filename -- see `docs/agent-lessons-learned.md`'s "Provider-cascade dispatch is -goto-based on purpose" entry -- is long fixed; the current, correct text is shown below): +**Disk space, REQ-025** (real capture -- warn-only, never a hard block, per REQ-001's rule that a +flag-detectable condition must never gate the Prime Directive). The block emits three raw `echo` +lines before the `[WARN]` line the underlying test asserts on; all four are console-visible in the +same run, so a real user sees the full four-line block below, not just the final `[WARN]`: ``` -*** Using drag-and-drop file: +*** WARNING: Only ~0 GB free disk space detected on this drive. +*** Downloading Python/Miniconda and building your app can need several GB. +*** If setup fails partway through, freeing up disk space is a likely fix. +[WARN] REQ-025: low disk space detected (~0 GB free); continuing (warn-only). ``` -**Interaction with the REQ-002 interactive entry picker: fully and structurally skipped.** -Providing a valid same-directory file makes entry selection return immediately, before the -auto-detection block (and therefore the picker, which is only ever invoked from inside that same -block) is even reached -- this is REQ-002's documented "priority 0": a co-located override always -wins over auto-detected names, and can never trigger the ambiguous-case timed picker. Confirmed -positively (not just "dragging works," but that override genuinely beats auto-detection) by -`self.entry.override`'s real capture: a scratch directory staged with BOTH `main.py` (which would -win plain auto-detection by name-priority) and `zzz_override.py`, with `zzz_override.py` passed as -the override -- the real run confirms the drag message names the override file, the entry-selected -log line names `zzz_override.py` (not `main.py`), and the override file's own distinguishing output -is what actually ran. - ---- - -## Part VI: Adversarial and recovery branches - -**Scope note:** excludes `HP_TEST_*` CI-only flags as the documented subject -- they're the -mechanism a test uses to force a scenario deterministically, but the resulting console text below -is exactly what a real user hits when the same underlying condition occurs for real (a genuine -flaky connection, a genuinely corrupted conda install, a genuinely missing bundled module, and so -on). Evidence for this entire Part is pulled from run `30328748330` (commit `5872028`, all lanes -green): `real` (job `90179708091`), `conda-full` (job `90179708094`), `uv` (job `90179708109`), -`justme-test` (job `90179708103`). - -### Scenario 17: Network connectivity check and transient-retry (REQ-013 + REQ-022) +**System directory guard** (the one guard that's a hard ABORT, not a warning -- exit code 1, +confirmed via a real, passing NDJSON row, though the specific console dump wasn't captured +verbatim in this run's log excerpt, so the exact banner text below is `[Extrapolated Branch]`, +cited directly from source): -**What's tested:** `self.ux.connectivity.*` rows (`tests/selfapps_ux_hardening.ps1`, real lane); -`self.stub.conda_retry`/`self.stub.conda_create_retry`/`self.stub.conda_perpkg` -(`tests/selftest.ps1`, conda-full lane). All real, all passing. +``` +*** ERROR: This script is located inside a Windows system folder. +*** Placing it here does not "install" it. Windows restricts writes to this location +*** without administrator rights, and this bootstrapper needs to create files right +*** next to itself to work. +*** Please move this script (and your .py files) to a normal folder -- your Desktop +*** or Documents folder both work well -- then run it again from there. +``` -When a download genuinely fails, `:check_net_after_dl_fail` (REQ-013) pings `8.8.8.8` (2 attempts) -then, if ICMP is blocked, tries an HTTPS reachability check against `conda.anaconda.org` (2 -attempts) before concluding the user is actually offline -- this doubled-attempt design exists -because a single dropped ICMP echo or a momentarily-contended connect on a busy machine is enough -to misclassify a genuinely-online host as offline (this was root-caused from a REAL CI flake, not -a hypothetical). If both checks fail, a real user sees an unbounded prompt: +**Path-length guard**: this one is genuinely hard to exercise in CI, for a structural reason worth +being upfront about -- GitHub-hosted Windows runners don't have `LongPathsEnabled` by default, so +PowerShell's own `Push-Location` can fail to navigate into a scratch directory built long enough to +trigger `run_setup.bat`'s own 200-char check before the sub-bootstrap ever gets a chance to run at +all. The test's own real NDJSON row makes this concrete: `pathLen: 312` (the directory really was +built long enough), `ranBootstrap: false` (the runner itself couldn't get there), `skip: true, +reason: 'runner-cannot-navigate-long-path'` -- an honest inconclusive result, not a false pass. The +guard's own text is confirmed from source only for this doc (`[Extrapolated Branch]`): ``` -[WARN] REQ-013: Connectivity check: no internet detected (ICMP and HTTPS check failed). -WARNING: No internet connection detected. Remote providers may fail. Retry? (Fix connection then press Y) or proceed offline (N): +*** WARNING: Script path is 312 chars. Paths near 260 chars may cause cmd.exe failures. ``` -Pressing Enter with no answer defaults to offline; `Y` re-tries (2 more ping attempts, then 2 more -HTTPS attempts) before re-prompting on continued failure; `N` proceeds offline (`HP_OFFLINE_MODE=1` -for the rest of the run). +--- -**Transient-retry for `conda create` and conda's bulk package install (REQ-022)** -- both use the -identical detect-and-retry-once pattern: scan the failure output for `CondaHTTPError`/`Failed to -fetch`/`timed out`/`ConnectionError`, wait 15 seconds, retry exactly once. If the retry ALSO fails, -it falls straight through to the normal (non-transient) failure/fallback chain -- this is not an -infinite-retry loop. +### Scenario 19: Concurrent-instance lock contention (REQ-024) -`conda create` retry, REAL CI CAPTURE (`tests/selftest.ps1`'s conda-full sub-bootstrap): +**What's tested:** `self.stub.lock_held_decline`/`self.stub.lock_stale_evict` +(`tests/selftest.ps1`, real lane, both real and passing). + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). + +A user double-clicking the bootstrapper twice in quick succession (or genuinely running two +instances) hits an NTFS-atomic `mkdir`-based lock. If a second instance finds the lock genuinely +held by a live first instance: ``` -Creating Python environment '_selftest_conda_create_retry' -- this may take several minutes... -Conda environment creation failed -- possible network or repository issue. Retrying once... -[INFO] conda create: transient failure detected; retrying after 15s. -Retrying environment creation... -[INFO] runtime.txt written: python-3.14.6 -[BOOT] REQ-009: Selected Python provider: Conda (Portable). +*** +*** Another instance of this setup appears to be running in this folder already. +*** If you are sure that is NOT the case (for example, a previous run crashed), +*** delete the "~bootstrap.lock" folder next to this script and run it again. +*** +pid= +started= +[WARN] REQ-024: setup already running (lock held, not stale); this instance is exiting. ``` -Bulk package install retry, REAL CI CAPTURE (same test file, a different scratch env): +exit code 1 -- the losing instance never touches the lock it doesn't own. If the lock directory +is instead STALE (left over from a crashed/killed prior run, older than the ~2 hour staleness +threshold), it's evicted automatically and the run proceeds normally with no user action needed: ``` -Conda install failed -- possible network or repository issue. Retrying once... -[INSTALL] conda bulk: transient failure detected; retrying after 15s. -Retrying package installation... +[INFO] REQ-024: stale lock evicted (older than the staleness threshold); proceeding. ``` -A NON-transient bulk failure (any error text that doesn't match the transient patterns above) -skips the retry entirely and instead falls back to installing packages one at a time -(`self.stub.conda_perpkg`, REAL CI CAPTURE, `[INSTALL] conda per-pkg fallback`) -- a completely -separate recovery path from the transient-retry one, chosen based on what kind of failure -actually occurred. +Staleness is deliberately age-based, not PID-liveness-based -- a dead process's PID can be +recycled by an unrelated program, so trusting PID liveness for automated eviction would be unsafe. --- -### Scenario 18: Corrupted-conda self-heal (detect / decline / accept) - -**What's tested:** `self.corrupt.conda.detect`/`.heal.decline`/`.heal.accept` -(`tests/selftest.ps1`, conda-full lane, all three real and passing). The `PVW_CONDA_EXE`-override -variant is already covered in Scenario 15 -- this scenario is the DEFAULT case, where the -bootstrapper owns the conda install and can offer to fix it. - -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane). +## Part V: Remaining branches (dependency source precedence, write-back, and misc) -When a health check on the resolved conda binary fails, the user sees: +**Scope note:** this part collects everything from the plan's 5-pass checklist that didn't fit +naturally into Parts I-IV: dependency and Python-version source precedence, the two write-back +mechanisms (`runtime.txt`, PEP 723 headers), the optional execute-mode discovery flag, NI-VISA/ +pandas per-package special-casing, the periodic conda maintenance timer, and the one REQ-014 +branch (consent ACCEPT) that Part IV's Scenario 15 didn't cover (it only showed decline). -``` -================================================================ - CORRUPTED PYTHON ENVIRONMENT DETECTED -================================================================ +--- - The local conda installation appears to be broken. - This can happen after a Windows update or OS migration - (example: DLL load error 0xc000007b). +### Scenario 20: Git config merge (`.gitignore`/`.gitattributes`, REQ-015) - Affected path: C:\Users\Public\Documents\Miniconda3 -``` +**What's tested:** `self.ux.gitignore.merge`/`.preserve`/`.idem`, `self.ux.gitattributes.merge` +(`tests/selfapps_ux_hardening.ps1`, `real` lane, all real and passing). -followed by an unbounded `[Y/N]` prompt (` Would you like to delete it and rebuild? [Y/N] `, -`[Extrapolated Branch]` for the exact prompt line, same reasoning as Scenario 11's two prompts -- -it lives inside `set /p`, never visible in a CI log even when a test forces the accept branch via -an answer override). +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). -**Decline** (real capture): +On a fresh checkout with no `.gitignore`/`.gitattributes` at all, `:merge_git_config` (`run_setup.bat`) +idempotently appends a standard block to each, guarded by an `findstr` signature check +(`# Automated Python Bootstrapper Standard Ignores` / `...Attributes`) so a second run is a clean +no-op rather than a duplicate append: ``` - Exiting without changes. Delete the folder above manually, - then run this setup again. +[INFO] REQ-015: Appending standard ignores to .gitignore. +[INFO] REQ-015: Appending standard attributes to .gitattributes. ``` -logged as `[ERROR] Corrupt conda env; user declined rebuild.`, exit code 2. - -**Accept** (real capture; this specific test run additionally forces the actual re-download step -to be skipped for CI-safety reasons, so the "Downloading fresh copy..." line below is -`[Extrapolated Branch]`, cited from source, while everything else is real): +The appended `.gitignore` block (verbatim from source): ``` - [INFO] Removing corrupt Miniconda installation... - [INFO] Corrupt installation removed. Downloading fresh copy... +# Automated Python Bootstrapper Standard Ignores +.*_env/ +.venv/ +.uv/ +.cache/ +.conda/ +dist/ +build/ +*~ +~* ``` +and `.gitattributes`: + ``` -[INFO] Self-healing: corrupt conda evicted from C:\Users\Public\Documents\Miniconda3. -[INFO] Workspace: ...\tests\~selftest_heal_accept -[INFO] Env name: _selftest_heal_accept -[INFO] HP_ENV_MODE=conda -[INFO] Creating Python environment '_selftest_heal_accept' -- this may take several minutes... -[INFO] runtime.txt written: python-3.14.6 -[BOOT] REQ-009: Selected Python provider: Conda (Portable). +# Automated Python Bootstrapper Attributes +*.bat eol=crlf +*.cmd eol=crlf +*.exe binary ``` -A real user's accept path re-downloads and reinstalls Miniconda from scratch (the full -acquisition sequence from Scenario 9), then proceeds exactly as a fresh first run would. +Real evidence confirms all three properties the tests assert: the signature is appended +(`self.ux.gitignore.merge`), any PRE-EXISTING content in the file (e.g. a user's own `node_modules/` +line) survives the merge untouched (`self.ux.gitignore.preserve`, `nodeModulesFound:true`), and +running the bootstrapper a second time does not duplicate the signature (`self.ux.gitignore.idem`, +`sigCount:1`). This runs unconditionally on every bootstrap invocation, independent of provider or +entry-file state -- it is one of the first things `run_setup.bat` does after the pre-flight guards. --- -### Scenario 19: Miniconda install chain (AllUsers -> JustMe -> both-failed) +### Scenario 21: Python-version precedence (REQ-004) and dependency-source precedence (`pyproject.toml`) -**What's tested:** `conda.install.justme` (`tests/selftest.ps1`, `justme-test` lane, real, -passing) and `self.conda.bothfail` (`tests/selfapps_conda_bothfail.ps1`, `uv` lane, real, -passing). +**What's tested:** `pyproject.precedence.detect`/`.writeback` (`tests/selfapps_pyproject_precedence.ps1`, +both real and passing) and `pyproject.dep.detect`/`.noproj` (same file). `pyproject.precedence.detect` +ran in the `conda-full` lane in this capture (the `real` lane's own copy of the row emits +`skip=true, reason=conda-not-installed-uv-first` since it happens to call `Get-CondaBatPath`, per +`docs/agent-interconnect.md`'s skip-pattern convention for this test file); `pyproject.dep.*` and +`.writeback` ran in `real`. -**Source:** REAL CI CAPTURE, run `30328748330`, jobs `90179708103` (`justme-test`) and -`90179708109` (`uv`). +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full`) for the version +tiers, job `90179708091` (`real`) for the dependency-source precedence. -Miniconda install first attempts an AllUsers (machine-wide) install; if UAC rejects elevation (or -the process simply isn't elevated), it skips straight to a JustMe (per-user) install instead, no -wasted attempt. **Both the "skip, never attempted" path and a genuine post-attempt AllUsers -failure fall through to the same shared `:tci_justme` label** (`run_setup.bat`), but (fixed -2026-07-31, Closed Active Backlog item 16, renumbered from 11 -- see `docs/agent-closed-backlog.md`) the label -now checks a flag set only right before the real AllUsers install attempt, so the two paths get -distinct wording instead of both unconditionally claiming AllUsers "failed." On the common -non-elevated machine (skip path, `[Extrapolated Branch]` for the new wording -- not yet -re-confirmed against a fresh CI capture): +These are two genuinely SEPARATE precedence systems that happen to both read `pyproject.toml` and +are easy to conflate -- worth documenting distinctly. -``` -[INFO] Not elevated; skipping AllUsers Miniconda install. -[INFO] Miniconda AllUsers install skipped (not elevated); trying JustMe install instead. -[INFO] Miniconda installed (JustMe fallback). -``` +**REQ-004 (Python VERSION precedence, three tiers)**: Tier 1 `runtime.txt` beats Tier 2 +`pyproject.toml`'s `[project].requires-python` beats Tier 3 "let the selected provider pick latest, +then write `runtime.txt` back." These two NDJSON rows deliberately test two DIFFERENT things in two +DIFFERENT scratch directories with two DIFFERENT constraints, not one continuous flow -- worth +being precise about, since the test file's own comments explain why: `.detect` calls +`~detect_python.py` directly (no bootstrap, no environment ever created) against a tight +`requires-python = ">=3.10,<3.11"` to check Tier 2's parse/forward precision in isolation; `.writeback` +runs the FULL bootstrapper against a deliberately loose `requires-python = ">=3.9"` in a separate +directory ("so conda picks a cached Python version and avoids a slow resolver round-trip for +Python 3.10 packages," per the test's own comment) to check Tier 3's write-back end to end. -A genuine, post-attempt AllUsers failure still gets the original WARN wording, now also carrying -the installer's own exit code and a reason token (`[Extrapolated Branch]`, cited from source -- -this branch requires a real elevated process whose AllUsers installer genuinely fails, which no -current CI hook forces without also forcing the skip path): +`.detect`'s real NDJSON output confirms Tier 2's parse/forward is exact: `output":"python>=3.10,<3.11"`. +`.writeback`'s real capture shows Tier 3 firing (since `runtime.txt` didn't pre-exist there either): ``` -[WARN] Miniconda AllUsers install failed (exitCode=1, reason=installer_failed); retrying with JustMe. +[INFO] runtime.txt written: python-3.14.6 ``` -If the installer instead hits `:run_installer_timeout`'s own 60-minute ceiling (see that -subroutine's header comment), the exit code is a hardcoded sentinel, not the installer's real -exit code -- reported as `reason=timeout` with no fabricated `exitCode` field instead -(`[Extrapolated Branch]`, an even rarer sub-case of the one above, never observed in CI): +(real capture; `pyproject.precedence.writeback`'s own NDJSON row confirms `runtimeVersion: +python-3.14.6` and `versionSatisfied:true` against ITS OWN, looser `>=3.9` constraint -- 3.14.6 +satisfies `>=3.9` comfortably. It does NOT satisfy the OTHER test's `<3.10,<3.11` constraint, but +that constraint was never used for this environment; the two tests are independent, and this +document originally conflated them into one implied sequence before being corrected.) Malformed +`pyproject.toml` TOML degrades gracefully rather than aborting the whole precedence chain (real +capture, `self.pyproject.malformed`): ``` -[WARN] Miniconda AllUsers install failed (reason=timeout); retrying with JustMe. +*** [WARN] pyproject.toml could not be parsed as valid TOML; falling back to requirements.txt or pipreqs. ``` -**If JustMe ALSO fails** (both installation options exhausted; REAL CI CAPTURE for the skip-path -lines, `[Extrapolated Branch]` for the now-corrected wording): +logged compactly too: `[WARN] pyproject.toml TOML parse error; falling back.` -- the bootstrap then +proceeds via Tier 3 (provider picks latest) exactly as if `pyproject.toml` had never existed. + +**Dependency-SOURCE precedence (a different mechanism, REQ-004/REQ-005.1 rows, unrelated to Python +version)**: when `pyproject.toml` declares a real `[project].dependencies` array, it takes priority +over any `requirements.txt` present -- this is decided independently of the version-tier logic +above and can fire even when `runtime.txt` already exists. Real capture: ``` -[INFO] Not elevated; skipping AllUsers Miniconda install. -[INFO] Miniconda AllUsers install skipped (not elevated); trying JustMe install instead. -[ERROR] Miniconda install failed (AllUsers skipped -- not elevated; JustMe also failed). +*** [INFO] pyproject.toml [project].dependencies found; overrides requirements.txt +[INFO] pyproject.toml [project].dependencies detected +[INFO] DEP_SOURCE=pyproject ``` -This is a genuine `:die` (`state=error`), not a graceful degrade -- with no Python interpreter -acquirable at all through conda, there's nothing left for this tier to hand back. Both installer -launches are bounded by a generous 60-minute timeout (not unbounded), based on real-world reports -of Miniconda's silent installer hanging indefinitely on some machines -- documented in -CLAUDE.md's Active Backlog history. +`~pyproj_deps.py` (`HP_PYPROJ_DEPS`) is the helper that extracts the array; real NDJSON detail from +`pyproject.dep.detect` shows it parsing a real two-line array (`"output":"requests>=2.28\r\ncolorama"`, +`exitCode:0`). When `pyproject.toml` has no `[project]` section at all, the helper exits 1 with no +output rather than a false match (`pyproject.dep.noproj`, `exitCode:1, outExists:false`) -- the +bootstrapper then falls through to `requirements.txt`/pipreqs as usual. --- -### Scenario 20: Standalone embed-tier download (REQ-009 Tier 5) -- decline and real success +### Scenario 22: PEP 723 dependency write-back (REQ-005.11) -- the fresh-install trigger -**What's tested:** `self.embed.fallback.decline`/`self.embed.fallback.real` -(`tests/selfapps_ux_hardening.ps1`, `uv` lane, both real and passing). +**What's tested:** `self.pep723.writeback.fresh`/`.skipflag` (`tests/selfapps_pep723_writeback.ps1`, +`real` lane, both real and passing). -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708109` (`uv` lane). +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). -When uv and conda have both failed (or been declined/exhausted), the bootstrapper tries -downloading a private, checksummed Python interpreter directly from python.org -- no consent -prompt (unlike system Python), since this is a bootstrapper-controlled, disposable extraction: +After a genuinely fresh, fully-successful `HP_ENV_MODE=uv` dependency install (see Part I, +Scenario 3), `:pep723_writeback` promotes the resolved dependency set into the entry file's own +PEP 723 header via `uv add --script`, so the pin travels with the user's source file rather than +staying only in `requirements.txt`/the lock file: ``` -[WARN] Attempting embedded Python download (REQ-009 Tier 5)... +[INFO] REQ-005.11: PEP 723 header write-back succeeded via uv add --script. ``` -**Real success** (a genuine end-to-end download/verify/extract/patch/pip-bootstrap/canary/build/ -run, REAL CI CAPTURE): +When there is nothing to write (a stdlib-only app, no third-party packages resolved), the +subroutine correctly no-ops rather than writing an empty/misleading header -- also a REAL capture: ``` -[INFO] Downloading embedded Python 3.14.6 from https://www.python.org/ftp/python/3.14.6/python-3.14.6-embed-amd64.zip... -[INFO] embed fallback: 3.14.6 extracted and verified. +[INFO] REQ-005.11: PEP 723 write-back skipped (no packages to write). ``` -**Tier exhaustion** (this specific tier also fails, e.g. offline -- REAL CI CAPTURE): +This is `HP_ENV_MODE=uv`-only (v1 scope, see `docs/agent-interconnect.md`) and best-effort/non-gating +-- any failure (malformed existing header not cleanly repairable, a file lock, non-UTF-8 source) +logs a `[WARN]` and the bootstrap continues unaffected; `HP_SKIP_PEP723_WRITEBACK=1` suppresses it +outright per REQ-019 (a genuine opt-OUT flag, not a gate). The warnfix-triggered SECOND write-back +call (after a successful repair round) is functionally identical and not separately captured here +-- same subroutine, same two possible outcomes, triggered from a different call site. + +--- + +### Scenario 23: `HP_PVW_KNOWN_IDEMPOTENT` execute-mode discovery (REQ-005.13) + +**What's tested:** `self.pvw_idempotent.discovery` (`tests/selfapps_pvw_idempotent.ps1`, `uv` lane, +real and passing). + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708109` (`uv` lane). + +This is an opt-in super-user flag (not part of the default happy path -- Part I never sets it): +when defined, `run_setup.bat` skips straight to actually RUNNING the entry file live via +`uvx autopep723 ` for dependency discovery, before pipreqs or any static analysis even +starts. Real capture: ``` -[WARN] embed fallback: offline mode; cannot download embedded Python. +[INFO] REQ-005.13: HP_PVW_KNOWN_IDEMPOTENT set; running entry via uvx autopep723 for execute-mode discovery. +[INFO] REQ-005.13: execute-mode discovery run succeeded (RAN:persisted). ``` -A genuine download failure (not offline, an actual failed transfer) retries the WHOLE -download+verify cycle once before giving up (`[WARN] embed fallback: download failed; retrying -once.`, `[Extrapolated Branch]`, cited from `:embed_dl_retry` -- not independently observed in -this run's real capture, since neither scenario above hits a genuine mid-download failure). A -checksum mismatch is treated the same way (redownload, not just re-verify, since a mismatch can -mean a truncated download rather than a bad pin). +The entry script's own stdout is inherited/passed through live during this discovery run (not +captured or suppressed) -- real NDJSON detail confirms `stdoutPassthroughFound:true, appRan:true` +-- and whatever dependency it needed (`requests`, in this real capture: `reqsHasRequests:true`) is +persisted back into the PEP 723 header via `uv add --script`, then re-extracted into +`requirements.txt` so the rest of the pipeline (pipreqs, Tier 1 autopep723 merge, the actual +install) sees it too. Deliberately ADDITIVE, not a replacement for pipreqs -- pipreqs and Tier 1's +own `autopep723 check` merge still run normally afterward to catch anything a single execution +path didn't happen to exercise. --- -### Scenario 21: REQ-009 provider cascade -- one real run showing the FULL chain +### Scenario 24: NI-VISA detection and install outcome (REQ-008) -**What's tested:** `self.cascade.exec` (`tests/selfapps_cascade.ps1`, `uv` lane, real, passing). -This single test happens to exercise BOTH mid-exhaustion (uv/conda/embed all cascade past) AND -full exhaustion (system Python, the final tier, declines) in one coherent real capture -- an -unusually complete real-world illustration of this mechanism. +**What's tested:** `pyvisa.detect`/`.nivisa.branch`/`.nivisa.outcome`/`.nivisa.reason`/`.nivisa.disabled` +(`tests/selfapps_pyvisa.ps1`, `real`/`conda-full` lanes, all real and passing). -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708109` (`uv` lane). +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real`) and job `90179708094` +(`conda-full`) -- both lanes captured a genuine NI-VISA install ATTEMPT in this run (not just the +"not required" skip), which is a more complete illustration than what an earlier pass in this +document assumed was CI's only available evidence. -After a build succeeds but a dependency repair genuinely fails (Scenario 23 covers what triggers -this), the user is offered (Scenario 11's timed `:cascade_consent_gate`, up to 30s, default -decline) a chance to try the next Python provider. On acceptance, the REAL capture below shows the -cascade working through every tier in order -- each hop re-attempts the full dependency phase -under the new provider, and each hop that ALSO can't fully resolve dependencies triggers another -verification failure and another cascade offer: +When `pyvisa`/`visa` is detected as an import, the bootstrapper attempts a real NI-VISA driver +install (downloads the online bootstrapper installer via curl, PE-validates it, then launches it +under a bounded timeout): ``` -[INFO] REQ-009: cascading provider uv to conda; re-attempting dependencies. -*** [INFO] Trying the next Python provider (conda) to resolve dependencies... +[INFO] Detected pyvisa/visa import; NI-VISA install may be required. +[VISA] download method: curl +[VISA] installer file size: 6769400 bytes +[VISA] installer PE check: PE_OK +[INFO] Launching NI-VISA installer (timeout ceiling: 5400000 ms). +[VISA] installer exit code: -125202 +[VISA] post-check waiting; retry 1/3 (installer_rc=-125202) +[VISA] post-check waiting; retry 2/3 (installer_rc=-125202) +[VISA] install_failed (post_check_timeout) installer_rc=-125202 ``` -``` -[INFO] REQ-009: cascading provider conda to embed; re-attempting dependencies. -*** [INFO] Trying the next Python provider (embed) to resolve dependencies... -``` +This matches CLAUDE.md's already-documented Known Finding ("NI-VISA real install fails fast in CI") +in shape and mechanism exactly -- a genuine, PE-valid installer download that exits fast and +unattended-incompatible on a CI runner -- though the SPECIFIC installer exit code observed here +(`-125202`) differs from that finding's originally-cited `-125083`. Consistent with the finding's +own framing (an online bootstrapper installer failing an unattended install, not a fixed/stable +error code), not a new discrepancy worth a separate backlog entry. The bootstrap proceeds +gracefully regardless -- a failed NI-VISA install is never treated as a bootstrap failure, only +logged and surfaced; the user's own program still builds and runs. -``` -[INFO] REQ-009: cascading provider embed to venv; re-attempting dependencies. -*** [INFO] Trying the next Python provider (venv) to resolve dependencies... -``` +Real evidence confirms the OTHER outcome branch too, via a dedicated `HP_SKIP_NIVISA=1` scenario in +the same test file: `[VISA] skipped (not_required)`, with `skippedDisabled:true, +noInstallAttempt:true` -- the flag suppresses the install attempt outright even when pyvisa IS +detected, per REQ-019's suppression-only convention. -**Note the log wording says "uv to conda", never "uv -> conda"** -- deliberate: `:log` echoes -unquoted, so a literal `>` would be parsed as shell redirection and silently eat the line. +--- -Right before each cascade offer, the elective postexec-checkpoint/optimized-build prompts from -Scenario 11 are SKIPPED (not just auto-declined) once cascade is approved -- confirmed in this -same real capture: +### Scenario 25: pandas/openpyxl heuristic dependency augmentation (REQ-005.8) + +**What's tested:** `pandas_excel.translate`/`.conda.install`/`.conda.install.req006`/`.runtime`, +`self.pandas.openpyxl.install`/`.import` (`tests/selfapps_pandas_excel.ps1`, `conda-full` lane, all +real and passing) -- plus a genuinely SEPARATE test that happens to exercise the same heuristic in +a different scratch directory, `self.exe.warnfix.real` (`tests/selftest.ps1`'s `real` scenario, +`conda-full` lane, also real and passing, `desc: "Heuristic pre-installed openpyxl via pandas +heuristic; EXE succeeded"`). + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane). + +`~prep_requirements.py` (`HP_PREP_REQUIREMENTS`) applies a small set of heuristic rules that inject +a commonly-needed-but-undeclared package when its "parent" package is present -- pandas's +`pd.read_excel()`/`to_excel()` need `openpyxl`/`xlsxwriter`, but pipreqs' static analysis has no +way to see that a lazily-imported optional engine is actually required at runtime. Real capture +from `tests/~pandas_excel/`'s own scratch directory: ``` -[INFO] Entry smoke exit=1 -[STATUS] Run Status: FAILED (Exit Code: 1) -[INFO] REQ-009: cascade approved; skipping the post-verification offers for this build. +[HEURISTIC] pandas->xlsxwriter ``` -**Full exhaustion**, reached when venv ALSO can't fully resolve dependencies and the cascade -offers the absolute last resort, system Python (REQ-014's consent gate, Scenario 11's other -cross-referenced gate) -- declined here (CI auto-decline; a real interactive user sees the full -timed/untimed prompt sequence documented in Scenario 11): +(the console line is a compact tag; the two package names themselves are appended to the conda +install spec list, confirmed by the real conda solve plan later in the same `~pandas_excel` log: +`openpyxl conda-forge/win-64::openpyxl-3.1.5-py314hccc76fc_3` and `xlsxwriter +conda-forge/noarch::xlsxwriter-3.2.9-pyhd8ed1ab_0`, and both packages installed into +`~pandas_excel`'s own `requirements.txt`/`~reqs_conda.txt`/`~reqs_pip.txt`). The claim that +`openpyxl` ends up genuinely bundled and importable in a frozen EXE is confirmed by the SIBLING +`self.exe.warnfix.real` test's OWN independent scratch directory (`tests/~selftest_warnfix_real/`, +a different app that also exercises the pandas heuristic, per its own NDJSON `desc` text quoted +above) rather than by `~pandas_excel`'s own PyInstaller warn-file -- its EXE's PyInstaller warn +file shows only expected, harmless optional-dependency lines for the bundled `openpyxl`: `missing +module named PIL - imported by openpyxl.drawing.image (optional)`, not a real gap. This is +`HP_ENV_MODE=conda`-lane-only coverage per this test file's own CI wiring (see +`docs/agent-interconnect.md`'s +"selfapps_pandas_excel.ps1" note) -- the SAME heuristic logic also runs for uv/venv/embed/system +providers via `requirements.txt` write-back (CLAUDE.md's own "Deep research pass" Closed Backlog +entry on this exact fix), just not captured here since this test is conda-only by design. -``` -[INFO] REQ-009: cascading provider venv to system; re-attempting dependencies. -[WARN] Attempting system Python fallback (degraded)... +--- -*** WARNING: System Python Execution *** -*** Using global system Python may pollute shared packages. *** +### Scenario 26: Conda base periodic update -Proceed with System Python? (Global pollution risk) [y/n]: y to accept, n to decline. -[INFO] REQ-014: System Python consent: user declined. -[INFO] REQ-014: System Python fallback aborted: consent not granted. -[WARN] REQ-009: cascade target system Python unavailable; keeping current build. -``` +**What's tested:** `self.conda.base.update` (`tests/selfapps_conda_update.ps1`) -- **NOT currently +wired into any CI lane** (per `docs/agent-ndjson.md`'s own explicit note: the `HP_TEST_CONDA_UPDATE` +injection flag was removed because it upgrades conda to a solver version that cascades failures +across the rest of the `conda-full` job). Only the "skipped" branch below has real CI evidence. -With every tier now exhausted, the bootstrapper keeps whatever build it had (venv, in this -capture) and prints the honest caveat panel (Scenario 7's REQ-027 messaging) instead of "SETUP -COMPLETE": +**Source (skipped branch):** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane, +appears identically across every fresh scratch-dir bootstrap in the run). -``` -============================================================ - SETUP COMPLETE -- WITH A CAVEAT -============================================================ - We packaged your app, but couldn't fully verify it runs as a - standalone program. Your environment and dependencies ARE - installed correctly -- you can always run your app directly: - "" "app.py" +`:conda_base_update` runs `conda update -n base` on a timer (30-day threshold, seeded from +`~conda.lastupdate` on first install) whenever `HP_ENV_MODE=conda`. On a genuinely first-ever +install (the common case in a fresh CI scratch dir, and for most real first-time users), it +correctly skips rather than updating a base that was just installed moments ago: - RUNNING YOUR APP - Double-click dist\_selftest_cascade_exec.exe to run it. +``` +[INFO] Conda base update: skipped (first install). ``` -**Each tier is tried at most once as a cascade source** (`HP_CASCADE_TRIED_` guards), -`HP_ENV_MODE` only ever advances (`uv -> conda -> embed -> venv -> system`), so the cascade -structurally cannot loop -- it either lands on a working tier or exhausts and stops, exactly as -shown here. +**`[Extrapolated Branch]`** -- the actual 30-day-elapsed UPDATE-firing branch (`:cbu_run`, +`run_setup.bat`) is not exercised by any current CI run (the only flag that could force it is +deliberately disabled, per the note above). Traced from source: once the timestamp in +`~conda.lastupdate` is more than 30 days old, the subroutine runs `conda update -n base -y` and +would log something in the shape of `[INFO] Conda base update: running (last updated N days +ago)...` followed by conda's own real update-solve output, then rewrites `~conda.lastupdate` to the +current time on completion. This branch realistically only fires for a long-lived, repeatedly-reused +project folder -- not the fresh-checkout scenarios this document otherwise captures -- and is +correctly out of scope for a dedicated CI test: a forced-update test previously broke conda's own +solver in shared CI runners, an accepted, documented tradeoff (`docs/agent-ndjson.md`'s +"conda-full lane rows" section). --- -### Scenario 22: `--hidden-import` auto-recovery (success and exhaustion) +### Scenario 27: REQ-014 system-Python consent -- ACCEPT -**What's tested:** `self.exe.hidden_import` (success, `tests/selfapps_hidden_import.ps1`) and -`self.exe.hidden_import.exhaust` (`tests/selfapps_hidden_import_exhaust.ps1`), both real/conda-full -lanes, real, passing. +**What's tested:** `self.ux.system.gate.accept` (`tests/selfapps_ux_hardening.ps1`, `real` lane, +real and passing). Part IV's Scenario 15 already showed the DECLINE branch of this same gate (as +the terminal step of a full provider-cascade exhaustion); this scenario completes the pair. **Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). -When a frozen EXE fails at runtime with `ModuleNotFoundError` for a module that IS installed in -the build interpreter, the bootstrapper rebuilds with `--hidden-import=` added, bounded to -3 attempts. This is strict by design -- an uninstalled module (a real missing dependency) or a -plain `ImportError` (not a missing module at all) never triggers a rebuild, since the fix is not -mechanically derivable. - -**Exhaustion** (three DIFFERENT modules missing across three rebuilds, still never fully -resolving -- REAL CI CAPTURE): +When every other REQ-009 provider tier has failed or been declined, the system-Python tier is +still reached by any default, no-flag run -- it is gated solely by the REQ-014 consent prompt +(Scenario 5 in Part I already documents the prompt's own framing text in full), never by an +env-var the user would need to set. On ACCEPT, the bootstrapper proceeds to use whatever Python is +already on the machine, unmanaged and unisolated: ``` -[WARN][HIDDEN_IMPORT] Auto-recovery exhausted after 3 attempts; module(s) still missing. +[INFO] REQ-014: System Python consent: user accepted. +[INFO] System fallback using C:\hostedtoolcache\windows\Python\3.12.10\x64\python.exe +[BOOT] REQ-009: Selected Python provider: System Python (degraded). ``` -`~bootstrap.status.json` still reads `state: ok` in this case -- the user's own program repeatedly -failing to import something is not a bootstrapper failure (the environment and build lifecycle -both succeeded); see CLAUDE.md's "User-code exit-code semantics" Known Finding. +`~bootstrap.status.json` still reports `state` as the degraded-but-successful `degraded_env` value +(real NDJSON detail: `"state":"degraded_env","exitCode":0`) -- accepting this tier is a genuine, +if suboptimal, path to a working run, not a failure. This is the ONLY REQ-009 tier gated by an +explicit human consent prompt rather than an automatic fallback, precisely because it is the one +tier that touches the user's real, shared Python environment instead of a private/disposable one. --- -### Scenario 23: Warnfix repair loop (success, and the failure that feeds the cascade) +## Part VI: Additional branches found in a full-file sweep -**What's tested:** `self.exe.warnfix.install`/`.pass` (success) and `.xfail` (a module install -genuinely fails), `tests/selfapps_warnfix.ps1`, real/conda-full lanes, all real, all passing. +**Scope note:** after the original 5-pass plan completed, a systematic label-by-label sweep of +every one of `run_setup.bat`'s 164 `:label`s (cross-checked against everything already written in +every other Part of this file) turned up four genuine, user-observable gaps -- three straightforward +missing scenarios, and one previously-undocumented, real bug in the bootstrapper's own error messaging, +found via real CI evidence and confirmed against source before being written up. Everything else +checked in the sweep (roughly 150 of the 164 labels) was either already covered, internal +control-flow plumbing with no independently observable behavior of its own (e.g. `:pfb_runapp`, +`:mgc_gi_done`), or a narrow edge case not worth a dedicated scenario (e.g. `:cascade_consent_no_ +choice_exe`, reached only on a Windows image stripped of `choice.exe`). -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). +- [Scenario 28: Interactive entry picker -- multiple `.py` files, no clear winner (REQ-002)](#scenario-28-interactive-entry-picker----multiple-py-files-no-clear-winner-req-002) +- [Scenario 29: Pre-flight syntax-error rejection (REQ-021), and a real bug it exposed](#scenario-29-pre-flight-syntax-error-rejection-req-021-and-a-real-bug-it-exposed) +- [Scenario 30: REQ-007 system-Python build consent, and the resulting no-EXE interpreter path](#scenario-30-req-007-system-python-build-consent-and-the-resulting-no-exe-interpreter-path) +- [Scenario 31: EXE smoke-run diagnostic hints (companion to Scenario 16)](#scenario-31-exe-smoke-run-diagnostic-hints-companion-to-scenario-16) -Unlike Scenario 22 (runs AFTER a launch fails), warnfix runs BEFORE the EXE is ever launched -- -PyInstaller's own warn file lists modules it couldn't bundle statically, and the bootstrapper -tries to install and rebuild: +--- -**Success** (real capture, a clean repair with no failures): +### Scenario 28: Interactive entry picker -- multiple `.py` files, no clear winner (REQ-002) + +**What's tested:** `self.entry.picker` (`tests/selfapps_entry_picker.ps1`, `conda-full` lane, real, +passing). + +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane). + +Part I's happy path covers the common single-`.py`-file case; this covers REQ-002's OTHER real +end-user scenario: a folder with several `.py` files where none is named `main.py`/`app.py`/ +`run.py`/`cli.py` and none has a substantive `if __name__ == "__main__":` block (`:determine_entry`'s +own priority ladder, in `tools/find_entry.py`, exhausts every tier and falls back to +`find_entry.py`'s own `AMBIGUOUS_RC` (3) alphabetical pick). Only THEN does `:pick_entry_interactive` +show a real, timed menu: ``` -[INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. -[REPAIR] missing modules detected; installing and rebuilding. -[REPAIR] rebuild complete after warnfix. +Multiple Python files detected -- no clear entry point, so please choose one to run: + [1] a_app.py + [2] b_app.py + + Tip: to skip this question next time, do any one of these: + 1. Drag a .py file onto run_setup.bat -- drop it on the batch file icon to run + that file directly. It must be in this same folder. + 2. Rename your main script to one of: main.py, app.py, run.py, or cli.py. + 3. Give exactly one script an if __name__ == "__main__": block. + If you do nothing, the alphabetically-first file is used: a_app.py + +Type a number 1-2, or wait 30s for the default [1]: ``` -**A module that genuinely can't be installed** (real capture -- `StringIO`, a Python-2-only -module some Python-3 code still conditionally imports, which no Python-3 environment can satisfy): +(the real capture's own timeout is shrunk to 2s via `HP_TEST_FORCE_PICKER`, the same CI-determinism +technique already used for the timed cascade prompt in Scenario 5/21 -- the default, real-user +window is 30 seconds, per `HP_PICK_T` in source, shown above as written). A real, non-interactive +CI run also can't feed `choice.exe` an actual keystroke, so the captured log shows one extra, +CI-only artifact line right after the prompt (`ERROR: The file is either empty or does not contain +the valid choices.`) before falling through to the timeout default -- a real interactive user typing +a number, or simply waiting, never sees that line. Either way, the resolution is logged: ``` -[REPAIR] missing modules detected; installing and rebuilding. -[WARN] Repair failed: StringIO -[WARN] One or more repair attempts failed -[REPAIR] rebuild complete after warnfix. +[INFO] REQ-002: Picker entry selected: a_app.py ``` -The rebuild still happens (bundling whatever WAS successfully installed), but this specific -unresolved-after-rebuild situation is exactly what feeds `HP_CASCADE_CANDIDATE` -- setting up the -provider-cascade offer documented fully in Scenario 21. +If MORE than 9 candidate files exist, the picker's own numbered menu is skipped entirely (no menu +can address more than the `123456789` `choice /C` charset) and the alphabetical pick is kept, logged +as `[INFO] REQ-002: candidates exceed picker limit; keeping (alphabetical).`. The same +three-item Tip list shown above (drag-and-drop, a preferred filename, or a `__main__` block) still +prints right after that log line -- worded "to avoid the alphabetical fallback next time" here, +since no question was actually asked to skip -- because it's exactly the guidance a user who just +hit a >9-file folder needs most to avoid landing here again; this used to be silently skipped along +with the rest of the menu, fixed in the same pass that wrote this scenario. `[Extrapolated Branch]` +for the exact console text, cited from `:pick_entry_interactive` -- the branch itself is now +covered by `self.entry.picker.overflow` (`tests/selfapps_entry_picker.ps1`, `conda-full` lane, +registered in `docs/agent-ndjson.md`), which stages 10 candidate files and asserts the overflow log +line, the Tip guidance, and the alphabetical default all fire correctly; that test only dumps a +full console log to CI on failure, so the exact text below is still not an independently captured +console dump. --- -### Scenario 24: Pre-flight guards actually firing +### Scenario 29: Pre-flight syntax-error rejection (REQ-021), and a real bug it exposed -**What's tested:** `self.warn.onedrive`, `self.warn.sysdir`, `self.stub.low_disk_warn` -(`tests/selftest.ps1`, real lane, all real and passing). Contrast with Scenario 8, which showed -these same four guards' CLEAN (silent) pass. +**What's tested:** `self.preflight.syntax` (`tests/selfapps_preflight.ps1`, `real` lane, real, +passing) for the ordinary case -- its `$pass` gate enforces the REQ-021 message firing, the real +`SyntaxError` detail appearing, `state: error`, and no PyInstaller-build-crash text; "no EXE was +produced" is separately computed and recorded (`noExe` in the row's own `details`, confirmed +`true` in the real capture below) but is NOT itself part of the pass/fail gate. A SEPARATE, +unrelated real capture (`self.embed.fallback.decline`, `tests/selfapps_ux_hardening.ps1`) +accidentally also reaches this code path under total provider exhaustion, which is what exposed +the bug documented below. **Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). -**OneDrive** (real capture): +**The ordinary case**: before ever attempting a doomed PyInstaller build, `:preflight_compile` +byte-compiles the entry file with the SAME parser the interpreter itself uses (`py_compile`, +zero false positives), reporting a genuine `SyntaxError` clearly and stopping before the build: + +``` +*** [ERROR] REQ-021: Your Python program has a syntax error and cannot run. *** +*** File: "app.py" *** +``` + +followed by the real Python compiler's own traceback (captured to `~preflight.err.txt`, echoed to +both console and log) and a closing `*** Fix the syntax error shown above, then run this batch +again. ***`. `~bootstrap.status.json` correctly reads `state: error`; the dependency install and +environment creation that ran BEFORE this check are left intact (nothing is torn down), so a fixed +file on the next run reuses them. + +**A real bug, found via this exact sweep, confirmed against a real, unrelated capture (not +fabricated for this scenario), and fixed.** The capture below is historical, from before the fix, +kept unedited as a real, timestamped log. `:preflight_compile` used to invoke `"%HP_PY%" -m +py_compile "%HP_ENTRY%"` with no check that `HP_PY` was actually a valid, non-empty interpreter +path first. On TOTAL REQ-009 provider-tier exhaustion (every tier fails or is declined), +`:after_env_mode_selection`'s own `HP_PY`-resolved guard +(`call :die "[ERROR] Active Python interpreter not resolved."`) does NOT actually halt the +pipeline -- per this repo's own long-documented `:die` semantics, `exit /b` inside `:die` only +returns from `:die`'s own call frame, so execution fell through and continued for another ~15 log +lines with an EMPTY `HP_PY`, all the way to `:preflight_compile`. There, `"" -m py_compile +"app.py"` was not a Python invocation at all -- it was cmd.exe trying to execute a program +literally named `""`, which produces a CMD.EXE ERROR, not a Python traceback. Because +`:preflight_compile` treated ANY nonzero exit as "syntax error," it reported this as if the user's +own code were broken. Real capture, from a test that deliberately force-fails uv (offline), conda, +the embed tier, and venv, and declines the REQ-014 system-Python prompt +(`tests/~selftest_embed_decline/`'s own sub-bootstrap): ``` -*** WARNING: Script appears to be in a OneDrive folder. File locking may cause failures. -[WARN] OneDrive path detected; file locking may cause failures. +[ERROR] Active Python interpreter not resolved. +Interpreter: +[WARN] Interpreter smoke test failed (continuing). ``` -**Disk space, REQ-025** (real capture -- warn-only, never a hard block, per REQ-001's rule that a -flag-detectable condition must never gate the Prime Directive). The block emits three raw `echo` -lines before the `[WARN]` line the test asserts on -- all four are console-visible in the same -run; the earlier scan of this scenario quoted only the last one, which undersold what a real user -actually sees: +(pipreqs, dependency install, and the pyvisa check all run to completion afterward, effectively as +no-ops against the broken interpreter, before the entry is finally selected and preflight runs) ``` -*** WARNING: Only ~0 GB free disk space detected on this drive. -*** Downloading Python/Miniconda and building your app can need several GB. -*** If setup fails partway through, freeing up disk space is a likely fix. -[WARN] REQ-025: low disk space detected (~0 GB free); continuing (warn-only). +*** [ERROR] REQ-021: Your Python program has a syntax error and cannot run. *** +*** File: "app.py" *** +'""' is not recognized as an internal or external command, +operable program or batch file. + +*** Fix the syntax error shown above, then run this batch again. *** ``` -**System directory guard** (the one guard that's a hard ABORT, not a warning -- exit code 1, -confirmed via a real, passing NDJSON row, though the specific console dump wasn't captured -verbatim in this run's log excerpt, so the exact banner text below is `[Extrapolated Branch]`, -cited directly from source): +That last block was genuinely misleading: `app.py` may have had no syntax problem whatsoever -- the +real cause, printed several screens earlier, was that no Python interpreter was ever found. A real +user was realistically reachable here for genuine (not test-only) reasons: README's own REQ-009 +table already notes that falling through three-plus provider tiers in one run is "almost always one +shared root cause" (no internet, a full disk, or a locked-down managed image), and a user hitting +exactly that plus declining the REQ-014 system-Python consent prompt would reach this identical +path. The status FILE was never affected (`state: error` was always written correctly, since +`:die`'s own state-set already happened before the fall-through) -- only the human-readable console +narrative misdirected a user who read just the last error rather than scrolling back. + +**Fixed**: `:after_env_mode_selection`'s guard now also sets `HP_NO_INTERPRETER=1` before calling +`:die` (the call-frame-only-return fall-through itself is left as-is -- a deeper refactor of that +mechanism was judged out of scope for this fix). `:preflight_compile` checks this flag first and, +if set, reports the real cause instead of running `py_compile` against an empty interpreter path: ``` -*** ERROR: This script is located inside a Windows system folder. -*** Placing it here does not "install" it. Windows restricts writes to this location -*** without administrator rights, and this bootstrapper needs to create files right -*** next to itself to work. -*** Please move this script (and your .py files) to a normal folder -- your Desktop -*** or Documents folder both work well -- then run it again from there. +*** [ERROR] No Python interpreter is available; your program was not run or built. *** +*** This is not a syntax error -- the Python interpreter itself could not be used. *** +*** Either every automatic Python-acquisition method -- uv, conda, a fresh download, *** +*** or a local virtual environment -- failed, usually from no internet connection, a *** +*** full disk, or a locked-down managed machine image -- or a PVW_PYTHON_EXE override *** +*** points at a path that does not run. Scroll up in this window for the specific reason. *** ``` -**Path-length guard**: this run's own long-path test scenario shows an interesting CI limitation -worth being honest about rather than papering over -- the test's own NDJSON row reports -`warnFound: false, ranBootstrap: false, pathLen: 312`, meaning the scratch directory this specific -CI run created didn't actually reach a state where the sub-bootstrap could run at all (likely an -OS-level path-length limit on the CI runner itself, hit before `run_setup.bat`'s own 200-char -check ever got a chance to fire) -- yet the test still reports an overall pass, since it's -apparently designed to tolerate this inconclusive outcome. The guard's own text is confirmed from -source only for this doc (`[Extrapolated Branch]`): +(The message text was later reworded during implementation to avoid a literal `(...)` pair split +across two `echo` lines inside the same parenthesized `if` block -- cmd.exe's block parser counts +parens in echo text too, so a `(` on one line and its `)` on the next silently mis-closed the +block and broke every CI lane reaching this branch in the same run. See +`docs/agent-lessons-learned.md`'s batch-syntax-quirks section for the full trace.) -``` -*** WARNING: Script path is chars. Paths near 260 chars may cause cmd.exe failures. -``` +This also skips the doomed PyInstaller build attempt entirely (`:run_entry_smoke`'s existing +`HP_PREFLIGHT_FAILED` check already short-circuits the build, unchanged by this fix), not just the +misleading message. --- -### Scenario 25: Concurrent-instance lock contention (REQ-024) +### Scenario 30: REQ-007 system-Python build consent, and the resulting no-EXE interpreter path -**What's tested:** `self.stub.lock_held_decline`/`self.stub.lock_stale_evict` -(`tests/selftest.ps1`, real lane, both real and passing). +**What's tested:** `self.sysbuild.decline` (`tests/selfapps_sysbuild.ps1`, `real` lane, real, +passing) -- its `$pass` gate enforces that the REQ-007 prompt text appears, the decline is +logged, packaging is skipped with a logged reason, and no EXE exists afterward. The REQ-014 +"use system Python at all" accept step that gets this test INTO system-Python mode in the first +place is the same mechanism Scenario 27's own `self.ux.system.gate.accept` test covers +independently, not re-asserted here. The interpreter-smoke success/status lines quoted below +(`Entry smoke exit=0`, `[STATUS] Run Status: SUCCESS`) are genuinely present in this same real +captured log but are NOT part of this test's own `$pass` gate -- shown here as observed fact from +the real capture, not as something this specific test independently verifies. -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). +**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane), +`tests/~selftest_sysbuild/`'s own sub-bootstrap -- ONE coherent, continuous real capture covering +the full journey below. -A user double-clicking the bootstrapper twice in quick succession (or genuinely running two -instances) hits an NTFS-atomic `mkdir`-based lock. If a second instance finds the lock genuinely -held by a live first instance: +Scenario 27 showed REQ-014's "use system Python at all?" consent being accepted. That is not the +only consent gate on this tier: once system Python is actually selected as the provider, a SECOND, +independent consent gate (`:system_build_consent_gate`, REQ-007) asks separately about installing +PyInstaller into that same system Python to build a standalone EXE -- distinct from REQ-014, and +reachable only in `HP_ENV_MODE=system` mode (every other provider tier gets no such prompt, since +none of them touch a shared, uncontrolled Python installation): ``` -*** -*** Another instance of this setup appears to be running in this folder already. -*** If you are sure that is NOT the case (for example, a previous run crashed), -*** delete the "~bootstrap.lock" folder next to this script and run it again. -*** -pid= -started= -[WARN] REQ-024: setup already running (lock held, not stale); this instance is exiting. +*** The standalone EXE build installs PyInstaller into your system Python. *** +*** This is the same PyInstaller build used for every provider -- not a special path -- and *** +*** its footprint is small and self-contained (it does not pin common libraries), so it is *** +*** unlikely to conflict with your existing packages. *** ``` -exit code 1 -- the losing instance never touches the lock it doesn't own. If the lock directory -is instead STALE (left over from a crashed/killed prior run, older than the ~2 hour staleness -threshold), it's evicted automatically and the run proceeds normally with no user action needed: +CI auto-declines (same CI-safe pattern as every other consent gate in this file: `HP_TEST_ +SYSBUILD_ANSWER` override checked first, then `HP_CI_LANE` auto-decline, then a real, unbounded +`set /p` for an interactive user). On decline, dependency install and environment setup are left +completely intact -- only the PyInstaller packaging step is skipped: ``` -[INFO] REQ-024: stale lock evicted (older than the staleness threshold); proceeding. +[INFO] REQ-007: system-Python EXE build consent: declined. +[INFO] REQ-007: system-Python EXE build not consented; skipping PyInstaller packaging. The environment and dependencies are installed; run the app directly via the prepared Python. ``` -Staleness is deliberately age-based, not PID-liveness-based -- a dead process's PID can be -recycled by an unrelated program, so trusting PID liveness for automated eviction would be unsafe. - ---- +With no EXE ever attempted, `:verify_no_exe_interpreter` becomes the sole verification run (this is +also the general no-EXE path reached whenever `dist\.exe` doesn't exist for any reason -- a +declined build here, or PyInstaller being entirely unavailable elsewhere -- not specific to this +consent gate): -## Part VII: Remaining branches (dependency source precedence, write-back, and misc) +``` +[INFO] Running entry script smoke test via system interpreter. +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) +[INFO] REQ-018: post-execution checkpoint (interpreter): declined (run footprint stays at one execution). +``` -**Scope note:** this part collects everything from the plan's 5-pass checklist that didn't fit -naturally into Parts III-VI: dependency and Python-version source precedence, the two write-back -mechanisms (`runtime.txt`, PEP 723 headers), the optional execute-mode discovery flag, NI-VISA/ -pandas per-package special-casing, the periodic conda maintenance timer, and the one REQ-014 -branch (consent ACCEPT) that Part VI's Scenario 21 didn't cover (it only showed decline). +The postflight briefing that follows is `:print_no_exe_briefing` (Part V/CLAUDE.md's REQ-027 P2 +work already documents its honest-messaging design and its `HP_NOEXE_VERIFY_FAILED`-gated caveat +variant) rather than the EXE-focused `:print_postflight_briefing` -- distinct panels for a +genuinely distinct outcome (no packaged deliverable exists, but the app runs fine via the prepared +interpreter). --- -### Scenario 26: Git config merge (`.gitignore`/`.gitattributes`, REQ-015) +### Scenario 31: EXE smoke-run diagnostic hints (companion to Scenario 16) -**What's tested:** `self.ux.gitignore.merge`/`.preserve`/`.idem`, `self.ux.gitattributes.merge` -(`tests/selfapps_ux_hardening.ps1`, `real` lane, all real and passing). +**What's tested:** the `[HINT]` mechanism fires as a byproduct of `selfapps_exedata_fail.ps1` +(`DATA_FILE` hint) and `selfapps_exedyn_fail.ps1`/`selfapps_hidden_import_exhaust.ps1`-family tests +(`HIDDEN_IMPORT` hint), both real/conda-full lanes, real, passing (the hint lines are a bonus +diagnostic these tests emit, not the tests' own primary assertion target). **Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). -On a fresh checkout with no `.gitignore`/`.gitattributes` at all, `:merge_git_config` (`run_setup.bat`) -idempotently appends a standard block to each, guarded by an `findstr` signature check -(`# Automated Python Bootstrapper Standard Ignores` / `...Attributes`) so a second run is a clean -no-op rather than a duplicate append: - -``` -[INFO] REQ-015: Appending standard ignores to .gitignore. -[INFO] REQ-015: Appending standard attributes to .gitattributes. -``` +Scenario 16 covers `--hidden-import` auto-recovery reaching its 3-attempt cap without resolving. In +that same failure family -- any EXE that still fails at runtime AFTER hidden-import recovery has +been tried (or was never applicable, e.g. a genuinely missing DATA file, not a missing module) -- +`:exe_smokerun_hints` re-runs the EXE briefly (no timeout needed; these failures exit immediately) +purely to pattern-match its stderr and offer a targeted, actionable hint. Two real captures, from +two different xfail scenarios: -The appended `.gitignore` block (verbatim from source): +**A missing bundled data file** (real capture, `FileNotFoundError`): ``` -# Automated Python Bootstrapper Standard Ignores -.*_env/ -.venv/ -.uv/ -.cache/ -.conda/ -dist/ -build/ -*~ -~* +[HINT][DATA_FILE] Missing data file detected: C:\Users\RUNNER~1\AppData\Local\Temp\_MEI41642\mypkg\data\info.txt +[HINT][DATA_FILE] Consider adding: --add-data C:\Users\RUNNER~1\AppData\Local\Temp\_MEI41642\mypkg\data\info.txt;. +[HINT][RUNTIME_MISMATCH] Standalone EXE behavior differs from the Python runtime (possible PyInstaller packaging issue in the EXE, not your environment or dependencies) ``` -and `.gitattributes`: +**A missing module hidden-import recovery couldn't resolve** (real capture, `ModuleNotFoundError`): ``` -# Automated Python Bootstrapper Attributes -*.bat eol=crlf -*.cmd eol=crlf -*.exe binary +[HINT][HIDDEN_IMPORT] Hidden import likely missing: absent_dynmod_xyz +[HINT][HIDDEN_IMPORT] Consider adding: --hidden-import=absent_dynmod_xyz +[HINT][RUNTIME_MISMATCH] Standalone EXE behavior differs from the Python runtime (possible PyInstaller packaging issue in the EXE, not your environment or dependencies) ``` -Real evidence confirms all three properties the tests assert: the signature is appended -(`self.ux.gitignore.merge`), any PRE-EXISTING content in the file (e.g. a user's own `node_modules/` -line) survives the merge untouched (`self.ux.gitignore.preserve`, `nodeModulesFound:true`), and -running the bootstrapper a second time does not duplicate the signature (`self.ux.gitignore.idem`, -`sigCount:1`). This runs unconditionally on every bootstrap invocation, independent of provider or -entry-file state -- it is one of the first things `run_setup.bat` does after the pre-flight guards. +Both hint types are logged via `:log` (console AND `~setup.log`), so a real user hitting either +failure sees them directly -- not buried in a diagnostic-only file. The `RUNTIME_MISMATCH` hint is +a universal closing line, not specific to the `DATA_FILE` branch -- both real captures above show +it: every path through `:exe_smokerun_hints` (data-file match, module-not-found match, or neither) +falls straight through to it with no `goto`/`exit` skipping it in between, so it always fires +alongside whichever more specific hint (if any) matched, as a general reminder that a frozen EXE's +behavior can differ from the interpreter's for packaging reasons unrelated to environment or +dependencies. A separate, optional machine-readable form exists too (`HINT_JSON=1`, an +undocumented-in-README super-user flag that additionally prints each hint as a compact JSON object +via PowerShell) -- not independently captured in this run. --- -### Scenario 27: Python-version precedence (REQ-004) and dependency-source precedence (`pyproject.toml`) - -**What's tested:** `pyproject.precedence.detect`/`.writeback` (`tests/selfapps_pyproject_precedence.ps1`, -both real and passing) and `pyproject.dep.detect`/`.noproj` (same file). `pyproject.precedence.detect` -ran in the `conda-full` lane in this capture (the `real` lane's own copy of the row emits -`skip=true, reason=conda-not-installed-uv-first` since it happens to call `Get-CondaBatPath`, per -`docs/agent-interconnect.md`'s skip-pattern convention for this test file); `pyproject.dep.*` and -`.writeback` ran in `real`. +## Part VII: Full startup-to-shutdown walkthroughs -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full`) for the version -tiers, job `90179708091` (`real`) for the dependency-source precedence. +**Scope note:** every scenario elsewhere in this file is deliberately narrow -- one feature area, +excerpted to the lines that illustrate it. This Part does the opposite: five composite panels that +show a run from its first console line to its last, so a reader can see the actual SHAPE of a +complete bootstrap without jumping between sections. None of these is sourced from one single, +continuous CI capture (no test in this repo is designed to produce that) -- each is assembled from +real fragments already individually cited elsewhere in this file, spliced into one continuous flow +and labeled where the splice itself (not any individual line) is `[Extrapolated Branch]`. Cross- +reference the originating scenario for that fragment's own detailed sourcing rather than re-deriving +it here. -These are two genuinely SEPARATE precedence systems that happen to both read `pyproject.toml` and -are easy to conflate -- worth documenting distinctly. +### Scenario 32: Full walkthrough -- the ordinary happy path, start to shutdown -**REQ-004 (Python VERSION precedence, three tiers)**: Tier 1 `runtime.txt` beats Tier 2 -`pyproject.toml`'s `[project].requires-python` beats Tier 3 "let the selected provider pick latest, -then write `runtime.txt` back." These two NDJSON rows deliberately test two DIFFERENT things in two -DIFFERENT scratch directories with two DIFFERENT constraints, not one continuous flow -- worth -being precise about, since the test file's own comments explain why: `.detect` calls -`~detect_python.py` directly (no bootstrap, no environment ever created) against a tight -`requires-python = ">=3.10,<3.11"` to check Tier 2's parse/forward precision in isolation; `.writeback` -runs the FULL bootstrapper against a deliberately loose `requires-python = ">=3.9"` in a separate -directory ("so conda picks a cached Python version and avoids a slow resolver round-trip for -Python 3.10 packages," per the test's own comment) to check Tier 3's write-back end to end. +Splices Scenario 2's pre-flight/entry-detection evidence and Scenario 3's provider/dependency +evidence (both from the `colorama`-importing stub app) with Scenario 4's own single, fully +self-consistent, zero-staleness real capture (the `hello_stub.py` trivial app, chosen there +specifically because it already spans build through the final status panel with no gaps). All three +are real CI captures from the same run (`30328748330`, job `90179708091`, "real" lane) -- just two +different scratch apps within that run, since no single test in this repo builds one app all the +way from a cold uv download through a `hello_stub`-style trivial verification. The splice point +(where the `colorama` app's pre-build evidence hands off to the `hello_stub` app's build-onward +evidence) is `[Extrapolated Branch]` -- the two are structurally identical at that point (both are +first-ever runs, no cached state, uv-first), but were never the same process. -`.detect`'s real NDJSON output confirms Tier 2's parse/forward is exact: `output":"python>=3.10,<3.11"`. -`.writeback`'s real capture shows Tier 3 firing (since `runtime.txt` didn't pre-exist there either): +``` +[INFO] REQ-015: Appending standard ignores to .gitignore. +[INFO] REQ-015: Appending standard attributes to .gitattributes. +Chosen entry: app.py +[INFO] uv: UV_PYTHON_PREFERENCE=only-managed (orchestration uses managed Python). +[INFO] uv: downloading to ~uv_bin... +[INFO] Downloading uv from https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip... +[INFO] uv: acquired at ~uv_bin\uv.exe +[INFO] uv-first: Miniconda download skipped. +[INFO] uv: creating venv at .uv_env... +[INFO] uv: venv created at .uv_env +[INFO] HP_ENV_MODE=uv +[BOOT] REQ-009: Selected Python provider: UV. +[INFO] runtime.txt written: python-3.14.6 +[INFO] pipreqs 0.4.13 installed successfully; using it for dependency discovery. +*** [WARN] Dependencies were auto-detected (pipreqs) +*** [WARN] Auto-detection may be incomplete or incorrect +*** [INFO] Consider adding requirements.txt or PEP 723 metadata for reliability +[INFO] REQ-005.5: dependency source diff computed -- ~pipreqs.diff.txt +[INFO] REQ-005.12: autopep723 discovery merge complete. +[INFO] UV_USED=1 +[INFO] DEP_INSTALLED_CAPTURED=1 +[INFO] Environment snapshot written: ~environment.lock.txt +[INFO] REQ-005.11: PEP 723 header write-back succeeded via uv add --script. +[INFO] Building standalone executable -- this may take a minute or two... +[INFO] (A stray one-line Windows message about a missing drive may appear next -- that is a known side effect from an unrelated background process, unrelated to your app; safe to ignore.) +The system cannot find the drive specified. +The system cannot find the drive specified. +[INFO] PyInstaller produced dist\.exe +[DEBUG] warnfix: warn file found +[INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. +[INFO] PyInstaller build artifacts cleaned up. +[INFO] EXE smokerun: testing dist\.exe +[INFO] Running entry script smoke test via packaged EXE. +[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +[INFO] Process ID 6076. If it seems stuck: Task Manager > Details tab > find this PID > End Task (this window stays open). +hello-from-stub +[INFO] EXE smokerun: exited 0 (ok) +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) -``` -[INFO] runtime.txt written: python-3.14.6 +*** Verification finished -- see the Run Status above. *** +*** You can run your program again now via the interpreter as an extra diagnostic check. *** + Run again via the interpreter now? [Y/N] _ ``` -(real capture; `pyproject.precedence.writeback`'s own NDJSON row confirms `runtimeVersion: -python-3.14.6` and `versionSatisfied:true` against ITS OWN, looser `>=3.9` constraint -- 3.14.6 -satisfies `>=3.9` comfortably. It does NOT satisfy the OTHER test's `<3.10,<3.11` constraint, but -that constraint was never used for this environment; the two tests are independent, and this -document originally conflated them into one implied sequence before being corrected.) Malformed -`pyproject.toml` TOML degrades gracefully rather than aborting the whole precedence chain (real -capture, `self.pyproject.malformed`): +(the checkpoint prompt line itself is `[Extrapolated Branch]`, same reasoning as Scenario 5 -- it +lives inside an unbounded `set /p`, never visible in a CI log). Declining both elective prompts +(the checkpoint above, then the optimized-build offer) reaches the same "SETUP COMPLETE" panel +already quoted in full in Scenario 4 -- not repeated a third time here; see that scenario for the +exact panel text, or Scenario 5 for what accepting either prompt does instead. -``` -*** [WARN] pyproject.toml could not be parsed as valid TOML; falling back to requirements.txt or pipreqs. -``` +### Scenario 33: Full walkthrough -- uv can't resolve a dependency, cascades to conda, which does -logged compactly too: `[WARN] pyproject.toml TOML parse error; falling back.` -- the bootstrap then -proceeds via Tier 3 (provider picks latest) exactly as if `pyproject.toml` had never existed. +**Honesty note up front:** this exact narrow case -- uv fails specifically at DEPENDENCY +RESOLUTION (not environment creation), cascades exactly once, and conda succeeds -- has no single +real capture in this repo's CI history to point to. Scenario 15's own real capture (the fullest +cascade evidence this repo has) walks uv through every tier to full exhaustion in one run, which is +genuinely a harder case to hit than "cascades once and the next tier just works." The pieces below +are real, individually cited fragments; the SPLICE connecting "uv cascades to conda" into "conda +then succeeds and the run completes" is `[Extrapolated Branch]`, reusing this doc's own +already-verified building blocks (the provider-selection log line and the generic build/verify/ +complete tail, both provider-agnostic in their wording) rather than inventing new text. -**Dependency-SOURCE precedence (a different mechanism, REQ-004/REQ-005.1 rows, unrelated to Python -version)**: when `pyproject.toml` declares a real `[project].dependencies` array, it takes priority -over any `requirements.txt` present -- this is decided independently of the version-tier logic -above and can fire even when `runtime.txt` already exists. Real capture: +Real trigger (Scenario 15, same wording, same reasoning about why it says "uv to conda" not +"uv -> conda"): ``` -*** [INFO] pyproject.toml [project].dependencies found; overrides requirements.txt -[INFO] pyproject.toml [project].dependencies detected -[INFO] DEP_SOURCE=pyproject +[WARN] Repair failed: +[WARN] One or more repair attempts failed +[REPAIR] rebuild complete after warnfix. +[INFO] Entry smoke exit=1 +[STATUS] Run Status: FAILED (Exit Code: 1) +[INFO] REQ-009: cascade approved; skipping the post-verification offers for this build. +[INFO] REQ-009: cascading provider uv to conda; re-attempting dependencies. +*** [INFO] Trying the next Python provider (conda) to resolve dependencies... ``` -`~pyproj_deps.py` (`HP_PYPROJ_DEPS`) is the helper that extracts the array; real NDJSON detail from -`pyproject.dep.detect` shows it parsing a real two-line array (`"output":"requests>=2.28\r\ncolorama"`, -`exitCode:0`). When `pyproject.toml` has no `[project]` section at all, the helper exits 1 with no -output rather than a false match (`pyproject.dep.noproj`, `exitCode:1, outExists:false`) -- the -bootstrapper then falls through to `requirements.txt`/pipreqs as usual. - ---- +`:cascade_acquire_conda` downloads and installs Miniconda on demand at this point if it wasn't +already on disk (uv-first runs skip Miniconda entirely until something actually needs it -- see +`docs/agent-interconnect.md`'s "uv-First Provider Architecture"), then re-enters the same +dependency-install machinery Scenario 3 already documents in full, just under +`HP_ENV_MODE=conda` this time (`[Extrapolated Branch]` for this specific re-entry's own console +text, though every individual line reused below is independently real elsewhere in this file): -### Scenario 28: PEP 723 dependency write-back (REQ-005.11) -- the fresh-install trigger +``` +[BOOT] REQ-009: Selected Python provider: Conda (Portable). +Creating Python environment '' -- this may take several minutes... +[INFO] runtime.txt written: python-3.14.6 +[INFO] Building standalone executable -- this may take a minute or two... +[INFO] PyInstaller produced dist\.exe +[INFO] EXE smokerun: testing dist\.exe +[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +[INFO] EXE smokerun: exited 0 (ok) +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) +``` -**What's tested:** `self.pep723.writeback.fresh`/`.skipflag` (`tests/selfapps_pep723_writeback.ps1`, -`real` lane, both real and passing). +followed by the ordinary "SETUP COMPLETE" panel (Scenario 4). The mechanism-level reason conda has +a genuine, above-average chance of resolving what uv couldn't -- a real, different package index +with pre-built native-extension wheels, not just a fresh attempt at the same resolution -- is +covered in `docs/agent-interconnect.md`'s "Cascade signal reliability" section; that section is +also why later cascade hops (embed/venv/system) carry comparatively less of this same justification. -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). +### Scenario 34: Full walkthrough -- warnfix repair and rebuild, start to finish -After a genuinely fresh, fully-successful `HP_ENV_MODE=uv` dependency install (see Part III, -Scenario 9), `:pep723_writeback` promotes the resolved dependency set into the entry file's own -PEP 723 header via `uv add --script`, so the pin travels with the user's source file rather than -staying only in `requirements.txt`/the lock file: +Splices Scenario 17's real repair-loop fragments with the generic build-start/verify/complete text +already verified real elsewhere in this file (Scenario 4). The connecting tissue (that these two +fragments belong to the same run) is `[Extrapolated Branch]`; every individual line is independently +a real capture cited in its own originating scenario. ``` -[INFO] REQ-005.11: PEP 723 header write-back succeeded via uv add --script. +[INFO] Building standalone executable -- this may take a minute or two... +[INFO] PyInstaller produced dist\.exe +[DEBUG] warnfix: warn file found +[INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. +[REPAIR] missing modules detected; installing and rebuilding. +[REPAIR] rebuild complete after warnfix. +[INFO] PyInstaller build artifacts cleaned up. +[INFO] EXE smokerun: testing dist\.exe +[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +[INFO] EXE smokerun: exited 0 (ok) +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) ``` -When there is nothing to write (a stdlib-only app, no third-party packages resolved), the -subroutine correctly no-ops rather than writing an empty/misleading header -- also a REAL capture: +followed by the ordinary "SETUP COMPLETE" panel. Note what does NOT appear here: no second +"[INFO] Building standalone executable" banner precedes the repair rebuild -- `[REPAIR] rebuild +complete after warnfix.` covers the whole re-invocation, PyInstaller's own build noise from that +second pass is not separately re-echoed. The failure variant of this same loop (a module that +genuinely can't be installed, e.g. `StringIO`) is already documented in Scenario 17 -- that variant +still reaches `[REPAIR] rebuild complete after warnfix.` (bundling whatever DID install) and is what +actually feeds the provider cascade Scenario 33 above walks through. + +### Scenario 35: Full walkthrough -- `--hidden-import` auto-recovery succeeds on the first rebuild + +**Honesty note:** Scenario 16 documents this mechanism's EXHAUSTION path with a real captured line; +the one-shot SUCCESS path's exact rebuild line has never been independently console-dumped in this +file, only confirmed present via `self.exe.hidden_import`'s own passing NDJSON row (the same +"test passes, so no full log was dumped" situation Scenario 39 already documents for a different +mechanism) and via the exact line format quoted from source at Scenario 39 (a different context -- +Tier A's hidden-import SKIP guard -- but quoting the identical `[REPAIR][HIDDEN_IMPORT]` line +PyInstaller's own recovery path would have printed). This whole panel is therefore +`[Extrapolated Branch]`, built from source, not a stitch of independently-real fragments the way +Scenario 32/33/34 above are. ``` -[INFO] REQ-005.11: PEP 723 write-back skipped (no packages to write). +[INFO] Building standalone executable -- this may take a minute or two... +[INFO] PyInstaller produced dist\.exe +[INFO] PyInstaller build artifacts cleaned up. +[INFO] EXE smokerun: testing dist\.exe +[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +[WARN] EXE smokerun: exited 1 (non-zero) +[REPAIR][HIDDEN_IMPORT] Adding --hidden-import=; rebuilding EXE (iter 1/3) +[INFO] PyInstaller produced dist\.exe +[INFO] EXE smokerun: testing dist\.exe +[INFO] EXE smokerun: exited 0 (ok) +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) ``` -This is `HP_ENV_MODE=uv`-only (v1 scope, see `docs/agent-interconnect.md`) and best-effort/non-gating --- any failure (malformed existing header not cleanly repairable, a file lock, non-UTF-8 source) -logs a `[WARN]` and the bootstrap continues unaffected; `HP_SKIP_PEP723_WRITEBACK=1` suppresses it -outright per REQ-019 (a genuine opt-OUT flag, not a gate). The warnfix-triggered SECOND write-back -call (after a successful repair round) is functionally identical and not separately captured here --- same subroutine, same two possible outcomes, triggered from a different call site. +followed by the ordinary "SETUP COMPLETE" panel. The gate that makes this rebuild worth attempting +at all -- the failure must be a real `ModuleNotFoundError` (not a bare `ImportError`) for a module +that IS installed in the build interpreter, and the EXE must have been built by PyInstaller, not +Tier A's Nuitka fallback -- is covered in full in `docs/agent-lessons-learned.md`'s "--hidden-import +auto-recovery must stay STRICT" entry; Scenario 16 covers the case where three rebuilds still don't +resolve it. ---- +### Scenario 36: Full walkthrough -- `HP_PVW_KNOWN_IDEMPOTENT`, with the actual input and output files -### Scenario 29: `HP_PVW_KNOWN_IDEMPOTENT` execute-mode discovery (REQ-005.13) +Scenario 23 already covers this flag's console output; this walkthrough adds what that scenario +doesn't show: the actual file contents a user would see before and after. Source: the real, +deterministic stub app `tests/selfapps_pvw_idempotent.ps1` stages for `self.pvw_idempotent.discovery` +(`uv` lane, real, passing) -- quoted directly from the test's own source, not a CI console dump, but +these are the literal bytes that test writes and asserts against. -**What's tested:** `self.pvw_idempotent.discovery` (`tests/selfapps_pvw_idempotent.ps1`, `uv` lane, -real and passing). +**Input, `app.py` (the user's only file, before running the bootstrapper at all):** -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708109` (`uv` lane). +```python +import requests +print('t2-idempotent-ok') +``` -This is an opt-in super-user flag (not part of the default happy path -- Part III never sets it): -when defined, `run_setup.bat` skips straight to actually RUNNING the entry file live via -`uvx autopep723 ` for dependency discovery, before pipreqs or any static analysis even -starts. Real capture: +No `requirements.txt`, no `pyproject.toml`, no PEP 723 header -- genuinely nothing else in the +folder besides `run_setup.bat` and this one file (the test isolates pipreqs via `HP_SKIP_PIPREQS=1` +specifically so this is the ONLY way `requests` can end up discovered -- see Scenario 23's own +production-vs-test-isolation note for why that flag is test-only, never how this feature runs for a +real user). + +**Console output** (real, `self.pvw_idempotent.discovery`'s own capture, already fully quoted in +Scenario 23 -- repeated here only for continuity with the file changes below): ``` [INFO] REQ-005.13: HP_PVW_KNOWN_IDEMPOTENT set; running entry via uvx autopep723 for execute-mode discovery. +t2-idempotent-ok [INFO] REQ-005.13: execute-mode discovery run succeeded (RAN:persisted). ``` -The entry script's own stdout is inherited/passed through live during this discovery run (not -captured or suppressed) -- real NDJSON detail confirms `stdoutPassthroughFound:true, appRan:true` --- and whatever dependency it needed (`requests`, in this real capture: `reqsHasRequests:true`) is -persisted back into the PEP 723 header via `uv add --script`, then re-extracted into -`requirements.txt` so the rest of the pipeline (pipreqs, Tier 1 autopep723 merge, the actual -install) sees it too. Deliberately ADDITIVE, not a replacement for pipreqs -- pipreqs and Tier 1's -own `autopep723 check` merge still run normally afterward to catch anything a single execution -path didn't happen to exercise. - ---- +(`t2-idempotent-ok` is the app's own live stdout, inherited straight through the discovery run -- +real NDJSON detail confirms `stdoutPassthroughFound:true`, the exact design point +`tools/pvw_known_idempotent.py` exists to preserve; see `docs/agent-interconnect.md`.) -### Scenario 30: NI-VISA detection and install outcome (REQ-008) +**Output, `app.py` (same file, now carrying a PEP 723 header `uv add --script` wrote in place):** +this is the STANDARD shape `uv add --script` is documented to produce (see +`docs/agent-lessons-learned.md`'s "`uv add --script` / PEP 723 empirical behavior" section) -- +`[Extrapolated Branch]` for the exact formatting shown, since this specific test doesn't assert +byte-for-byte header content, only that the resulting `requirements.txt` (below) ends up containing +`requests`: -**What's tested:** `pyvisa.detect`/`.nivisa.branch`/`.nivisa.outcome`/`.nivisa.reason`/`.nivisa.disabled` -(`tests/selfapps_pyvisa.ps1`, `real`/`conda-full` lanes, all real and passing). +```python +# /// script +# dependencies = [ +# "requests", +# ] +# /// -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real`) and job `90179708094` -(`conda-full`) -- both lanes captured a genuine NI-VISA install ATTEMPT in this run (not just the -"not required" skip), which is a more complete illustration than what an earlier pass in this -document assumed was CI's only available evidence. +import requests +print('t2-idempotent-ok') +``` -When `pyvisa`/`visa` is detected as an import, the bootstrapper attempts a real NI-VISA driver -install (downloads the online bootstrapper installer via curl, PE-validates it, then launches it -under a bounded timeout): +**Output, `requirements.txt` (newly created -- did not exist before this run):** re-extracted from +the header above via the same `:extract_pep723_requirements` subroutine the pre-existing-header case +already uses (see `docs/agent-interconnect.md`'s "HP_PVW_KNOWN_IDEMPOTENT execute-mode discovery" +section for why re-extraction, not the header alone, is what feeds the rest of the pipeline). Real, +directly confirmed: the test's own assertion is `$reqsText -match 'requests'` against this exact +file: ``` -[INFO] Detected pyvisa/visa import; NI-VISA install may be required. -[VISA] download method: curl -[VISA] installer file size: 6769400 bytes -[VISA] installer PE check: PE_OK -[INFO] Launching NI-VISA installer (timeout ceiling: 5400000 ms). -[VISA] installer exit code: -125202 -[VISA] post-check waiting; retry 1/3 (installer_rc=-125202) -[VISA] post-check waiting; retry 2/3 (installer_rc=-125202) -[VISA] install_failed (post_check_timeout) installer_rc=-125202 +requests ``` -This matches CLAUDE.md's already-documented Known Finding ("NI-VISA real install fails fast in CI") -in shape and mechanism exactly -- a genuine, PE-valid installer download that exits fast and -unattended-incompatible on a CI runner -- though the SPECIFIC installer exit code observed here -(`-125202`) differs from that finding's originally-cited `-125083`. Consistent with the finding's -own framing (an online bootstrapper installer failing an unattended install, not a fixed/stable -error code), not a new discrepancy worth a separate backlog entry. The bootstrap proceeds -gracefully regardless -- a failed NI-VISA install is never treated as a bootstrap failure, only -logged and surfaced; the user's own program still builds and runs. +Production behavior (unlike this test's isolated setup) does NOT stop here -- pipreqs and the +Tier 1 `autopep723 check` merge (Scenario 3) both still run normally afterward, additively layering +on top of whatever this execute-mode pass already found, in case anything conditionally-imported +didn't execute during this one discovery run. -Real evidence confirms the OTHER outcome branch too, via a dedicated `HP_SKIP_NIVISA=1` scenario in -the same test file: `[VISA] skipped (not_required)`, with `skippedDisabled:true, -noInstallAttempt:true` -- the flag suppresses the install attempt outright even when pyvisa IS -detected, per REQ-019's suppression-only convention. +## Part VIII: AV-Safe Build Path (Nuitka fallback) ---- +### Scenario 37: PyInstaller build fails, Tier A (Nuitka) fallback succeeds -### Scenario 31: pandas/openpyxl heuristic dependency augmentation (REQ-005.8) +**What's tested:** `self.exe.build.tiera` (`tests/selfapps_nuitka_tiera.ps1`, uv lane, +non-gating). `HP_TEST_FORCE_PYINSTALLER_FAIL=1` forces the primary build to fail deterministically; +the Nuitka fallback (`:try_nuitka_tier_a`) then runs for real -- a genuine compile, not simulated. -**What's tested:** `pandas_excel.translate`/`.conda.install`/`.conda.install.req006`/`.runtime`, -`self.pandas.openpyxl.install`/`.import` (`tests/selfapps_pandas_excel.ps1`, `conda-full` lane, all -real and passing) -- plus a genuinely SEPARATE test that happens to exercise the same heuristic in -a different scratch directory, `self.exe.warnfix.real` (`tests/selftest.ps1`'s `real` scenario, -`conda-full` lane, also real and passing, `desc: "Heuristic pre-installed openpyxl via pandas -heuristic; EXE succeeded"`). +**What appears on screen**, from the moment PyInstaller's build is attempted through to the final +summary -- a mix of real CI capture and lines updated to reflect current source, not a single +uniform capture. Real CI capture (run `29788624195`, job `88506013149`): the +"(fallback build system)" verification line and the drive-message reassurance line, exactly as +captured. Updated to reflect current source (`docs/plan-cli-interactive-verification.md` +requirement 3's activity-aware kill, and REQ-026's argv passthrough, both of which shipped after +this specific run): the "Verifying the built standalone EXE" line and the "Does your program need +launch arguments" paragraph. Every other line below is real capture, unmodified: -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane). +``` +[INFO] Building standalone executable -- this may take a minute or two... +[INFO] (A stray one-line Windows message about a missing drive may appear next -- that is a known side effect from an unrelated background process, unrelated to your app; safe to ignore.) +The system cannot find the drive specified. +The system cannot find the drive specified. +[TEST] HP_TEST_FORCE_PYINSTALLER_FAIL: simulating PyInstaller build failure. +[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). +[INFO] Fallback build succeeded: dist\.exe was produced using the fallback build system. +[DEBUG] warnfix: warn file not found +[INFO] PyInstaller build artifacts cleaned up. +[INFO] EXE smokerun: testing dist\.exe +[INFO] Running entry script smoke test via packaged EXE. +[WARN] Verifying the built standalone EXE (fallback build system) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +[INFO] EXE smokerun: exited 0 (ok) +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) -`~prep_requirements.py` (`HP_PREP_REQUIREMENTS`) applies a small set of heuristic rules that inject -a commonly-needed-but-undeclared package when its "parent" package is present -- pandas's -`pd.read_excel()`/`to_excel()` need `openpyxl`/`xlsxwriter`, but pipreqs' static analysis has no -way to see that a lazily-imported optional engine is actually required at runtime. Real capture -from `tests/~pandas_excel/`'s own scratch directory: +*** Verification finished -- see the Run Status above. *** +*** You can run your program again now via the interpreter as an extra diagnostic check. *** +[INFO] REQ-018: post-execution checkpoint (exe): declined (run footprint stays at one execution). -``` -[HEURISTIC] pandas->xlsxwriter +============================================================ + SETUP COMPLETE +============================================================ + Your standalone application is ready: + dist\.exe + + RUNNING YOUR APP + Double-click dist\.exe to run it. + + STARTUP MAY BE SLOW: a one-file .exe unpacks itself each time it + starts, so allow 10-15 seconds (longer for big libraries like + numpy/scipy/matplotlib, or when extra packages were bundled to fix + missing imports) before assuming it has hung. + + If the window flashes and closes instantly: that's normal if + your program finished quickly or hit an error before printing + anything. To see what happened, open Command Prompt, cd to + this folder, and run: + dist\.exe + This keeps the window open so you can read any messages. + + A progress indicator that updates in place may appear all at + once instead of live when run as the .exe -- that is a stdout + buffering difference between the .exe and the script, not an error. + + Does your program need launch arguments (e.g. --input file.csv)? Run + this bootstrapper again with them added after the entry file, e.g. + run_setup.bat "" --input file.csv + and they will be forwarded to your program during THIS setup run + (up to 8 extra arguments). This does not change how a plain + double-click of dist\.exe launches it afterward -- for that, + make a Windows shortcut to the .exe and add the arguments to its + Target field, or launch it yourself from a Command Prompt. + + KEEP these files with your project: + requirements.txt -- packages your app depends on + runtime.txt -- Python version pin + + SAFE TO DELETE to reclaim disk space: + .*_env\ folders -- environment directories + ~* files -- tilde-prefix work files (e.g. ~setup.log) + build\ -- PyInstaller build cache +============================================================ ``` -(the console line is a compact tag; the two package names themselves are appended to the conda -install spec list, confirmed by the real conda solve plan later in the same `~pandas_excel` log: -`openpyxl conda-forge/win-64::openpyxl-3.1.5-py314hccc76fc_3` and `xlsxwriter -conda-forge/noarch::xlsxwriter-3.2.9-pyhd8ed1ab_0`, and both packages installed into -`~pandas_excel`'s own `requirements.txt`/`~reqs_conda.txt`/`~reqs_pip.txt`). The claim that -`openpyxl` ends up genuinely bundled and importable in a frozen EXE is confirmed by the SIBLING -`self.exe.warnfix.real` test's OWN independent scratch directory (`tests/~selftest_warnfix_real/`, -a different app that also exercises the pandas heuristic, per its own NDJSON `desc` text quoted -above) rather than by `~pandas_excel`'s own PyInstaller warn-file -- its EXE's PyInstaller warn -file shows only expected, harmless optional-dependency lines for the bundled `openpyxl`: `missing -module named PIL - imported by openpyxl.drawing.image (optional)`, not a real gap. This is -`HP_ENV_MODE=conda`-lane-only coverage per this test file's own CI wiring (see -`docs/agent-interconnect.md`'s -"selfapps_pandas_excel.ps1" note) -- the SAME heuristic logic also runs for uv/venv/embed/system -providers via `requirements.txt` write-back (CLAUDE.md's own "Deep research pass" Closed Backlog -entry on this exact fix), just not captured here since this test is conda-only by design. +The "Verifying the built standalone EXE" line now correctly says "(fallback build system)" +instead of a hardcoded "(PyInstaller)" when the EXE being verified was actually Nuitka-built +(`:warn_user_code_launch` branches on `HP_NUITKA_FALLBACK_USED`). The postflight briefing's +"PyInstaller build cache" line is left as-is -- Nuitka never creates a `build\\` folder of +its own (its `--remove-output` flag cleans up its own intermediates), so that line stays literally +true regardless of which tool actually built the current EXE: if a `build\` folder exists, it's +PyInstaller's. See Scenario 42 for the argv-passthrough paragraph's own dedicated writeup. --- -### Scenario 32: Conda base periodic update +### Scenario 38: PyInstaller build fails, Tier A fallback ALSO fails (tier exhaustion) + +**What's tested:** `self.exe.build.xfail` (`tests/selfapps_pyinstaller_fail.ps1`, real/conda-full +lanes, gating). Three sub-scenarios share one NDJSON row id: `execfail` (the PyInstaller build +command itself fails), `output_vanish` (PyInstaller succeeds, then the output EXE vanishes +immediately -- simulating AV-style post-creation removal), and `execfail_runtimefail` (packaging +fails AND the interpreter fallback that runs next ALSO exits non-zero -- see Scenario 43a for that +one's console text, since it's really a REQ-027 demo). The first two additionally force +`HP_TEST_FORCE_NUITKA_FAIL=1` so the fallback also fails, proving genuine tier exhaustion. + +#### 38a. `execfail` -- the PyInstaller build command itself fails + +Real CI capture, run `29788624195`, job `88506013028` ("real" lane): + +``` +[INFO] Building standalone executable -- this may take a minute or two... +[TEST] HP_TEST_FORCE_PYINSTALLER_FAIL: simulating PyInstaller build failure. +[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). +[TEST] HP_TEST_FORCE_NUITKA_FAIL: simulating fallback build failure. +[ERROR] PyInstaller execution failed. +[DEBUG] warnfix: warn file not found +[INFO] PyInstaller build artifacts cleaned up. +[WARN] EXE smokerun: dist\.exe not found; skipping +[INFO] Running entry script smoke test via uv interpreter. +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) + +[INFO] REQ-018: post-execution checkpoint (interpreter): declined (run footprint stays at one execution). +``` -**What's tested:** `self.conda.base.update` (`tests/selfapps_conda_update.ps1`) -- **NOT currently -wired into any CI lane** (per `docs/agent-ndjson.md`'s own explicit note: the `HP_TEST_CONDA_UPDATE` -injection flag was removed because it upgrades conda to a solver version that cascades failures -across the rest of the `conda-full` job). Only the "skipped" branch below has real CI evidence. +When BOTH the PyInstaller build and the Nuitka fallback fail outright but the interpreter +fallback's own run exits 0 (this trivial stub script does), the final line is +`[STATUS] Run Status: SUCCESS (Exit Code: 0)` and the postflight panel is the plain +"YOUR CODE RAN -- BUT NO STANDALONE .EXE WAS PRODUCED" variant -- see Scenario 43a for the +different, honest "we can't confirm" panel this same tier-exhaustion path shows instead when the +interpreter run ALSO fails. -**Source (skipped branch):** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane, -appears identically across every fresh scratch-dir bootstrap in the run). +#### 38b. `output_vanish` -- PyInstaller succeeds, then the EXE disappears immediately -`:conda_base_update` runs `conda update -n base` on a timer (30-day threshold, seeded from -`~conda.lastupdate` on first install) whenever `HP_ENV_MODE=conda`. On a genuinely first-ever -install (the common case in a fresh CI scratch dir, and for most real first-time users), it -correctly skips rather than updating a base that was just installed moments ago: +Real CI capture, same run/job as 2a: ``` -[INFO] Conda base update: skipped (first install). -``` +[INFO] Building standalone executable -- this may take a minute or two... +[TEST] HP_TEST_FORCE_OUTPUT_VANISH: deleting freshly-built EXE to simulate post-creation removal. +[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). +[TEST] HP_TEST_FORCE_NUITKA_FAIL: simulating fallback build failure. +[ERROR] PyInstaller did not produce dist\.exe +[DEBUG] warnfix: warn file found +[INFO] warnfix: some modules could not be automatically bundled (full list in ~warnfile.txt / ~setup.log); modules such as posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, and _frozen_importlib_external are expected on Windows and are filtered out automatically. +[INFO] PyInstaller build artifacts cleaned up. +[WARN] EXE smokerun: dist\.exe not found; skipping +[INFO] Running entry script smoke test via uv interpreter. +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) -**`[Extrapolated Branch]`** -- the actual 30-day-elapsed UPDATE-firing branch (`:cbu_run`, -`run_setup.bat`) is not exercised by any current CI run (the only flag that could force it is -deliberately disabled, per the note above). Traced from source: once the timestamp in -`~conda.lastupdate` is more than 30 days old, the subroutine runs `conda update -n base -y` and -would log something in the shape of `[INFO] Conda base update: running (last updated N days -ago)...` followed by conda's own real update-solve output, then rewrites `~conda.lastupdate` to the -current time on completion. This branch realistically only fires for a long-lived, repeatedly-reused -project folder -- not the fresh-checkout scenarios this document otherwise captures -- and is -correctly out of scope for a dedicated CI test per the reasoning already on record (a forced-update -test previously broke conda's own solver in shared CI runners). No new CLAUDE.md backlog entry -added -- this gap is already fully documented and deliberately accepted in -`docs/agent-ndjson.md`'s "conda-full lane rows" section. +*** Verification finished -- see the Run Status above. *** +*** You can run your program again now via the interpreter as an extra diagnostic check. *** +[INFO] REQ-018: post-execution checkpoint (interpreter): declined (run footprint stays at one execution). +``` --- -### Scenario 33: REQ-014 system-Python consent -- ACCEPT - -**What's tested:** `self.ux.system.gate.accept` (`tests/selfapps_ux_hardening.ps1`, `real` lane, -real and passing). Part VI's Scenario 21 already showed the DECLINE branch of this same gate (as -the terminal step of a full provider-cascade exhaustion); this scenario completes the pair. +### Scenario 39: Tier A + hidden-import auto-recovery skip guard -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). +**What's tested:** `self.exe.tiera.hidden_skip` (`tests/selfapps_nuitka_tiera_hidden_skip.ps1`, uv +lane, non-gating). Forces Tier A to trigger and succeed for real, then has the stub app fabricate +a `ModuleNotFoundError: No module named 'nuitka'` on stderr and exit 1 -- the exact signature that +used to (before this fix) trigger an incorrect PyInstaller rebuild attempt against a Nuitka-built +EXE. -When every other REQ-009 provider tier has failed or been declined, the system-Python tier is -still reached by any default, no-flag run -- it is gated solely by the REQ-014 consent prompt -(Scenario 11 in Part III already documents the prompt's own framing text in full), never by an -env-var the user would need to set. On ACCEPT, the bootstrapper proceeds to use whatever Python is -already on the machine, unmanaged and unisolated: +**Source:** confirmed in real CI run `29877805447`, uv lane, job `88792048278`: ``` -[INFO] REQ-014: System Python consent: user accepted. -[INFO] System fallback using C:\hostedtoolcache\windows\Python\3.12.10\x64\python.exe -[BOOT] REQ-009: Selected Python provider: System Python (degraded). +{"details":{"appStdoutFound":true,"noRepairRebuild":true,"successLogged":true,"skipGuardLogged":true,"exeExists":true,"statusState":"ok","bootstrapExit":0,"smokerunNonzeroLogged":true,"attemptLogged":true,"log":"~nuitka_tiera_hidden_skip_bootstrap.log"},"req":"REQ-AV","pass":true,"desc":"AV-Safe Build Path Tier A: hidden-import auto-recovery correctly skips (never rebuilds via PyInstaller) against a Nuitka-built EXE","id":"self.exe.tiera.hidden_skip","lane":"uv"} ``` -`~bootstrap.status.json` still reports `state` as the degraded-but-successful `degraded_env` value -(real NDJSON detail: `"state":"degraded_env","exitCode":0`) -- accepting this tier is a genuine, -if suboptimal, path to a working run, not a failure. This is the ONLY REQ-009 tier gated by an -explicit human consent prompt rather than an automatic fallback, precisely because it is the one -tier that touches the user's real, shared Python environment instead of a private/disposable one. - ---- - -## Part VIII: Additional branches found in a full-file sweep +The test only dumps a full console log to CI when a scenario fails; since this one passes, the +exact console text below is reconstructed from `run_setup.bat`'s source rather than copied from a +console dump -- the NDJSON row's `skipGuardLogged`/`noRepairRebuild` fields are the test's own +regex-verified confirmation that these exact lines were present/absent in the real captured log: -**Scope note:** after the original 5-pass plan completed, a systematic label-by-label sweep of -every one of `run_setup.bat`'s 164 `:label`s (cross-checked against everything already written in -Parts I-VII) turned up four genuine, user-observable gaps -- three straightforward missing -scenarios, and one previously-undocumented, real bug in the bootstrapper's own error messaging, -found via real CI evidence and confirmed against source before being written up. Everything else -checked in the sweep (roughly 150 of the 164 labels) was either already covered, internal -control-flow plumbing with no independently observable behavior of its own (e.g. `:pfb_runapp`, -`:mgc_gi_done`), or a narrow edge case not worth a dedicated scenario (e.g. `:cascade_consent_no_ -choice_exe`, reached only on a Windows image stripped of `choice.exe`). +``` +[INFO] Building standalone executable -- this may take a minute or two... +[TEST] HP_TEST_FORCE_PYINSTALLER_FAIL: simulating PyInstaller build failure. +[INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). +[INFO] Fallback build succeeded: dist\.exe was produced using the fallback build system. +[INFO] EXE smokerun: testing dist\.exe +[WARN] EXE smokerun: exited 1 (non-zero) +[INFO][HIDDEN_IMPORT] Skipping --hidden-import auto-recovery: dist\.exe was built via the fallback build system (Nuitka), which uses a different missing-import mechanism than PyInstaller's --hidden-import flag. +``` -- [Scenario 34: Interactive entry picker -- multiple `.py` files, no clear winner (REQ-002)](#scenario-34-interactive-entry-picker----multiple-py-files-no-clear-winner-req-002) -- [Scenario 35: Pre-flight syntax-error rejection (REQ-021), and a real bug it exposed](#scenario-35-pre-flight-syntax-error-rejection-req-021-and-a-real-bug-it-exposed) -- [Scenario 36: REQ-007 system-Python build consent, and the resulting no-EXE interpreter path](#scenario-36-req-007-system-python-build-consent-and-the-resulting-no-exe-interpreter-path) -- [Scenario 37: EXE smoke-run diagnostic hints (companion to Scenario 22)](#scenario-37-exe-smoke-run-diagnostic-hints-companion-to-scenario-22) +Without this guard, `run_setup.bat` would instead print +`[REPAIR][HIDDEN_IMPORT] Adding --hidden-import=nuitka; rebuilding EXE (iter 1/3)` here and +attempt a PyInstaller rebuild against a Nuitka-built EXE. --- -### Scenario 34: Interactive entry picker -- multiple `.py` files, no clear winner (REQ-002) - -**What's tested:** `self.entry.picker` (`tests/selfapps_entry_picker.ps1`, `conda-full` lane, real, -passing). +### Scenario 40: Requirement 9 -- elective "want an optimized build too?" offer -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708094` (`conda-full` lane). +**What's tested:** `self.optbuild.offer` (`tests/selfapps_optimized_build.ps1`, uv lane, +non-gating), four scenarios sharing one row id. -Part III's happy path covers the common single-`.py`-file case; this covers REQ-002's OTHER real -end-user scenario: a folder with several `.py` files where none is named `main.py`/`app.py`/ -`run.py`/`cli.py` and none has a substantive `if __name__ == "__main__":` block (`:determine_entry`'s -own priority ladder, in `tools/find_entry.py`, exhausts every tier and falls back to -`find_entry.py`'s own `AMBIGUOUS_RC` (3) alphabetical pick). Only THEN does `:pick_entry_interactive` -show a real, timed menu: +**Source:** confirmed in real CI run `29877805447`, uv lane, job `88792048278`, all four scenarios +passing: ``` -Multiple Python files detected -- no clear entry point, so please choose one to run: - [1] a_app.py - [2] b_app.py - - Tip: to skip this question next time, do any one of these: - 1. Drag a .py file onto run_setup.bat -- drop it on the batch file icon to run - that file directly. It must be in this same folder. - 2. Rename your main script to one of: main.py, app.py, run.py, or cli.py. - 3. Give exactly one script an if __name__ == "__main__": block. - If you do nothing, the alphabetically-first file is used: a_app.py - -Type a number 1-2, or wait 30s for the default [1]: +{"lane":"uv","details":{"log":"~optbuild_accept_bootstrap.log","statusState":"ok","scenario":"accept","successLogged":true,"promptShown":true,"tmpExeGone":true,"exeExists":true,"bootstrapExit":0,"acceptedLogged":true,"appStillRuns":true},"desc":"AV-Safe Build Path requirement 9 (accept): a real optimized build succeeds, verifies, and is swapped into place","req":"REQ-AV","id":"self.optbuild.offer","pass":true} +{"req":"REQ-AV","lane":"uv","desc":"AV-Safe Build Path requirement 9 (forcefail): a failed optimized build leaves the original PyInstaller EXE completely untouched","id":"self.optbuild.offer","details":{"originalStillRuns":true,"bootstrapExit":0,"log":"~optbuild_forcefail_bootstrap.log","tmpExeGone":true,"promptShown":true,"exeExists":true,"testHookFired":true,"scenario":"forcefail","statusState":"ok","noSuccessMsg":true},"pass":true} +{"req":"REQ-AV","id":"self.optbuild.offer","lane":"uv","pass":true,"details":{"bootstrapExit":0,"acceptedLogged":true,"originalStillRuns":true,"log":"~optbuild_swapfail_bootstrap.log","promptShown":true,"exeExists":true,"tmpExeGone":true,"scenario":"swapfail","statusState":"ok","noSuccessMsg":true,"swapFailLogged":true},"desc":"AV-Safe Build Path requirement 9 (swapfail): a verified optimized build whose final swap fails leaves the original PyInstaller EXE completely untouched and cleans up the leftover temp file"} +{"desc":"AV-Safe Build Path requirement 9 (decline): default/CI path shows the prompt but never attempts a build","lane":"uv","details":{"statusState":"ok","noBuildAttempt":true,"tmpExeGone":true,"scenario":"decline","log":"~optbuild_decline_bootstrap.log","bootstrapExit":0,"exeExists":true,"declinedLogged":true,"promptShown":true},"req":"REQ-AV","id":"self.optbuild.offer","pass":true} ``` -(the real capture's own timeout is shrunk to 2s via `HP_TEST_FORCE_PICKER`, the same CI-determinism -technique already used for the timed cascade prompt in Scenario 11/21 -- the default, real-user -window is 30 seconds, per `HP_PICK_T` in source, shown above as written). A real, non-interactive -CI run also can't feed `choice.exe` an actual keystroke, so the captured log shows one extra, -CI-only artifact line right after the prompt (`ERROR: The file is either empty or does not contain -the valid choices.`) before falling through to the timeout default -- a real interactive user typing -a number, or simply waiting, never sees that line. Either way, the resolution is logged: +#### 40a. `accept` -- a real optimized build succeeds and is swapped in + +Real CI capture (`~selftest_optbuild_accept\~optbuild_accept_bootstrap.log`) with one line updated +in place to reflect current source: the "Verifying the built standalone EXE" line now shows +requirement 3's activity-aware kill plus the quit-prompt hint, both of which shipped after this +capture -- every other line below is exactly as captured: ``` -[INFO] REQ-002: Picker entry selected: a_app.py -``` +[INFO] Building standalone executable -- this may take a minute or two... +The system cannot find the drive specified. +The system cannot find the drive specified. +[INFO] PyInstaller produced dist\.exe +[INFO] warnfix: Platform-specific modules in the list above are expected on Windows: posix, fcntl, grp, pwd, resource, _scproxy, _posixsubprocess, collections.abc, _frozen_importlib_external. These will be filtered out automatically. +[INFO] PyInstaller build artifacts cleaned up. +[INFO] EXE smokerun: testing dist\.exe +[INFO] Running entry script smoke test via packaged EXE. +[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. +[INFO] EXE smokerun: exited 0 (ok) +[INFO] Entry smoke exit=0 +[STATUS] Run Status: SUCCESS (Exit Code: 0) -If MORE than 9 candidate files exist, the picker is skipped entirely (no menu can address more than -the `123456789` `choice /C` charset) and the alphabetical pick is kept silently, logged as `[INFO] -REQ-002: candidates exceed picker limit; keeping (alphabetical).` -- `[Extrapolated -Branch]`, cited from `:pick_entry_interactive`, not independently captured in this run (would need a -10th-plus stub `.py` file staged, which no current test does). +*** Verification finished -- see the Run Status above. *** +*** You can run your program again now via the interpreter as an extra diagnostic check. *** +[INFO] REQ-018: post-execution checkpoint (exe): declined (run footprint stays at one execution). ---- +*** Your app is ready. *** +*** Want to build an optimized version too? It takes a bit longer to build right now, *** +*** but it starts up more reliably on Windows and runs faster once it is built. *** +[INFO] Optimized build: accepted; building now (this may take a minute or two). +[INFO] Optimized build succeeded and verified: dist\.exe now uses the fallback build system. +``` -### Scenario 35: Pre-flight syntax-error rejection (REQ-021), and a real bug it exposed +The "warnfix: Platform-specific modules..." line above is this specific capture's own pre-existing +wording (not updated, unlike the line noted above); current wording for that same line is shown in +Scenario 34. -**What's tested:** `self.preflight.syntax` (`tests/selfapps_preflight.ps1`, `real` lane, real, -passing) for the ordinary case -- its `$pass` gate enforces the REQ-021 message firing, the real -`SyntaxError` detail appearing, `state: error`, and no PyInstaller-build-crash text; "no EXE was -produced" is separately computed and recorded (`noExe` in the row's own `details`, confirmed -`true` in the real capture below) but is NOT itself part of the pass/fail gate. A SEPARATE, -unrelated real capture (`self.embed.fallback.decline`, `tests/selfapps_ux_hardening.ps1`) -accidentally also reaches this code path under total provider exhaustion, which is what exposed -the bug documented below. +**The interactive `Build the optimized version now? [Y/N]` prompt line is echoed unconditionally +by design** (same pattern as `:run_postexec_checkpoint`), but does not appear literally in any +CI capture -- CI answers via the `HP_TEST_OPTBUILD_ANSWER` env-var override, not the interactive +`set /p` path, so only the resolution lines (`accepted`/`declined`) show up in these logs. This is +expected (CI is non-interactive by design), not a gap. -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). +#### 40b. `forcefail` -- accepted, but the build fails; original EXE is left untouched -**The ordinary case**: before ever attempting a doomed PyInstaller build, `:preflight_compile` -byte-compiles the entry file with the SAME parser the interpreter itself uses (`py_compile`, -zero false positives), reporting a genuine `SyntaxError` clearly and stopping before the build: +Real, verbatim console dump (`~selftest_optbuild_forcefail\~optbuild_forcefail_bootstrap.log`): ``` -*** [ERROR] REQ-021: Your Python program has a syntax error and cannot run. *** -*** File: "app.py" *** +*** Your app is ready. *** +*** Want to build an optimized version too? It takes a bit longer to build right now, *** +*** but it starts up more reliably on Windows and runs faster once it is built. *** +[INFO] Optimized build: accepted; building now (this may take a minute or two). +[TEST] HP_TEST_FORCE_OPTBUILD_FAIL: simulating optimized-build failure. +[INFO] REQ-016: Post-flight briefing printed. ``` -followed by the real Python compiler's own traceback (captured to `~preflight.err.txt`, echoed to -both console and log) and a closing `*** Fix the syntax error shown above, then run this batch -again. ***`. `~bootstrap.status.json` correctly reads `state: error`; the dependency install and -environment creation that ran BEFORE this check are left intact (nothing is torn down), so a fixed -file on the next run reuses them. +No further message prints between the forced-fail log line and the (unrelated, always-present) +post-flight briefing -- the subroutine cleans up the temp file and returns silently. This is a +narrower silence than the wording used on a REAL build-failure branch (which explicitly says +"your app is still ready to use as-is" -- see the reactive hint below); a forced-test-hook failure +and a genuine build failure currently give the user different amounts of reassurance for what is, +from their perspective, the same outcome. -**A real bug, found via this exact sweep, confirmed against a real, unrelated capture (not -fabricated for this scenario), and FIXED 2026-08-01 (formerly CLAUDE.md Active Backlog item 14, -now in `docs/agent-closed-backlog.md`).** The capture below is historical, from before the fix, -kept unedited as a real, timestamped log. `:preflight_compile` used to invoke `"%HP_PY%" -m -py_compile "%HP_ENTRY%"` with no check that `HP_PY` was actually a valid, non-empty interpreter -path first. On TOTAL REQ-009 provider-tier exhaustion (every tier fails or is declined), -`:after_env_mode_selection`'s own `HP_PY`-resolved guard -(`call :die "[ERROR] Active Python interpreter not resolved."`) does NOT actually halt the -pipeline -- per this repo's own long-documented `:die` semantics, `exit /b` inside `:die` only -returns from `:die`'s own call frame, so execution fell through and continued for another ~15 log -lines with an EMPTY `HP_PY`, all the way to `:preflight_compile`. There, `"" -m py_compile -"app.py"` was not a Python invocation at all -- it was cmd.exe trying to execute a program -literally named `""`, which produces a CMD.EXE ERROR, not a Python traceback. Because -`:preflight_compile` treated ANY nonzero exit as "syntax error," it reported this as if the user's -own code were broken. Real capture, from a test that deliberately force-fails uv (offline), conda, -the embed tier, and venv, and declines the REQ-014 system-Python prompt -(`tests/~selftest_embed_decline/`'s own sub-bootstrap): +#### 40c. `swapfail` -- verified build, but the final swap step fails; original EXE is left untouched + +Regression test for a real bug: the swap-verification check used to test the DESTINATION file +(which already exists before the move, success or failure alike) instead of the SOURCE (which +should be gone only on success) -- a genuinely failed swap would have been silently misreported +as success. Fixed; console text (expected from source, not yet dumped in a CI console capture +since this scenario has passed on every run so far): ``` -[ERROR] Active Python interpreter not resolved. -Interpreter: -[WARN] Interpreter smoke test failed (continuing). +*** Your app is ready. *** +*** Want to build an optimized version too? ... *** +[INFO] Optimized build: accepted; building now (this may take a minute or two). +[WARN] Optimized build verified successfully but could not be swapped into place; your app is still ready to use as-is. ``` -(pipreqs, dependency install, and the pyvisa check all run to completion afterward, effectively as -no-ops against the broken interpreter, before the entry is finally selected and preflight runs) +#### 40d. `decline` -- default/CI path, prompt shown but nothing built -``` -*** [ERROR] REQ-021: Your Python program has a syntax error and cannot run. *** -*** File: "app.py" *** -'""' is not recognized as an internal or external command, -operable program or batch file. +Real, verbatim console dump (`~selftest_optbuild_decline\~optbuild_decline_bootstrap.log`): -*** Fix the syntax error shown above, then run this batch again. *** +``` +*** Your app is ready. *** +*** Want to build an optimized version too? It takes a bit longer to build right now, *** +*** but it starts up more reliably on Windows and runs faster once it is built. *** +[INFO] Optimized build: declined. ``` -That last block was genuinely misleading: `app.py` may have had no syntax problem whatsoever -- the -real cause, printed several screens earlier, was that no Python interpreter was ever found. A real -user was realistically reachable here for genuine (not test-only) reasons: README's own REQ-009 -table already notes that falling through three-plus provider tiers in one run is "almost always one -shared root cause" (no internet, a full disk, or a locked-down managed image), and a user hitting -exactly that plus declining the REQ-014 system-Python consent prompt would reach this identical -path. The status FILE was never affected (`state: error` was always written correctly, since -`:die`'s own state-set already happened before the fall-through) -- only the human-readable console -narrative misdirected a user who read just the last error rather than scrolling back. +#### Reactive-only failure hint (both Tier A and requirement 9's real-build-failure paths) -**Fixed**: `:after_env_mode_selection`'s guard now also sets `HP_NO_INTERPRETER=1` before calling -`:die` (the call-frame-only-return fall-through itself is left as-is -- a deeper refactor of that -mechanism was judged out of scope for this fix). `:preflight_compile` checks this flag first and, -if set, reports the real cause instead of running `py_compile` against an empty interpreter path: +Fires only on a GENUINE Nuitka compiler failure (not the `forcefail` test hook, which bypasses it +entirely). No CI run to date has exercised a real Nuitka compiler failure, so this is sourced from +`run_setup.bat` rather than a console capture: ``` -*** [ERROR] No Python interpreter is available; your program was not run or built. *** -*** This is not a syntax error -- the Python interpreter itself could not be used. *** -*** Either every automatic Python-acquisition method -- uv, conda, a fresh download, *** -*** or a local virtual environment -- failed, usually from no internet connection, a *** -*** full disk, or a locked-down managed machine image -- or a PVW_PYTHON_EXE override *** -*** points at a path that does not run. Scroll up in this window for the specific reason. *** +[WARN] Optimized build did not complete; your app is still ready to use as-is. +[WARN] Hint: if you have Visual Studio 2022 (or newer) with the 'Desktop development with C++' workload installed, this should use it automatically -- no extra setup needed. If not, installing the free Visual Studio Build Tools with that workload can help. ``` -(The message text was later reworded during implementation to avoid a literal `(...)` pair split -across two `echo` lines inside the same parenthesized `if` block -- cmd.exe's block parser counts -parens in echo text too, so a `(` on one line and its `)` on the next silently mis-closed the -block and broke every CI lane reaching this branch in the same run. See -`docs/agent-lessons-learned.md`'s batch-syntax-quirks section and -`docs/agent-closed-backlog.md`'s Item 14 entry for the full trace.) +--- -This also skips the doomed PyInstaller build attempt entirely (`:run_entry_smoke`'s existing -`HP_PREFLIGHT_FAILED` check already short-circuits the build, unchanged by this fix), not just the -misleading message. +## Part IX: CLI interactivity, argv passthrough & honest messaging ---- +Covers `docs/plan-cli-interactive-verification.md` (P0/P1/P2, all shipped): the live-tee +verification redesign that lets an interactive `input()`-driven program's prompts actually reach +the console, the activity-aware 30-second kill, argv passthrough (REQ-026), and the honest +ambiguous-exit messaging panels (REQ-027). -### Scenario 36: REQ-007 system-Python build consent, and the resulting no-EXE interpreter path +### Scenario 41: Interactive verification -- live-tee, activity-aware kill, and the quit-prompt hint -**What's tested:** `self.sysbuild.decline` (`tests/selfapps_sysbuild.ps1`, `real` lane, real, -passing) -- its `$pass` gate enforces that the REQ-007 prompt text appears, the decline is -logged, packaging is skipped with a logged reason, and no EXE exists afterward. The REQ-014 -"use system Python at all" accept step that gets this test INTO system-Python mode in the first -place is the same mechanism Scenario 33's own `self.ux.system.gate.accept` test covers -independently, not re-asserted here. The interpreter-smoke success/status lines quoted below -(`Entry smoke exit=0`, `[STATUS] Run Status: SUCCESS`) are genuinely present in this same real -captured log but are NOT part of this test's own `$pass` gate -- shown here as observed fact from -the real capture, not as something this specific test independently verifies. +**What changed, in one sentence:** before this plan, `:run_exe_smokerun`'s verification launch +force-killed at a hard 30 seconds regardless of output, and captured stdout/stderr only to a file +(never live to the console) -- so a program correctly waiting on its first `input()` prompt looked +identical to a genuinely hung one, and the user watching the window saw nothing until the process +either finished or got killed. -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane), -`tests/~selftest_sysbuild/`'s own sub-bootstrap -- ONE coherent, continuous real capture covering -the full journey below. +**What's tested (the plumbing):** `self.interactive.stdin.roundtrip` +(`tests/selfapps_interactive_stdin.ps1`, uv lane, non-gating) builds a real PyInstaller EXE from a +multi-round `input()`-driven stub app and pipes a scripted answer sequence into `cmd.exe`'s own +stdin, exercising the full `cmd.exe -> :run_exe_smokerun -> ~exe_smokerun.ps1 -> the built EXE` +chain and asserting each answer lands in the right round via ordering checks on the captured log +-- it proves the plumbing doesn't drop or reorder stdin/stdout, not a live human's own typing +timing (which can't be automated). -Scenario 33 showed REQ-014's "use system Python at all?" consent being accepted. That is not the -only consent gate on this tier: once system Python is actually selected as the provider, a SECOND, -independent consent gate (`:system_build_consent_gate`, REQ-007) asks separately about installing -PyInstaller into that same system Python to build a standalone EXE -- distinct from REQ-014, and -reachable only in `HP_ENV_MODE=system` mode (every other provider tier gets no such prompt, since -none of them touch a shared, uncontrolled Python installation): +**The WARN line a user sees right before the verification launch**, current shipped wording +(source: `run_setup.bat`, `:warn_user_code_launch` -- not a console capture, since CI answers +scripted stdin rather than a human watching the window; Scenario 40a above shows this exact line in +situ too, though there it is the one line explicitly edited into that capture to match current +source, not part of that scenario's own real-captured text; Scenario 37 shows the same line's +"(fallback build system)" variant instead of "(PyInstaller)"): ``` -*** The standalone EXE build installs PyInstaller into your system Python. *** -*** This is the same PyInstaller build used for every provider -- not a special path -- and *** -*** its footprint is small and self-contained (it does not pin common libraries), so it is *** -*** unlikely to conflict with your existing packages. *** +[WARN] Verifying the built standalone EXE (PyInstaller) now: if it stays completely silent for about 30 seconds it will be force-stopped, but any output (including a prompt waiting on your input) keeps it running as long as needed. If your program is interactive, try answering its prompts through to its own quit/exit option now so we can confirm it exits cleanly. Either way, do not start real work in it yet or any unsaved work will be lost. ``` -CI auto-declines (same CI-safe pattern as every other consent gate in this file: `HP_TEST_ -SYSBUILD_ANSWER` override checked first, then `HP_CI_LANE` auto-decline, then a real, unbounded -`set /p` for an interactive user). On decline, dependency install and environment setup are left -completely intact -- only the PyInstaller packaging step is skipped: +Three things this one line is doing: +1. **States the actual kill rule truthfully**: the 30-second cap is a classification checkpoint, + not an unconditional deadline -- `Kill()` only fires if the process has stayed COMPLETELY + silent that whole time. Any output (including a bare prompt with no trailing newline, the exact + shape of Python's own `input("...")`) switches to an unbounded wait. +2. **Actively guides the user toward a clean result**: driving an interactive program to its own + quit/exit option during this pass turns an otherwise-ambiguous exit into a genuine, confirmed + `[STATUS] Run Status: SUCCESS` -- this directly reduces how often a real user ever sees either + of Scenario 43's ambiguous-exit panels. +3. **Still warns it's a throwaway pass, not the user's real, saveable session** -- this verification + EXE is never reused; only the file it's already tested is kept for later double-clicks. + +The `hidden_import` recovery loop's own separate, narrower verification check (see +`docs/agent-interconnect.md`'s "Activity-aware EXE-smoke kill" section) deliberately keeps the +OLDER, unconditional 30-second wording -- it's a bounded repair-verification check on an +already-built EXE, not the user's primary run, so it never got the interactive-friendly rewrite. + +### Scenario 42: Argv passthrough (REQ-026) -- launch arguments through the bootstrapper + +Extra arguments after the entry file on `run_setup.bat`'s own command line (up to 8) are forwarded +verbatim to the target program at every real launch site -- the cached-EXE fast path, the fresh EXE +verification, the no-EXE interpreter run, and the post-execution checkpoint's elective second run. +This is a documented, opt-in escape hatch (no detection or heuristics involved) for a program that +needs `--flag value`-style launch arguments to run correctly, on top of this bootstrapper's usual +zero-argument double-click flow. + +**The postflight guidance a user sees after a successful EXE build** (Scenario 37's full panel +above shows this in context): ``` -[INFO] REQ-007: system-Python EXE build consent: declined. -[INFO] REQ-007: system-Python EXE build not consented; skipping PyInstaller packaging. The environment and dependencies are installed; run the app directly via the prepared Python. + Does your program need launch arguments (e.g. --input file.csv)? Run + this bootstrapper again with them added after the entry file, e.g. + run_setup.bat "" --input file.csv + and they will be forwarded to your program during THIS setup run + (up to 8 extra arguments). This does not change how a plain + double-click of dist\.exe launches it afterward -- for that, + make a Windows shortcut to the .exe and add the arguments to its + Target field, or launch it yourself from a Command Prompt. ``` -With no EXE ever attempted, `:verify_no_exe_interpreter` becomes the sole verification run (this is -also the general no-EXE path reached whenever `dist\.exe` doesn't exist for any reason -- a -declined build here, or PyInstaller being entirely unavailable elsewhere -- not specific to this -consent gate): +**The equivalent guidance on the no-EXE path** (direct-interpreter-invocation form, part of +Scenario 43a's panel below): ``` -[INFO] Running entry script smoke test via system interpreter. -[INFO] Entry smoke exit=0 -[STATUS] Run Status: SUCCESS (Exit Code: 0) -[INFO] REQ-018: post-execution checkpoint (interpreter): declined (run footprint stays at one execution). + Need launch arguments? Add them directly after that command, e.g. + "" "" --input file.csv ``` -The postflight briefing that follows is `:print_no_exe_briefing` (Part VII/CLAUDE.md's REQ-027 P2 -work already documents its honest-messaging design and its `HP_NOEXE_VERIFY_FAILED`-gated caveat -variant) rather than the EXE-focused `:print_postflight_briefing` -- distinct panels for a -genuinely distinct outcome (no packaged deliverable exists, but the app runs fine via the prepared -interpreter). - ---- +Both are additive to the launch commands already shown in each panel, not a separate prompt -- +matching this bootstrapper's general rule that env-var/CLI flags only ever add an opt-in path or +suppress an optional step, never gate a behavior the Prime Directive needs (see CLAUDE.md's +`[REQ-019]`). -### Scenario 37: EXE smoke-run diagnostic hints (companion to Scenario 22) +### Scenario 43: Honest ambiguous-exit messaging (REQ-027) -**What's tested:** the `[HINT]` mechanism fires as a byproduct of `selfapps_exedata_fail.ps1` -(`DATA_FILE` hint) and `selfapps_exedyn_fail.ps1`/`selfapps_hidden_import_exhaust.ps1`-family tests -(`HIDDEN_IMPORT` hint), both real/conda-full lanes, real, passing (the hint lines are a bonus -diagnostic these tests emit, not the tests' own primary assertion target). +Both panels below fire only when a verification run ends AMBIGUOUSLY -- the program exited with an +error, and no automatic repair (`--hidden-import` auto-recovery, the REQ-009 dependency-resolution +cascade) fixed it. Neither panel claims to know WHY: a bug in the program's own code, something +this bootstrapper missed, or an unresolved dependency are all indistinguishable from here, and both +panels say so plainly rather than guessing. This is messaging only -- `~bootstrap.status.json` +semantics, the process exit code, and consent-gate behavior are all unchanged. -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). +Both are new enough (shipped, then refined once more for wording, entirely within this same +session) that no CI run has yet produced a console capture including the current wording -- both +quotes below are sourced directly from `run_setup.bat`, not a job log. -Scenario 22 covers `--hidden-import` auto-recovery reaching its 3-attempt cap without resolving. In -that same failure family -- any EXE that still fails at runtime AFTER hidden-import recovery has -been tried (or was never applicable, e.g. a genuinely missing DATA file, not a missing module) -- -`:exe_smokerun_hints` re-runs the EXE briefly (no timeout needed; these failures exit immediately) -purely to pattern-match its stderr and offer a targeted, actionable hint. Two real captures, from -two different xfail scenarios: +#### 43a. No-EXE path, interpreter also failed -**A missing bundled data file** (real capture, `FileNotFoundError`): +Fires when BOTH PyInstaller and the Nuitka fallback fail to package the app outright, AND the +interpreter fallback that runs next (the only way left to run the program at all) also exits +non-zero -- the scenario Scenario 38a's own `execfail` sub-case would hit if its trivial stub script +didn't happen to exit cleanly. Source: `:print_no_exe_briefing`'s `:noexe_caveat` branch, +`run_setup.bat`: ``` -[HINT][DATA_FILE] Missing data file detected: C:\Users\RUNNER~1\AppData\Local\Temp\_MEI41642\mypkg\data\info.txt -[HINT][DATA_FILE] Consider adding: --add-data C:\Users\RUNNER~1\AppData\Local\Temp\_MEI41642\mypkg\data\info.txt;. -[HINT][RUNTIME_MISMATCH] Standalone EXE behavior differs from the Python runtime (possible PyInstaller packaging issue in the EXE, not your environment or dependencies) -``` +============================================================ + NO STANDALONE .EXE -- AND WE CAN'T CONFIRM YOUR CODE RAN CLEANLY +============================================================ + We could not package your app into a double-clickable .exe + (see the ERROR message above for why). We also just ran it + directly via the prepared Python environment, and it exited + with an error (see the [STATUS] line above) -- so we can't + tell whether that's a bug in the Python code we tried to run + or something this bootstrapper missed. Your environment and + dependencies ARE still installed correctly; run it yourself + below to see the full output. -**A missing module hidden-import recovery couldn't resolve** (real capture, `ModuleNotFoundError`): + RUNNING YOUR APP (without an .exe) -- the most direct option + "" "" + Need launch arguments? Add them directly after that command, e.g. + "" "" --input file.csv -``` -[HINT][HIDDEN_IMPORT] Hidden import likely missing: absent_dynmod_xyz -[HINT][HIDDEN_IMPORT] Consider adding: --hidden-import=absent_dynmod_xyz -[HINT][RUNTIME_MISMATCH] Standalone EXE behavior differs from the Python runtime (possible PyInstaller packaging issue in the EXE, not your environment or dependencies) -``` + Want to try different arguments through the bootstrapper itself + instead? Your already-installed environment is reused either + way; it will just attempt the .exe build again too: + run_setup.bat "" arg1 arg2 -Both hint types are logged via `:log` (console AND `~setup.log`), so a real user hitting either -failure sees them directly -- not buried in a diagnostic-only file. The `RUNTIME_MISMATCH` hint is -a universal closing line, not specific to the `DATA_FILE` branch -- both real captures above show -it: every path through `:exe_smokerun_hints` (data-file match, module-not-found match, or neither) -falls straight through to it with no `goto`/`exit` skipping it in between, so it always fires -alongside whichever more specific hint (if any) matched, as a general reminder that a frozen EXE's -behavior can differ from the interpreter's for packaging reasons unrelated to environment or -dependencies. A separate, optional machine-readable form exists too (`HINT_JSON=1`, an -undocumented-in-README super-user flag that additionally prints each hint as a compact JSON object -via PowerShell) -- not independently captured in this run. + KEEP these files with your project: + requirements.txt -- packages your app depends on + runtime.txt -- Python version pin ---- + SAFE TO DELETE to reclaim disk space: + .*_env\ folders -- environment directories + ~* files -- tilde-prefix work files (e.g. ~setup.log) +============================================================ +``` -### Scenario 38: No `.py` files at all -- the graceful `no_python_files` exit +The direct-run command stays the visually primary option (matches this panel's own established +preference for running the program directly over going back through the bootstrapper); the +bootstrapper-rerun mention is deliberately secondary and uses the real entry filename (`%HP_ENTRY%` +is reliably set by this point in the pipeline -- unlike Scenario 43b below). -**What's tested:** `self.empty_repo.msg` (`tests/selftest.ps1`, `real` lane, real, passing). +When the interpreter run instead exits CLEANLY (the common case, and what Scenario 38a's own capture +shows), this panel's header and opening paragraph read differently -- plain "YOUR CODE RAN -- BUT +NO STANDALONE .EXE WAS PRODUCED", with no claim of an unconfirmed run -- but the rest of the panel +(launch commands, KEEP/SAFE TO DELETE lists) is identical either way. -**Source:** REAL CI CAPTURE, run `30328748330`, job `90179708091` (`real` lane). +#### 43b. Cached-EXE fast path, kept despite a non-zero exit -Referenced throughout this document (e.g. Scenario 9's note that this repo's own bootstrapper -root, which has no loose `.py` files, exercises this exact path) but never shown directly: when -`PYCOUNT` (a plain `dir /b /a-d *.py` count) is zero, the bootstrapper takes the shortest path in -the entire file -- no provider selection, no dependency install, nothing network-touching at all, -skipping straight to a graceful, successful exit: +Fires when the fail-fast probe classifies a REUSED `dist\.exe` (the top-of-file fast path, +before any provider/entry-file logic runs) as alive/healthy -- so it's kept, never +discarded-and-rebuilt -- and it later exits non-zero. Before this fix, this exact case had no +postflight signal at all beyond one `[WARN]` log line buried among other console output. Source: +`:print_fastpath_ambiguous_note`, `run_setup.bat`: ``` -[INFO] Environment name: _selftest_empty -[INFO] Host OS: Microsoft Windows [Version 10.0.26100.32995] -[INFO] Host PowerShell: 5.1.26100.32995 -[INFO] Python file count: 0 -Python file count: 0 -No Python files detected; skipping environment bootstrap. -[INFO] No Python files detected; skipping environment bootstrap. +============================================================ + SETUP COMPLETE -- BUT WE CAN'T CONFIRM YOUR LAST RUN WORKED +============================================================ + Your existing standalone application was reused (dist\.exe), + and it exited with an error just now (see the [STATUS] line + above) -- so we can't tell whether that's a bug in the Python + code we tried to run, or something else. Your environment and + dependencies ARE still installed correctly. + + RUNNING YOUR APP + Double-click dist\.exe to run it, or run it from a + Command Prompt to see the full output. + + WANT TO TRY AGAIN? You do not have to start over from scratch -- + just run this bootstrapper again the same way you did before; + your already-installed environment and built .exe are reused. + + WANT A FRESH BUILD instead (re-checks all dependencies from scratch)? + Delete dist\.exe and run this bootstrapper again. +============================================================ ``` -(the last message genuinely appears twice, back to back, in the real captured log -- once as a -plain `echo` straight to console with no timestamp, once through `:log`'s own timestamped form -written to both console and `~setup.log`; the block above shows both, with the second line's -real timestamp prefix, e.g. `Tue 07/28/2026 4:29:15.97`, omitted here since it carries no -information beyond confirming the two lines are adjacent). `~bootstrap.status.json` reads -`{"state":"no_python_files","exitCode":0, -"pyFiles":0}` -- a real user who double-clicks the bootstrapper in an empty folder, or in the -wrong folder entirely, gets a clear, immediate, non-alarming message rather than the bootstrapper -attempting (and inevitably failing) to build an environment for nothing. +This panel is a PLAIN INFORMATIONAL PRINT, never a consent gate -- the cached-EXE fast path is +deliberately zero-friction for prompts (see `docs/agent-interconnect.md`'s "Fast path = ZERO +friction" design requirement), and this doesn't violate that since it never asks a question. + +**No entry filename appears anywhere in this panel, unlike 7a's rerun mention -- deliberately.** +`HP_ENTRY` is not set yet at the point the top-of-file fast path runs (it fires before +`:determine_entry` ever executes, since the cached EXE is self-contained and doesn't need the +original source filename to relaunch), so naming one here would show blank or stale text. The two +rerun options are worded to distinguish a genuinely different tradeoff instead: rerunning WITHOUT +deleting the EXE reuses it (via the same fast path that got the user here) with no promise about +whether it's actually faster overall, while deleting it first forces a full, slower, from-scratch +dependency check. + +--- diff --git a/run_setup.bat b/run_setup.bat index 1dec41f6..2fee7ddb 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -2642,6 +2642,11 @@ for /f "usebackq delims=" %%F in ("~entry.menu") do set /a HP_PICK_N+=1 if %HP_PICK_N% LSS 2 ( del "~entry.menu" >nul 2>&1 & exit /b 0 ) if %HP_PICK_N% GTR 9 ( call :log "[INFO] REQ-002: %HP_PICK_N% candidates exceed picker limit; keeping %HP_ENTRY% (alphabetical)." + echo Tip: to avoid the alphabetical fallback next time, do any one of these: + echo 1. Drag a .py file onto run_setup.bat -- drop it on the batch file icon to run + echo that file directly. It must be in this same folder. + echo 2. Rename your main script to one of: main.py, app.py, run.py, or cli.py. + echo 3. Give exactly one script an if __name__ == "__main__": block. del "~entry.menu" >nul 2>&1 exit /b 0 ) @@ -4152,7 +4157,7 @@ set "HP_EMBED_EXTRACT=IyBSRVEtMDA5IFRpZXIgNTogdmVyaWZpZXMgY2hlY2tzdW0sIGV4dHJhY3 set "HP_EMBED_PYVER_CHECK=IyBSRVEtMDA5IFRpZXIgNSwgUHl0aG9uIHN0YWdlOiBydW5zIHVuZGVyIHRoZSAiYWx3YXlzIGxhdGVzdCIgaW50ZXJwcmV0ZXIgfmVtYmVkX2V4dHJhY3QucHMxCiMgKFBvd2VyU2hlbGwgc3RhZ2UpIGFscmVhZHkgZG93bmxvYWRlZC92ZXJpZmllZC9leHRyYWN0ZWQuIFRoaXMgaXMgdGhlIE9OTFkgcGxhY2UgcGVyLXJlcXVlc3QKIyB2ZXJzaW9uIGxvZ2ljIGxpdmVzIC0tIGRlbGliZXJhdGVseSBQeXRob24sIG5vdCBQb3dlclNoZWxsLCByZXVzaW5nIHRoaXMgY29kZWJhc2UncyBwcm92ZW4KIyB2ZXJzaW9uLWRldGVjdGlvbiBwYXR0ZXJuIGluc3RlYWQgb2YgcmUtZGVyaXZpbmcgaXQgaW4gUG93ZXJTaGVsbC4gRnVsbCByYXRpb25hbGU6CiMgZG9jcy9hZ2VudC1pbnRlcmNvbm5lY3QubWQgIlN0YW5kYWxvbmUgUHl0aG9uLWRvd25sb2FkIHRpZXIiLiAiMy4xNCIgZW50cnkgYmVsb3cgTVVTVCBtYXRjaAojIEhQX0VNQkVEX0xBVEVTVF9QQVRDSC9IUF9FTUJFRF9MQVRFU1RfU0hBMjU2IGluIHJ1bl9zZXR1cC5iYXQgLS0gYSBQYXlsb2FkU3luYy1zdHlsZSB1bml0IHRlc3QKIyBhc3NlcnRzIHRoaXMuIExhc3QgcmVmcmVzaGVkOiAyMDI2LTA3LTA5LgppbXBvcnQgaGFzaGxpYgppbXBvcnQgb3MKaW1wb3J0IHJlCmltcG9ydCBzaHV0aWwKaW1wb3J0IHNvY2tldAppbXBvcnQgc3lzCmltcG9ydCB1cmxsaWIucmVxdWVzdAppbXBvcnQgemlwZmlsZQoKIyBkZXJpdmVkIHJlcXVpcmVtZW50OiB1cmxsaWIucmVxdWVzdC51cmxyZXRyaWV2ZSBoYXMgbm8gdGltZW91dD0gcGFyYW1ldGVyICh2ZXJpZmllZCB2aWEKIyBpbnNwZWN0LnNpZ25hdHVyZSAtLSBwYXNzaW5nIG9uZSByYWlzZXMgVHlwZUVycm9yKSwgc28gYSBzdGFsbGVkIChub3QgcmVmdXNlZCkgY29ubmVjdGlvbgojIGR1cmluZyBkb3dubG9hZF9hbmRfdmVyaWZ5KCkgd291bGQgb3RoZXJ3aXNlIGhhbmcgdGhpcyBvbmUtc2hvdCBzY3JpcHQgZm9yZXZlci4gQSBnbG9iYWwKIyBkZWZhdWx0IHRpbWVvdXQgaXMgc2FmZSBoZXJlIHNpbmNlIHRoZSB3aG9sZSBzY3JpcHQgZXhpdHMgaW1tZWRpYXRlbHkgYWZ0ZXIgdXNlIC0tIG5vdGhpbmcKIyBlbHNlIGluIHRoaXMgc2hvcnQtbGl2ZWQgcHJvY2VzcyBpcyBhZmZlY3RlZC4gTWlycm9ycyB0aGUgY3VybCAtLW1heC10aW1lIDEyMCBhbHJlYWR5IHVzZWQKIyBmb3IgdGhlIFBvd2VyU2hlbGwtc3RhZ2UgZG93bmxvYWQgb2YgdGhlIHNhbWUgemlwIGZhbWlseS4Kc29ja2V0LnNldGRlZmF1bHR0aW1lb3V0KDEyMCkKCiMgbWlub3IgLT4gKHBhdGNoLCBzaGEyNTYpCkVNQkVEX1BZVEhPTl9UQUJMRSA9IHsKICAgICIzLjEwIjogKCIzLjEwLjExIiwgIjYwODYxOWY4NjE5MDc1NjI5YzljNjlmMzYxMzUyYTBkYTZlZDdlNjJmODNhMGUxOWM2M2UwZWEzMmViNzYyOWQiKSwKICAgICIzLjExIjogKCIzLjExLjkiLCAiMDA5ZDZiZjdlM2IyZGRjYTNkNzg0ZmEwOWY5MGZlNTQzMzZkNWI2MGYwZTBmMzA1YzM3ZjQwMGJmODNjZmQzYiIpLAogICAgIjMuMTIiOiAoIjMuMTIuMTAiLCAiNGFjYmVkNmRkMWM3NDRiMDM3NmUzYjFjZjU3Y2U5MDZmOWRjOWU5NWU2ODgyNDU4NGM4MDk5YTYzMDI1YTNjMyIpLAogICAgIjMuMTMiOiAoIjMuMTMuMTQiLCAiOTBiNGU1Yjk4OThiNzJkNzQ0NjUwNTI0YmZmOTIzNzdjMzY3ZjQ0YmQ1ZmJkMDllMzE0ODY1NmMwODBhZDkwNyIpLAogICAgIjMuMTQiOiAoIjMuMTQuNiIsICJkZjkwMWU4NGE4OTZmZjFlZTcyMGFkMDMzNzdlMGM4ZDhjMjI0NGZkYTc5ODA4YWVlYWZmNjMxNmRmMWNiNzVjIiksCn0KTEFURVNUX01JTk9SID0gIjMuMTQiCkZMT09SX01JTk9SID0gIjMuMTAiCgpTUEVDX01JTk9SX1JFID0gcmUuY29tcGlsZShyIihbMC05XStcLlswLTldKykiKQoKCmRlZiBfbWlub3Jfa2V5KG1pbm9yKToKICAgIHRyeToKICAgICAgICBtYWpvciwgc3ViID0gbWlub3Iuc3BsaXQoIi4iKQogICAgICAgIHJldHVybiAoaW50KG1ham9yKSwgaW50KHN1YikpCiAgICBleGNlcHQgKFZhbHVlRXJyb3IsIEF0dHJpYnV0ZUVycm9yKToKICAgICAgICByZXR1cm4gKDAsIDApCgoKZGVmIHJlc29sdmVfcmVxdWVzdGVkX21pbm9yKHB5c3BlYyk6CiAgICAjIEV4dHJhY3RzICJYLlkiIGZyb20gYSBQWVNQRUMgc3RyaW5nIChlLmcuICJweXRob24+PTMuMTAsPDQuMCIpOyBOb25lIGlmIGVtcHR5L3VucGFyc2VhYmxlLgogICAgaWYgbm90IHB5c3BlYzoKICAgICAgICByZXR1cm4gTm9uZQogICAgbWF0Y2ggPSBTUEVDX01JTk9SX1JFLnNlYXJjaChweXNwZWMpCiAgICByZXR1cm4gbWF0Y2guZ3JvdXAoMSkgaWYgbWF0Y2ggZWxzZSBOb25lCgoKZGVmIHJlc29sdmVfdGFibGVfZW50cnkocmVxdWVzdGVkX21pbm9yKToKICAgICMgUmV0dXJucyAobWlub3IsIHBhdGNoLCBzaGEyNTYsIGZlbGxfYmFjayk7IG1pcnJvcnMgdGhlIFBvd2VyU2hlbGwgc3RhZ2UncyBvd24gcnVsZXMuCiAgICBpZiByZXF1ZXN0ZWRfbWlub3IgaW4gRU1CRURfUFlUSE9OX1RBQkxFOgogICAgICAgIHBhdGNoLCBzaGEyNTYgPSBFTUJFRF9QWVRIT05fVEFCTEVbcmVxdWVzdGVkX21pbm9yXQogICAgICAgIHJldHVybiByZXF1ZXN0ZWRfbWlub3IsIHBhdGNoLCBzaGEyNTYsIEZhbHNlCiAgICBtaW5vciA9IEZMT09SX01JTk9SIGlmIF9taW5vcl9rZXkocmVxdWVzdGVkX21pbm9yKSA8IF9taW5vcl9rZXkoRkxPT1JfTUlOT1IpIGVsc2UgTEFURVNUX01JTk9SCiAgICBwYXRjaCwgc2hhMjU2ID0gRU1CRURfUFlUSE9OX1RBQkxFW21pbm9yXQogICAgcmV0dXJuIG1pbm9yLCBwYXRjaCwgc2hhMjU2LCBUcnVlCgoKZGVmIGRvd25sb2FkX2FuZF92ZXJpZnkodXJsLCBleHBlY3RlZF9zaGEyNTYsIGRlc3RfemlwKToKICAgIHVybGxpYi5yZXF1ZXN0LnVybHJldHJpZXZlKHVybCwgZGVzdF96aXApCiAgICBkaWdlc3QgPSBoYXNobGliLnNoYTI1NigpCiAgICB3aXRoIG9wZW4oZGVzdF96aXAsICJyYiIpIGFzIGZoOgogICAgICAgIGZvciBjaHVuayBpbiBpdGVyKGxhbWJkYTogZmgucmVhZCgxIDw8IDIwKSwgYiIiKToKICAgICAgICAgICAgZGlnZXN0LnVwZGF0ZShjaHVuaykKICAgIGFjdHVhbCA9IGRpZ2VzdC5oZXhkaWdlc3QoKS5sb3dlcigpCiAgICBpZiBhY3R1YWwgIT0gZXhwZWN0ZWRfc2hhMjU2Lmxvd2VyKCk6CiAgICAgICAgb3MucmVtb3ZlKGRlc3RfemlwKQogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImNoZWNrc3VtIG1pc21hdGNoOiBleHBlY3RlZCB7fSwgZ290IHt9Ii5mb3JtYXQoZXhwZWN0ZWRfc2hhMjU2LCBhY3R1YWwpKQoKCmRlZiBleHRyYWN0X2FuZF9wYXRjaCh6aXBfcGF0aCwgZGVzdF9kaXIpOgogICAgaWYgb3MucGF0aC5pc2RpcihkZXN0X2Rpcik6CiAgICAgICAgc2h1dGlsLnJtdHJlZShkZXN0X2RpcikKICAgIHdpdGggemlwZmlsZS5aaXBGaWxlKHppcF9wYXRoKSBhcyB6ZjoKICAgICAgICB6Zi5leHRyYWN0YWxsKGRlc3RfZGlyKQogICAgcHRoX2ZpbGVzID0gW2YgZm9yIGYgaW4gb3MubGlzdGRpcihkZXN0X2RpcikgaWYgcmUubWF0Y2gociJecHl0aG9uXGQrXC5fcHRoJCIsIGYpXQogICAgaWYgbm90IHB0aF9maWxlczoKICAgICAgICByYWlzZSBGaWxlTm90Rm91bmRFcnJvcigibm8gcHl0aG9uKi5fcHRoIGZpbGUgZm91bmQgYWZ0ZXIgZXh0cmFjdGlvbiIpCiAgICBwdGhfcGF0aCA9IG9zLnBhdGguam9pbihkZXN0X2RpciwgcHRoX2ZpbGVzWzBdKQogICAgd2l0aCBvcGVuKHB0aF9wYXRoLCAiciIsIGVuY29kaW5nPSJhc2NpaSIpIGFzIGZoOgogICAgICAgIGNvbnRlbnQgPSBmaC5yZWFkKCkKICAgIGNvbnRlbnQgPSByZS5zdWIociIoP20pXiNpbXBvcnQgc2l0ZSQiLCAiaW1wb3J0IHNpdGUiLCBjb250ZW50KQogICAgd2l0aCBvcGVuKHB0aF9wYXRoLCAidyIsIGVuY29kaW5nPSJhc2NpaSIsIG5ld2xpbmU9IiIpIGFzIGZoOgogICAgICAgIGZoLndyaXRlKGNvbnRlbnQpCiAgICBweV9leGUgPSBvcy5wYXRoLmpvaW4oZGVzdF9kaXIsICJweXRob24uZXhlIikKICAgIGlmIG5vdCBvcy5wYXRoLmlzZmlsZShweV9leGUpOgogICAgICAgIHJhaXNlIEZpbGVOb3RGb3VuZEVycm9yKCJweXRob24uZXhlIG1pc3NpbmcgYWZ0ZXIgZXh0cmFjdGlvbiIpCiAgICByZXR1cm4gcHlfZXhlCgoKZGVmIG1haW4oKToKICAgICMgZGVzdF9kaXIgaXMgd2hlcmUgVEhJUyBydW5uaW5nIGludGVycHJldGVyIGxpdmVzOyBXaW5kb3dzIHdvbid0IGxldCBhIHByb2Nlc3MgcmVwbGFjZSBpdHMKICAgICMgb3duIGZpbGVzLCBzbyBhIHN3YXAgZXh0cmFjdHMgaW50byBhIHNpYmxpbmcgX3N3YXAgZGlyIGFuZCBiYXRjaCBtb3ZlcyBpdCBpbnRvIHBsYWNlIG9ubHkKICAgICMgYWZ0ZXIgdGhpcyBwcm9jZXNzIGV4aXRzIChsb2NrcyByZWxlYXNlZCkuIFNlZSBkb2NzL2FnZW50LWludGVyY29ubmVjdC5tZC4KICAgIGRlc3RfZGlyID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiIgogICAgc3dhcF9kaXIgPSBkZXN0X2Rpci5yc3RyaXAoIlxcLyIpICsgIl9zd2FwIgogICAgcHlzcGVjID0gb3MuZW52aXJvbi5nZXQoIlBZU1BFQyIsICIiKQogICAgcmVxdWVzdGVkX21pbm9yID0gcmVzb2x2ZV9yZXF1ZXN0ZWRfbWlub3IocHlzcGVjKQoKICAgIGlmIHJlcXVlc3RlZF9taW5vciBpcyBOb25lIG9yIHJlcXVlc3RlZF9taW5vciA9PSBMQVRFU1RfTUlOT1I6CiAgICAgICAgc3lzLnN0ZG91dC53cml0ZSgidW5jaGFuZ2VkfHt9XG4iLmZvcm1hdChMQVRFU1RfTUlOT1IpKQogICAgICAgIHJldHVybiAwCgogICAgbWlub3IsIHBhdGNoLCBzaGEyNTYsIGZlbGxfYmFjayA9IHJlc29sdmVfdGFibGVfZW50cnkocmVxdWVzdGVkX21pbm9yKQogICAgaWYgbWlub3IgPT0gTEFURVNUX01JTk9SOgogICAgICAgICMgQWJvdmUtY2VpbGluZyByZXF1ZXN0IChvbmx5IHBhdGggaGVyZSwgc2luY2UgZXhhY3QtbWF0Y2gtbGF0ZXN0IGlzIGhhbmRsZWQgYWJvdmUpOgogICAgICAgICMgbm8gc3dhcCBuZWVkZWQsIGJ1dCB0YWcgImZlbGxiYWNrIiBub3QgInVuY2hhbmdlZCIgc28gdGhlIGNhbGxlcidzIFdBUk4gc3RpbGwgZmlyZXMuCiAgICAgICAgc3lzLnN0ZG91dC53cml0ZSgiZmVsbGJhY2t8e31cbiIuZm9ybWF0KG1pbm9yKSkKICAgICAgICByZXR1cm4gMAoKICAgIHVybCA9ICJodHRwczovL3d3dy5weXRob24ub3JnL2Z0cC9weXRob24ve3B9L3B5dGhvbi17cH0tZW1iZWQtYW1kNjQuemlwIi5mb3JtYXQocD1wYXRjaCkKICAgIHppcF9wYXRoID0gb3MucGF0aC5qb2luKG9zLmVudmlyb24uZ2V0KCJURU1QIiwgIi4iKSwgInB5dGhvbi17fS1lbWJlZC1hbWQ2NC56aXAiLmZvcm1hdChwYXRjaCkpCiAgICB0cnk6CiAgICAgICAgZG93bmxvYWRfYW5kX3ZlcmlmeSh1cmwsIHNoYTI1NiwgemlwX3BhdGgpCiAgICAgICAgZXh0cmFjdF9hbmRfcGF0Y2goemlwX3BhdGgsIHN3YXBfZGlyKQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBleGM6CiAgICAgICAgc3lzLnN0ZGVyci53cml0ZSgiZW1iZWQgdmVyc2lvbiBzd2FwIGZhaWxlZDoge31cbiIuZm9ybWF0KGV4YykpCiAgICAgICAgaWYgb3MucGF0aC5pc2Rpcihzd2FwX2Rpcik6CiAgICAgICAgICAgIHNodXRpbC5ybXRyZWUoc3dhcF9kaXIsIGlnbm9yZV9lcnJvcnM9VHJ1ZSkKICAgICAgICByZXR1cm4gMQoKICAgIHRhZyA9ICJmZWxsYmFjayIgaWYgZmVsbF9iYWNrIGVsc2UgInN3YXBwZWQiCiAgICBzeXMuc3Rkb3V0LndyaXRlKCJ7fXx7fXx7fVxuIi5mb3JtYXQodGFnLCBtaW5vciwgc3dhcF9kaXIpKQogICAgcmV0dXJuIDAKCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgc3lzLmV4aXQobWFpbigpKQo=" set "HP_FAILFAST_PROBE=IyBTbGljZSAyYi1DIGZhaWwtZmFzdCBwcm9iZTogbGF1bmNoZXMgdGhlIGNhbGxlcidzIHByb2dyYW0sIHdhaXRzIHVwIHRvIEhQX0ZBSUxGQVNUX1BST0JFX01TIHRvCiMgY2xhc3NpZnkgaXQgYXMgImV4aXRlZCBmYXN0IiAoc3RhbGUvYnJva2VuIGNhY2hlZCBhcnRpZmFjdCAtLSBkaXNjYXJkK3JlYnVpbGQgY2FuZGlkYXRlKSB2cy4KIyAic3RpbGwgcnVubmluZyIgKHRoZSB1c2VyJ3MgcmVhbCwgcG9zc2libHkgbG9uZy1ydW5uaW5nIHByb2dyYW0gLS0gbmV2ZXIgdG91Y2hlZCBhZ2FpbikuIE5ldmVyCiMgY2FsbHMgJHAuS2lsbCgpIC0tIHBhc3QgdGhlIHByb2JlIHdpbmRvdyB0aGUgd2FpdCBpcyB1bmJvdW5kZWQgc28gYSBoZWFsdGh5IGFwcCBpcyBuZXZlcgojIGZvcmNlLXN0b3BwZWQuCiMKIyBJbnB1dHMgdmlhIGVudiB2YXJzIChhdm9pZHMgY21kLmV4ZSBxdW90aW5nIGhhemFyZHMpOiBIUF9QUk9CRV9FWEUsIEhQX1BST0JFX0FSR1MsIEhQX1BST0JFX0NXRCwKIyBIUF9GQUlMRkFTVF9QUk9CRV9NUywgSFBfUFJPQkVfT1VUL0hQX1BST0JFX0VSUiAoZGVmYXVsdCB+cnVuLm91dC50eHQvfnJ1bi5lcnIudHh0KSwKIyBIUF9QUk9CRV9SRVNVTFQgKGRlZmF1bHQgfnByb2JlX3Jlc3VsdC50eHQgLS0gIiRleGNlZWRlZHwkZXhpdGNvZGUiLCBOT1Qgc3Rkb3V0KS4gQ2FsbGVyIG11c3QKIyBwcmUtdHJ1bmNhdGUgb3V0cHV0L3Jlc3VsdCBmaWxlcyBiZWZvcmUgaW52b2tpbmcuCiMKIyBkZXJpdmVkIHJlcXVpcmVtZW50IChbUkVRLTAyNl0gYXJndiBwYXNzdGhyb3VnaCk6IEhQX1BST0JFX0FSR1MgaXMgbm93IGEgZnVsbCwgYWxyZWFkeS1xdW90ZWQKIyBBcmd1bWVudHMgc3RyaW5nIChlLmcuIGAiZW50cnkucHkiICItLWZvbyIgImJhciJgKSwgdXNlZCB2ZXJiYXRpbSAtLSBub3QgYSBzaW5nbGUgYmFyZSBwYXRoCiMgcmUtcXVvdGVkIGhlcmUuIENhbGxlciBxdW90ZXMgZWFjaCB0b2tlbjsgc2VlIHJ1bl9zZXR1cC5iYXQncyBIUF9BUFBfQVJHUy4KIwojIExpdmUtdGVlcyB0aGUgY2hpbGQncyBzdGRvdXQvc3RkZXJyIHNvIGEgc3RkaW4taW50ZXJhY3RpdmUgcHJvZ3JhbSdzIHByb21wdHMgcmVhY2ggYSByZWFsCiMgZG91YmxlLWNsaWNrZWQgdXNlciwgaW5zdGVhZCBvZiBvbmx5IHdyaXRpbmcgY2FwdHVyZWQgb3V0cHV0IHRvIGRpc2sgYXQgZXhpdC4KIwojIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IGRvZXMgTk9UIHVzZSBSZWdpc3Rlci1PYmplY3RFdmVudCBvbiBPdXRwdXREYXRhUmVjZWl2ZWQvRXJyb3JEYXRhUmVjZWl2ZWQuCiMgVGhhdCB3YXMgdGhlIG9yaWdpbmFsIGRlc2lnbiBhbmQgd2FzIGZvdW5kLCB2aWEgdGhpcyByZXBvJ3Mgb3duIGxvY2FsIHB3c2ggdGVzdGluZywgdG8KIyByZW9yZGVyIGxpbmVzIFdJVEhJTiBhIHNpbmdsZSBzdHJlYW0gKGUuZy4gcm91bmQgMidzIG91dHB1dCBsYW5kaW5nIGJlZm9yZSByb3VuZCAxJ3MgaW4gdGhlCiMgY2FwdHVyZWQvdGVlZCB0ZXh0LCBub24tZGV0ZXJtaW5pc3RpY2FsbHkpIC0tIHJvb3QtY2F1c2VkIHRvIGEgY29uZmlybWVkLCBmaWxlZCBQb3dlclNoZWxsIGJ1ZwojIChQb3dlclNoZWxsL1Bvd2VyU2hlbGwjMTE5MzcpOiB0aG9zZSBldmVudHMgZGlzcGF0Y2ggdmlhIFRocmVhZFBvb2wuUXVldWVVc2VyV29ya0l0ZW0sIHdoaWNoCiMgZG9lcyBub3QgZ3VhcmFudGVlIGRlbGl2ZXJ5IG9yZGVyIHdoZW4gc2V2ZXJhbCBsaW5lcyBhcnJpdmUgY2xvc2UgdG9nZXRoZXIuIEZpeGVkIGJ5IHBvbGxpbmcKIyByZWFkcyBkaXJlY3RseSBpbnN0ZWFkOiBvbmx5IE9ORSByZWFkIGlzIGV2ZXIgaW4gZmxpZ2h0IHBlciBzdHJlYW0gYXQgYSB0aW1lICh0aGUgbmV4dCByZWFkIGlzCiMgbm90IGlzc3VlZCB1bnRpbCB0aGUgY3VycmVudCBvbmUgaXMgY29uc3VtZWQpLCBzbyB0aGVyZSBpcyBubyBwb3NzaWJsZSBvdXQtb2Ytb3JkZXIgZGVsaXZlcnkKIyBmb3IgYSBzaW5nbGUgc3RyZWFtIC0tIG9yZGVyaW5nIGlzIHNlbGYtc2VxdWVuY2VkLCBub3QgZGVwZW5kZW50IG9uIGFueSBydW50aW1lJ3MKIyBjYWxsYmFjay1zY2hlZHVsaW5nIGd1YXJhbnRlZS4gQ3Jvc3Mtc3RyZWFtIChzdGRvdXQgdnMgc3RkZXJyKSBpbnRlcmxlYXZpbmcgd2FzIG5ldmVyCiMgZ3VhcmFudGVlZCBhbmQgc3RpbGwgaXNuJ3QgLS0gdGhhdCByZWZsZWN0cyB0aGUgY2hpbGQncyBvd24gdHdvIGluZGVwZW5kZW50IHBpcGVzLCBub3QgYSBidWcuCiMKIyBkZXJpdmVkIHJlcXVpcmVtZW50IChGaW5kaW5nIDksIDIwMjYtMDctMjQpOiByZWFkcyB2aWEgU3RyZWFtUmVhZGVyLlJlYWRBc3luYyhjaGFyW10sIGludCwgaW50KQojIChyYXcgY2h1bmtzKSwgTk9UIFJlYWRMaW5lQXN5bmMoKS4gUmVhZExpbmVBc3luYygpIG9ubHkgcmV0dXJucyBvbmNlIGl0IHNlZXMgYSBmdWxsCiMgbmV3bGluZS10ZXJtaW5hdGVkIGxpbmUgLS0gY29uZmlybWVkIGVtcGlyaWNhbGx5IHRoYXQgUHl0aG9uJ3Mgb3duIGBpbnB1dCgicHJvbXB0IilgIChubwojIHRyYWlsaW5nIG5ld2xpbmUgYnkgZGVzaWduLCBzbyB0aGUgY3Vyc29yIHN0YXlzIG9uIHRoZSBzYW1lIGxpbmUpIGlzIGdlbnVpbmVseSBmbHVzaGVkIHRvIHRoZQojIE9TIHBpcGUgaW1tZWRpYXRlbHkgYnV0IHN0YXlzIGludmlzaWJsZSB0byBhIFJlYWRMaW5lQXN5bmMtYmFzZWQgcmVhZGVyIHVudGlsIHNvbWV0aGluZyBlbHNlCiMgbGF0ZXIgZmx1c2hlcyBhIG5ld2xpbmUsIG9yIHRoZSBwcm9jZXNzIGV4aXRzLiBBIGNodW5rLWJhc2VkIHJlYWQgc3VyZmFjZXMgd2hhdGV2ZXIgYnl0ZXMgYXJlCiMgYWN0dWFsbHkgYXZhaWxhYmxlIHRoZSBtb21lbnQgdGhleSBhcnJpdmUsIG1hdGNoaW5nIGhvdyBhIHJlYWwgdGVybWluYWwgYmVoYXZlcy4gRU9GIGlzIGEKIyAwLWxlbmd0aCByZWFkIChub3QgYSBudWxsIHJlc3VsdCB0aGUgd2F5IFJlYWRMaW5lQXN5bmMgc2lnbmFscyBpdCkuIFNlZQojIGRvY3MvcGxhbi1jbGktaW50ZXJhY3RpdmUtdmVyaWZpY2F0aW9uLm1kIEZpbmRpbmcgOSBmb3IgdGhlIGZ1bGwgZW1waXJpY2FsIHRyYWNlLgojCiMgRnVsbCByYXRpb25hbGUgKyBjaXRhdGlvbnM6IGRvY3MvcGxhbi1jbGktaW50ZXJhY3RpdmUtdmVyaWZpY2F0aW9uLm1kIEZpbmRpbmdzIDViLzYvNy84LzkuCiMKIyBUaGlzIGlzIHRoZSBjYW5vbmljYWwgc291cmNlIGZvciB0aGUgSFBfRkFJTEZBU1RfUFJPQkUgYmFzZTY0IHBheWxvYWQgZW1iZWRkZWQgaW4KIyBydW5fc2V0dXAuYmF0LiBBZnRlciBlZGl0aW5nLCByZS1lbmNvZGUgYW5kIHBhc3RlIGl0IGludG8gdGhlIGBzZXQgIkhQX0ZBSUxGQVNUX1BST0JFPS4uLiJgCiMgbGluZTsgdGVzdHMvdGVzdF9mYWlsZmFzdF9wcm9iZS5weSBhc3NlcnRzIHRoZSBlbWJlZGRlZCBwYXlsb2FkIG1hdGNoZXMgdGhpcyBmaWxlICh3aXRoCiMgQ1JMRi9MRiBub3JtYWxpemVkLCBwZXIgdGhlIC5wczEgUGF5bG9hZFN5bmMgY29udmVudGlvbiAtLSBzZWUKIyBkb2NzL2FnZW50LWxlc3NvbnMtbGVhcm5lZC5tZCAiRW1iZWRkZWQgSGVscGVyIFVwZGF0ZSBXb3JrZmxvdyIpLgokZXhlID0gJGVudjpIUF9QUk9CRV9FWEUKJHJhd0FyZ3MgPSAkZW52OkhQX1BST0JFX0FSR1MKJHdvcmtEaXIgPSAkZW52OkhQX1BST0JFX0NXRAokcHJvYmVNcyA9IFtpbnRdJGVudjpIUF9GQUlMRkFTVF9QUk9CRV9NUwokb3V0UGF0aCA9ICRlbnY6SFBfUFJPQkVfT1VUCmlmICgtbm90ICRvdXRQYXRoKSB7ICRvdXRQYXRoID0gJ35ydW4ub3V0LnR4dCcgfQokZXJyUGF0aCA9ICRlbnY6SFBfUFJPQkVfRVJSCmlmICgtbm90ICRlcnJQYXRoKSB7ICRlcnJQYXRoID0gJ35ydW4uZXJyLnR4dCcgfQokcmVzdWx0UGF0aCA9ICRlbnY6SFBfUFJPQkVfUkVTVUxUCmlmICgtbm90ICRyZXN1bHRQYXRoKSB7ICRyZXN1bHRQYXRoID0gJ35wcm9iZV9yZXN1bHQudHh0JyB9Cgokc2kgPSBOZXctT2JqZWN0IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzU3RhcnRJbmZvCiRzaS5GaWxlTmFtZSA9ICRleGUKaWYgKCRyYXdBcmdzKSB7ICRzaS5Bcmd1bWVudHMgPSAkcmF3QXJncyB9CiRzaS5Xb3JraW5nRGlyZWN0b3J5ID0gJHdvcmtEaXIKJHNpLlVzZVNoZWxsRXhlY3V0ZSA9ICRmYWxzZQokc2kuUmVkaXJlY3RTdGFuZGFyZE91dHB1dCA9ICR0cnVlCiRzaS5SZWRpcmVjdFN0YW5kYXJkRXJyb3IgPSAkdHJ1ZQokcCA9IE5ldy1PYmplY3QgU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MKJHAuU3RhcnRJbmZvID0gJHNpCiRwLlN0YXJ0KCkgfCBPdXQtTnVsbApXcml0ZS1Ib3N0ICJbSU5GT10gUHJvY2VzcyBJRCAkKCRwLklkKS4gSWYgaXQgc2VlbXMgc3R1Y2s6IFRhc2sgTWFuYWdlciA+IERldGFpbHMgdGFiID4gZmluZCB0aGlzIFBJRCA+IEVuZCBUYXNrICh0aGlzIHdpbmRvdyBzdGF5cyBvcGVuKS4iCgokb3V0QnVmID0gTmV3LU9iamVjdCBTeXN0ZW0uVGV4dC5TdHJpbmdCdWlsZGVyCiRlcnJCdWYgPSBOZXctT2JqZWN0IFN5c3RlbS5UZXh0LlN0cmluZ0J1aWxkZXIKJG91dENodW5rQnVmID0gTmV3LU9iamVjdCBjaGFyW10gNDA5NgokZXJyQ2h1bmtCdWYgPSBOZXctT2JqZWN0IGNoYXJbXSA0MDk2CiRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZEFzeW5jKCRvdXRDaHVua0J1ZiwgMCwgJG91dENodW5rQnVmLkxlbmd0aCkKJGVyclRhc2sgPSAkcC5TdGFuZGFyZEVycm9yLlJlYWRBc3luYygkZXJyQ2h1bmtCdWYsIDAsICRlcnJDaHVua0J1Zi5MZW5ndGgpCiRvdXREb25lID0gJGZhbHNlCiRlcnJEb25lID0gJGZhbHNlCgokc3cgPSBbU3lzdGVtLkRpYWdub3N0aWNzLlN0b3B3YXRjaF06OlN0YXJ0TmV3KCkKJGV4Y2VlZGVkID0gMAp3aGlsZSAoKC1ub3QgJHAuSGFzRXhpdGVkKSAtb3IgKC1ub3QgJG91dERvbmUpIC1vciAoLW5vdCAkZXJyRG9uZSkpIHsKICAgIGlmICgoLW5vdCAkb3V0RG9uZSkgLWFuZCAkb3V0VGFzay5Jc0NvbXBsZXRlZCkgewogICAgICAgICRuID0gJG91dFRhc2suUmVzdWx0CiAgICAgICAgaWYgKCRuIC1lcSAwKSB7CiAgICAgICAgICAgICRvdXREb25lID0gJHRydWUKICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAkY2h1bmsgPSBbc3RyaW5nXTo6bmV3KCRvdXRDaHVua0J1ZiwgMCwgJG4pCiAgICAgICAgICAgIFtDb25zb2xlXTo6T3V0LldyaXRlKCRjaHVuaykKICAgICAgICAgICAgJG51bGwgPSAkb3V0QnVmLkFwcGVuZCgkY2h1bmspCiAgICAgICAgICAgICRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZEFzeW5jKCRvdXRDaHVua0J1ZiwgMCwgJG91dENodW5rQnVmLkxlbmd0aCkKICAgICAgICB9CiAgICB9CiAgICBpZiAoKC1ub3QgJGVyckRvbmUpIC1hbmQgJGVyclRhc2suSXNDb21wbGV0ZWQpIHsKICAgICAgICAkbiA9ICRlcnJUYXNrLlJlc3VsdAogICAgICAgIGlmICgkbiAtZXEgMCkgewogICAgICAgICAgICAkZXJyRG9uZSA9ICR0cnVlCiAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgJGNodW5rID0gW3N0cmluZ106Om5ldygkZXJyQ2h1bmtCdWYsIDAsICRuKQogICAgICAgICAgICBbQ29uc29sZV06OkVycm9yLldyaXRlKCRjaHVuaykKICAgICAgICAgICAgJG51bGwgPSAkZXJyQnVmLkFwcGVuZCgkY2h1bmspCiAgICAgICAgICAgICRlcnJUYXNrID0gJHAuU3RhbmRhcmRFcnJvci5SZWFkQXN5bmMoJGVyckNodW5rQnVmLCAwLCAkZXJyQ2h1bmtCdWYuTGVuZ3RoKQogICAgICAgIH0KICAgIH0KICAgIGlmICgoLW5vdCAkZXhjZWVkZWQpIC1hbmQgKCRzdy5FbGFwc2VkTWlsbGlzZWNvbmRzIC1nZSAkcHJvYmVNcykpIHsKICAgICAgICAkZXhjZWVkZWQgPSAxCiAgICB9CiAgICBTdGFydC1TbGVlcCAtTWlsbGlzZWNvbmRzIDIwCn0KJHAuV2FpdEZvckV4aXQoKQoKJG91dEJ1Zi5Ub1N0cmluZygpIHwgU2V0LUNvbnRlbnQgLVBhdGggJG91dFBhdGggLUVuY29kaW5nIEFTQ0lJCiRlcnJCdWYuVG9TdHJpbmcoKSB8IFNldC1Db250ZW50IC1QYXRoICRlcnJQYXRoIC1FbmNvZGluZyBBU0NJSQoiJGV4Y2VlZGVkfCQoJHAuRXhpdENvZGUpIiB8IFNldC1Db250ZW50IC1QYXRoICRyZXN1bHRQYXRoIC1FbmNvZGluZyBBU0NJSQo=" set "HP_EXE_SMOKERUN=IyA6cnVuX2V4ZV9zbW9rZXJ1bidzIGRlZGljYXRlZCBoZWxwZXIgLS0gdGhlIE9OTFkgcGxhY2UgaW4gdGhpcyBmaWxlIGZhbWlseSBhbGxvd2VkIHRvCiMgZm9yY2Uta2lsbCAoS2lsbCgpKSB0aGUgdmVyaWZpY2F0aW9uIHJ1bi4gVW5saWtlIH5mYWlsZmFzdF9wcm9iZS5wczEgKG5ldmVyIGtpbGxzIC0tIGNvdmVycwojIHRoZSB1bnRpbWVkIGZhc3RwYXRoL2ludGVycHJldGVyL2NoZWNrcG9pbnQgY2FsbCBzaXRlcyksIHRoaXMgSVMgdGhlIGZyZXNoLWJ1aWxkIHZlcmlmaWNhdGlvbgojIHJ1biBpdHNlbGY6IG5vdGhpbmcgZWxzZSB3aWxsIGV2ZXIgY29uZmlybSB0aGlzIHBhcnRpY3VsYXIgYnVpbGQgd29ya2VkLCBzbyBhbiB1bnJlc3BvbnNpdmUKIyBwcm9jZXNzIGhlcmUgY2Fubm90IGJlIHRydXN0ZWQgdG8gZXZlbnR1YWxseSBmaW5pc2ggdGhlIHdheSBhIHByZXZpb3VzbHktdmVyaWZpZWQgY2FjaGVkCiMgYXJ0aWZhY3Qgb3IgaW50ZXJwcmV0ZXIgcnVuIGNhbi4KIwojIFJlYWRzIGlucHV0cyBmcm9tIGVudiB2YXJzIChzYW1lIGNtZC5leGUtcXVvdGluZy1oYXphcmQtYXZvaWRhbmNlIHJlYXNvbmluZyBhcwojIH5mYWlsZmFzdF9wcm9iZS5wczEncyBvd24gaGVhZGVyIGNvbW1lbnQpOiBIUF9TTU9LRVJVTl9FWEUgKGJhcmUgZmlsZW5hbWU7IGNhbGxlciBydW5zIHRoaXMKIyBzY3JpcHQgd2l0aCBDV0QgYWxyZWFkeSBzZXQgdG8gZGlzdFwsIG1hdGNoaW5nIDpydW5fZXhlX3Ntb2tlcnVuJ3MgZXhpc3RpbmcgcHVzaGQgZGlzdAojIGNvbnZlbnRpb24gLS0gUkVRLTAxOCAyYi1BLjIsIGxvYWQtYmVhcmluZyBmb3IgdGhlIENXRC1yZWxhdGl2ZSBjb25maWcuanNvbiB4ZmFpbCBjYXNlLCBzZWUKIyBkb2NzL2FnZW50LWludGVyY29ubmVjdC5tZCAiU2luZ2xlLXZlcmlmaWNhdGlvbiBzbW9rZSBtb2RlbCIpLiBIUF9TTU9LRVJVTl9PVVQvSFBfU01PS0VSVU5fRVJSCiMgZGVmYXVsdCB0byAuLlx+cnVuLm91dC50eHQgLyAuLlx+cnVuLmVyci50eHQgKHJlbGF0aXZlIHRvIGRpc3RcLCBtYXRjaGluZyB0aGUgcHJlLWV4aXN0aW5nCiMgY29udmVudGlvbikuIEhQX1NNT0tFUlVOX1JFU1VMVCAoZGVmYXVsdCB+c21va2VydW5fcmVzdWx0LnR4dCkgaXMgd2hlcmUgdGhpcyBzY3JpcHQgd3JpdGVzIGl0cwojIGV4aXQtY29kZSByZXN1bHQgLS0gTk9UIHN0ZG91dDsgc2VlIH5mYWlsZmFzdF9wcm9iZS5wczEncyBoZWFkZXIgY29tbWVudCAoc2FtZSByZWFzb25pbmc6IHRoZQojIGNhbGxlciBpbnZva2VzIHRoaXMgc2NyaXB0IGRpcmVjdGx5LCBubyBmb3IgL2YvYmFja3RpY2sgc3Rkb3V0IGNhcHR1cmUsIHNvIGxpdmUtdGVlZCBvdXRwdXQKIyByZWFjaGVzIHRoZSBjb25zb2xlIGluc3RlYWQgb2YgYmVpbmcgc2lsZW50bHkgc3dhbGxvd2VkIGFuZCBjb3JydXB0aW5nIHJlc3VsdCBwYXJzaW5nKS4gQ2FsbGVyCiMgbXVzdCBwcmUtdHJ1bmNhdGUgdGhlIG91dHB1dC9yZXN1bHQgZmlsZXMgYmVmb3JlIGludm9raW5nLCBzYW1lIGFzIH5mYWlsZmFzdF9wcm9iZS5wczEuCiMKIyBTYW1lIGxpdmUtdGVlIGFzIH5mYWlsZmFzdF9wcm9iZS5wczEgLS0gc2VlIHRoYXQgZmlsZSdzIGhlYWRlciBjb21tZW50IGZvciB0aGUgZnVsbCByYXRpb25hbGUKIyAoc2VsZi1zZXF1ZW5jZWQgY2h1bmsgcmVhZHMgdmlhIFN0cmVhbVJlYWRlci5SZWFkQXN5bmMoY2hhcltdLCBpbnQsIGludCksIE5PVAojIFJlZ2lzdGVyLU9iamVjdEV2ZW50IG9yIFJlYWRMaW5lQXN5bmMoKSAtLSBQb3dlclNoZWxsL1Bvd2VyU2hlbGwjMTE5MzcgYW5kIEZpbmRpbmcgOSwKIyBkb2NzL3BsYW4tY2xpLWludGVyYWN0aXZlLXZlcmlmaWNhdGlvbi5tZCwgY292ZXIgd2h5KS4KIwojIGRlcml2ZWQgcmVxdWlyZW1lbnQgKE9wZW4gUXVlc3Rpb24gMSwgb3duZXIgZGVjaXNpb24gMjAyNi0wNy0yNCk6IEhQX1NNT0tFUlVOX0tJTExfTVMgKGRlZmF1bHQKIyAzMDAwMCwgdW5jaGFuZ2VkKSBpcyBhIGNsYXNzaWZpY2F0aW9uIGNoZWNrcG9pbnQsIG5vdCBhbiB1bmNvbmRpdGlvbmFsIGRlYWRsaW5lIC0tIEtpbGwoKSBmaXJlcwojIG9ubHkgaWYgJHNhd091dHB1dCBpcyBzdGlsbCBmYWxzZSBhdCBraWxsTXMgKGZ1bGx5IHNpbGVudCA9IHByZXN1bWVkIGh1bmcpLiBBbnkgYnl0ZXMgb2JzZXJ2ZWQKIyBvbiBlaXRoZXIgc3RyZWFtIHNraXBzIHRoZSBraWxsIGFuZCB0aGUgd2FpdCBiZWNvbWVzIHVuYm91bmRlZCwgbWlycm9yaW5nCiMgfmZhaWxmYXN0X3Byb2JlLnBzMSdzIHBoaWxvc29waHkgLS0gc2VlIGRvY3MvYWdlbnQtaW50ZXJjb25uZWN0Lm1kICJBY3Rpdml0eS1hd2FyZSBFWEUtc21va2UKIyBraWxsIiBmb3IgdGhlIGZ1bGwgcmF0aW9uYWxlL3RyYWRlLW9mZi4gQ2h1bmstYmFzZWQgKG5vdCBsaW5lLWJhc2VkKSByZWFkcyBhcmUgd2hhdCBtYWtlIHRoaXMKIyBhY3R1YWxseSBmaXJlIGZvciB0aGUgY2Fub25pY2FsIGBpbnB1dCgicHJvbXB0IilgIGNhc2UgLS0gc2VlIEZpbmRpbmcgOSBpbiB0aGUgcGxhbiBkb2MgYWJvdmUuCiMKIyBUaGlzIGlzIHRoZSBjYW5vbmljYWwgc291cmNlIGZvciB0aGUgSFBfRVhFX1NNT0tFUlVOIGJhc2U2NCBwYXlsb2FkIGVtYmVkZGVkIGluIHJ1bl9zZXR1cC5iYXQuCiMgQWZ0ZXIgZWRpdGluZywgcmUtZW5jb2RlIGFuZCBwYXN0ZSBpdCBpbnRvIHRoZSBgc2V0ICJIUF9FWEVfU01PS0VSVU49Li4uImAgbGluZTsKIyB0ZXN0cy90ZXN0X2V4ZV9zbW9rZXJ1bi5weSBhc3NlcnRzIHRoZSBlbWJlZGRlZCBwYXlsb2FkIG1hdGNoZXMgdGhpcyBmaWxlIChDUkxGL0xGIG5vcm1hbGl6ZWQsCiMgcGVyIHRoZSAucHMxIFBheWxvYWRTeW5jIGNvbnZlbnRpb24gLS0gc2VlIGRvY3MvYWdlbnQtbGVzc29ucy1sZWFybmVkLm1kCiMgIkVtYmVkZGVkIEhlbHBlciBVcGRhdGUgV29ya2Zsb3ciKS4KIwojIGRlcml2ZWQgcmVxdWlyZW1lbnQgKFtSRVEtMDI2XSBhcmd2IHBhc3N0aHJvdWdoKTogSFBfU01PS0VSVU5fQVJHUywgaWYgc2V0LCBpcyBhIGZ1bGwsCiMgYWxyZWFkeS1xdW90ZWQgV2luZG93cyBBcmd1bWVudHMgc3RyaW5nICh0aGUgRVhFIGlzIHNlbGYtY29udGFpbmVkLCBzbyBubyBzZXBhcmF0ZSBlbnRyeS1maWxlCiMgYXJndiBpcyBuZWVkZWQgaGVyZSAtLSBqdXN0IGFueSBmb3J3YXJkZWQgZXh0cmEgYXJncykuIFVzZWQgdmVyYmF0aW0sIG5vIHJlLXF1b3RpbmcuCiRleGUgPSAkZW52OkhQX1NNT0tFUlVOX0VYRQoka2lsbE1zID0gMzAwMDAKaWYgKCRlbnY6SFBfU01PS0VSVU5fS0lMTF9NUykgeyAka2lsbE1zID0gW2ludF0kZW52OkhQX1NNT0tFUlVOX0tJTExfTVMgfQokYXJnc1JhdyA9ICRlbnY6SFBfU01PS0VSVU5fQVJHUwokb3V0UGF0aCA9ICRlbnY6SFBfU01PS0VSVU5fT1VUCmlmICgtbm90ICRvdXRQYXRoKSB7ICRvdXRQYXRoID0gJy4uXH5ydW4ub3V0LnR4dCcgfQokZXJyUGF0aCA9ICRlbnY6SFBfU01PS0VSVU5fRVJSCmlmICgtbm90ICRlcnJQYXRoKSB7ICRlcnJQYXRoID0gJy4uXH5ydW4uZXJyLnR4dCcgfQokcmVzdWx0UGF0aCA9ICRlbnY6SFBfU01PS0VSVU5fUkVTVUxUCmlmICgtbm90ICRyZXN1bHRQYXRoKSB7ICRyZXN1bHRQYXRoID0gJ35zbW9rZXJ1bl9yZXN1bHQudHh0JyB9Cgokc2kgPSBOZXctT2JqZWN0IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzU3RhcnRJbmZvCiRzaS5GaWxlTmFtZSA9ICRleGUKaWYgKCRhcmdzUmF3KSB7ICRzaS5Bcmd1bWVudHMgPSAkYXJnc1JhdyB9CiRzaS5Vc2VTaGVsbEV4ZWN1dGUgPSAkZmFsc2UKJHNpLlJlZGlyZWN0U3RhbmRhcmRPdXRwdXQgPSAkdHJ1ZQokc2kuUmVkaXJlY3RTdGFuZGFyZEVycm9yID0gJHRydWUKJHAgPSBOZXctT2JqZWN0IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzCiRwLlN0YXJ0SW5mbyA9ICRzaQokcC5TdGFydCgpIHwgT3V0LU51bGwKV3JpdGUtSG9zdCAiW0lORk9dIFByb2Nlc3MgSUQgJCgkcC5JZCkuIElmIGl0IHNlZW1zIHN0dWNrOiBUYXNrIE1hbmFnZXIgPiBEZXRhaWxzIHRhYiA+IGZpbmQgdGhpcyBQSUQgPiBFbmQgVGFzayAodGhpcyB3aW5kb3cgc3RheXMgb3BlbikuIgoKJG91dEJ1ZiA9IE5ldy1PYmplY3QgU3lzdGVtLlRleHQuU3RyaW5nQnVpbGRlcgokZXJyQnVmID0gTmV3LU9iamVjdCBTeXN0ZW0uVGV4dC5TdHJpbmdCdWlsZGVyCiRvdXRDaHVua0J1ZiA9IE5ldy1PYmplY3QgY2hhcltdIDQwOTYKJGVyckNodW5rQnVmID0gTmV3LU9iamVjdCBjaGFyW10gNDA5Ngokb3V0VGFzayA9ICRwLlN0YW5kYXJkT3V0cHV0LlJlYWRBc3luYygkb3V0Q2h1bmtCdWYsIDAsICRvdXRDaHVua0J1Zi5MZW5ndGgpCiRlcnJUYXNrID0gJHAuU3RhbmRhcmRFcnJvci5SZWFkQXN5bmMoJGVyckNodW5rQnVmLCAwLCAkZXJyQ2h1bmtCdWYuTGVuZ3RoKQokb3V0RG9uZSA9ICRmYWxzZQokZXJyRG9uZSA9ICRmYWxzZQoKJHN3ID0gW1N5c3RlbS5EaWFnbm9zdGljcy5TdG9wd2F0Y2hdOjpTdGFydE5ldygpCiRraWxsZWQgPSAkZmFsc2UKJHNhd091dHB1dCA9ICRmYWxzZQp3aGlsZSAoKC1ub3QgJHAuSGFzRXhpdGVkKSAtb3IgKC1ub3QgJG91dERvbmUpIC1vciAoLW5vdCAkZXJyRG9uZSkpIHsKICAgIGlmICgoLW5vdCAkb3V0RG9uZSkgLWFuZCAkb3V0VGFzay5Jc0NvbXBsZXRlZCkgewogICAgICAgICRuID0gJG91dFRhc2suUmVzdWx0CiAgICAgICAgaWYgKCRuIC1lcSAwKSB7CiAgICAgICAgICAgICRvdXREb25lID0gJHRydWUKICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAkc2F3T3V0cHV0ID0gJHRydWUKICAgICAgICAgICAgJGNodW5rID0gW3N0cmluZ106Om5ldygkb3V0Q2h1bmtCdWYsIDAsICRuKQogICAgICAgICAgICBbQ29uc29sZV06Ok91dC5Xcml0ZSgkY2h1bmspCiAgICAgICAgICAgICRudWxsID0gJG91dEJ1Zi5BcHBlbmQoJGNodW5rKQogICAgICAgICAgICAkb3V0VGFzayA9ICRwLlN0YW5kYXJkT3V0cHV0LlJlYWRBc3luYygkb3V0Q2h1bmtCdWYsIDAsICRvdXRDaHVua0J1Zi5MZW5ndGgpCiAgICAgICAgfQogICAgfQogICAgaWYgKCgtbm90ICRlcnJEb25lKSAtYW5kICRlcnJUYXNrLklzQ29tcGxldGVkKSB7CiAgICAgICAgJG4gPSAkZXJyVGFzay5SZXN1bHQKICAgICAgICBpZiAoJG4gLWVxIDApIHsKICAgICAgICAgICAgJGVyckRvbmUgPSAkdHJ1ZQogICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgICRzYXdPdXRwdXQgPSAkdHJ1ZQogICAgICAgICAgICAkY2h1bmsgPSBbc3RyaW5nXTo6bmV3KCRlcnJDaHVua0J1ZiwgMCwgJG4pCiAgICAgICAgICAgIFtDb25zb2xlXTo6RXJyb3IuV3JpdGUoJGNodW5rKQogICAgICAgICAgICAkbnVsbCA9ICRlcnJCdWYuQXBwZW5kKCRjaHVuaykKICAgICAgICAgICAgJGVyclRhc2sgPSAkcC5TdGFuZGFyZEVycm9yLlJlYWRBc3luYygkZXJyQ2h1bmtCdWYsIDAsICRlcnJDaHVua0J1Zi5MZW5ndGgpCiAgICAgICAgfQogICAgfQogICAgaWYgKCgtbm90ICRraWxsZWQpIC1hbmQgKC1ub3QgJHNhd091dHB1dCkgLWFuZCAoLW5vdCAkcC5IYXNFeGl0ZWQpIC1hbmQgKCRzdy5FbGFwc2VkTWlsbGlzZWNvbmRzIC1nZSAka2lsbE1zKSkgewogICAgICAgIHRyeSB7ICRwLktpbGwoKSB9IGNhdGNoIHt9CiAgICAgICAgJGtpbGxlZCA9ICR0cnVlCiAgICB9CiAgICBTdGFydC1TbGVlcCAtTWlsbGlzZWNvbmRzIDIwCn0KJHAuV2FpdEZvckV4aXQoKQoKJG91dEJ1Zi5Ub1N0cmluZygpIHwgU2V0LUNvbnRlbnQgLVBhdGggJG91dFBhdGggLUVuY29kaW5nIEFTQ0lJCiRlcnJCdWYuVG9TdHJpbmcoKSB8IFNldC1Db250ZW50IC1QYXRoICRlcnJQYXRoIC1FbmNvZGluZyBBU0NJSQppZiAoJGtpbGxlZCkgewogICAgIi0xIiB8IFNldC1Db250ZW50IC1QYXRoICRyZXN1bHRQYXRoIC1FbmNvZGluZyBBU0NJSQp9IGVsc2UgewogICAgIiQoJHAuRXhpdENvZGUpIiB8IFNldC1Db250ZW50IC1QYXRoICRyZXN1bHRQYXRoIC1FbmNvZGluZyBBU0NJSQp9CgpHZXQtRXZlbnRTdWJzY3JpYmVyIC1FcnJvckFjdGlvbiBTaWxlbnRseUNvbnRpbnVlIHwgVW5yZWdpc3Rlci1FdmVudCAtRXJyb3JBY3Rpb24gU2lsZW50bHlDb250aW51ZQo=" -set "HP_EXE_HINT_RERUN=IyA6ZXhlX3Ntb2tlcnVuX2hpbnRzJyBib3VuZGVkIGRpYWdub3N0aWMgcmUtcnVuIGhlbHBlci4gVW5saWtlIGV4ZV9zbW9rZXJ1bi5wczEvZmFpbGZhc3RfcHJvYmUucHMxDQojIChhY3Rpdml0eS1hd2FyZSAtLSBvbmNlIGEgcHJvY2VzcyBoYXMgcHJpbnRlZCBhbnl0aGluZywgdGhlIGtpbGwgaXMgc2tpcHBlZCBhbmQgdGhlIHdhaXQgYmVjb21lcw0KIyB1bmJvdW5kZWQsIHNpbmNlIHRob3NlIGNvdmVyIFJFQUwgdmVyaWZpY2F0aW9uIHJ1bnMgd29ydGggd2FpdGluZyBvbiksIHRoaXMgcmUtcnVuIGlzIGRpYWdub3N0aWMNCiMgT05MWTogaXRzIHNvbGUgcHVycG9zZSBpcyBhIHN0ZG91dCtzdGRlcnIgc25hcHNob3QgZm9yIHN0ZGVyciBwYXR0ZXJuLW1hdGNoaW5nIChNb2R1bGVOb3RGb3VuZEVycm9yLw0KIyBGaWxlTm90Rm91bmRFcnJvciBzaWduYXR1cmVzKSwgbmV2ZXIgc2hvd24gbGl2ZSB0byB0aGUgdXNlci4gUGFydGlhbCBvdXRwdXQgb24gYSBoYW5nIGlzIGZpbmUgYW5kDQojIHN0cmljdGx5IHByZWZlcnJlZCBvdmVyIGhhbmdpbmcgdGhlIHdob2xlIGJvb3RzdHJhcCBhIHNlY29uZCB0aW1lIG9uIGEgcnVuIG5vYm9keSBpcyB3YXRjaGluZyAtLQ0KIyBzbyB0aGUga2lsbCBoZXJlIGlzIFVOQ09ORElUSU9OQUwgYXQgdGhlIGRlYWRsaW5lLCBub3QgYWN0aXZpdHktYXdhcmUuIFNlZSBDTEFVREUubWQncyBmb3JtZXINCiMgQWN0aXZlIEJhY2tsb2cgaXRlbSAxNSAvIGRvY3MvYWdlbnQtY2xvc2VkLWJhY2tsb2cubWQgZm9yIHRoZSBnYXAgdGhpcyBjbG9zZXM6IHRoZSBwcmlvciBpbmxpbmUNCiMgIjpleGVfc21va2VydW5faGludHMiIGJvZHkgZGlkIGEgcGxhaW4sIHVudGltZWQgYCIlRU5WTkFNRSUuZXhlIiA+ICJ+ZXhlX291dC50eHQiIDI+JjFgIC0tIHRoZQ0KIyBPTkUgdXNlci1jb2RlIGxhdW5jaCBwb2ludCBpbiB0aGlzIGZpbGUgd2l0aCBubyB0aW1lb3V0IGF0IGFsbCwgb24gdGhlIHRoZW9yeSB0aGF0IGEgZ2VudWluZQ0KIyBNb2R1bGVOb3RGb3VuZEVycm9yL0ZpbGVOb3RGb3VuZEVycm9yIGFsd2F5cyBleGl0cyBpbW1lZGlhdGVseS4gVGhhdCB0aGVvcnkgaG9sZHMgZm9yIGENCiMgREVURVJNSU5JU1RJQyBmYWlsdXJlLCBidXQgdGhpcyBpcyBhIGZyZXNoIHJlLXJ1biBvZiB0aGUgc2FtZSBiaW5hcnk7IGFueSBub24tZGV0ZXJtaW5pc20gKGENCiMgcmFjZSwgYW4gZW52aXJvbm1lbnQgY2hlY2sgdGhhdCBzb21ldGltZXMgc3VjY2VlZHMsIGFueXRoaW5nIHRoYXQgb2NjYXNpb25hbGx5IGJsb2NrcyBvbg0KIyBpbmhlcml0ZWQgc3RkaW4gaW5zdGVhZCBvZiBleGl0aW5nIGZhc3QpIGNvdWxkIGhhbmcgdGhpcyBzZWNvbmQsIHVudGltZWQgaW52b2NhdGlvbiBldmVuIHRob3VnaA0KIyB0aGUgRklSU1QgaW52b2NhdGlvbiBsZWdpdGltYXRlbHkgY2xhc3NpZmllZCBhcyAiZmFzdCwgcmVhbCwgbm9uLWhhbmcgZmFpbHVyZSIuDQojDQojIFJlYWRzOiBIUF9ISU5UX1JFUlVOX0VYRSAoYmFyZSBmaWxlbmFtZTsgY2FsbGVyIHJ1bnMgdGhpcyB3aXRoIENXRCBhbHJlYWR5IHNldCB0byBkaXN0XCwgbWlycm9yaW5nDQojIGV4ZV9zbW9rZXJ1bi5wczEncyBvd24gY29udmVudGlvbikuIEhQX0hJTlRfUkVSVU5fT1VUIChkZWZhdWx0IH5leGVfb3V0LnR4dCwgcmVsYXRpdmUgdG8gZGlzdFwpDQojIGlzIHdoZXJlIGNvbWJpbmVkIHN0ZG91dCtzdGRlcnIgaXMgd3JpdHRlbiAtLSBtYXRjaGluZyB0aGUgT1JJR0lOQUwgYDI+JjFgIG1lcmdlLWludG8tb25lLWZpbGUNCiMgYmVoYXZpb3IgZXhhY3RseSwgc2luY2UgdGhlIGV4aXN0aW5nIGZpbmRzdHIgaGludC1tYXRjaGluZyBpbiA6ZXhlX3Ntb2tlcnVuX2hpbnRzIG9ubHkgY2hlY2tzIGZvcg0KIyBzdWJzdHJpbmcgcHJlc2VuY2UgaW4gdGhhdCBvbmUgZmlsZSwgbmV2ZXIgd2hpY2ggc3RyZWFtIGEgbGluZSBjYW1lIGZyb20uIEhQX0hJTlRfUkVSVU5fS0lMTF9NUw0KIyAoZGVmYXVsdCAxMDAwMCAtLSBhIGRpYWdub3N0aWMgY2FwdHVyZSBvbiBhbiBhbHJlYWR5LWZhaWxlZCBydW4gZG9lcyBub3QgbmVlZCB0aGUgZnVsbCAzMHMNCiMgcHJpbWFyeS12ZXJpZmljYXRpb24gYnVkZ2V0KSBpcyBhIHRlc3Qtb25seSBvdmVycmlkZSBwb2ludCwgbWlycm9yaW5nIEhQX1NNT0tFUlVOX0tJTExfTVMncw0KIyBlc3RhYmxpc2hlZCBwYXR0ZXJuLg0KIw0KIyBkZXJpdmVkIHJlcXVpcmVtZW50OiB0aGUgcmVzb2x2ZWQgJGtpbGxNcyBpcyBBTFdBWVMgd3JpdHRlbiB0byBIUF9ISU5UX1JFUlVOX0tJTExNU19PVVQgKGRlZmF1bHQNCiMgfmV4ZV9oaW50X2tpbGxtcy50eHQpIHJpZ2h0IGFmdGVyIGl0J3MgY29tcHV0ZWQsIHVuY29uZGl0aW9uYWxseSAtLSBwcm9kdWN0aW9uIGNhbGxlcnMgbmV2ZXIgcmVhZA0KIyB0aGlzIGZpbGUsIHNvIGl0IGNvc3RzIG5vdGhpbmcgdGhlcmUuIEl0IGV4aXN0cyBzbyB0ZXN0cyBjYW4gYXNzZXJ0IHRoZSBvdmVycmlkZSB3YXMgYWN0dWFsbHkNCiMgaG9ub3JlZCBieSByZWFkaW5nIHRoaXMgRElSRUNUIHZhbHVlLCBpbnN0ZWFkIG9mIGluZmVycmluZyBpdCBmcm9tIHdhbGwtY2xvY2sgZWxhcHNlZCB0aW1lICh3aGljaA0KIyBwcm92ZWQgdW5yZWxpYWJsZSBvbiBzaGFyZWQgcmVhbC1XaW5kb3dzIENJIHJ1bm5lcnMgLS0gdGhlIHNhbWUgdGVzdCBtZWFzdXJlZCA5LjJzLCAxMy41cywgMTQuNnMsDQojIGFuZCAxNi4zcyBvZiBvdmVyaGVhZCBhY3Jvc3MgZm91ciBkaWZmZXJlbnQgcmVhbC1DSSBydW5zIGZvciB0aGUgaWRlbnRpY2FsIDUwMG1zIG92ZXJyaWRlLCBhDQojIG1vdmluZyB0YXJnZXQgdGhhdCBubyBmaXhlZCBib3VuZCBjb3VsZCBjaGFzZTsgc2VlIGRvY3MvYWdlbnQtbGVzc29ucy1sZWFybmVkLm1kKS4NCiMNCiMgVGhpcyBpcyB0aGUgY2Fub25pY2FsIHNvdXJjZSBmb3IgdGhlIEhQX0VYRV9ISU5UX1JFUlVOIGJhc2U2NCBwYXlsb2FkIGVtYmVkZGVkIGluIHJ1bl9zZXR1cC5iYXQuDQojIEFmdGVyIGVkaXRpbmcsIHJ1biBgcHl0aG9uIHRvb2xzL3N5bmNfcGF5bG9hZC5weSBIUF9FWEVfSElOVF9SRVJVTiB0b29scy9leGVfaGludF9yZXJ1bi5wczFgOw0KIyB0ZXN0cy90ZXN0X2V4ZV9oaW50X3JlcnVuLnB5IGFzc2VydHMgdGhlIGVtYmVkZGVkIHBheWxvYWQgbWF0Y2hlcyB0aGlzIGZpbGUgKENSTEYvTEYgbm9ybWFsaXplZCwNCiMgcGVyIHRoZSAucHMxIFBheWxvYWRTeW5jIGNvbnZlbnRpb24gLS0gc2VlIGRvY3MvYWdlbnQtbGVzc29ucy1sZWFybmVkLm1kDQojICJFbWJlZGRlZCBIZWxwZXIgVXBkYXRlIFdvcmtmbG93IikuDQokZXhlID0gJGVudjpIUF9ISU5UX1JFUlVOX0VYRQ0KJG91dFBhdGggPSAkZW52OkhQX0hJTlRfUkVSVU5fT1VUDQppZiAoLW5vdCAkb3V0UGF0aCkgeyAkb3V0UGF0aCA9ICd+ZXhlX291dC50eHQnIH0NCiRraWxsTXMgPSAxMDAwMA0KaWYgKCRlbnY6SFBfSElOVF9SRVJVTl9LSUxMX01TKSB7ICRraWxsTXMgPSBbaW50XSRlbnY6SFBfSElOVF9SRVJVTl9LSUxMX01TIH0NCiRraWxsTXNPdXRQYXRoID0gJGVudjpIUF9ISU5UX1JFUlVOX0tJTExNU19PVVQNCmlmICgtbm90ICRraWxsTXNPdXRQYXRoKSB7ICRraWxsTXNPdXRQYXRoID0gJ35leGVfaGludF9raWxsbXMudHh0JyB9DQoiJGtpbGxNcyIgfCBTZXQtQ29udGVudCAtUGF0aCAka2lsbE1zT3V0UGF0aCAtRW5jb2RpbmcgQVNDSUkNCg0KJHNpID0gTmV3LU9iamVjdCBTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2Vzc1N0YXJ0SW5mbw0KJHNpLkZpbGVOYW1lID0gJGV4ZQ0KJHNpLlVzZVNoZWxsRXhlY3V0ZSA9ICRmYWxzZQ0KJHNpLlJlZGlyZWN0U3RhbmRhcmRPdXRwdXQgPSAkdHJ1ZQ0KJHNpLlJlZGlyZWN0U3RhbmRhcmRFcnJvciA9ICR0cnVlDQokcCA9IE5ldy1PYmplY3QgU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MNCiRwLlN0YXJ0SW5mbyA9ICRzaQ0KJHAuU3RhcnQoKSB8IE91dC1OdWxsDQoNCiRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZFRvRW5kQXN5bmMoKQ0KJGVyclRhc2sgPSAkcC5TdGFuZGFyZEVycm9yLlJlYWRUb0VuZEFzeW5jKCkNCiRleGl0ZWQgPSAkcC5XYWl0Rm9yRXhpdCgka2lsbE1zKQ0KaWYgKC1ub3QgJGV4aXRlZCkgew0KICAgICMgZGVyaXZlZCByZXF1aXJlbWVudDogUHJvY2Vzcy5LaWxsKCkgKHRoZSBwYXJhbWV0ZXJsZXNzIG92ZXJsb2FkIC0tIFdpbmRvd3MgUG93ZXJTaGVsbCA1LjENCiAgICAjIHRhcmdldHMgLk5FVCBGcmFtZXdvcmssIHdoaWNoIGhhcyBubyBQcm9jZXNzLktpbGwoZW50aXJlUHJvY2Vzc1RyZWUpIG92ZXJsb2FkOyB0aGF0J3MNCiAgICAjIC5ORVQgNSsgb25seSkgdGVybWluYXRlcyBPTkxZICRwIGl0c2VsZi4gQSBQeUluc3RhbGxlciBvbmVmaWxlIGJvb3Rsb2FkZXIgKG9yIGFueSBwcm9ncmFtKQ0KICAgICMgdGhhdCBzcGF3bnMgYSBjaGlsZCBpbmhlcml0aW5nIHRoZSByZWRpcmVjdGVkIHN0ZG91dC9zdGRlcnIgaGFuZGxlcyBjYW4gbGVhdmUgdGhhdCBjaGlsZA0KICAgICMgcnVubmluZyBhZnRlciAkcCBpcyBraWxsZWQgLS0gdGhlIHBpcGUgdGhlbiBuZXZlciByZWFjaGVzIEVPRiwgYW5kIGFuIHVuYm91bmRlZA0KICAgICMgUmVhZFRvRW5kQXN5bmMoKS5SZXN1bHQgd291bGQgaGFuZyBmb3JldmVyLCBkZWZlYXRpbmcgdGhlIGVudGlyZSBwb2ludCBvZiB0aGlzIGJvdW5kZWQNCiAgICAjIGhlbHBlci4gdGFza2tpbGwgL1QgdGVybWluYXRlcyB0aGUgd2hvbGUgcHJvY2VzcyB0cmVlLCBub3QganVzdCAkcC4gTk9UIGluZGVwZW5kZW50bHkNCiAgICAjIHZlcmlmaWVkIG9uIHJlYWwgV2luZG93cyBDSSB0aGF0IGEgZ2VudWluZSBkZXNjZW5kYW50LWhvbGRzLXRoZS1waXBlIHNjZW5hcmlvIGlzIGZ1bGx5DQogICAgIyBjb3ZlcmVkIGJ5IHRoaXMgKG5vIFdpbmRvd3MgZW52aXJvbm1lbnQgYXZhaWxhYmxlIHRvIGNvbnN0cnVjdCB0aGF0IHJlcHJvKSAtLSB0aGUgYm91bmRlZA0KICAgICMgZmluYWwgcmVhZCBiZWxvdyBpcyBhIHNlY29uZCwgaW5kZXBlbmRlbnQgc2FmZXR5IG5ldCBmb3IgZXhhY3RseSB0aGF0IHJlc2lkdWFsIHJpc2suDQogICAgdHJ5IHsgJiB0YXNra2lsbC5leGUgL0YgL1QgL1BJRCAkcC5JZCAyPiRudWxsIDE+JG51bGwgfSBjYXRjaCB7fQ0KICAgIHRyeSB7ICRwLktpbGwoKSB9IGNhdGNoIHt9DQp9DQokcC5XYWl0Rm9yRXhpdCgpDQoNCiMgQm91bmRlZCBmaW5hbCByZWFkLCBOT1QgYSBibGluZCAuUmVzdWx0IGJsb2NrOiBldmVuIGFmdGVyIGtpbGxpbmcgdGhlIHByb2Nlc3MgdHJlZSwgYQ0KIyBkZXNjZW5kYW50IHRhc2traWxsIC9UIGRpZCBub3QgY2F0Y2ggKG9yIHNvbWUgb3RoZXIgZXhvdGljIGhhbmRsZS1pbmhlcml0YW5jZSBlZGdlIGNhc2UpDQojIHNob3VsZCBub3QgYmUgYWJsZSB0byBoYW5nIHRoaXMgZGlhZ25vc3RpYy1vbmx5IGhlbHBlciBpbmRlZmluaXRlbHkuIFRhc2suV2FpdChtcykgcmV0dXJucw0KIyBmYWxzZSBvbiB0aW1lb3V0IHdpdGhvdXQgdGhyb3dpbmcsIHNvIGEgc3R1Y2sgcGlwZSBkZWdyYWRlcyB0byBwYXJ0aWFsL2VtcHR5IG91dHB1dCBpbnN0ZWFkDQojIG9mIGFuIHVuYm91bmRlZCB3YWl0Lg0KJGRyYWluTXMgPSA1MDAwDQokb3V0ID0gJycNCiRlcnIgPSAnJw0KaWYgKCRvdXRUYXNrLldhaXQoJGRyYWluTXMpKSB7IHRyeSB7ICRvdXQgPSAkb3V0VGFzay5SZXN1bHQgfSBjYXRjaCB7fSB9DQppZiAoJGVyclRhc2suV2FpdCgkZHJhaW5NcykpIHsgdHJ5IHsgJGVyciA9ICRlcnJUYXNrLlJlc3VsdCB9IGNhdGNoIHt9IH0NCigkb3V0ICsgJGVycikgfCBTZXQtQ29udGVudCAtUGF0aCAkb3V0UGF0aCAtRW5jb2RpbmcgQVNDSUkNCg==" +set "HP_EXE_HINT_RERUN=IyA6ZXhlX3Ntb2tlcnVuX2hpbnRzJyBib3VuZGVkIGRpYWdub3N0aWMgcmUtcnVuIGhlbHBlci4gVW5saWtlIGV4ZV9zbW9rZXJ1bi5wczEvZmFpbGZhc3RfcHJvYmUucHMxDQojIChhY3Rpdml0eS1hd2FyZSAtLSBvbmNlIGEgcHJvY2VzcyBoYXMgcHJpbnRlZCBhbnl0aGluZywgdGhlIGtpbGwgaXMgc2tpcHBlZCBhbmQgdGhlIHdhaXQgYmVjb21lcw0KIyB1bmJvdW5kZWQsIHNpbmNlIHRob3NlIGNvdmVyIFJFQUwgdmVyaWZpY2F0aW9uIHJ1bnMgd29ydGggd2FpdGluZyBvbiksIHRoaXMgcmUtcnVuIGlzIGRpYWdub3N0aWMNCiMgT05MWTogaXRzIHNvbGUgcHVycG9zZSBpcyBhIHN0ZG91dCtzdGRlcnIgc25hcHNob3QgZm9yIHN0ZGVyciBwYXR0ZXJuLW1hdGNoaW5nIChNb2R1bGVOb3RGb3VuZEVycm9yLw0KIyBGaWxlTm90Rm91bmRFcnJvciBzaWduYXR1cmVzKSwgbmV2ZXIgc2hvd24gbGl2ZSB0byB0aGUgdXNlci4gUGFydGlhbCBvdXRwdXQgb24gYSBoYW5nIGlzIGZpbmUgYW5kDQojIHN0cmljdGx5IHByZWZlcnJlZCBvdmVyIGhhbmdpbmcgdGhlIHdob2xlIGJvb3RzdHJhcCBhIHNlY29uZCB0aW1lIG9uIGEgcnVuIG5vYm9keSBpcyB3YXRjaGluZyAtLQ0KIyBzbyB0aGUga2lsbCBoZXJlIGlzIFVOQ09ORElUSU9OQUwgYXQgdGhlIGRlYWRsaW5lLCBub3QgYWN0aXZpdHktYXdhcmUuIFNlZSBDTEFVREUubWQncyBmb3JtZXINCiMgQWN0aXZlIEJhY2tsb2cgaXRlbSAxNSAvIGRvY3MvYWdlbnQtY2xvc2VkLWJhY2tsb2cubWQgZm9yIHRoZSBnYXAgdGhpcyBjbG9zZXM6IHRoZSBwcmlvciBpbmxpbmUNCiMgIjpleGVfc21va2VydW5faGludHMiIGJvZHkgZGlkIGEgcGxhaW4sIHVudGltZWQgYCIlRU5WTkFNRSUuZXhlIiA+ICJ+ZXhlX291dC50eHQiIDI+JjFgIC0tIHRoZQ0KIyBPTkUgdXNlci1jb2RlIGxhdW5jaCBwb2ludCBpbiB0aGlzIGZpbGUgd2l0aCBubyB0aW1lb3V0IGF0IGFsbCwgb24gdGhlIHRoZW9yeSB0aGF0IGEgZ2VudWluZQ0KIyBNb2R1bGVOb3RGb3VuZEVycm9yL0ZpbGVOb3RGb3VuZEVycm9yIGFsd2F5cyBleGl0cyBpbW1lZGlhdGVseS4gVGhhdCB0aGVvcnkgaG9sZHMgZm9yIGENCiMgREVURVJNSU5JU1RJQyBmYWlsdXJlLCBidXQgdGhpcyBpcyBhIGZyZXNoIHJlLXJ1biBvZiB0aGUgc2FtZSBiaW5hcnk7IGFueSBub24tZGV0ZXJtaW5pc20gKGENCiMgcmFjZSwgYW4gZW52aXJvbm1lbnQgY2hlY2sgdGhhdCBzb21ldGltZXMgc3VjY2VlZHMsIGFueXRoaW5nIHRoYXQgb2NjYXNpb25hbGx5IGJsb2NrcyBvbg0KIyBpbmhlcml0ZWQgc3RkaW4gaW5zdGVhZCBvZiBleGl0aW5nIGZhc3QpIGNvdWxkIGhhbmcgdGhpcyBzZWNvbmQsIHVudGltZWQgaW52b2NhdGlvbiBldmVuIHRob3VnaA0KIyB0aGUgRklSU1QgaW52b2NhdGlvbiBsZWdpdGltYXRlbHkgY2xhc3NpZmllZCBhcyAiZmFzdCwgcmVhbCwgbm9uLWhhbmcgZmFpbHVyZSIuDQojDQojIFJlYWRzOiBIUF9ISU5UX1JFUlVOX0VYRSAoYmFyZSBmaWxlbmFtZTsgY2FsbGVyIHJ1bnMgdGhpcyB3aXRoIENXRCBhbHJlYWR5IHNldCB0byBkaXN0XCwgbWlycm9yaW5nDQojIGV4ZV9zbW9rZXJ1bi5wczEncyBvd24gY29udmVudGlvbikuIEhQX0hJTlRfUkVSVU5fT1VUIChkZWZhdWx0IH5leGVfb3V0LnR4dCwgcmVsYXRpdmUgdG8gZGlzdFwpDQojIGlzIHdoZXJlIGNvbWJpbmVkIHN0ZG91dCtzdGRlcnIgaXMgd3JpdHRlbiAtLSBtYXRjaGluZyB0aGUgT1JJR0lOQUwgYDI+JjFgIG1lcmdlLWludG8tb25lLWZpbGUNCiMgYmVoYXZpb3IgZXhhY3RseSwgc2luY2UgdGhlIGV4aXN0aW5nIGZpbmRzdHIgaGludC1tYXRjaGluZyBpbiA6ZXhlX3Ntb2tlcnVuX2hpbnRzIG9ubHkgY2hlY2tzIGZvcg0KIyBzdWJzdHJpbmcgcHJlc2VuY2UgaW4gdGhhdCBvbmUgZmlsZSwgbmV2ZXIgd2hpY2ggc3RyZWFtIGEgbGluZSBjYW1lIGZyb20uIEhQX0hJTlRfUkVSVU5fS0lMTF9NUw0KIyAoZGVmYXVsdCAxMDAwMCAtLSBhIGRpYWdub3N0aWMgY2FwdHVyZSBvbiBhbiBhbHJlYWR5LWZhaWxlZCBydW4gZG9lcyBub3QgbmVlZCB0aGUgZnVsbCAzMHMNCiMgcHJpbWFyeS12ZXJpZmljYXRpb24gYnVkZ2V0KSBpcyBhIHRlc3Qtb25seSBvdmVycmlkZSBwb2ludCwgbWlycm9yaW5nIEhQX1NNT0tFUlVOX0tJTExfTVMncw0KIyBlc3RhYmxpc2hlZCBwYXR0ZXJuLg0KIw0KIyBkZXJpdmVkIHJlcXVpcmVtZW50OiB0aGUgcmVzb2x2ZWQgJGtpbGxNcyBpcyBBTFdBWVMgd3JpdHRlbiB0byBIUF9ISU5UX1JFUlVOX0tJTExNU19PVVQgKGRlZmF1bHQNCiMgfmV4ZV9oaW50X2tpbGxtcy50eHQpIHJpZ2h0IGFmdGVyIGl0J3MgY29tcHV0ZWQsIHVuY29uZGl0aW9uYWxseSAtLSBwcm9kdWN0aW9uIGNhbGxlcnMgbmV2ZXIgcmVhZA0KIyB0aGlzIGZpbGUsIHNvIGl0IGNvc3RzIG5vdGhpbmcgdGhlcmUuIEl0IGV4aXN0cyBzbyB0ZXN0cyBjYW4gYXNzZXJ0IHRoZSBvdmVycmlkZSB3YXMgYWN0dWFsbHkNCiMgaG9ub3JlZCBieSByZWFkaW5nIHRoaXMgRElSRUNUIHZhbHVlLCBpbnN0ZWFkIG9mIGluZmVycmluZyBpdCBmcm9tIHdhbGwtY2xvY2sgZWxhcHNlZCB0aW1lICh3aGljaA0KIyBwcm92ZWQgdW5yZWxpYWJsZSBvbiBzaGFyZWQgcmVhbC1XaW5kb3dzIENJIHJ1bm5lcnMgLS0gdGhlIHNhbWUgdGVzdCBtZWFzdXJlZCA5LjJzLCAxMy41cywgMTQuNnMsDQojIGFuZCAxNi4zcyBvZiBvdmVyaGVhZCBhY3Jvc3MgZm91ciBkaWZmZXJlbnQgcmVhbC1DSSBydW5zIGZvciB0aGUgaWRlbnRpY2FsIDUwMG1zIG92ZXJyaWRlLCBhDQojIG1vdmluZyB0YXJnZXQgdGhhdCBubyBmaXhlZCBib3VuZCBjb3VsZCBjaGFzZTsgc2VlIGRvY3MvYWdlbnQtbGVzc29ucy1sZWFybmVkLm1kKS4NCiMNCiMgVGhpcyBpcyB0aGUgY2Fub25pY2FsIHNvdXJjZSBmb3IgdGhlIEhQX0VYRV9ISU5UX1JFUlVOIGJhc2U2NCBwYXlsb2FkIGVtYmVkZGVkIGluIHJ1bl9zZXR1cC5iYXQuDQojIEFmdGVyIGVkaXRpbmcsIHJ1biBgcHl0aG9uIHRvb2xzL3N5bmNfcGF5bG9hZC5weSBIUF9FWEVfSElOVF9SRVJVTiB0b29scy9leGVfaGludF9yZXJ1bi5wczFgOw0KIyB0ZXN0cy90ZXN0X2V4ZV9oaW50X3JlcnVuLnB5IGFzc2VydHMgdGhlIGVtYmVkZGVkIHBheWxvYWQgbWF0Y2hlcyB0aGlzIGZpbGUgKENSTEYvTEYgbm9ybWFsaXplZCwNCiMgcGVyIHRoZSAucHMxIFBheWxvYWRTeW5jIGNvbnZlbnRpb24gLS0gc2VlIGRvY3MvYWdlbnQtbGVzc29ucy1sZWFybmVkLm1kDQojICJFbWJlZGRlZCBIZWxwZXIgVXBkYXRlIFdvcmtmbG93IikuDQokZXhlID0gJGVudjpIUF9ISU5UX1JFUlVOX0VYRQ0KJG91dFBhdGggPSAkZW52OkhQX0hJTlRfUkVSVU5fT1VUDQppZiAoLW5vdCAkb3V0UGF0aCkgeyAkb3V0UGF0aCA9ICd+ZXhlX291dC50eHQnIH0NCiRraWxsTXMgPSAxMDAwMA0KaWYgKCRlbnY6SFBfSElOVF9SRVJVTl9LSUxMX01TKSB7ICRraWxsTXMgPSBbaW50XSRlbnY6SFBfSElOVF9SRVJVTl9LSUxMX01TIH0NCiRraWxsTXNPdXRQYXRoID0gJGVudjpIUF9ISU5UX1JFUlVOX0tJTExNU19PVVQNCmlmICgtbm90ICRraWxsTXNPdXRQYXRoKSB7ICRraWxsTXNPdXRQYXRoID0gJ35leGVfaGludF9raWxsbXMudHh0JyB9DQoiJGtpbGxNcyIgfCBTZXQtQ29udGVudCAtUGF0aCAka2lsbE1zT3V0UGF0aCAtRW5jb2RpbmcgQVNDSUkNCg0KJHNpID0gTmV3LU9iamVjdCBTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2Vzc1N0YXJ0SW5mbw0KJHNpLkZpbGVOYW1lID0gJGV4ZQ0KJHNpLlVzZVNoZWxsRXhlY3V0ZSA9ICRmYWxzZQ0KJHNpLlJlZGlyZWN0U3RhbmRhcmRPdXRwdXQgPSAkdHJ1ZQ0KJHNpLlJlZGlyZWN0U3RhbmRhcmRFcnJvciA9ICR0cnVlDQokcCA9IE5ldy1PYmplY3QgU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MNCiRwLlN0YXJ0SW5mbyA9ICRzaQ0KJHAuU3RhcnQoKSB8IE91dC1OdWxsDQoNCiRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZFRvRW5kQXN5bmMoKQ0KJGVyclRhc2sgPSAkcC5TdGFuZGFyZEVycm9yLlJlYWRUb0VuZEFzeW5jKCkNCiRleGl0ZWQgPSAkcC5XYWl0Rm9yRXhpdCgka2lsbE1zKQ0KaWYgKC1ub3QgJGV4aXRlZCkgew0KICAgICMgZGVyaXZlZCByZXF1aXJlbWVudDogUHJvY2Vzcy5LaWxsKCkgKHRoZSBwYXJhbWV0ZXJsZXNzIG92ZXJsb2FkIC0tIFdpbmRvd3MgUG93ZXJTaGVsbCA1LjENCiAgICAjIHRhcmdldHMgLk5FVCBGcmFtZXdvcmssIHdoaWNoIGhhcyBubyBQcm9jZXNzLktpbGwoZW50aXJlUHJvY2Vzc1RyZWUpIG92ZXJsb2FkOyB0aGF0J3MNCiAgICAjIC5ORVQgNSsgb25seSkgdGVybWluYXRlcyBPTkxZICRwIGl0c2VsZi4gQSBQeUluc3RhbGxlciBvbmVmaWxlIGJvb3Rsb2FkZXIgKG9yIGFueSBwcm9ncmFtKQ0KICAgICMgdGhhdCBzcGF3bnMgYSBjaGlsZCBpbmhlcml0aW5nIHRoZSByZWRpcmVjdGVkIHN0ZG91dC9zdGRlcnIgaGFuZGxlcyBjYW4gbGVhdmUgdGhhdCBjaGlsZA0KICAgICMgcnVubmluZyBhZnRlciAkcCBpcyBraWxsZWQgLS0gdGhlIHBpcGUgdGhlbiBuZXZlciByZWFjaGVzIEVPRiwgYW5kIGFuIHVuYm91bmRlZA0KICAgICMgUmVhZFRvRW5kQXN5bmMoKS5SZXN1bHQgd291bGQgaGFuZyBmb3JldmVyLCBkZWZlYXRpbmcgdGhlIGVudGlyZSBwb2ludCBvZiB0aGlzIGJvdW5kZWQNCiAgICAjIGhlbHBlci4gdGFza2tpbGwgL1QgdGVybWluYXRlcyB0aGUgd2hvbGUgcHJvY2VzcyB0cmVlLCBub3QganVzdCAkcC4gQ29uZmlybWVkIG9uIHJlYWwNCiAgICAjIFdpbmRvd3MgQ0kgKHR3byBsYW5lcyBvbiBQUiAjNDEwKTogYSBncmFuZGNoaWxkLWluaGVyaXRzLXRoZS1waXBlIHJlZ3Jlc3Npb24gdGVzdCdzDQogICAgIyByZXR1cm5jb2RlL3RpbWluZyBhc3NlcnRpb25zIHBhc3NlZCB0aGVyZSwgbWVhbmluZyB0aGUgdGFza2tpbGwgL1QgcGF0aCBpdHNlbGYgd2FzDQogICAgIyBnZW51aW5lbHkgZXhlcmNpc2VkLCBub3QganVzdCB0aGUgZmFsbGJhY2sgYmVsb3cgLS0gdGhlIGJvdW5kZWQgZmluYWwgcmVhZCBzdGlsbCBzdGF5cyBhcw0KICAgICMgYSBzZWNvbmQsIGluZGVwZW5kZW50IHNhZmV0eSBuZXQgZm9yIHdoYXRldmVyIGEgZGVzY2VuZGFudCB0YXNra2lsbCAvVCBtaWdodCBzdGlsbCBtaXNzLg0KICAgIHRyeSB7ICYgdGFza2tpbGwuZXhlIC9GIC9UIC9QSUQgJHAuSWQgMj4kbnVsbCAxPiRudWxsIH0gY2F0Y2gge30NCiAgICB0cnkgeyAkcC5LaWxsKCkgfSBjYXRjaCB7fQ0KfQ0KJHAuV2FpdEZvckV4aXQoKQ0KDQojIEJvdW5kZWQgZmluYWwgcmVhZCwgTk9UIGEgYmxpbmQgLlJlc3VsdCBibG9jazogZXZlbiBhZnRlciBraWxsaW5nIHRoZSBwcm9jZXNzIHRyZWUsIGENCiMgZGVzY2VuZGFudCB0YXNra2lsbCAvVCBkaWQgbm90IGNhdGNoIChvciBzb21lIG90aGVyIGV4b3RpYyBoYW5kbGUtaW5oZXJpdGFuY2UgZWRnZSBjYXNlKQ0KIyBzaG91bGQgbm90IGJlIGFibGUgdG8gaGFuZyB0aGlzIGRpYWdub3N0aWMtb25seSBoZWxwZXIgaW5kZWZpbml0ZWx5LiBUYXNrLldhaXQobXMpIHJldHVybnMNCiMgZmFsc2Ugb24gdGltZW91dCB3aXRob3V0IHRocm93aW5nLCBzbyBhIHN0dWNrIHBpcGUgZGVncmFkZXMgdG8gcGFydGlhbC9lbXB0eSBvdXRwdXQgaW5zdGVhZA0KIyBvZiBhbiB1bmJvdW5kZWQgd2FpdC4NCiRkcmFpbk1zID0gNTAwMA0KJG91dCA9ICcnDQokZXJyID0gJycNCmlmICgkb3V0VGFzay5XYWl0KCRkcmFpbk1zKSkgeyB0cnkgeyAkb3V0ID0gJG91dFRhc2suUmVzdWx0IH0gY2F0Y2gge30gfQ0KaWYgKCRlcnJUYXNrLldhaXQoJGRyYWluTXMpKSB7IHRyeSB7ICRlcnIgPSAkZXJyVGFzay5SZXN1bHQgfSBjYXRjaCB7fSB9DQooJG91dCArICRlcnIpIHwgU2V0LUNvbnRlbnQgLVBhdGggJG91dFBhdGggLUVuY29kaW5nIEFTQ0lJDQo=" set "HP_INSTALLER_TIMEOUT=IyBSdW5zIGFuIGV4dGVybmFsIGluc3RhbGxlciBleGVjdXRhYmxlIHdpdGggYSBnZW5lcm91cywgY29uZmlndXJhYmxlIHRpbWVvdXQgY2VpbGluZy4KIwojIENsb3NlcyBDTEFVREUubWQgQWN0aXZlIEJhY2tsb2cgaXRlbSAxNDogdGhlIHRocmVlICJzdGFydCAiIiAvd2FpdCIgZXh0ZXJuYWwtaW5zdGFsbGVyCiMgbGF1bmNoZXMgKE1pbmljb25kYSBBbGxVc2VycywgTWluaWNvbmRhIEp1c3RNZSwgTkktVklTQSkgcHJldmlvdXNseSBoYWQgbm8gcHJvY2Vzcy1sZXZlbAojIHRpbWVvdXQgYXQgYWxsLCB1bmxpa2UgdGhpcyBmaWxlJ3MgZGVsaWJlcmF0ZWx5LXdyYXBwZWQgdXNlci1jb2RlIGxhdW5jaGVzIC0tIGEgZ2VudWluZWx5CiMgc3R1Y2sgaW5zdGFsbGVyIChhIHNpbGVudGx5LWJsb2NraW5nIFVBQy9yZWJvb3QgcHJvbXB0LCBhIHdlZGdlZCBzdWItaW5zdGFsbGVyKSBjb3VsZCBoYW5nCiMgdGhlIHdob2xlIGJvb3RzdHJhcCBmb3JldmVyIHdpdGggemVybyByZWNvdXJzZS4KIwojIERlbGliZXJhdGVseSBOT1QgbW9kZWxlZCBvbiB0b29scy9leGVfc21va2VydW4ucHMxJ3MgfjMwcyBhZ2dyZXNzaXZlIGtpbGwgd2luZG93IC0tIHRoZXNlIGFyZQojIFJFQUwgaW5zdGFsbGVyIHByb2Nlc3NlcywgYW5kIGtpbGxpbmcgb25lIHRvbyBlYXJseSAod2hpbGUgaXQncyBzdGlsbCBsZWdpdGltYXRlbHkgd3JpdGluZwojIGZpbGVzL3JlZ2lzdHJ5IGtleXMpIHJpc2tzIGEgd29yc2Ugb3V0Y29tZSB0aGFuIGEgc2xvdy1idXQtc3VjY2VlZGluZyBpbnN0YWxsOiBhIGdlbnVpbmVseQojIGNvcnJ1cHRlZCBoYWxmLWluc3RhbGxlZCB0YXJnZXQuIFRoZSB0aW1lb3V0IGhlcmUgaXMgYSBnZW5lcm91cyBzYWZldHkgQ0VJTElORyBhZ2FpbnN0IGEgdHJ1bHkKIyBodW5nIHByb2Nlc3MsIG5vdCBhIHJlc3BvbnNpdmVuZXNzIGNoZWNrIC0tIHNlZSB0aGUgcGVyLWNhbGwtc2l0ZSB0aW1lb3V0IHZhbHVlcyBpbgojIHJ1bl9zZXR1cC5iYXQncyBvd24gY29tbWVudHMgZm9yIHRoZSByZWFsLXdvcmxkIHJlc2VhcmNoIGJlaGluZCBlYWNoIG51bWJlciAoZG9jdW1lbnRlZAojIE1pbmljb25kYS9OSS1WSVNBIGluc3RhbGwtdGltZSByZXBvcnRzLCBub3QgZ3Vlc3NlcykuCiMKIyBJbnB1dHMgdmlhIGVudiB2YXJzIChhdm9pZHMgY21kLmV4ZSBxdW90aW5nIGhhemFyZHMsIG1hdGNoZXMgfmZhaWxmYXN0X3Byb2JlLnBzMSdzIGNvbnRyYWN0KToKIyAgIEhQX0lOU1RBTExFUl9FWEUgICAgICAgICAgLSBwYXRoIHRvIHRoZSBpbnN0YWxsZXIgZXhlY3V0YWJsZS4KIyAgIEhQX0lOU1RBTExFUl9BUkdTICAgICAgICAgLSBhIHNpbmdsZSwgYWxyZWFkeS1wcmVwYXJlZCBBcmd1bWVudHMgc3RyaW5nIChjYWxsZXIncwojICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNwb25zaWJpbGl0eSB0byBxdW90ZS9qb2luIHRva2VuczsgbWF0Y2hlcyBIUF9QUk9CRV9BUkdTJ3MKIyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3duICJjYWxsZXIgcHJvdmlkZXMgYSByZWFkeSBzdHJpbmciIGNvbnRyYWN0KS4KIyAgIEhQX0lOU1RBTExFUl9USU1FT1VUX01TICAgLSB0aW1lb3V0IGluIG1pbGxpc2Vjb25kcyBiZWZvcmUgdGhlIHByb2Nlc3MgaXMgZm9yY2Uta2lsbGVkLgojICAgSFBfSU5TVEFMTEVSX1JFU1VMVCAgICAgICAtIG91dHB1dCByZXN1bHQgZmlsZSBwYXRoIChkZWZhdWx0IH5pbnN0YWxsZXJfcmVzdWx0LnR4dCksCiMgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdyaXR0ZW4gYXMgIjxleGl0Y29kZT58PHRpbWVkb3V0OjAvMT4iLgojCiMgVXNlU2hlbGxFeGVjdXRlPSR0cnVlIChub3QgJGZhbHNlKSBpcyBkZWxpYmVyYXRlOiBpdCBwcmVzZXJ2ZXMgdGhlIHNhbWUgVUFDLWVsZXZhdGlvbi12aWEtCiMgbWFuaWZlc3QgYmVoYXZpb3IgInN0YXJ0ICIiIC93YWl0IiBhbHJlYWR5IGhhZCAoU2hlbGxFeGVjdXRlIGlzIHdoYXQgc3VwcG9ydHMgYW4gZXhlJ3Mgb3duCiMgInJlcXVpcmVBZG1pbmlzdHJhdG9yIiBtYW5pZmVzdCBzaGltIHRyYW5zcGFyZW50bHkpLiBObyBzdGRvdXQvc3RkZXJyIHJlZGlyZWN0aW9uIGlzIG5lZWRlZCAtLQojIHRoZXNlIGluc3RhbGxlcnMgcnVuIHNpbGVudGx5ICgvUywgLS1xdWlldCkgLS0gc28gdGhpcyBkb2Vzbid0IGNvc3QgYW55dGhpbmcgbXkgY2FsbGVyIG5lZWRzLgojCiMgT24gdGltZW91dCwgdXNlcyB0YXNra2lsbCAvRiAvVCAobm90ICRwLktpbGwoKSkgZGVsaWJlcmF0ZWx5OiBXaW5kb3dzIFBvd2VyU2hlbGwgNS4xJ3MKIyBTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2Vzcy5LaWxsKCkgKC5ORVQgRnJhbWV3b3JrKSBoYXMgbm8gImtpbGwgdGhlIHdob2xlIHByb2Nlc3MgdHJlZSIKIyBvdmVybG9hZCAtLSB0aGF0J3MgYSAuTkVUIENvcmUgMy4wKyBhZGRpdGlvbiB0aGlzIHJ1bnRpbWUgZG9lc24ndCBoYXZlIC0tIHNvIGl0IHdvdWxkIG9ubHkKIyBraWxsIHRoZSBkaXJlY3QgcHJvY2VzcywgcG90ZW50aWFsbHkgb3JwaGFuaW5nIGNoaWxkIHN1Yi1pbnN0YWxsZXIgcHJvY2Vzc2VzICh0aGlzIHJlcG8ncyBvd24KIyBOSS1WSVNBIGNvbW1lbnQgYWxyZWFkeSBub3RlcyAiTkkgaW5zdGFsbGVycyBtYXkgc3Bhd24gY2hpbGQgcHJvY2Vzc2VzIikuIHRhc2traWxsJ3MgL1QgZmxhZwojIGtpbGxzIHRoZSBmdWxsIHByb2Nlc3MgdHJlZSByZWdhcmRsZXNzIG9mIC5ORVQgcnVudGltZSB2ZXJzaW9uLgojCiMgVGhpcyBpcyB0aGUgY2Fub25pY2FsIHNvdXJjZSBmb3IgdGhlIEhQX0lOU1RBTExFUl9USU1FT1VUIGJhc2U2NCBwYXlsb2FkIGVtYmVkZGVkIGluCiMgcnVuX3NldHVwLmJhdC4gQWZ0ZXIgZWRpdGluZywgcmUtc3luYyB3aXRoIHRvb2xzL3N5bmNfcGF5bG9hZC5weTsgdGVzdHMvdGVzdF9ydW5faW5zdGFsbGVyXwojIHdpdGhfdGltZW91dC5weSBhc3NlcnRzIHRoZSBlbWJlZGRlZCBwYXlsb2FkIG1hdGNoZXMgdGhpcyBmaWxlIChDUkxGL0xGIG5vcm1hbGl6ZWQsIHBlciB0aGUKIyAucHMxIFBheWxvYWRTeW5jIGNvbnZlbnRpb24pLgojCiMgZGVyaXZlZCByZXF1aXJlbWVudDogYSByZWFsIFdpbmRvd3MgQ0kgcnVuIGNhdWdodCBhIGdlbnVpbmUgYnVnIHRoaXMgcmVwbydzIG90aGVyIGVtaXR0ZWQKIyAucHMxIGhlbHBlcnMgbmV2ZXIgaGl0OiB3aGVuIGEgbmF0aXZlIEVYRSBpbnZva2VkIHZpYSB0aGUgIiYiIGNhbGwgb3BlcmF0b3IgKHRhc2traWxsLmV4ZQojIGhlcmUgLS0gdGhlIE9OTFkgbmF0aXZlLWNvbW1hbmQgaW52b2NhdGlvbiBpbiB0aGlzIGZpbGU7IGV4ZV9zbW9rZXJ1bi5wczEvZmFpbGZhc3RfcHJvYmUucHMxCiMgb25seSBldmVyIGxhdW5jaCB0aGVpciBtb25pdG9yZWQgcHJvY2VzcyB2aWEgLk5FVCdzIFByb2Nlc3MgQVBJLCB3aGljaCBkb2VzIG5vdCBzZXQKIyAkTEFTVEVYSVRDT0RFKSBzZXRzICRMQVNURVhJVENPREUgdG8gYSBub256ZXJvIHZhbHVlLCBwd3NoIC1GaWxlIHNpbGVudGx5IGluaGVyaXRzIHRoYXQgYXMKIyBJVFMgT1dOIHByb2Nlc3MgZXhpdCBjb2RlIHdoZW4gdGhlIHNjcmlwdCBlbmRzIHdpdGhvdXQgYW4gZXhwbGljaXQgImV4aXQiIHN0YXRlbWVudCAtLSBldmVuCiMgdGhvdWdoIHRhc2traWxsIHdhcyBub3QgdGhlIGxhc3Qgc3RhdGVtZW50IGV4ZWN1dGVkIGFuZCBpdHMgb3duIGZhaWx1cmUgd2FzIGFscmVhZHkgY2F1Z2h0CiMgYW5kIHN3YWxsb3dlZCBieSB0cnkvY2F0Y2guIFRoaXMgaXMgYSB3ZWxsLWtub3duIFBvd2VyU2hlbGwgZ290Y2hhLCBub3Qgc3BlY2lmaWMgdG8gdGFza2tpbGw6CiMgYW55IGVhcmxpZXIgbmF0aXZlLWNvbW1hbmQgZmFpbHVyZSBjYW4gbGVhayB0aHJvdWdoIGFzIHRoZSB3aG9sZSBzY3JpcHQncyBleGl0IGNvZGUgdW5sZXNzCiMgZXhwbGljaXRseSByZXNldC4gdGFza2tpbGwgY2FuIGxlZ2l0aW1hdGVseSByZXR1cm4gbm9uemVybyBoZXJlIChlLmcuIHRoZSB0aW1lZC1vdXQgcHJvY2VzcwojIGFscmVhZHkgZXhpdGVkIG9uIGl0cyBvd24gaW4gdGhlIHJhY2UgYmV0d2VlbiBXYWl0Rm9yRXhpdCh0aW1lb3V0TXMpIHJldHVybmluZyBmYWxzZSBhbmQKIyB0YXNra2lsbCBhY3R1YWxseSBydW5uaW5nKSB3aXRob3V0IHRoYXQgYmVpbmcgYSByZWFsIHByb2JsZW0gLS0gdGhlIHJlc3VsdCBGSUxFIChub3QgdGhlCiMgc2NyaXB0J3Mgb3duIHByb2Nlc3MgZXhpdCBjb2RlKSBpcyB0aGlzIHNjcmlwdCdzIHJlYWwgY29udHJhY3Qgd2l0aCBpdHMgY2FsbGVyLCBzbyB0aGUgZml4IGlzCiMgYW4gZXhwbGljaXQgImV4aXQgMCIgYXMgdGhlIHNjcmlwdCdzIGxhc3Qgc3RhdGVtZW50LCBndWFyYW50ZWVpbmcgcHdzaCdzIG93biBleGl0IGNvZGUgaXMKIyBhbHdheXMgY2xlYW4gcmVnYXJkbGVzcyBvZiB3aGF0ICRMQVNURVhJVENPREUgaGFwcGVuZWQgdG8gYmUgbGVmdCBob2xkaW5nLgokZXhlID0gJGVudjpIUF9JTlNUQUxMRVJfRVhFCiRhcmdTdHIgPSAkZW52OkhQX0lOU1RBTExFUl9BUkdTCiR0aW1lb3V0TXMgPSBbaW50XSRlbnY6SFBfSU5TVEFMTEVSX1RJTUVPVVRfTVMKJHJlc3VsdFBhdGggPSAkZW52OkhQX0lOU1RBTExFUl9SRVNVTFQKaWYgKC1ub3QgJHJlc3VsdFBhdGgpIHsgJHJlc3VsdFBhdGggPSAnfmluc3RhbGxlcl9yZXN1bHQudHh0JyB9Cgokc2kgPSBOZXctT2JqZWN0IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzU3RhcnRJbmZvCiRzaS5GaWxlTmFtZSA9ICRleGUKaWYgKCRhcmdTdHIpIHsgJHNpLkFyZ3VtZW50cyA9ICRhcmdTdHIgfQokc2kuVXNlU2hlbGxFeGVjdXRlID0gJHRydWUKJHAgPSBOZXctT2JqZWN0IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzCiRwLlN0YXJ0SW5mbyA9ICRzaQokcC5TdGFydCgpIHwgT3V0LU51bGwKCmlmICgkcC5XYWl0Rm9yRXhpdCgkdGltZW91dE1zKSkgewogICAgIiQoJHAuRXhpdENvZGUpfDAiIHwgU2V0LUNvbnRlbnQgLVBhdGggJHJlc3VsdFBhdGggLUVuY29kaW5nIEFTQ0lJCn0gZWxzZSB7CiAgICB0cnkgeyAmIHRhc2traWxsLmV4ZSAvRiAvVCAvUElEICRwLklkIDI+JG51bGwgfCBPdXQtTnVsbCB9IGNhdGNoIHt9CiAgICAiMXwxIiB8IFNldC1Db250ZW50IC1QYXRoICRyZXN1bHRQYXRoIC1FbmNvZGluZyBBU0NJSQp9CmV4aXQgMAo=" :: --- Embedded helper: HP_PREP_REQUIREMENTS (~prep_requirements.py) --- :: Purpose: diff --git a/tests/selfapps_entry_picker.ps1 b/tests/selfapps_entry_picker.ps1 index 3a7dd2c5..8758dae8 100644 --- a/tests/selfapps_entry_picker.ps1 +++ b/tests/selfapps_entry_picker.ps1 @@ -10,6 +10,14 @@ # test verifies the picker runs, shows the menu, and resolves to the default without # hanging. True human selection needs a console and is out of CI scope. # +# Second scenario (self.entry.picker.overflow): 10 non-PREFERRED, no-__main__ files -- +# one past :pick_entry_interactive's own >9 GTR-check limit (choice /C only supports the +# 123456789 charset). Asserts the numbered menu is skipped entirely (no +# "Multiple Python files detected" prompt), the overflow log line and its Tip guidance +# both print, and the alphabetically-first file is still chosen -- closes the gap flagged +# in docs/demo-bootstrapper-output.md's picker scenario ("a real coverage gap, not yet a +# dedicated test"). +# # Lane: conda-full only (one real bootstrap; behavior is provider-independent). param() $ErrorActionPreference = 'Continue' @@ -92,4 +100,63 @@ Write-NdjsonRow ([ordered]@{ }) if (-not $pass) { exit 1 } + +# --- Second scenario: >9 candidates -> numbered menu skipped, Tip guidance still shown --- +$overflowDir = Join-Path $here '~selftest_entry_picker_overflow' +if (Test-Path -LiteralPath $overflowDir) { Remove-Item -LiteralPath $overflowDir -Recurse -Force } +New-Item -ItemType Directory -Force -Path $overflowDir | Out-Null +Copy-Item -LiteralPath (Join-Path $repo 'run_setup.bat') -Destination $overflowDir -Force +1..10 | ForEach-Object { + $letter = [char](96 + $_) + Set-Content -LiteralPath (Join-Path $overflowDir "${letter}_app.py") -Value "print('entry-$letter')`n" -Encoding ASCII +} + +$prevForce2 = if (Test-Path Env:HP_TEST_FORCE_PICKER) { $env:HP_TEST_FORCE_PICKER } else { $null } +$prevPip2 = if (Test-Path Env:HP_SKIP_PIPREQS) { $env:HP_SKIP_PIPREQS } else { $null } +$env:HP_TEST_FORCE_PICKER = '1' +$env:HP_SKIP_PIPREQS = '1' +$overflowLog = '~entry_picker_overflow_bootstrap.log' +$overflowExit = -1 +Push-Location -LiteralPath $overflowDir +try { + cmd /c "call run_setup.bat > $overflowLog 2>&1" + $overflowExit = $LASTEXITCODE +} finally { + Pop-Location + if ($null -eq $prevForce2) { Remove-Item Env:HP_TEST_FORCE_PICKER -ErrorAction SilentlyContinue } else { $env:HP_TEST_FORCE_PICKER = $prevForce2 } + if ($null -eq $prevPip2) { Remove-Item Env:HP_SKIP_PIPREQS -ErrorAction SilentlyContinue } else { $env:HP_SKIP_PIPREQS = $prevPip2 } +} + +$overflowLogPath = Join-Path $overflowDir $overflowLog +$overflowSetupLog = Join-Path $overflowDir '~setup.log' +$overflowLogText = if (Test-Path $overflowLogPath) { Get-Content -LiteralPath $overflowLogPath -Raw -Encoding ASCII } else { '' } +$overflowSetupTxt = if (Test-Path $overflowSetupLog) { Get-Content -LiteralPath $overflowSetupLog -Raw -Encoding ASCII } else { '' } +$overflowCombined = $overflowLogText + "`n" + $overflowSetupTxt + +$overflowMenuAbsent = $overflowCombined -notmatch 'Multiple Python files detected' +# :pick_entry_interactive logs %HP_ENTRY% directly here, unlike :record_chosen_entry's separate +# "Chosen entry:" line -- HP_ENTRY at this point is find_entry.py's own bare-filename stdout +# (os.path.normpath() on a bare name adds no ".\" prefix), so no prefix is expected below. +$overflowLimitLogged = $overflowCombined -match [regex]::Escape('candidates exceed picker limit; keeping a_app.py (alphabetical)') +$overflowTipShown = $overflowCombined -match [regex]::Escape('Tip: to avoid the alphabetical fallback next time') +$overflowChosenDefault = $overflowCombined -match 'Chosen entry:.*a_app\.py' + +$overflowPass = ($overflowExit -eq 0) -and $overflowMenuAbsent -and $overflowLimitLogged -and $overflowTipShown -and $overflowChosenDefault + +Write-NdjsonRow ([ordered]@{ + id='self.entry.picker.overflow' + req='REQ-002' + pass=$overflowPass + desc='More than 9 candidate files skips the numbered menu entirely but still shows Tip guidance, keeping the alphabetical default' + details=[ordered]@{ + exitCode = $overflowExit + menuAbsent = $overflowMenuAbsent + limitLogged = $overflowLimitLogged + tipShown = $overflowTipShown + chosenDefault = $overflowChosenDefault + log = $overflowLog + } +}) + +if (-not $overflowPass) { exit 1 } exit 0 diff --git a/tests/test_ci_cache_selfheal.ps1 b/tests/test_ci_cache_selfheal.ps1 new file mode 100644 index 00000000..37cd09c9 --- /dev/null +++ b/tests/test_ci_cache_selfheal.ps1 @@ -0,0 +1,174 @@ +# ASCII only +# test_ci_cache_selfheal.ps1 - deterministic regression test for tools/ci_cache_selfheal.ps1 +# (the cache-lane self-heal logic, Item 19 follow-on -- docs/agent-closed-backlog.md). +# +# Unlike the ambient `cache` CI lane, which only exercises the corrupted-and-heal path when +# GitHub's own cache happens to be organically corrupted (rare, unpredictable) and is entirely +# swallowed by that lane's job-level continue-on-error either way (see batch-check.yml's CI lane +# gating maturity notes), this test exercises every branch of ci_cache_selfheal.ps1 directly +# against a scratch temp directory, on every single CI run, with no dependency on real cache +# state. Wired into the `real` lane (a GATING lane, not in the job-level continue-on-error list) +# so a regression in the self-heal logic actually fails CI, not just logs a warning nobody sees. +# +# Windows-only: the script under test shells out to `conda.bat` via cmd.exe (a Windows batch +# wrapper) and the locked-directory scenario depends on Windows file-locking semantics, neither +# of which is meaningfully reproducible on Linux. +# +# Five scenarios, all against fake `condabin\conda.bat` stand-ins (never a real Miniconda +# install -- this test is pure logic, no conda/network dependency): +# healthy - conda.bat exits 0 -> exit code 0, directory untouched. +# prefix_healed - conda.bat exits 1, PREFIX match -> exit code 2, directory deleted +# (the ordinary self-heal path). +# exact_hit_corrupted - conda.bat exits 1, EXACT hit -> exit code 1, directory left in place +# (documented, accepted -- cannot self-heal an exact-key blob in place). +# prefix_heal_failed - conda.bat exits 1, PREFIX match, but a held file handle blocks +# deletion (simulates an AV/indexer lock) -> exit code 3, directory +# still present. This is the scenario that would have silently +# regressed back into Item 19's original "always corrupted, never +# self-heals" trap if it went undetected. +# no_binary - no conda.bat present at all -> exit code 0, directory untouched +# (a genuine cache miss, the other branch that shares exit code 0 +# with the healthy scenario above). +param() +$ErrorActionPreference = 'Continue' +$here = $PSScriptRoot +$repo = Split-Path -Path $here -Parent +$script = Join-Path $repo 'tools\ci_cache_selfheal.ps1' +$nd = Join-Path $here '~test-results.ndjson' +$ciNd = Join-Path $repo 'ci_test_results.ndjson' +if (-not (Test-Path $nd)) { New-Item -ItemType File -Path $nd -Force | Out-Null } +if (-not (Test-Path $ciNd)) { New-Item -ItemType File -Path $ciNd -Force | Out-Null } + +function Write-NdjsonRow { + param([hashtable]$Row) + $lane = [Environment]::GetEnvironmentVariable('HP_CI_LANE') + if ($lane -and -not $Row.ContainsKey('lane')) { $Row['lane'] = $lane } + $json = $Row | ConvertTo-Json -Compress -Depth 8 + Add-Content -LiteralPath $nd -Value $json -Encoding Ascii + Add-Content -LiteralPath $ciNd -Value $json -Encoding Ascii +} + +if (-not $IsWindows) { + Write-NdjsonRow ([ordered]@{ + id='self.ci.cache_selfheal'; req='N/A'; pass=$true + desc='ci_cache_selfheal.ps1 regression test (skipped on non-Windows: shells out to conda.bat via cmd.exe, and the locked-directory scenario needs Windows file-locking semantics)' + details=[ordered]@{ skip=$true; reason='non-windows-host' } + }) + exit 0 +} + +function New-FakeCondaDir { + # derived requirement: verify the fixture actually landed on disk before returning -- a + # silently-failed Set-Content here would leave $Dir with no conda.bat, which + # ci_cache_selfheal.ps1 correctly treats as a genuine cache miss (exit 0) -- the SAME exit + # code Scenario 1 expects for a HEALTHY conda.bat, so a silent fixture-creation failure + # would make that scenario pass for the wrong reason instead of failing loudly. + param([string]$Dir, [int]$ExitCode) + if (Test-Path -LiteralPath $Dir) { Remove-Item -LiteralPath $Dir -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -ItemType Directory -Force -Path (Join-Path $Dir 'condabin') | Out-Null + $bat = Join-Path $Dir 'condabin\conda.bat' + Set-Content -LiteralPath $bat -Value "@echo off`r`nexit /b $ExitCode`r`n" -Encoding Ascii + if (-not (Test-Path -LiteralPath $bat)) { + throw "New-FakeCondaDir: fake conda.bat was not created at $bat" + } +} + +$scratchRoot = Join-Path $here '~selftest_cache_selfheal' +if (Test-Path -LiteralPath $scratchRoot) { Remove-Item -LiteralPath $scratchRoot -Recurse -Force } +New-Item -ItemType Directory -Force -Path $scratchRoot | Out-Null + +$allPass = $true + +# --- Scenario 1: healthy --- +$dir1 = Join-Path $scratchRoot 'healthy' +New-FakeCondaDir -Dir $dir1 -ExitCode 0 +& $script -CondaDir $dir1 +$rc1 = $LASTEXITCODE +$pass1 = ($rc1 -eq 0) -and (Test-Path -LiteralPath $dir1) +Write-NdjsonRow ([ordered]@{ + id='self.ci.cache_selfheal.healthy'; pass=$pass1 + desc='ci_cache_selfheal.ps1: a healthy conda.bat exits 0 and is left untouched' + details=[ordered]@{ exitCode=$rc1; dirStillExists=(Test-Path -LiteralPath $dir1) } +}) +if (-not $pass1) { $allPass = $false } + +# --- Scenario 2: prefix match, corrupted, self-heals --- +$dir2 = Join-Path $scratchRoot 'prefix_healed' +New-FakeCondaDir -Dir $dir2 -ExitCode 1 +& $script -CondaDir $dir2 +$rc2 = $LASTEXITCODE +$pass2 = ($rc2 -eq 2) -and (-not (Test-Path -LiteralPath $dir2)) +Write-NdjsonRow ([ordered]@{ + id='self.ci.cache_selfheal.prefix_healed'; pass=$pass2 + desc='ci_cache_selfheal.ps1: a corrupted conda.bat on a restore-keys prefix match self-heals (stale dir deleted)' + details=[ordered]@{ exitCode=$rc2; dirRemoved=(-not (Test-Path -LiteralPath $dir2)) } +}) +if (-not $pass2) { $allPass = $false } + +# --- Scenario 3: exact hit, corrupted, cannot self-heal --- +$dir3 = Join-Path $scratchRoot 'exact_hit' +New-FakeCondaDir -Dir $dir3 -ExitCode 1 +& $script -CondaDir $dir3 -ExactHit +$rc3 = $LASTEXITCODE +$pass3 = ($rc3 -eq 1) -and (Test-Path -LiteralPath $dir3) +Write-NdjsonRow ([ordered]@{ + id='self.ci.cache_selfheal.exact_hit_corrupted'; pass=$pass3 + desc='ci_cache_selfheal.ps1: a corrupted conda.bat on an EXACT key hit cannot self-heal; directory is left in place (documented, accepted gap)' + details=[ordered]@{ exitCode=$rc3; dirStillExists=(Test-Path -LiteralPath $dir3) } +}) +if (-not $pass3) { $allPass = $false } + +# --- Scenario 4: prefix match, corrupted, self-heal FAILS (locked directory) --- +# derived requirement: this is the specific case tools/ci_cache_selfheal.ps1's exit code 3 +# exists for -- if a future edit ever silently swallows a Remove-Item failure and reports exit +# code 2 (healed) instead, this is the assertion that catches it. +$dir4 = Join-Path $scratchRoot 'prefix_heal_failed' +New-FakeCondaDir -Dir $dir4 -ExitCode 1 +$lockedFile = Join-Path $dir4 'locked.txt' +Set-Content -LiteralPath $lockedFile -Value 'lock' -Encoding Ascii +$handle = [System.IO.File]::Open($lockedFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read) +try { + & $script -CondaDir $dir4 + $rc4 = $LASTEXITCODE +} finally { + $handle.Close() +} +$pass4 = ($rc4 -eq 3) -and (Test-Path -LiteralPath $dir4) +Write-NdjsonRow ([ordered]@{ + id='self.ci.cache_selfheal.prefix_heal_failed'; pass=$pass4 + desc='ci_cache_selfheal.ps1: a locked file prevents full directory removal; self-heal correctly reports failure (exit 3) instead of silently proceeding' + details=[ordered]@{ exitCode=$rc4; dirStillExists=(Test-Path -LiteralPath $dir4) } +}) +if (-not $pass4) { $allPass = $false } + +# --- Scenario 5: no conda.bat restored at all (genuine cache miss) --- +# derived requirement: exit code 0 covers two distinct branches in ci_cache_selfheal.ps1 -- a +# healthy conda.bat (Scenario 1) and no conda.bat present at all. Only the former was covered +# above; this closes the gap on the latter. +$dir5 = Join-Path $scratchRoot 'no_binary' +if (Test-Path -LiteralPath $dir5) { Remove-Item -LiteralPath $dir5 -Recurse -Force -ErrorAction SilentlyContinue } +New-Item -ItemType Directory -Force -Path $dir5 | Out-Null +# derived requirement: assert the cache genuinely starts empty -- $scratchRoot is freshly +# recreated at the top of this file, but if a PRIOR interrupted run's teardown left content +# behind AND that top-level recreation also failed to fully clear it, New-Item -Force above +# would silently preserve a stale conda.bat here, making this scenario exercise the wrong +# branch (healthy or corrupted-detect) instead of the no-binary one it's meant to test. +$dir5CondaMain = Join-Path $dir5 'condabin\conda.bat' +$dir5CondaAlt = Join-Path $dir5 'Scripts\conda.bat' +if ((Test-Path -LiteralPath $dir5CondaMain) -or (Test-Path -LiteralPath $dir5CondaAlt)) { + throw "Scenario 5 setup: $dir5 unexpectedly still contains a conda.bat" +} +& $script -CondaDir $dir5 +$rc5 = $LASTEXITCODE +$pass5 = ($rc5 -eq 0) -and (Test-Path -LiteralPath $dir5) +Write-NdjsonRow ([ordered]@{ + id='self.ci.cache_selfheal.no_binary'; pass=$pass5 + desc='ci_cache_selfheal.ps1: no conda.bat restored at all is treated as a genuine cache miss (exit 0, no-op)' + details=[ordered]@{ exitCode=$rc5; dirStillExists=(Test-Path -LiteralPath $dir5) } +}) +if (-not $pass5) { $allPass = $false } + +Remove-Item -LiteralPath $scratchRoot -Recurse -Force -ErrorAction SilentlyContinue + +if (-not $allPass) { exit 1 } +exit 0 diff --git a/tools/ci_cache_selfheal.ps1 b/tools/ci_cache_selfheal.ps1 new file mode 100644 index 00000000..b74a9e61 --- /dev/null +++ b/tools/ci_cache_selfheal.ps1 @@ -0,0 +1,69 @@ +<# +ASCII only. Validates a restored Miniconda cache directory and self-heals a corrupted +restore-keys PREFIX match (the common case -- run_setup.bat hashes into the cache key, which +changes on nearly every PR) by deleting the stale directory so the caller falls through to a +real fresh install + fresh save. An EXACT primary-key hit that's corrupted cannot be +self-healed this way (a GitHub Actions cache blob is immutable once saved under a key) -- +that narrower case still just reports "skip this run." See docs/agent-closed-backlog.md's +Item 19 entry for the full incident/mechanism history this closes. + +Extracted out of .github/workflows/batch-check.yml's "Validate restored conda binary" step so +it can be exercised deterministically by tests/test_ci_cache_selfheal.ps1 on every CI run -- +the ambient `cache` lane only reaches this logic when GitHub's own cache happens to be +organically corrupted, which is rare and unpredictable, and that lane is intentionally +non-gating besides (see CLAUDE.md's CI lane gating maturity notes). + +Exit codes: + 0 = healthy -- conda.bat present and "conda info" succeeded, or no conda.bat restored at + all (a genuine cache miss; nothing to heal, fresh install proceeds normally either way) + 1 = corrupted on an EXACT key hit -- cannot self-heal; caller should skip this run + 2 = corrupted on a PREFIX match -- self-healed (stale directory deleted) + 3 = corrupted on a PREFIX match -- self-heal FAILED (directory could not be fully cleared, + e.g. an AV/indexer file lock); caller should fall back to skip-this-run +#> +param( + [Parameter(Mandatory = $true)][string]$CondaDir, + [switch]$ExactHit +) + +$condaMain = Join-Path $CondaDir 'condabin\conda.bat' +$condaAlt = Join-Path $CondaDir 'Scripts\conda.bat' +$condaBat = if (Test-Path -LiteralPath $condaMain) { $condaMain } elseif (Test-Path -LiteralPath $condaAlt) { $condaAlt } else { $null } + +if ($null -eq $condaBat) { + Write-Host "No conda binary found at $CondaDir; nothing to validate." + exit 0 +} + +$output = & cmd /c "`"$condaBat`" info" 2>&1 +if ($LASTEXITCODE -eq 0) { + Write-Host "Conda health OK: $output" + exit 0 +} + +if ($ExactHit) { + # derived requirement: an EXACT primary-key hit that's corrupted can never be replaced in + # place (a GitHub Actions cache entry's blob is immutable once saved under a key) -- fully + # closing this needs an explicit cache-deletion API call, a smaller follow-on not + # implemented here. Keep the original skip-this-run behavior for this narrow case. + Write-Host "::warning::Conda binary health check failed (exit=$LASTEXITCODE) on an EXACT cache-key hit; cache corrupted, skipping fast-path tests this run." + exit 1 +} + +# derived requirement: a restore-keys PREFIX match is not a guarantee the restored blob is +# still valid. Treat it like a genuine cache miss instead of a hard skip -- delete the stale +# directory and let the caller fall through to a real fresh install and a real fresh save +# under the current key, breaking the self-perpetuating-corruption loop (Item 19). +Write-Host "::warning::Conda binary health check failed (exit=$LASTEXITCODE) on a restore-keys prefix match; deleting stale cache directory and proceeding as a fresh install." +Remove-Item -LiteralPath $CondaDir -Recurse -Force -ErrorAction SilentlyContinue +if (Test-Path -LiteralPath $CondaDir) { + # derived requirement: same AV/indexer file-lock hazard class already documented for + # :try_embed_fallback's own directory swap in run_setup.bat -- if deletion didn't fully + # succeed, do not proceed into an uncertain half-deleted state; fall back to the original, + # safe skip-this-run behavior instead. + Write-Host "::warning::Stale cache directory could not be fully removed (possible file lock); falling back to skip-this-run." + exit 3 +} + +Write-Host "Stale cache directory removed; fresh install will proceed normally." +exit 2 diff --git a/tools/exe_hint_rerun.ps1 b/tools/exe_hint_rerun.ps1 index 5450f65c..071a222a 100644 --- a/tools/exe_hint_rerun.ps1 +++ b/tools/exe_hint_rerun.ps1 @@ -64,10 +64,11 @@ if (-not $exited) { # that spawns a child inheriting the redirected stdout/stderr handles can leave that child # running after $p is killed -- the pipe then never reaches EOF, and an unbounded # ReadToEndAsync().Result would hang forever, defeating the entire point of this bounded - # helper. taskkill /T terminates the whole process tree, not just $p. NOT independently - # verified on real Windows CI that a genuine descendant-holds-the-pipe scenario is fully - # covered by this (no Windows environment available to construct that repro) -- the bounded - # final read below is a second, independent safety net for exactly that residual risk. + # helper. taskkill /T terminates the whole process tree, not just $p. Confirmed on real + # Windows CI (two lanes on PR #410): a grandchild-inherits-the-pipe regression test's + # returncode/timing assertions passed there, meaning the taskkill /T path itself was + # genuinely exercised, not just the fallback below -- the bounded final read still stays as + # a second, independent safety net for whatever a descendant taskkill /T might still miss. try { & taskkill.exe /F /T /PID $p.Id 2>$null 1>$null } catch {} try { $p.Kill() } catch {} }