From 9da53f5066093e8ef69b9a11ad629b021909bc3a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 06:47:38 +0000 Subject: [PATCH 1/3] Item 33: reason= tokens for PyInstaller/Nuitka build-tool failures The three build-exhaustion :die call sites in :run_entry_after_smoke (HP_TEST_FORCE_PYINSTALLER_FAIL simulation, a genuine nonzero PyInstaller exit, and a claimed-success-but-missing dist\.exe) all produced one of two generic [ERROR] messages with no way to distinguish which condition actually fired. Added reason=test_forced_fail / reason=build_error / reason=missing_output tokens, mirroring the existing UV_FALLBACK reason= convention -- no new probing of PyInstaller's own stderr needed, since the three conditions are already distinguishable purely by which code branch reached :die. self.exe.smokerun's NDJSON row turned out not to need a matching change: :run_exe_smokerun exits before ever reaching that row whenever dist\.exe is missing, so no row is emitted for a total build failure in the first place. No tests/harness.ps1 static guard referenced either message string. Updated tests/selfapps_pyinstaller_fail.ps1 to assert the expected reason= token per sub-scenario, and docs/demo-bootstrapper-output.md's Scenario 38a/38b real-CI-capture quotes to include the new token (labeled as a post-capture splice from current source, not a fresh capture). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01A4iE1BRSkUETwz237XeuTW --- CLAUDE.md | 21 ---------------- docs/agent-closed-backlog.md | 39 +++++++++++++++++++++++++++++ docs/demo-bootstrapper-output.md | 25 ++++++++++++------ run_setup.bat | 10 +++++--- tests/selfapps_pyinstaller_fail.ps1 | 25 +++++++++++------- 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 30ca782c..c1a913ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -544,27 +544,6 @@ is not. that can collapse without losing accuracy or completeness, and cut it. Do not sacrifice correctness or completeness for terseness -- the goal is removing WORDS, not INFORMATION. -- **Item 33: classify PyInstaller/Nuitka build-tool failures with a `reason=`-style token, not - just a plain `[ERROR]` message.** CodeRabbit review finding on PR #423 (`docs/demo- - bootstrapper-output.md` lines 2977-2980, citing AGENTS.md's "external/environmental failures ... - make them legible in logs/NDJSON ... emit a `reason=` token" guideline). Verified: `run_setup.bat` - already uses `reason=` tokens for several failure classes (`UV_FALLBACK reason=dep_install_ - failed`, Miniconda AllUsers install `reason=timeout`/`reason=installer_failed`, `pipreqs.install`'s - NDJSON `reason` field), but the PyInstaller/Nuitka Tier A build-failure call sites - (`:die "[ERROR] PyInstaller execution failed."` / `"[ERROR] PyInstaller did not produce - dist\%ENVNAME%.exe"`, inside `:run_entry_after_smoke`) and `self.exe.smokerun`'s own NDJSON row - (emitted from `:smokerun_ndjson`, `exitCode` only) do not carry an equivalent classification -- - a real user hitting a genuine - PyInstaller crash, an AV lock, disk-full, or a plain compile error all currently produce the - identical generic message. NOT implemented in PR #423 (docs-only housekeeping PR; this needs a - design pass over which failure signatures are actually distinguishable and worth a separate - `reason=` value, plus updating `docs/agent-ndjson.md`'s row registry and probably - `tests/harness.ps1`'s static guards -- a real feature slice, not a docs fix). Note the AGENTS.md - guideline this cites is scoped to CI-lane self-test legibility (avoiding a flaky external - download being mistaken for a repo/test regression); whether the same treatment is warranted for - `run_setup.bat`'s own real-user-facing build-failure messages is part of what this item needs to - resolve, not something to assume. - ## Cold Storage (promising ideas, deliberately shelved -- revisit only if a named trigger fires) Moved to `docs/agent-cold-storage.md` (2026-07-31, to reduce this file's per-session context diff --git a/docs/agent-closed-backlog.md b/docs/agent-closed-backlog.md index e30ce497..3ab3315f 100644 --- a/docs/agent-closed-backlog.md +++ b/docs/agent-closed-backlog.md @@ -1815,6 +1815,45 @@ this belongs to). no_python_files`, `pyFiles == 0`, `exitCode == 0` (identical to the plain empty-folder case) AND the hint text appears in the bootstrap log. Registered in `docs/agent-ndjson.md`. +### Item 33 (closed 2026-08-09) + +- **`reason=` tokens for the PyInstaller/Nuitka Tier A build-exhaustion `:die` call sites** + (`:run_entry_after_smoke`), a CodeRabbit finding on PR #423 not implemented there since it + needed its own design pass. Design finding: the three call sites correspond to three genuinely + distinguishable, non-overlapping trigger conditions (no probing of PyInstaller's own stderr + needed -- distinguishable purely from which code branch reached `:die`), so each got its own + token, mirroring the existing `UV_FALLBACK reason=` convention exactly (which already + has a `reason=test_forced_fail` precedent for the identical "this is a CI test hook, not a real + failure" distinction): + - `reason=test_forced_fail` -- `HP_TEST_FORCE_PYINSTALLER_FAIL` simulated the failure; no real + PyInstaller invocation ever ran. + - `reason=build_error` -- a genuine PyInstaller invocation returned a nonzero exit code. Real + code, but not exercised by any deterministic CI hook (same "extrapolated, not tested" status + as a real Nuitka compiler failure elsewhere in this repo -- see the "Reactive-only failure + hint" section, Part VIII). + - `reason=missing_output` -- the build reported success (exit 0) but `dist\.exe` never + appeared (`HP_TEST_FORCE_OUTPUT_VANISH` in CI; a real AV/indexer deletion in production -- + same underlying condition either way, so the same token covers both). + - `self.exe.smokerun`'s own NDJSON row turned out not to need a matching change: `:run_exe_ + smokerun` exits immediately (`exit /b 0`, no row emitted) whenever `dist\.exe` is + missing, so that row is never emitted at all for a total build failure -- there was no row to + add a `reason` field to. The two `:die` messages' own `reason=` tokens (now in `%LOG%`) are + the actual legibility improvement. + - No `tests/harness.ps1` static guard referenced either message string (confirmed via grep), so + no harness change was needed, contrary to Item 33's own speculation. + - Updated `tests/selfapps_pyinstaller_fail.ps1`'s `self.exe.build.xfail` scenario to assert the + expected `reason=` token per sub-scenario (`execfail`/`execfail_runtimefail` -> + `test_forced_fail`, `output_vanish` -> `missing_output`) in addition to its existing message + check. + - Updated `docs/demo-bootstrapper-output.md`'s Scenario 38a/38b real-CI-capture quotes to + include the new token (spliced from current `run_setup.bat` source, labeled honestly as a + post-capture splice rather than a fresh capture) so they stay accurate to current source. + - AGENTS.md's own `reason=` guideline is scoped to CI-lane self-test legibility (avoiding a + flaky external failure being mistaken for a repo/test regression) -- this change extends the + same pattern to `run_setup.bat`'s real-user-facing build-failure messages too, since the two + genuinely differ in actionability (`missing_output` suggests an AV/indexer conflict a user + could investigate; `build_error`/`test_forced_fail` do not) and the token costs nothing to add. + ## Closed Backlog - **Cascade-vs-postexec fix (Active Backlog item 9), 2026-07-25, owner-directed follow-up to a diff --git a/docs/demo-bootstrapper-output.md b/docs/demo-bootstrapper-output.md index 8b00b51d..6ad6744d 100644 --- a/docs/demo-bootstrapper-output.md +++ b/docs/demo-bootstrapper-output.md @@ -3107,15 +3107,18 @@ one's console text, since it's really a REQ-027 demo). The first two additionall #### 38a. `execfail` -- the PyInstaller build command itself fails -Real CI capture, run `29788624195`, job `88506013028` ("real" lane). **This scenario is triggered -by two CI test hooks (forcing both the PyInstaller build and the Nuitka fallback to fail), but a -real user hitting the same genuine failure sees nothing at either trigger point** -- both -PyInstaller's and Nuitka's own error output go only to the log file, never the console: +Real CI capture, run `29788624195`, job `88506013028` ("real" lane), with the `reason=` +token (CLAUDE.md Item 33, added after that capture) spliced in verbatim from `run_setup.bat`'s +current literal source -- the rest of the block is unmodified real console output. **This +scenario is triggered by two CI test hooks (forcing both the PyInstaller build and the Nuitka +fallback to fail), but a real user hitting the same genuine failure sees nothing at either +trigger point** -- both PyInstaller's and Nuitka's own error output go only to the log file, +never the console: ``` [INFO] Building standalone executable -- this may take a minute or two... [INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). -[ERROR] PyInstaller execution failed. +[ERROR] PyInstaller execution failed. reason=test_forced_fail [DEBUG] warnfix: warn file not found [INFO] PyInstaller build artifacts cleaned up. [WARN] EXE smokerun: dist\.exe not found; skipping @@ -3126,6 +3129,13 @@ PyInstaller's and Nuitka's own error output go only to the log file, never the c [INFO] REQ-018: post-execution checkpoint (interpreter): declined (run footprint stays at one execution). ``` +The `reason=test_forced_fail` token (CLAUDE.md Item 33) reflects the actual trigger here -- this +scenario reaches `:die` via the `HP_TEST_FORCE_PYINSTALLER_FAIL` test hook, never a genuine +PyInstaller error. A real, unforced build failure would instead read `reason=build_error` -- +`[Extrapolated Branch]`, since no deterministic CI hook forces a genuine nonzero PyInstaller exit +(the same "real trigger, no CI hook" situation as Tier A's own Nuitka compiler-failure hint, Part +VIII above). + 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 @@ -3135,7 +3145,8 @@ interpreter run ALSO fails. #### 38b. `output_vanish` -- PyInstaller succeeds, then the EXE disappears immediately -Real CI capture, same run/job as 38a. **Unlike the test hooks elsewhere in this file, the +Real CI capture, same run/job as 38a, with the same post-capture `reason=` splice described +there. **Unlike the test hooks elsewhere in this file, the underlying trigger here stands in for a real external event, not a purely internal simulation** -- a real user could hit this exact same gap if antivirus software or a file indexer deletes the freshly-built EXE in the instant right after PyInstaller creates it. Neither the deletion itself @@ -3146,7 +3157,7 @@ code path that announces either one, so both are omitted below (see 38a's note f ``` [INFO] Building standalone executable -- this may take a minute or two... [INFO] Standard build did not complete; attempting a fallback build (this may take a minute or two). -[ERROR] PyInstaller did not produce dist\.exe +[ERROR] PyInstaller did not produce dist\.exe reason=missing_output [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. diff --git a/run_setup.bat b/run_setup.bat index ce05e245..bf04d5d9 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -3457,7 +3457,7 @@ if not defined HP_BUILD_OK ( call :log "[TEST] HP_TEST_FORCE_PYINSTALLER_FAIL: simulating PyInstaller build failure." call :try_nuitka_tier_a if errorlevel 1 ( - call :die "[ERROR] PyInstaller execution failed." + call :die "[ERROR] PyInstaller execution failed. reason=test_forced_fail" set "HP_BOOTSTRAP_STATE=error" ) else ( set "HP_NUITKA_FALLBACK_USED=1" @@ -3467,7 +3467,11 @@ if not defined HP_BUILD_OK ( if errorlevel 1 ( call :try_nuitka_tier_a if errorlevel 1 ( - call :die "[ERROR] PyInstaller execution failed." + rem CLAUDE.md Item 33: reason=build_error means PyInstaller's own process exited + rem nonzero -- distinct from reason=missing_output below (exit 0 but no EXE) and + rem reason=test_forced_fail above (no real build ever ran). Mirrors the existing + rem UV_FALLBACK reason= token convention; see docs/agent-lessons-learned.md. + call :die "[ERROR] PyInstaller execution failed. reason=build_error" set "HP_BOOTSTRAP_STATE=error" ) else ( set "HP_NUITKA_FALLBACK_USED=1" @@ -3480,7 +3484,7 @@ if not defined HP_BUILD_OK ( if not exist "dist\%ENVNAME%.exe" ( call :try_nuitka_tier_a if errorlevel 1 ( - call :die "[ERROR] PyInstaller did not produce dist\%ENVNAME%.exe" + call :die "[ERROR] PyInstaller did not produce dist\%ENVNAME%.exe reason=missing_output" set "HP_BOOTSTRAP_STATE=error" ) else ( set "HP_NUITKA_FALLBACK_USED=1" diff --git a/tests/selfapps_pyinstaller_fail.ps1 b/tests/selfapps_pyinstaller_fail.ps1 index 05e2d7ba..eb3024b8 100644 --- a/tests/selfapps_pyinstaller_fail.ps1 +++ b/tests/selfapps_pyinstaller_fail.ps1 @@ -138,6 +138,12 @@ try { $expectedMsg = if ($scenario -eq 'execfail' -or $scenario -eq 'execfail_runtimefail') { 'PyInstaller execution failed' } else { 'PyInstaller did not produce dist' } $expectedMsgFound = $combined -match [regex]::Escape($expectedMsg) $testHookFired = $combined -match [regex]::Escape('HP_TEST_FORCE') + # CLAUDE.md Item 33: both scenarios here reach the :die call via HP_TEST_FORCE_PYINSTALLER_FAIL + # or HP_TEST_FORCE_OUTPUT_VANISH, never a genuine nonzero PyInstaller exit (reason=build_error + # is real code but not exercised by any deterministic CI hook -- same "extrapolated, not tested" + # status as a real Nuitka compiler failure elsewhere in this repo). + $expectedReason = if ($scenario -eq 'execfail' -or $scenario -eq 'execfail_runtimefail') { 'reason=test_forced_fail' } else { 'reason=missing_output' } + $expectedReasonFound = $combined -match [regex]::Escape($expectedReason) # docs/open-questions.md item 1: when packaging fails outright but the interpreter fallback # still runs cleanly, :print_no_exe_briefing (run_setup.bat) prints a dedicated panel instead # of leaving the bare "[STATUS] Run Status: SUCCESS" line as the only thing the user sees. @@ -165,17 +171,18 @@ try { # "graceful stop" contract for this class of failure -- see selfapps_preflight.ps1's own # $pass condition, the sibling test for the pre-existing HP_BOOTSTRAP_STATE=error precedent, # which likewise never checks the process exit code). - $xfailPass = $testHookFired -and $expectedMsgFound -and ($statusState -eq 'error') -and $noExeBriefingFound + $xfailPass = $testHookFired -and $expectedMsgFound -and $expectedReasonFound -and ($statusState -eq 'error') -and $noExeBriefingFound Write-PyiFailRow -Pass $xfailPass -Desc "PyInstaller build XFAIL ($scenario): build failure correctly reported, not masked as success" -Details ([ordered]@{ - scenario = $scenario - bootstrapExit = $runExit - testHookFired = [bool]$testHookFired - expectedMsgFound = [bool]$expectedMsgFound - statusState = $statusState - noExeBriefingFound = [bool]$noExeBriefingFound - xfailPass = $xfailPass - log = $bootstrapLog + scenario = $scenario + bootstrapExit = $runExit + testHookFired = [bool]$testHookFired + expectedMsgFound = [bool]$expectedMsgFound + expectedReasonFound = [bool]$expectedReasonFound + statusState = $statusState + noExeBriefingFound = [bool]$noExeBriefingFound + xfailPass = $xfailPass + log = $bootstrapLog }) } finally { if ($null -eq $prevSkipPipreqs) { Remove-Item Env:HP_SKIP_PIPREQS -ErrorAction SilentlyContinue } else { $env:HP_SKIP_PIPREQS = $prevSkipPipreqs } From bf20001b3562ec22a16dce180853b332c9b940eb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 06:54:00 +0000 Subject: [PATCH 2/3] Match reason= token to the specific PyInstaller error line, not the whole log CodeRabbit review: the reason= assertion matched anywhere in the combined bootstrap log, but this scenario also forces the Nuitka fallback to fail -- a whole-log match could in principle be satisfied by an unrelated line rather than genuinely proving the PyInstaller [ERROR] line itself carries the right token. Now scopes the match to log lines containing the expected [ERROR] message text first. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01A4iE1BRSkUETwz237XeuTW --- tests/selfapps_pyinstaller_fail.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/selfapps_pyinstaller_fail.ps1 b/tests/selfapps_pyinstaller_fail.ps1 index eb3024b8..d1fe15dd 100644 --- a/tests/selfapps_pyinstaller_fail.ps1 +++ b/tests/selfapps_pyinstaller_fail.ps1 @@ -143,7 +143,12 @@ try { # is real code but not exercised by any deterministic CI hook -- same "extrapolated, not tested" # status as a real Nuitka compiler failure elsewhere in this repo). $expectedReason = if ($scenario -eq 'execfail' -or $scenario -eq 'execfail_runtimefail') { 'reason=test_forced_fail' } else { 'reason=missing_output' } - $expectedReasonFound = $combined -match [regex]::Escape($expectedReason) + # Match the reason token on the SAME log line as $expectedMsg, not anywhere in the combined + # log -- this scenario also forces the Nuitka fallback to fail, so a whole-log match could in + # principle be satisfied by an unrelated line rather than genuinely proving the PyInstaller + # [ERROR] line itself carries the right token. + $expectedFailureLines = @($logLines | Where-Object { $_ -match [regex]::Escape($expectedMsg) }) + $expectedReasonFound = [bool]($expectedFailureLines | Where-Object { $_ -match [regex]::Escape($expectedReason) } | Select-Object -First 1) # docs/open-questions.md item 1: when packaging fails outright but the interpreter fallback # still runs cleanly, :print_no_exe_briefing (run_setup.bat) prints a dedicated panel instead # of leaving the bare "[STATUS] Run Status: SUCCESS" line as the only thing the user sees. From 9c6954eb842fb718935708b6626ec504d784fe63 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 06:57:53 +0000 Subject: [PATCH 3/3] Tag the same-line reason-match constraint as a derived requirement CodeRabbit review: use the repo's established "# derived requirement: " comment format for this non-obvious test constraint, matching the style guide convention. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01A4iE1BRSkUETwz237XeuTW --- tests/selfapps_pyinstaller_fail.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/selfapps_pyinstaller_fail.ps1 b/tests/selfapps_pyinstaller_fail.ps1 index d1fe15dd..f7b6eac9 100644 --- a/tests/selfapps_pyinstaller_fail.ps1 +++ b/tests/selfapps_pyinstaller_fail.ps1 @@ -143,10 +143,10 @@ try { # is real code but not exercised by any deterministic CI hook -- same "extrapolated, not tested" # status as a real Nuitka compiler failure elsewhere in this repo). $expectedReason = if ($scenario -eq 'execfail' -or $scenario -eq 'execfail_runtimefail') { 'reason=test_forced_fail' } else { 'reason=missing_output' } - # Match the reason token on the SAME log line as $expectedMsg, not anywhere in the combined - # log -- this scenario also forces the Nuitka fallback to fail, so a whole-log match could in - # principle be satisfied by an unrelated line rather than genuinely proving the PyInstaller - # [ERROR] line itself carries the right token. + # derived requirement: match the reason token on the SAME log line as $expectedMsg, not + # anywhere in the combined log -- this scenario also forces the Nuitka fallback to fail, so a + # whole-log match could in principle be satisfied by an unrelated line rather than genuinely + # proving the PyInstaller [ERROR] line itself carries the right token. $expectedFailureLines = @($logLines | Where-Object { $_ -match [regex]::Escape($expectedMsg) }) $expectedReasonFound = [bool]($expectedFailureLines | Where-Object { $_ -match [regex]::Escape($expectedReason) } | Select-Object -First 1) # docs/open-questions.md item 1: when packaging fails outright but the interpreter fallback