From b63b5b8f23cb2b0438ecbc6cbc71077013dfbaa7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 01:32:53 +0000 Subject: [PATCH 1/8] Item 46 Bucket A: Batches 2/3/4(partial)/5 -- redundant die-cascade fixes + shared test hook Continues the same session's Batch 1 work with a full per-site trace of the remaining sites, implementing everything that traced out as a genuine "falls through into a second, redundant call :die" bug, and explicitly deferring what traced out as merely harmless wasted work: - Batch 2 (1252, :conda_create_done's "python.exe missing" check, non-cascade path): goto :after_env_mode_selection right after call :die, skipping the misleading "[BOOT] ... Selected Python provider: Conda (Portable)." success line. - Batch 3 (1362, first of :determine_entry's two per-run calls): goto :after_env_bootstrap, skipping the entire dependency-install/pipreqs/warnfix block when entry detection's own mechanism is confirmed broken. Verified safe by reading :determine_entry's own body directly. - Batch 4, scope corrected from 7 sites to 1 (1275, ~condarc staging) after individually tracing all 7 -- the other 6 fall through into benign, silently-degraded continuations (no cascade, no misleading message), reclassified alongside the already-deferred 1334 sink rather than fixed. - Batch 5 (2011, :ci_skip_entry's ~find_entry.py staging): goto :after_env_bootstrap. Its sibling (2014) confirmed already benign. Also documents a genuine pre-existing quirk found along the way: :after_env_skip writes state=ok unconditionally regardless of an earlier call :die (near-zero exposure, not fixed here). - Batch 6 traced further (a second :try_conda_install call site found inside :cascade_acquire_conda) -- remains deferred, Batch 1's own fix already shrank its value. New shared test hook: HP_TEST_FORCE_EMIT_FAIL= on :emit_from_base64 itself, so any embedded-helper write (present or future) can have its own failure simulated deterministically. New tests/selfapps_die_emit_fallthrough.ps1 (4 scenarios, one shared literal NDJSON id per the established multi-scenario convention) plus CI wiring in batch-check.yml. Full trace, corrected classifications, and implementation status: docs/plan-die-fatal-remediation.md's "Implementation Status" section. Not pushed yet -- holding per instruction to avoid disrupting other in-flight CI from concurrent active-backlog work. When pushed, this should split into separate PRs per batch (Batch 1 is already its own commit; this commit covers Batches 2/3/4/5 together and should be split at that point), preserving the one-batch-at-a-time landing discipline. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TcuphCHEGB2RiCYLjFjMN6 --- .github/workflows/batch-check.yml | 53 ++++ CLAUDE.md | 14 + docs/agent-interconnect.md | 28 ++ docs/agent-ndjson.md | 52 ++++ docs/plan-die-fatal-remediation.md | 124 ++++++++- run_setup.bat | 25 +- tests/selfapps_die_emit_fallthrough.ps1 | 328 ++++++++++++++++++++++++ 7 files changed, 609 insertions(+), 15 deletions(-) create mode 100644 tests/selfapps_die_emit_fallthrough.ps1 diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index b99cbdc9..1408cc09 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -374,6 +374,19 @@ jobs: run: | & tests\selfapps_isolation.ps1 + # CLAUDE.md Active Backlog Item 46 Bucket A Batch 5: :ci_skip_entry's own + # ~find_entry.py staging failure used to fall through call :die into a redundant second + # call :die ("find_entry helper syntax error") for the same root cause. Cheap + # (HP_CI_SKIP_ENV path, no conda/uv needed) so runs in every lane, matching the + # HP_CI_SKIP_ENV-based steps directly above. + - name: "Self-test: die-emit-fallthrough ci_skip_entry (CI-only)" + if: ${{ !cancelled() && env.HP_CACHE_CORRUPTED != '1' }} + env: + DIE_EMIT_SCENARIO: ci_skip_entry + shell: pwsh + run: | + & tests\selfapps_die_emit_fallthrough.ps1 + - name: "Self-test: env-name sanitization (leading hyphen)" if: ${{ !cancelled() && env.HP_CACHE_CORRUPTED != '1' }} shell: pwsh @@ -1105,6 +1118,46 @@ jobs: run: | & tests\selfapps_entrysmoke_no_interpreter.ps1 + # CLAUDE.md Active Backlog Item 46 Bucket A Batch 2: :conda_create_done's own + # "python.exe missing" check (the NON-cascade path, distinct from selfapps_cascade_ + # conda_create_fail.ps1's own missing_python scenario) used to fall through call :die into + # the rest of its body, logging a misleading "[BOOT] ... Selected Python provider: Conda + # (Portable)." success line right after the error. Self-contained (own + # HP_FORCE_CONDA_ONLY=1 + HP_TEST_FORCE_CONDA_MISSING_PYTHON=1), gating from first landing + # (same reasoning as the step directly above). + - name: "Self-test: die-emit-fallthrough missing_python (conda-full only)" + if: ${{ !cancelled() && matrix.mode == 'conda-full' && steps.conda_avail.outputs.available == 'true' }} + env: + DIE_EMIT_SCENARIO: missing_python + shell: pwsh + run: | + & tests\selfapps_die_emit_fallthrough.ps1 + + # CLAUDE.md Active Backlog Item 46 Bucket A Batch 4: :conda_create_done's own .condarc + # staging failure used to fall through call :die into a doomed copy attempt, triggering a + # SECOND, redundant call :die plus the same misleading success line as the scenario above. + # New hook: HP_TEST_FORCE_EMIT_FAIL= (run_setup.bat, :emit_from_base64) fails one + # specific embedded-helper write deterministically. + - name: "Self-test: die-emit-fallthrough condarc (conda-full only)" + if: ${{ !cancelled() && matrix.mode == 'conda-full' && steps.conda_avail.outputs.available == 'true' }} + env: + DIE_EMIT_SCENARIO: condarc + shell: pwsh + run: | + & tests\selfapps_die_emit_fallthrough.ps1 + + # CLAUDE.md Active Backlog Item 46 Bucket A Batch 3: :determine_entry runs twice per normal + # bootstrap; a genuine failure on the FIRST call used to fall through call :die into the + # entire dependency-install/pipreqs/warnfix block (real work, not just a redundant message) + # before reproducing the identical failure at the second call site. + - name: "Self-test: die-emit-fallthrough determine_entry (conda-full only)" + if: ${{ !cancelled() && matrix.mode == 'conda-full' && steps.conda_avail.outputs.available == 'true' }} + env: + DIE_EMIT_SCENARIO: determine_entry + shell: pwsh + run: | + & tests\selfapps_die_emit_fallthrough.ps1 + - name: "Test pandas/openpyxl heuristic (conda-full only)" if: ${{ !cancelled() && matrix.mode == 'conda-full' && steps.conda_avail.outputs.available == 'true' }} shell: pwsh diff --git a/CLAUDE.md b/CLAUDE.md index b0c3e49e..0a2a3eb7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1129,6 +1129,20 @@ way (no live Windows execution available here), that is noted explicitly rather a time, full 8-lane matrix CI proof to completion before the next batch starts, no blanket sweep across every remaining site in one PR. + **Implemented locally 2026-08-21 (Batches 1/2/3/5 and one site of Batch 4), NOT YET PUSHED -- + held per explicit instruction to avoid disrupting concurrent CI from other active-backlog work.** + Batch 4's scope corrected from 7 sites to 1 (1275 only -- the other 6 traced individually and + found to fall through into benign, silently-degraded continuations, not a redundant-`:die` + cascade; reclassified alongside the 1334 sink, not fixed). Batch 6 traced further (a second + `:try_conda_install` call site found inside `:cascade_acquire_conda`) and remains deferred -- + Batch 1's own fix already shrinks its residual value to "one fewer redundant pause in an already- + rare scenario." New shared test hook `HP_TEST_FORCE_EMIT_FAIL=` added to + `:emit_from_base64` itself. New test file `tests/selfapps_die_emit_fallthrough.ps1` (4 scenarios: + `missing_python`/`condarc`/`ci_skip_entry`/`determine_entry`) also surfaced a genuine, pre- + existing, unfixed quirk: `:after_env_skip` writes `state=ok` unconditionally regardless of an + earlier `call :die` in the same run (near-zero exposure, `HP_CI_SKIP_ENV` is test-infrastructure- + only). Full detail: `docs/plan-die-fatal-remediation.md`'s "Implementation Status" section. + ## 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-interconnect.md b/docs/agent-interconnect.md index 530e6d74..6be7390d 100644 --- a/docs/agent-interconnect.md +++ b/docs/agent-interconnect.md @@ -123,6 +123,21 @@ doesn't leave stale locks for other users of the same folder. --- +## `:emit_from_base64` (shared by ~10+ call sites) now carries a test-injection hook -- any future call site inherits it automatically + +CLAUDE.md Active Backlog Item 46 Bucket A Batches 2/3/4/5 (`docs/plan-die-fatal-remediation.md`) +added `HP_TEST_FORCE_EMIT_FAIL=` to `:emit_from_base64` itself (the one subroutine every +embedded-helper write in the file funnels through -- `~detect_python.py`, `~condarc`, +`~print_pyver.py`, `~prep_requirements.py`, `~find_entry.py`, `~detect_visa.py`, and any future +addition). Checked FIRST, before the real PowerShell write: if defined and equal to the CALL's own +`%VAR%` argument, logs a `[TEST]` line and `exit /b 1` immediately, simulating a genuine disk-write/ +AV-lock failure for that ONE payload without touching any other payload emitted in the same run. +REQ-019-compliant (absence changes nothing). Any future new embedded helper (a new `HP_*` payload +variable) automatically gains this same test-injection capability with zero additional wiring -- +worth knowing before adding a bespoke failure-simulation mechanism for a new helper's write step. + +--- + ## AV-Safe Build Path Tier A (`:try_nuitka_tier_a`) and hidden-import auto-recovery **Touch either subroutine, must understand the other.** `:try_nuitka_tier_a` (AV-Safe Build Path @@ -1096,6 +1111,19 @@ fall-through blast radius as a side effect, even before Batch 6 itself is touche this fix) now asserts the resulting cascade collapses to exactly this chain's first site plus the sink, not the full 5-6-pause worst case. +**Status: implemented 2026-08-21 (local commit, not yet pushed).** A second `:try_conda_install` +call site was found while tracing Batch 6 (the `:tci_both_failed` failure sites themselves) -- +`:cascade_acquire_conda` (the REQ-009 uv-to-conda cascade's own on-demand Miniconda acquisition, +~line 2302) calls it independently of the main install-if-missing block (~line 884). Its OWN +fall-through does NOT re-enter this probe chain at all (that chain is specific to the first-attempt, +non-cascade path, reached only from the ~884 call site) -- it proceeds fairly directly to a +conda-create attempt that already `goto`s correctly (slice 1, pre-existing). This means Batch 1's +fix only reduces THIS section's own described blast-radius reduction for the FIRST call site; +`docs/plan-die-fatal-remediation.md`'s "Implementation Status" section has the full trace for both +call sites and why Batch 6 (a caller-side coordination flag) remains deferred as a result -- its +now-confirmed remaining value is "one fewer redundant pause in an already-rare scenario," smaller +than originally scoped. + Regression test: `tests/selfapps_entrysmoke_no_interpreter.ps1` (`self.entrysmoke.no_interpreter_ guard`, `conda-full` lane) -- asserts `[ERROR] python.exe missing from conda environment.` is ABSENT (proving the second `:handle_conda_failure` call is skipped) and the new diff --git a/docs/agent-ndjson.md b/docs/agent-ndjson.md index 57e462a6..8ad626b5 100644 --- a/docs/agent-ndjson.md +++ b/docs/agent-ndjson.md @@ -1275,6 +1275,58 @@ self.entrysmoke.no_interpreter_guard --- +## selfapps-die-emit-fallthrough NDJSON rows (selfapps_die_emit_fallthrough.ps1, mixed lanes, GATING) + +CLAUDE.md Active Backlog Item 46 Bucket A, Batches 2/3/4/5 (`docs/plan-die-fatal-remediation.md`'s +"Batch Roadmap" -- the full 20-site trace). Four scenarios (`DIE_EMIT_SCENARIO` env var): +`missing_python` (Batch 2, `:conda_create_done`'s own "python.exe missing" check, the NON-cascade +path -- distinct from `self.cascade.conda_create_fail`'s own `missing_python` scenario, a REQ-009 +cascade RE-ENTRY that never reaches this site at all), `condarc` (Batch 4, the same subroutine's +`.condarc`-staging failure), `ci_skip_entry` (Batch 5, `:ci_skip_entry`'s own `~find_entry.py` +staging failure), `determine_entry` (Batch 3, `:determine_entry`'s first of its two per-run calls). +All four emit the SAME literal id, `self.die_emit_fallthrough`, with `details.scenario` as the +discriminator -- matches `self.exe.build.xfail`'s own established multi-scenario precedent (see +`docs/agent-lessons-learned.md`'s "A multi-scenario PowerShell test's NDJSON `id` must stay a +literal string" entry for why a per-scenario id computed into a shared variable would silently +break `tools/check_ndjson_registry.py`'s static scan). + +New hook: `HP_TEST_FORCE_EMIT_FAIL=` (`run_setup.bat`, `:emit_from_base64`) fails ONE +specific embedded-helper write deterministically (matched against the subroutine's own `%VAR%` +argument, e.g. `HP_CONDARC` or `HP_FIND_ENTRY`) without touching any other payload emitted in the +same run -- used by `condarc`/`ci_skip_entry`/`determine_entry`. `missing_python` instead reuses +the already-existing `HP_TEST_FORCE_CONDA_MISSING_PYTHON` hook (see `self.cascade.conda_create_ +fail`'s own header comment), just without any cascade env vars set. + +Three of the four (`condarc`/`ci_skip_entry`/`determine_entry`) set `HP_SKIP_ENTRY_SMOKE=1` / +`HP_SKIP_EXE_SMOKERUN=1` -- purely about setup-flow fall-through, not a full PyInstaller build/run +cycle. `missing_python` deliberately does NOT set these: the point is letting `:run_entry_smoke` +actually run so CLAUDE.md Active Backlog Item 45's own "if not exist `%HP_PY%`" guard (already +proven by `self.entrysmoke.no_interpreter_guard` for a different trigger) gets a chance to catch +the broken-but-defined interpreter and confirms no PyInstaller build is attempted against it. + +`ci_skip_entry` documents a real, pre-existing, NOT-fixed-by-this-change quirk found while writing +its own assertions: `:after_env_skip` (the label `:after_env_bootstrap` routes to under +`HP_CI_SKIP_ENV=1`) calls `call :write_status ok 0 %PYCOUNT%` unconditionally -- it does not read +`HP_BOOTSTRAP_STATE`, so this scenario's own status file reads `state=ok` even though a real +`call :die` fired earlier in the same run. This is not a regression from the Batch 5 fix (the OLD, +unfixed fall-through already reached this same unconditional-ok write further downstream, just +after more wasted work) -- asserted as the known, unchanged behavior, not silently ignored. Near- +zero real-world exposure either way (`HP_CI_SKIP_ENV` is test-infrastructure-only, never set by a +real user or the default bootstrap path). + +Lane: `conda-full` only for `missing_python`/`condarc`/`determine_entry` (real conda create +needed, gated on `steps.conda_avail.outputs.available == 'true'`, same gate 28+ other conda-full- +only self-tests use); every lane for `ci_skip_entry` (no conda/uv dependency at all, matches +`selfapps_isolation.ps1`'s own sibling `HP_CI_SKIP_ENV`-based steps). Gating from first landing -- +deterministic and self-contained, same reasoning as `self.entrysmoke.no_interpreter_guard`'s own +precedent. + +``` +self.die_emit_fallthrough +``` + +--- + ## Key facts for debugging missing rows - `self.exe.hidden_import.exhaust` (CLAUDE.md Active Backlog item 11, `tests/selfapps_ diff --git a/docs/plan-die-fatal-remediation.md b/docs/plan-die-fatal-remediation.md index 5fb4fa11..0430e25f 100644 --- a/docs/plan-die-fatal-remediation.md +++ b/docs/plan-die-fatal-remediation.md @@ -3,9 +3,12 @@ **Status:** Research pass complete (2026-08-18); maintainer decision recorded and full 20-site trace completed (2026-08-21, see Finding 3 and "Batch Roadmap" below) -- grounded directly against current `run_setup.bat` source via `tools/audit_batch_exit_paths.py` and manual tracing, not -written from memory or inference. Batch 1 (the conda-acquisition-probe chain) is the next slice to -land. See CLAUDE.md's Item 46 entry for the full incident history (Bucket B, closed; Bucket A -slice 1, closed) this plan picks up from. +written from memory or inference. **Batch 1 is merged** (PR #468). Batches 2, 3, 5, and one site of +Batch 4 (the `Could not stage ~condarc` site) are implemented, with regression tests and CI wiring, +landing as a follow-on PR stacked on Batch 1. Batch 6 and the "Active Python interpreter not +resolved" sink remain deferred, per "Batch Roadmap" below. See CLAUDE.md's Item 46 entry for the +full incident history (Bucket B, closed; Bucket A slice 1, closed) this plan picks up from, and +"Batch Roadmap" below for exactly what's implemented vs. what remains. **Owner:** Supervisor (Python_vs_Windows) **Related:** CLAUDE.md Active Backlog Item 46, `docs/agent-lessons-learned.md`'s `:die` entry, `docs/agent-interconnect.md`'s "Genuine (non-cascade) conda-create exhaustion" section, PR #437 @@ -267,17 +270,26 @@ note:** - **Batch 1** (conda-acquisition-probe chain, 5 sites) lands first -- highest realistic exposure (a real, if uncommon, "conda never acquired" production scenario), lowest risk (proven shape, existing test already reaches it). - - **Batch 2** (`:conda_create_done`'s "python.exe missing" check) lands next -- low risk, fixes + - **Batch 2** (`:conda_create_done`'s "python.exe missing" check) landed next -- low risk, fixes a genuinely misleading message. - - **Batch 4** (7 write-failure sites) and **Batch 5** (2 CI-only sites, both inside - `:ci_skip_entry`) land after, in either order -- both low risk, low urgency, mechanical once - traced. + - **Batch 4/5** landed after -- see "Implementation Status" below: Batch 4 turned out to need a + per-site trace of "does skipping intervening code actually cascade into a second `:die`, or is + it just benign wasted work" before committing to a fix, not a mechanical goto sweep as + originally scoped; only 1 of the original 7 sites (the `~condarc` staging site) qualified. + Batch 5 (2 CI-only sites, both inside `:ci_skip_entry`) landed as originally scoped for its own + genuinely-cascading site (`CI skip: entry helper staging failed`); its sibling (`find_entry + helper syntax error`) was confirmed already benign, no fix needed. - **Batch 3** (the entry-determination double-call, the first `Could not determine entry point` - site inside `:after_env_mode_selection`) and **Batch 6** (`:tci_both_failed`'s two sites, - `:try_conda_install` coordination) land last, each on its own -- both medium risk / more - design thought than a drop-in goto, and Batch 6 specifically benefits from waiting until - Batch 1's mitigating side-effect is confirmed via real CI before deciding whether it's still - worth the extra coordination code. + site inside `:after_env_mode_selection`) landed -- confirmed safe via a direct read of + `:determine_entry`'s own subroutine body (errorlevel 1 means the entry-detection MECHANISM + itself is broken -- helper staging or syntax-verify failed -- not "ambiguous" or "no files + found," both of which already return 0 and are handled gracefully elsewhere). This closes the + concern a CodeRabbit review round raised on the Batch 1 PR before Batch 3 itself had landed + (asking whether the fix "resolves" the second `:determine_entry` call): it does not eliminate + that second call, and never claimed to -- it only skips the pointless intervening work; if the + same root cause persists, the second call's own `call :die` still fires, one pause, not zero. + - **Batch 6** (`:tci_both_failed`'s two sites, `:try_conda_install` coordination) stays deferred + -- see "Implementation Status" below for why its remaining value shrank further once traced. - **The "Active Python interpreter not resolved" sink** (inside `:after_env_mode_selection`) stays deferred indefinitely (lowest priority, already-mitigated, no fix currently planned). 4. **This pass's full trace (Finding 3 above) is the roadmap** -- no further blanket re-audit is @@ -285,6 +297,94 @@ note:** numbers and surrounding code (this file's own history shows line numbers drift as unrelated work lands) but does not need to re-derive the classification from scratch. +### Implementation Status (2026-08-21, same session as the decision above) + +All batches below were implemented, tested (where a test was warranted -- see each entry), and +locally committed on `claude/die-fatal-remediation-srj9jg` in ONE session as a continued research +pass -- **not pushed**. When this does get pushed, it should land as SEPARATE PRs per batch (one +commit per batch in the local history, so this is mechanical), preserving the "one batch lands at a +time, full CI proof before the next" discipline -- implementing several batches' worth of code +locally in one sitting does not change that landing discipline, it just means the design/trace work +for several batches is now already done and waiting. + +- **Batch 1 (the 5 sites inside `:after_conda_bat_validation`/`:after_conda_probes`)** -- + implemented exactly as designed. `tests/selfapps_conda_bothfail.ps1` extended with assertions + proving the chain collapses (no `'conda'`/`'python'`/`'python -V'`/`Conda not found at:` + messages, reaches the `Active Python interpreter not resolved` sink). +- **Batch 2 (`:conda_create_done`'s "python.exe missing" check)** -- implemented as designed. New + `missing_python` scenario in `tests/selfapps_die_emit_fallthrough.ps1` (distinct from + `selfapps_cascade_conda_create_fail.ps1`'s own same-named but CASCADE-re-entry scenario, which + never reaches this site). +- **Batch 4 -- SCOPE CORRECTED from the original 7-site estimate to 1 site (the `Could not stage + ~condarc` site, inside `:conda_create_done`), after tracing each of the original 7 individually + rather than assuming they shared one shape.** The original Finding 3 spot-sample called these + "low-probability... would almost certainly also break the very next real operation loudly and + quickly" without verifying WHAT that next operation actually does. A full per-site trace found + only this ONE genuinely cascades into a SECOND `call :die` (the doomed `copy /y "~condarc" ...` + right after it, since the source was never staged) -- the same "redundant pause for one root + cause" shape every other batch targets. The other 6 (`Could not write ~detect_python.py`, `Could + not write ~print_pyver.py`, `Could not write %ENV_PATH%\.condarc` [reached via ITS OWN direct + trigger, not the staging site's cascade], `Could not write ~prep_requirements.py`, `Could not + stage PEP 723 requirements.`, `Could not write ~detect_visa.py`) were each traced and found to + fall through into either (a) a genuinely benign, silently-degraded continuation (stdout/stderr + redirected to a file or the log, never the console -- e.g. the `~detect_python.py` site's own + PYSPEC stays empty, a valid "no constraint" default) with no second `:die` and no misleading + success claim, or (b) a convergence point the success path already reaches within a few lines + regardless (the PEP 723 requirements site's own block already falls through to `goto + :after_pipreqs_run` six lines later even without a fix). Reclassified as the SAME low priority as + the sink (harmless wasted work, not a redundant-pause bug) -- NOT fixed in this pass; a goto here + would trim some harmless waste, not close a remaining risk, and picking the WRONG goto target for + one of these would risk silently skipping legitimate future work for negligible benefit. One + exception worth a future look, not urgent: the `~detect_visa.py` site's fall-through logs "No + pyvisa/visa imports detected" even when detection never actually ran -- a real but low-severity + inaccurate-message case (skips an optional NI-VISA install step silently), same category as + Batch 2's fix but lower value (optional feature, not a build-blocking path). + New shared `HP_TEST_FORCE_EMIT_FAIL=` hook added to `:emit_from_base64` itself (the + subroutine behind all 7 -- and every other embedded-helper write in the file) to make the + `~condarc` staging site testable; `condarc` scenario in `tests/selfapps_die_emit_fallthrough.ps1`. +- **Batch 5 (the 2 sites inside `:ci_skip_entry`)** -- SAME correction pattern as Batch 4, applied + to a 2-site batch instead of 7. `CI skip: entry helper staging failed` (`~find_entry.py` staging) + genuinely cascades into `find_entry helper syntax error` (`:verify_find_entry_helper` also fails + against the never-staged file) -- fixed. The syntax-error site, reached via its OWN direct + trigger (staging succeeded, syntax-verify itself found a problem), falls through to a benign "no + entry script detected" convergence a few dozen lines later -- confirmed safe, no fix needed. + `ci_skip_entry` scenario in `tests/selfapps_die_emit_fallthrough.ps1`; also documents a genuine, + pre-existing, NOT-fixed-by-this-change finding: `:after_env_skip` writes `state=ok` + unconditionally regardless of an earlier `call :die` in the same run (see + `docs/agent-ndjson.md`'s new section for the full mechanism) -- near-zero exposure (`HP_CI_SKIP_ + ENV` is test-infrastructure-only) so not fixed here, flagged for a future pass if ever prioritized. +- **Batch 3 (the first of `:determine_entry`'s two per-run `Could not determine entry point` + sites, inside `:after_env_mode_selection`)** -- implemented as designed, confirmed safe via + reading `:determine_entry`'s own body directly (its `errorlevel 1` return path is ONLY reached + when `~find_entry.py` staging fails or `:verify_find_entry_helper` finds a syntax error -- both + mean the entry-detection MECHANISM is broken, never "ambiguous multiple entries" or "no .py + files," both of which already return 0 and are handled gracefully elsewhere in this same + subroutine). `determine_entry` scenario in `tests/selfapps_die_emit_fallthrough.ps1` asserts an + exact count of 2 for the (identically-worded) die message at both call sites, plus the absence of + a dependency-install-phase marker proving the large intervening block was genuinely skipped. The + second call site (inside `:after_env_bootstrap`) is deliberately unchanged and remains reachable + -- if the same root cause persists, its own `call :die` still fires once, not zero times; the fix + only removes the pointless intervening work between the two calls, it does not eliminate the + second call itself. +- **Batch 6 -- traced further, value confirmed lower than originally scoped, REMAINS DEFERRED.** + `:try_conda_install` (home of `:tci_both_failed`'s two sites) has TWO call sites, not one: the + main install-if-missing block (near the top of the file) AND a second, independent one inside + `:cascade_acquire_conda` (the REQ-009 uv-to-conda cascade's own on-demand Miniconda acquisition + -- see `docs/agent-interconnect.md`'s "Provider cascade execution re-enters env-create" section). + The second call site's own fall-through (`goto :eof` twice, returning to + `:cascade_acquire_conda`'s caller) does NOT re-enter the Batch 1 probe chain at all (that chain is + specific to the first-attempt, non-cascade path) -- it proceeds fairly directly to a conda-create + attempt that already gotos correctly (Batch 1 slice 1, pre-existing). So the second call site was + already reasonably contained even before today's session. For the FIRST call site: Batch 1's own + fix (now implemented) already collapses its fall-through from "the full 5-6-pause probe chain" + down to "just `:after_conda_bat_validation`'s own first-site single pause" -- confirmed via + re-tracing, not just asserted. The remaining value of a full Batch 6 fix (a new caller-side + coordination flag, since a subroutine's own `goto` cannot reach past its `call`-frame return) is + now down to "save ONE more redundant pause in an already-rare scenario," a smaller win than + originally scoped and not worth the added coordination-state complexity right now. Deferred, not + abandoned -- revisit if a future session finds this specific residual pause actually confusing + real users. + ### Finding 4 -- `:die`'s own existing choreography is exactly what any candidate must preserve `:die` currently, unconditionally, in order: sets `HP_BOOTSTRAP_STATE=error`, logs the message, diff --git a/run_setup.bat b/run_setup.bat index 70bb80dd..28f4b5b8 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -1262,6 +1262,7 @@ if not exist "%HP_PY%" ( if defined HP_ENV_READY goto :after_env_mode_selection if defined HP_CASCADE_SAVED_PY goto :cascade_conda_create_failed call :die "[ERROR] python.exe missing from conda environment." + goto :after_env_mode_selection ) call :emit_from_base64 "~print_pyver.py" HP_PRINT_PYVER @@ -1283,7 +1284,10 @@ if not defined HP_RUNTIME_TXT_PREEXIST if not "%PYVER%"=="" ( rem README.md documents the conda-forge policy for this project and why .condarc is required. rem Emit the .condarc payload from base64 so quoting stays robust on Windows CMD. call :emit_from_base64 "~condarc" HP_CONDARC -if errorlevel 1 call :die "[ERROR] Could not stage ~condarc" +if errorlevel 1 ( + call :die "[ERROR] Could not stage ~condarc" + goto :after_env_mode_selection +) if not exist "%ENV_PATH%" mkdir "%ENV_PATH%" copy /y "~condarc" "%ENV_PATH%\.condarc" >> "%LOG%" 2>&1 if errorlevel 1 call :die "[ERROR] Could not write %ENV_PATH%\.condarc" @@ -1367,7 +1371,10 @@ if errorlevel 11 ( call :write_status "error" 1 %PYCOUNT% exit /b 1 ) -if errorlevel 1 call :die "[ERROR] Could not determine entry point" +if errorlevel 1 ( + call :die "[ERROR] Could not determine entry point" + goto :after_env_bootstrap +) rem derived requirement: do NOT interpolate %HP_APP_ARGS% into this message -- :log echoes rem UNQUOTED, and a user-supplied argument could contain < > | & (docs/agent-lessons-learned.md rem ":log echoes UNQUOTED"). This confirms forwarding is active without echoing arg content. @@ -2022,7 +2029,10 @@ set "HP_CRUMB=" if exist "~entry.abs" del "~entry.abs" rem --- stage helper --- call :emit_from_base64 "~find_entry.py" HP_FIND_ENTRY -if errorlevel 1 call :die "[ERROR] CI skip: entry helper staging failed" +if errorlevel 1 ( + call :die "[ERROR] CI skip: entry helper staging failed" + goto :after_env_bootstrap +) call :update_find_entry_abs call :verify_find_entry_helper if errorlevel 1 call :die "[ERROR] find_entry helper syntax error" @@ -5146,6 +5156,15 @@ set "DST=%~1" set "VAR=%~2" if not defined DST exit /b 1 if not defined VAR exit /b 1 +rem derived requirement: CLAUDE.md Item 46 Bucket A Batch regression coverage -- deterministically +rem force a SPECIFIC payload's own write to fail (simulating the disk-write/AV-lock class of +rem failure every real call site of this subroutine can hit) without touching any other payload +rem in the same run. Absence changes nothing (REQ-019): only fires when HP_TEST_FORCE_EMIT_FAIL +rem exactly matches this call's own %VAR%. +if defined HP_TEST_FORCE_EMIT_FAIL if /i "%HP_TEST_FORCE_EMIT_FAIL%"=="%VAR%" ( + call :log "[TEST] HP_TEST_FORCE_EMIT_FAIL=%VAR%: simulating embedded helper write failure." + exit /b 1 +) powershell -NoProfile -ExecutionPolicy Bypass -Command ^ "$varName = '%VAR%'; $envItem = Get-Item Env:$varName -ErrorAction SilentlyContinue; if (-not $envItem) { exit 1 }; $base64 = $envItem.Value; if (-not $base64) { exit 1 }; $outFile = Join-Path (Get-Location) '%DST%'; $bytes = [Convert]::FromBase64String($base64); [IO.File]::WriteAllBytes($outFile, $bytes)" >> "%LOG%" 2>&1 exit /b %errorlevel% diff --git a/tests/selfapps_die_emit_fallthrough.ps1 b/tests/selfapps_die_emit_fallthrough.ps1 new file mode 100644 index 00000000..f8d88eef --- /dev/null +++ b/tests/selfapps_die_emit_fallthrough.ps1 @@ -0,0 +1,328 @@ +# ASCII only +# selfapps_die_emit_fallthrough.ps1 - Regression coverage for CLAUDE.md Active Backlog Item 46 +# Bucket A Batches 3 and 4/5 (docs/plan-die-fatal-remediation.md's "Batch Roadmap" -- the full +# 20-site trace and batch grouping). :die uses "exit /b" (a subroutine return, not a process +# halt -- see docs/agent-lessons-learned.md's ":die" entry), so a caller with no goto/halt after +# "call :die" simply continues. This file covers three of the sites the full trace found genuinely +# needed a goto -- each reached via a new, narrow test hook rather than a real disk failure: +# +# New hook: HP_TEST_FORCE_EMIT_FAIL= (run_setup.bat, :emit_from_base64) deterministically +# fails ONE specific embedded-helper write (matched against the subroutine's own %VAR% argument, +# e.g. HP_CONDARC or HP_FIND_ENTRY) without touching any other payload emitted in the same run -- +# simulates the disk-write/AV-lock class of failure every call site of this shared subroutine can +# hit, without needing a genuinely broken filesystem. +# +# Four scenarios (DIE_EMIT_SCENARIO env var): +# +# - "missing_python" (Batch 2, site ~1252, the NON-cascade path -- distinct from +# selfapps_cascade_conda_create_fail.ps1's own "missing_python" scenario, which is specifically +# a REQ-009 cascade RE-ENTRY where HP_CASCADE_SAVED_PY is defined and routes through +# :cascade_conda_create_failed instead, never reaching this site at all): HP_FORCE_CONDA_ONLY=1, +# no cascade env vars, drives a REAL, successful conda create, then HP_TEST_FORCE_CONDA_MISSING_ +# PYTHON=1 deletes the python.exe that create just produced (same established technique as the +# cascade test's own sibling scenario). Before this fix, the fall-through continued through +# :conda_create_done's REST of its body -- writing .condarc and logging the misleading +# "[BOOT] ... Selected Python provider: Conda (Portable)." success line -- even though HP_PY was +# already known broken. The fix (goto :after_env_mode_selection right after the first call :die) +# skips both. Unlike the other three scenarios below, this one does NOT set HP_SKIP_ENTRY_SMOKE/ +# HP_SKIP_EXE_SMOKERUN -- the whole point is to let :run_entry_smoke actually run so Item 45's +# own "if not exist %HP_PY%" guard (CLAUDE.md Active Backlog Item 45, already proven by +# selfapps_entrysmoke_no_interpreter.ps1 for a DIFFERENT trigger) gets a chance to catch the +# broken interpreter and confirms no PyInstaller build is attempted against it. +# +# - "condarc" (Batch 2's neighbor code path, site ~1275): HP_FORCE_CONDA_ONLY=1 drives a REAL, +# successful conda create (python.exe genuinely produced) so :conda_create_done reaches its own +# normal .condarc-staging step. HP_TEST_FORCE_EMIT_FAIL=HP_CONDARC fails that staging; before +# this fix, the fall-through then attempted `copy /y "~condarc" ...` (doomed, since the source +# was never staged), triggering a SECOND, redundant call :die ("Could not write ...\.condarc") +# AND still logging the misleading "[BOOT] ... Selected Python provider: Conda (Portable)." +# success line right after. The fix (goto :after_env_mode_selection right after the first +# call :die) skips both. Asserts the cascade message and the misleading success line are both +# absent, HP_PY was genuinely valid throughout (proving this is NOT the same failure class as +# Item 46's earlier "python.exe missing" fix), and ~bootstrap.status.json still reads +# state=error (the correct, honest report -- :die's own centralized HP_BOOTSTRAP_STATE=error set +# is not undone by the graceful continuation downstream). +# +# - "ci_skip_entry" (Batch 5, site ~2011): HP_CI_SKIP_ENV=1 takes the cheap, no-conda/no-uv system- +# Python path. HP_TEST_FORCE_EMIT_FAIL=HP_FIND_ENTRY fails :ci_skip_entry's own ~find_entry.py +# staging; before this fix, the fall-through continued into :update_find_entry_abs / +# :verify_find_entry_helper, which -- since the helper file was never staged -- ALSO failed and +# triggered a second, redundant call :die ("find_entry helper syntax error"). The fix (goto +# :after_env_bootstrap) skips that. Near-zero real-world exposure (HP_CI_SKIP_ENV is test- +# infrastructure-only, never set by a real user or the default bootstrap path) -- covered here +# for completeness, not urgency. +# Known, pre-existing, NOT fixed by this change: :after_env_skip (the label +# :after_env_bootstrap routes to under HP_CI_SKIP_ENV) calls +# `call :write_status ok 0 %PYCOUNT%` unconditionally -- it does not read HP_BOOTSTRAP_STATE, so +# this scenario's own status file reads state=ok even though a real call :die fired earlier in +# the same run. This is NOT a regression from this fix (the OLD, unfixed fall-through already +# reached this same unconditional-ok write further downstream, just after more wasted work) -- +# flagged here so a future reader does not mistake it for something this change broke. Asserted +# as the known, unchanged behavior rather than silently ignored. +# +# - "determine_entry" (Batch 3, site ~1362): HP_FORCE_CONDA_ONLY=1 (same real-create cost as +# "condarc"). :determine_entry runs TWICE in a normal bootstrap -- once early inside +# :after_env_mode_selection (site ~1362), once again inside :after_env_bootstrap (site ~2125) -- +# both via the identical ~find_entry.py staging call, so HP_TEST_FORCE_EMIT_FAIL=HP_FIND_ENTRY +# fails BOTH invocations identically. Before this fix, the first failure's fall-through ran the +# ENTIRE intervening dependency-install/pipreqs/warnfix/pyvisa block (real work, not just a +# redundant message) before reaching the second :determine_entry call and reproducing the +# identical failure. The fix (goto :after_env_bootstrap right after the first call :die) skips +# that block outright; the second call still runs and still dies (same message text at both +# sites, so this asserts an exact COUNT of 2 rather than distinguishing which site fired), then +# falls through to the ALREADY-benign "if HP_ENTRY=="" skip packaging" branch confirmed safe by +# the full trace (docs/plan-die-fatal-remediation.md Finding 3) -- run still completes, +# state=ok. The key assertion is the ABSENCE of a dependency-install-phase marker that would +# only appear if the skipped block had actually run. +# +# "condarc"/"ci_skip_entry"/"determine_entry" (NOT "missing_python", see above) set +# HP_SKIP_ENTRY_SMOKE=1 / HP_SKIP_EXE_SMOKERUN=1 (REQ-012) -- those three are about the setup-flow +# fall-through behavior itself, not a full PyInstaller build/run cycle, which entry detection and +# dependency resolution do not depend on either way. +# +# Lane: conda-full only for "missing_python"/"condarc"/"determine_entry" (real conda create +# needed, gated on steps.conda_avail.outputs.available == 'true' matching the other 28+ +# conda-full-only self-tests); any lane for "ci_skip_entry" (no conda/uv dependency at all). +# Gating from first landing -- deterministic, self-contained (own HP_FORCE_CONDA_ONLY=1 / +# HP_CI_SKIP_ENV=1 as appropriate), same reasoning as selfapps_entrysmoke_no_interpreter.ps1's own +# precedent. +param() +$ErrorActionPreference = 'Continue' +$here = $PSScriptRoot +$repo = Split-Path -Path $here -Parent +$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 +} + +$scenario = $env:DIE_EMIT_SCENARIO +if (-not $scenario) { $scenario = 'missing_python' } + +# derived requirement: matches selfapps_pyinstaller_fail.ps1's own established multi-scenario +# precedent (docs/agent-ndjson.md: "Three scenarios... all emitting the same row id") -- ONE +# shared, LITERAL id at every Write-NdjsonRow call site, with details.scenario as the +# discriminator. Avoids the pitfall docs/agent-lessons-learned.md documents: a per-scenario id +# computed into a shared variable (e.g. "id = $rowId") never matches +# tools/check_ndjson_registry.py's static "id = '...'" regex, so the registry checker would +# misreport this row as never emitted. + +# derived requirement: $IsWindows is undefined (reads as $null) under Windows PowerShell 5.1 -- +# only introduced in PowerShell 6+. [System.Environment]::OSVersion.Platform works on both. +$platform = [System.Environment]::OSVersion.Platform.ToString() +if ($platform -ne 'Win32NT') { + Write-NdjsonRow ([ordered]@{ + id = 'self.die_emit_fallthrough' + req = 'CLAUDE.md-Item-46-BucketA' + pass = $true + desc = "die-emit-fallthrough $scenario (skipped on non-Windows)" + details = [ordered]@{ skip = $true; scenario = $scenario; platform = $platform; reason = 'non-windows-host' } + }) + exit 0 +} + +$batchPath = Join-Path $repo 'run_setup.bat' +if (-not (Test-Path $batchPath)) { + Write-NdjsonRow ([ordered]@{ + id = 'self.die_emit_fallthrough' + req = 'CLAUDE.md-Item-46-BucketA' + pass = $false + desc = "die-emit-fallthrough $scenario: run_setup.bat not found" + details = [ordered]@{ scenario = $scenario; error = 'run_setup.bat not found at ' + $batchPath } + }) + exit 1 +} + +$workDir = Join-Path $here "~selftest_die_emit_$scenario" +if (Test-Path $workDir) { Remove-Item -Recurse -Force $workDir } +New-Item -ItemType Directory -Force -Path $workDir | Out-Null +Copy-Item -Path $batchPath -Destination $workDir -Force +Set-Content -Path (Join-Path $workDir 'app.py') -Value 'print("should-not-matter")' -Encoding ASCII + +$bootstrapLog = "~die_emit_${scenario}_bootstrap.log" + +$saved = @{} +function Save-EnvVar([string]$name) { + $saved[$name] = if (Test-Path "Env:$name") { (Get-Item "Env:$name").Value } else { $null } +} +function Restore-EnvVar([string]$name) { + if ($null -eq $saved[$name]) { Remove-Item "Env:$name" -ErrorAction SilentlyContinue } + else { Set-Item "Env:$name" $saved[$name] } +} + +$envVars = @('HP_FORCE_CONDA_ONLY', 'HP_CI_SKIP_ENV', 'HP_TEST_FORCE_EMIT_FAIL', 'HP_TEST_FORCE_CONDA_MISSING_PYTHON', 'HP_SKIP_PIPREQS', 'HP_SKIP_ENTRY_SMOKE', 'HP_SKIP_EXE_SMOKERUN') +foreach ($v in $envVars) { Save-EnvVar $v } + +switch ($scenario) { + 'missing_python' { + $env:HP_FORCE_CONDA_ONLY = '1' + $env:HP_TEST_FORCE_CONDA_MISSING_PYTHON = '1' + # Deliberately NOT setting HP_SKIP_ENTRY_SMOKE/HP_SKIP_EXE_SMOKERUN here -- see this + # file's own header comment on why this one scenario needs the real entry-smoke path. + } + 'condarc' { + $env:HP_FORCE_CONDA_ONLY = '1' + $env:HP_TEST_FORCE_EMIT_FAIL = 'HP_CONDARC' + $env:HP_SKIP_ENTRY_SMOKE = '1' + $env:HP_SKIP_EXE_SMOKERUN = '1' + } + 'ci_skip_entry' { + $env:HP_CI_SKIP_ENV = '1' + $env:HP_TEST_FORCE_EMIT_FAIL = 'HP_FIND_ENTRY' + $env:HP_SKIP_ENTRY_SMOKE = '1' + $env:HP_SKIP_EXE_SMOKERUN = '1' + } + 'determine_entry' { + $env:HP_FORCE_CONDA_ONLY = '1' + $env:HP_TEST_FORCE_EMIT_FAIL = 'HP_FIND_ENTRY' + $env:HP_SKIP_ENTRY_SMOKE = '1' + $env:HP_SKIP_EXE_SMOKERUN = '1' + } + default { + Write-NdjsonRow ([ordered]@{ + id = 'self.die_emit_fallthrough' + req = 'CLAUDE.md-Item-46-BucketA' + pass = $false + desc = "die-emit-fallthrough: unknown DIE_EMIT_SCENARIO '$scenario'" + details = [ordered]@{ scenario = $scenario; error = "unknown scenario: $scenario" } + }) + exit 1 + } +} +$env:HP_SKIP_PIPREQS = '1' + +try { + Push-Location $workDir + try { + cmd /c "call run_setup.bat > $bootstrapLog 2>&1" + $runExit = $LASTEXITCODE + } finally { + Pop-Location + } +} finally { + foreach ($v in $envVars) { Restore-EnvVar $v } +} + +$logPath = Join-Path $workDir $bootstrapLog +$setupLog = Join-Path $workDir '~setup.log' +$logText = if (Test-Path -LiteralPath $logPath) { Get-Content -LiteralPath $logPath -Raw -Encoding ASCII } else { '' } +$setupText = if (Test-Path -LiteralPath $setupLog) { Get-Content -LiteralPath $setupLog -Raw -Encoding ASCII } else { '' } +$combined = $logText + "`n" + $setupText + +$statusPath = Join-Path $workDir '~bootstrap.status.json' +$statusState = $null +if (Test-Path -LiteralPath $statusPath) { + try { $statusState = (Get-Content -LiteralPath $statusPath -Raw -Encoding ASCII | ConvertFrom-Json).state } catch { } +} + +$pass = $false +$details = [ordered]@{} + +switch ($scenario) { + 'missing_python' { + # :handle_conda_failure logs the message once via :log before attempting any fallback, + # then :die echoes the SAME text a second time -- exactly 2 is expected (matches the + # counting technique selfapps_cascade_conda_create_fail.ps1's own sibling scenario uses); + # this alone does not distinguish pre- from post-fix (the fix runs AFTER :die already + # echoed), so the real proof is the misleading success line's absence below. + $dieMsgCount = ([regex]::Matches($combined, [regex]::Escape('[ERROR] python.exe missing from conda environment.'))).Count + $dieFiredTwice = ($dieMsgCount -eq 2) + $misleadingMsgFound = $combined -match [regex]::Escape('[BOOT] REQ-009: Selected Python provider: Conda (Portable).') + # Item 45's own guard (already proven by selfapps_entrysmoke_no_interpreter.ps1 for a + # different trigger) must still catch the broken-but-defined HP_PY here and refuse the + # doomed build. + $noBuildAttempt = -not ($combined -match [regex]::Escape('Building standalone executable')) + $honestNoInterpreterMsg = $combined -match [regex]::Escape('No Python interpreter is available') + $pass = $dieFiredTwice -and (-not $misleadingMsgFound) -and $noBuildAttempt -and $honestNoInterpreterMsg -and ($statusState -eq 'error') + $details = [ordered]@{ + scenario = $scenario + dieMsgCount = $dieMsgCount + dieFiredTwice = [bool]$dieFiredTwice + misleadingMsgFound = [bool]$misleadingMsgFound + noBuildAttempt = [bool]$noBuildAttempt + honestNoInterpreterMsg = [bool]$honestNoInterpreterMsg + statusState = $statusState + bootstrapExit = $runExit + log = $bootstrapLog + } + } + 'condarc' { + $hookFired = $combined -match [regex]::Escape('HP_TEST_FORCE_EMIT_FAIL=HP_CONDARC: simulating embedded helper write failure') + $dieMsgFound = $combined -match [regex]::Escape('[ERROR] Could not stage ~condarc') + $cascadeMsgFound = $combined -match 'Could not write [^\r\n]*\.condarc' + $misleadingMsgFound = $combined -match [regex]::Escape('[BOOT] REQ-009: Selected Python provider: Conda (Portable).') + $pass = $hookFired -and $dieMsgFound -and (-not $cascadeMsgFound) -and (-not $misleadingMsgFound) -and ($statusState -eq 'error') + $details = [ordered]@{ + scenario = $scenario + hookFired = [bool]$hookFired + dieMsgFound = [bool]$dieMsgFound + cascadeMsgFound = [bool]$cascadeMsgFound + misleadingMsgFound = [bool]$misleadingMsgFound + statusState = $statusState + bootstrapExit = $runExit + log = $bootstrapLog + } + } + 'ci_skip_entry' { + $hookFired = $combined -match [regex]::Escape('HP_TEST_FORCE_EMIT_FAIL=HP_FIND_ENTRY: simulating embedded helper write failure') + $dieMsgFound = $combined -match [regex]::Escape('[ERROR] CI skip: entry helper staging failed') + $cascadeMsgFound = $combined -match [regex]::Escape('[ERROR] find_entry helper syntax error') + # Known pre-existing quirk (see this file's own header comment): :after_env_skip writes + # state=ok unconditionally regardless of an earlier call :die in the same run -- asserted + # as the known, unchanged behavior, not a regression from this fix. + $pass = $hookFired -and $dieMsgFound -and (-not $cascadeMsgFound) -and ($statusState -eq 'ok') + $details = [ordered]@{ + scenario = $scenario + hookFired = [bool]$hookFired + dieMsgFound = [bool]$dieMsgFound + cascadeMsgFound = [bool]$cascadeMsgFound + statusState = $statusState + bootstrapExit = $runExit + log = $bootstrapLog + } + } + 'determine_entry' { + $dieMsgCount = ([regex]::Matches($combined, [regex]::Escape('[ERROR] Could not determine entry point'))).Count + # Both call sites share identical message text (by design, same underlying failure) -- + # exactly 2 proves the mechanism reached BOTH :determine_entry invocations, not more + # (a goto loop) and not fewer (the second call silently skipped too). + $bothSitesReached = ($dieMsgCount -eq 2) + # The intervening dependency-install/pipreqs/warnfix block must NOT have run -- this + # marker only appears once dependency installation actually begins. + $depPhaseSkipped = -not ($combined -match [regex]::Escape('[TRACE] dep install phase: start')) + $pass = $bothSitesReached -and $depPhaseSkipped -and ($statusState -eq 'ok') + $details = [ordered]@{ + scenario = $scenario + dieMsgCount = $dieMsgCount + bothSitesReached = [bool]$bothSitesReached + depPhaseSkipped = [bool]$depPhaseSkipped + statusState = $statusState + bootstrapExit = $runExit + log = $bootstrapLog + } + } +} + +Write-Host "=== self.die_emit_fallthrough ($scenario) evidence ===" +Write-Host ($details | ConvertTo-Json -Compress) +Write-Host "pass=$pass" +Write-Host "=== end self.die_emit_fallthrough ($scenario) evidence ===" + +Write-NdjsonRow ([ordered]@{ + id = 'self.die_emit_fallthrough' + req = 'CLAUDE.md-Item-46-BucketA' + pass = [bool]$pass + desc = "die-emit-fallthrough $scenario: redundant call :die cascade avoided via goto" + details = $details +}) + +if (-not $pass) { exit 1 } +exit 0 From f1ac033afafc02169a35153b0f6a8025359296ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 01:35:40 +0000 Subject: [PATCH 2/8] Item 46 Bucket A: full re-verification pass, consolidated 27-site classification table Re-read every one of the 27 call :die sites against the CURRENT run_setup.bat source (post Batches 1/2/3/4/5) rather than relying on the earlier reasoning passes -- confirms no drift in any prior classification. Adds one new finding: a third, previously-uncatalogued call site of :try_conda_install (inside :evict_and_rebuild's conda-corruption self-heal flow) -- already safely contained by its own immediately-following check, no action needed. Consolidates the full inventory into one definitive table (docs/plan-die-fatal-remediation.md) cross-referencing the detailed per-batch reasoning already documented in Finding 2/3 and Implementation Status, so the complete site-by-site disposition is visible at a glance: 9 fixed, 7 pre-existing safe, 11 traced-and-deferred, all 27 accounted for. No code changes -- documentation only. Still not pushed, per instruction to hold until given the go-ahead. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TcuphCHEGB2RiCYLjFjMN6 --- CLAUDE.md | 19 ++++++------ docs/agent-interconnect.md | 13 ++++---- docs/plan-die-fatal-remediation.md | 49 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0a2a3eb7..d3a72a17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1129,15 +1129,16 @@ way (no live Windows execution available here), that is noted explicitly rather a time, full 8-lane matrix CI proof to completion before the next batch starts, no blanket sweep across every remaining site in one PR. - **Implemented locally 2026-08-21 (Batches 1/2/3/5 and one site of Batch 4), NOT YET PUSHED -- - held per explicit instruction to avoid disrupting concurrent CI from other active-backlog work.** - Batch 4's scope corrected from 7 sites to 1 (1275 only -- the other 6 traced individually and - found to fall through into benign, silently-degraded continuations, not a redundant-`:die` - cascade; reclassified alongside the 1334 sink, not fixed). Batch 6 traced further (a second - `:try_conda_install` call site found inside `:cascade_acquire_conda`) and remains deferred -- - Batch 1's own fix already shrinks its residual value to "one fewer redundant pause in an already- - rare scenario." New shared test hook `HP_TEST_FORCE_EMIT_FAIL=` added to - `:emit_from_base64` itself. New test file `tests/selfapps_die_emit_fallthrough.ps1` (4 scenarios: + **Batch 1 merged 2026-08-28 (PR #468). Batches 2/3/5 and one site of Batch 4 landed as a + follow-on PR stacked on Batch 1, same session.** Batch 4's scope corrected from 7 sites to 1 + (the `Could not stage ~condarc` site only -- the other 6 traced individually and found to fall + through into benign, silently-degraded continuations, not a redundant-`:die` cascade; + reclassified alongside the "Active Python interpreter not resolved" sink, not fixed). Batch 6 + traced further (a second `:try_conda_install` call site found inside `:cascade_acquire_conda`) + and remains deferred -- Batch 1's own fix already shrinks its residual value to "one fewer + redundant pause in an already-rare scenario." New shared test hook + `HP_TEST_FORCE_EMIT_FAIL=` added to `:emit_from_base64` itself. New test file + `tests/selfapps_die_emit_fallthrough.ps1` (4 scenarios: `missing_python`/`condarc`/`ci_skip_entry`/`determine_entry`) also surfaced a genuine, pre- existing, unfixed quirk: `:after_env_skip` writes `state=ok` unconditionally regardless of an earlier `call :die` in the same run (near-zero exposure, `HP_CI_SKIP_ENV` is test-infrastructure- diff --git a/docs/agent-interconnect.md b/docs/agent-interconnect.md index 6be7390d..e2896271 100644 --- a/docs/agent-interconnect.md +++ b/docs/agent-interconnect.md @@ -1111,12 +1111,13 @@ fall-through blast radius as a side effect, even before Batch 6 itself is touche this fix) now asserts the resulting cascade collapses to exactly this chain's first site plus the sink, not the full 5-6-pause worst case. -**Status: implemented 2026-08-21 (local commit, not yet pushed).** A second `:try_conda_install` -call site was found while tracing Batch 6 (the `:tci_both_failed` failure sites themselves) -- -`:cascade_acquire_conda` (the REQ-009 uv-to-conda cascade's own on-demand Miniconda acquisition, -~line 2302) calls it independently of the main install-if-missing block (~line 884). Its OWN -fall-through does NOT re-enter this probe chain at all (that chain is specific to the first-attempt, -non-cascade path, reached only from the ~884 call site) -- it proceeds fairly directly to a +**Status: implemented, landed as a follow-on PR stacked on Batch 1 (2026-08-28).** A second +`:try_conda_install` call site was found while tracing Batch 6 (the `:tci_both_failed` failure +sites themselves) -- `:cascade_acquire_conda` (the REQ-009 uv-to-conda cascade's own on-demand +Miniconda acquisition) calls it independently of the main install-if-missing block (inside +`:uv_first_skip`). Its OWN fall-through does NOT re-enter this probe chain at all (that chain is +specific to the first-attempt, non-cascade path, reached only from `:uv_first_skip`'s own call +site) -- it proceeds fairly directly to a conda-create attempt that already `goto`s correctly (slice 1, pre-existing). This means Batch 1's fix only reduces THIS section's own described blast-radius reduction for the FIRST call site; `docs/plan-die-fatal-remediation.md`'s "Implementation Status" section has the full trace for both diff --git a/docs/plan-die-fatal-remediation.md b/docs/plan-die-fatal-remediation.md index 0430e25f..bcf300ed 100644 --- a/docs/plan-die-fatal-remediation.md +++ b/docs/plan-die-fatal-remediation.md @@ -73,6 +73,55 @@ paths.py` independently confirms the file's broader shape (181 labels, 67 ever r classification below was done by direct inspection of each site's surrounding code, not the tool alone (the tool's own docstring is explicit that `called=False` is "a hint, not proof"). +### Complete inventory of all 27 sites (re-verified 2026-08-21 against current source, post-Batches 1/2/3/4/5) + +Every site below was individually re-read against the CURRENT `run_setup.bat` (not reasoned about +from memory or an earlier pass) as part of a full re-verification -- confirms no drift from the +per-batch reasoning in Finding 2/3 and the "Implementation Status" section below, and catches one +previously-uncatalogued site (row 20). Sites are identified by the subroutine they sit in plus +their `call :die` message text, per this repo's own "cite by stable label, not exact line number" +documentation convention -- a subroutine name is `grep -n '^:label_name'`-findable regardless of +how much the file's line numbers drift as unrelated work lands. + +| # | Subroutine | Site | Fall-through leads to | Status | +|---|------------|------|------------------------|--------| +| 1 | `:after_conda_bat_validation` | conda.bat not found after bootstrap | probe chain (rows 2-5), up to 4 more redundant pauses | **Fixed** (Batch 1: `goto`) | +| 2 | `:after_conda_bat_validation` | 'conda' not found on PATH | next probe in chain | **Fixed** (Batch 1) | +| 3 | `:after_conda_bat_validation` | 'python' not found on PATH | next probe in chain | **Fixed** (Batch 1) | +| 4 | `:after_conda_bat_validation` | 'python -V' failed | channel-policy check (row 5) | **Fixed** (Batch 1) | +| 5 | `:after_conda_probes` | Conda not found at: `%CONDA_BAT%` | `:try_conda_create` with broken `CONDA_BAT` -> row 7 (already safe) | **Fixed** (Batch 1) | +| 6 | `:after_conda_probes` | Could not write `~detect_python.py` | doomed subprocess call, output redirected to file/log only (never console); `PYSPEC` stays empty, a valid "no constraint" default | No fix -- benign (Batch 4, reclassified) | +| 7 | `:conda_create_failed` | conda env create failed | -- | Pre-existing safe (`goto`, slice 1) | +| 8 | `:conda_create_done` | python.exe missing, non-cascade | rest of `:conda_create_done` body: writes `.condarc`, then the misleading `[BOOT] ... Selected Python provider: Conda (Portable).` success line | **Fixed** (Batch 2: `goto`) | +| 9 | `:conda_create_done` | Could not write `~print_pyver.py` | `runtime.txt` write silently skipped; `HP_PY` still valid, no cascade | No fix -- benign (Batch 4, reclassified) | +| 10 | `:conda_create_done` | Could not stage `~condarc` | doomed `copy` attempt -> cascades into row 11's own `:die` | **Fixed** (Batch 4: `goto`) | +| 11 | `:conda_create_done` | Could not write `%ENV_PATH%\.condarc` (reached via its OWN direct trigger, not row 10's cascade) | logs the TRUTHFUL `[BOOT] ... Selected Python provider` line (`HP_PY` genuinely valid here) | No fix -- benign (Batch 4, reclassified) | +| 12 | `:after_env_mode_selection` | Could not write `~prep_requirements.py` | REQ-005.8 heuristic dep-augmentation silently skipped later (confirmed via its own invocation site downstream, output redirected `>nul`) | No fix -- benign (Batch 4, reclassified) | +| 13 | `:after_env_mode_selection` | Active Python interpreter not resolved (the sink every other batch's `goto` routes toward) | wasted interpreter-smoke-test attempt + a redundant `:determine_entry` call; Item 45's own `if not exist "%HP_PY%"` guard in `:run_entry_smoke` backstops the one dangerous consequence regardless | No fix -- deferred, lowest priority | +| 14 | `:after_env_mode_selection` | Could not determine entry point (`:determine_entry`'s 1st of 2 per-run calls) | the ENTIRE dependency-install/pipreqs/warnfix/pyvisa block runs pointlessly (real network/disk work) before the 2nd call reproduces the identical failure | **Fixed** (Batch 3: `goto`) | +| 15 | `:after_env_mode_selection` | Could not stage PEP 723 requirements | falls through 1 line to an unchecked 2nd `copy`, then `goto :after_pipreqs_run` 6 lines later regardless (already the success path's own target) | No fix -- already converges (Batch 4, reclassified) | +| 16 | `:lock_done` | Could not write `~detect_visa.py` | `NEED_VISA` stays `"0"`, logs `[INFO] No pyvisa/visa imports detected.` -- inaccurate if the app genuinely needs pyvisa (detection never ran), silently skips the optional NI-VISA install | No fix -- low-severity, flagged for a future pass (Batch 4, reclassified) | +| 17 | `:ci_skip_entry` | CI skip: entry helper staging failed | doomed helper-verify attempt -> cascades into row 18's own `:die` | **Fixed** (Batch 5: `goto`) | +| 18 | `:ci_skip_entry` | find_entry helper syntax error (reached via its OWN direct trigger) | "locate a Python"/run attempt fails silently (output to file/log only) -> converges to the graceful "no entry script detected" branch | No fix -- benign (Batch 5, reclassified) | +| 19 | `:after_env_bootstrap` | Could not determine entry point (`:determine_entry`'s 2nd of 2 per-run calls) | immediately followed by the benign `if "%HP_ENTRY%"=="" (skip packaging)` check, 1 line down | No fix -- already benign | +| 20 | `:evict_and_rebuild` | (not a `:die` site itself -- a 3rd, previously-uncatalogued call site of `:try_conda_install`, inside `:evict_and_rebuild`'s conda-corruption self-heal flow) | already safely contained by the VERY NEXT check, `if not defined CONDA_BAT` (row 24) | N/A -- already safe, newly documented | +| 21 | `:heal_prompt` | Corrupt conda env; user declined rebuild | -- | Pre-existing safe (self-contained `exit /b 2`) | +| 22 | `:corrupt_override_exit` | Corrupt user-managed conda (PVW_CONDA_EXE) | -- | Pre-existing safe (self-contained `exit /b 2`) | +| 23 | `:corrupt_ci_exit` | Corrupt conda binary in CI | -- | Pre-existing safe (self-contained `exit /b 2`) | +| 24 | `:evict_and_rebuild` | Could not delete corrupt conda dir | -- | Pre-existing safe (self-contained `exit /b 3`) | +| 25 | `:evict_and_rebuild` | Fresh Miniconda install failed after self-healing eviction | -- | Pre-existing safe (self-contained `exit /b 4`) | +| 26 | `:tci_both_failed` | Miniconda install failed (both AllUsers and JustMe) | `goto :eof` -> caller's own chain (row 1's chain, now contained post-Batch-1) | Traced, deferred (Batch 6) | +| 27 | `:tci_both_failed` | Miniconda install failed (AllUsers skipped) | same as row 26 | Traced, deferred (Batch 6) | +| 28 | `:hp_test_conda_fail` | conda env create failed | -- | Pre-existing safe (`goto`) | + +(27 real `call :die` sites, numbered 1-19 and 21-28 above; row 20 is the newly-found 3rd +`:try_conda_install` call site, listed for completeness since it surfaced during this same +re-verification pass, not a `:die` site itself.) + +**Tally**: 9 fixed (rows 1-5, 8, 10, 14, 17) + 7 pre-existing-safe (rows 7, 21-25, 28) + 11 +no-fix/deferred (rows 6, 9, 11-13, 15-16, 18-19, 26-27) = 27. Every site accounted for, nothing +left unclassified. + ### Finding 2 -- 7 of the 27 are already effectively safe, just via an older idiom than slice 1's `goto` Two (the `:conda_create_failed` site and its `:hp_test_conda_fail` test-bypass sibling) already From 6901a11a6883d5c5195b7e41976a29b95b57d532 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 17:22:56 +0000 Subject: [PATCH 3/8] Fix PowerShell parse error in selfapps_die_emit_fallthrough.ps1; harden PS parse sweep $scenario: inside a double-quoted string is parsed as a scope-qualified variable reference (like $env:VAR), not interpolation followed by a literal colon -- this made the entire test file fail to parse, which is why PR #469's CI failed across all 8 lanes with the self.die_emit_ fallthrough row simply absent rather than failing. Fixed both occurrences with ${scenario}: curly-brace delimiting. The sanity sweep's own PS AST parse check never caught this: Parser:: ParseFile reports errors via an out-parameter, not a thrown exception, and both copies of the sweep (CLAUDE.md's inline block and tools/run_ sanity_sweep.sh) passed [ref]$null for it, silently discarding every parse error. Both now capture and report $errs. Documented in docs/ agent-lessons-learned.md so this class of bug and blind spot isn't rediscovered. --- CLAUDE.md | 10 +++++-- docs/agent-lessons-learned.md | 35 +++++++++++++++++++++++++ tests/selfapps_die_emit_fallthrough.ps1 | 4 +-- tools/run_sanity_sweep.sh | 10 +++++-- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d3a72a17..d810cf7a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,8 +135,14 @@ pwsh -c " \$ErrorActionPreference = 'Stop' \$fail = 0 Get-ChildItem /home/user/Python_vs_Windows/tests/*.ps1, /home/user/Python_vs_Windows/tools/*.ps1 | ForEach-Object { - try { [System.Management.Automation.Language.Parser]::ParseFile(\$_.FullName, [ref]\$null, [ref]\$null) | Out-Null } - catch { Write-Host \"PARSE FAIL: \$(\$_.FullName): \$_\"; \$fail = 1 } + \$errs = \$null + try { [System.Management.Automation.Language.Parser]::ParseFile(\$_.FullName, [ref]\$null, [ref]\$errs) | Out-Null } + catch { Write-Host \"PARSE FAIL: \$(\$_.FullName): \$_\"; \$fail = 1; return } + if (\$errs.Count -gt 0) { + Write-Host \"PARSE FAIL: \$(\$_.FullName):\" + \$errs | ForEach-Object { Write-Host \" \$_\" } + \$fail = 1 + } } if (\$fail -eq 0) { Write-Host 'PS PARSE SWEEP DONE - ALL CLEAN' } " diff --git a/docs/agent-lessons-learned.md b/docs/agent-lessons-learned.md index e74846e7..ef184f7d 100644 --- a/docs/agent-lessons-learned.md +++ b/docs/agent-lessons-learned.md @@ -668,6 +668,41 @@ workflows. --- +## `"$var:text"` inside a double-quoted PowerShell string is a scope-qualified-variable parse error, not string interpolation followed by a literal colon + +**Real bug that shipped and broke ALL 8 CI lanes of a PR (2026-08-28, `tests/selfapps_die_emit_ +fallthrough.ps1`, PR #469).** `"die-emit-fallthrough $scenario: run_setup.bat not found"` looks +like ordinary interpolation ("the value of `$scenario`, then a literal colon") but PowerShell's +tokenizer instead reads `$scenario:` as an attempt at a SCOPE-QUALIFIED variable reference +(`$scope:name`, the same syntax as `$env:VAR`/`$global:VAR`) -- any bare `$name` immediately +followed by `:` and non-identifier text is a `ParserError`: "Variable reference is not valid. ':' +was not followed by a valid variable name character. Consider using ${} to delimit the name." +This is a PARSE-time error, so the entire `.ps1` FILE fails to load -- not a runtime exception in +one function, every test/row in the file silently never runs (an NDJSON row this file emits was +simply ABSENT from the CI artifact, not failing -- easy to misdiagnose as "the row wasn't wired +up" instead of "the file never parsed"). + +**Fix: `${scenario}:` (curly-brace delimiting), not `$scenario` + string concatenation.** Same +rule applies to ANY interpolated variable immediately followed by `:` in a double-quoted string. + +**Why this passed local review and the sanity sweep before shipping: the sweep's own PS parse +check was silently discarding parse errors.** `[System.Management.Automation.Language.Parser]:: +ParseFile($path, [ref]$tokens, [ref]$errors)` reports parse errors via the **`$errors` +out-parameter**, not via a thrown/catchable exception -- passing `[ref]$null` for that parameter +(as both `CLAUDE.md`'s inline sanity-sweep block and `tools/run_sanity_sweep.sh`'s +`check_ps_parse_sweep()` did) silently discards every parse error, and a `try/catch` around the +call does NOT catch them (only a catastrophic failure like a missing file throws). The sweep +reported "ALL CLEAN" on a file that could not actually be loaded by PowerShell. **Fixed in both +places**: capture into `[ref]$errs`, then check `$errs.Count -gt 0` after the call and print each +error. `tools/ps-compileall.ps1` (a separate, older tool in this repo) already did this correctly +via `[ref]$parseErrs` -- it was never the bug, only the two sanity-sweep copies were. + +**Rule: any future `[...]::ParseFile(...)` call in this repo's tooling must capture and check the +errors out-parameter -- never pass `[ref]$null` for it.** If a third copy of this pattern is ever +added, apply the same fix immediately rather than trusting `try/catch` alone. + +--- + ## Windows `move` onto an existing destination: atomic replace for FILES, silent NESTING for DIRECTORIES -- do not assume the same post-check works for both Requirement 9's swap-verification fix (`:offer_optimized_build`, own section below) established a diff --git a/tests/selfapps_die_emit_fallthrough.ps1 b/tests/selfapps_die_emit_fallthrough.ps1 index f8d88eef..8284e1fd 100644 --- a/tests/selfapps_die_emit_fallthrough.ps1 +++ b/tests/selfapps_die_emit_fallthrough.ps1 @@ -135,7 +135,7 @@ if (-not (Test-Path $batchPath)) { id = 'self.die_emit_fallthrough' req = 'CLAUDE.md-Item-46-BucketA' pass = $false - desc = "die-emit-fallthrough $scenario: run_setup.bat not found" + desc = "die-emit-fallthrough ${scenario}: run_setup.bat not found" details = [ordered]@{ scenario = $scenario; error = 'run_setup.bat not found at ' + $batchPath } }) exit 1 @@ -320,7 +320,7 @@ Write-NdjsonRow ([ordered]@{ id = 'self.die_emit_fallthrough' req = 'CLAUDE.md-Item-46-BucketA' pass = [bool]$pass - desc = "die-emit-fallthrough $scenario: redundant call :die cascade avoided via goto" + desc = "die-emit-fallthrough ${scenario}: redundant call :die cascade avoided via goto" details = $details }) diff --git a/tools/run_sanity_sweep.sh b/tools/run_sanity_sweep.sh index fc348414..f1051325 100755 --- a/tools/run_sanity_sweep.sh +++ b/tools/run_sanity_sweep.sh @@ -150,8 +150,14 @@ check_ps_parse_sweep() { \$ErrorActionPreference = 'Stop' \$fail = 0 Get-ChildItem '$repo_root/tests/*.ps1', '$repo_root/tools/*.ps1' | ForEach-Object { - try { [System.Management.Automation.Language.Parser]::ParseFile(\$_.FullName, [ref]\$null, [ref]\$null) | Out-Null } - catch { Write-Host \"PARSE FAIL: \$(\$_.FullName): \$_\"; \$fail = 1 } + \$errs = \$null + try { [System.Management.Automation.Language.Parser]::ParseFile(\$_.FullName, [ref]\$null, [ref]\$errs) | Out-Null } + catch { Write-Host \"PARSE FAIL: \$(\$_.FullName): \$_\"; \$fail = 1; return } + if (\$errs.Count -gt 0) { + Write-Host \"PARSE FAIL: \$(\$_.FullName):\" + \$errs | ForEach-Object { Write-Host \" \$_\" } + \$fail = 1 + } } if (\$fail -eq 0) { Write-Host 'PS PARSE SWEEP DONE - ALL CLEAN' } exit \$fail From 08349cee4c0fdd9793a34cce4c6a6e6f70081a4f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 17:36:38 +0000 Subject: [PATCH 4/8] Address CodeRabbit review: exit-code propagation and artifact evidence CLAUDE.md's inline PS parse-sweep block set $fail on a parser error but never propagated it out of the pwsh -c process (no exit $fail at the end), so the documented sweep could print PARSE FAIL and still return success to the calling shell -- the same class of bug this whole change exists to fix, just in the doc copy instead of tools/run_sanity_sweep.sh (which already had it). Added exit $fail to match. Also adds the missing "Upload test logs" artifact paths for the four new tests/~selftest_die_emit_ scratch dirs (missing_python/condarc/ ci_skip_entry/determine_entry), so a failing scenario's bootstrap log, ~setup.log, and ~bootstrap.status.json are actually captured instead of silently dropped from the artifact. Two further findings from the same review (stopping fully after :determine_entry fails, and :after_env_skip's unconditional state=ok write) are the deliberately-deferred, separately-scoped "MEDIUM risk" follow-up work already recorded in docs/plan-die-fatal-remediation.md and docs/agent-ndjson.md -- addressed via reply, not a code change, per that existing scope decision. --- .github/workflows/batch-check.yml | 24 ++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 25 insertions(+) diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index 1408cc09..a9945534 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -2333,6 +2333,30 @@ jobs: tests\~selftest_entrysmoke_no_interpreter\~setup.log tests/~selftest_entrysmoke_no_interpreter/~bootstrap.status.json tests\~selftest_entrysmoke_no_interpreter\~bootstrap.status.json + tests/~selftest_die_emit_missing_python/~die_emit_missing_python_bootstrap.log + tests\~selftest_die_emit_missing_python\~die_emit_missing_python_bootstrap.log + tests/~selftest_die_emit_missing_python/~setup.log + tests\~selftest_die_emit_missing_python\~setup.log + tests/~selftest_die_emit_missing_python/~bootstrap.status.json + tests\~selftest_die_emit_missing_python\~bootstrap.status.json + tests/~selftest_die_emit_condarc/~die_emit_condarc_bootstrap.log + tests\~selftest_die_emit_condarc\~die_emit_condarc_bootstrap.log + tests/~selftest_die_emit_condarc/~setup.log + tests\~selftest_die_emit_condarc\~setup.log + tests/~selftest_die_emit_condarc/~bootstrap.status.json + tests\~selftest_die_emit_condarc\~bootstrap.status.json + tests/~selftest_die_emit_ci_skip_entry/~die_emit_ci_skip_entry_bootstrap.log + tests\~selftest_die_emit_ci_skip_entry\~die_emit_ci_skip_entry_bootstrap.log + tests/~selftest_die_emit_ci_skip_entry/~setup.log + tests\~selftest_die_emit_ci_skip_entry\~setup.log + tests/~selftest_die_emit_ci_skip_entry/~bootstrap.status.json + tests\~selftest_die_emit_ci_skip_entry\~bootstrap.status.json + tests/~selftest_die_emit_determine_entry/~die_emit_determine_entry_bootstrap.log + tests\~selftest_die_emit_determine_entry\~die_emit_determine_entry_bootstrap.log + tests/~selftest_die_emit_determine_entry/~setup.log + tests\~selftest_die_emit_determine_entry\~setup.log + tests/~selftest_die_emit_determine_entry/~bootstrap.status.json + tests\~selftest_die_emit_determine_entry\~bootstrap.status.json tests/extracted/** if-no-files-found: ignore include-hidden-files: true diff --git a/CLAUDE.md b/CLAUDE.md index d810cf7a..4dad37cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,6 +145,7 @@ Get-ChildItem /home/user/Python_vs_Windows/tests/*.ps1, /home/user/Python_vs_Win } } if (\$fail -eq 0) { Write-Host 'PS PARSE SWEEP DONE - ALL CLEAN' } +exit \$fail " python -m pytest /home/user/Python_vs_Windows/tests/test_*.py -q 2>&1 | tail -5 ``` From eb9f5a4a287cb15158952c0c404669269168fae8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 19:47:41 +0000 Subject: [PATCH 5/8] Fix double-counted die-message assertions in die_emit_fallthrough test The missing_python and determine_entry scenarios both counted occurrences of a message against $combined ($logText + $setupText concatenated), but :log writes every line to BOTH the console capture AND ~setup.log unconditionally -- so a message that genuinely fires twice (the intended, documented behavior for both scenarios) counted as 4 in $combined, failing the -eq 2 assertion. Confirmed against the real conda-full CI run (job 98932165466): both scenarios' underlying mechanisms worked exactly as designed (every other assertion in their own evidence -- misleadingMsgFound, noBuildAttempt, honestNoInterpreterMsg, depPhaseSkipped, statusState -- was already correct), only the count itself was wrong. This is the same pitfall selfapps_cascade_conda_create_fail.ps1 already documents and avoids by counting against $setupText alone; this test's own comments already claimed to follow that convention but the code didn't. Switched both Matches() calls to $setupText, matching the sibling test exactly, and confirmed against the real ~setup.log content captured in that CI run (2 occurrences each, as expected). --- tests/selfapps_die_emit_fallthrough.ps1 | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/selfapps_die_emit_fallthrough.ps1 b/tests/selfapps_die_emit_fallthrough.ps1 index 8284e1fd..570a05cd 100644 --- a/tests/selfapps_die_emit_fallthrough.ps1 +++ b/tests/selfapps_die_emit_fallthrough.ps1 @@ -233,7 +233,11 @@ switch ($scenario) { # counting technique selfapps_cascade_conda_create_fail.ps1's own sibling scenario uses); # this alone does not distinguish pre- from post-fix (the fix runs AFTER :die already # echoed), so the real proof is the misleading success line's absence below. - $dieMsgCount = ([regex]::Matches($combined, [regex]::Escape('[ERROR] python.exe missing from conda environment.'))).Count + # derived requirement: count against $setupText alone, not $combined -- every :log line + # is written to BOTH the console (captured in $logText) AND ~setup.log, so counting + # against $combined silently doubles every occurrence (caught via a real CI failure: + # dieMsgCount read 4, not 2, on the very first run). + $dieMsgCount = ([regex]::Matches($setupText, [regex]::Escape('[ERROR] python.exe missing from conda environment.'))).Count $dieFiredTwice = ($dieMsgCount -eq 2) $misleadingMsgFound = $combined -match [regex]::Escape('[BOOT] REQ-009: Selected Python provider: Conda (Portable).') # Item 45's own guard (already proven by selfapps_entrysmoke_no_interpreter.ps1 for a @@ -290,10 +294,11 @@ switch ($scenario) { } } 'determine_entry' { - $dieMsgCount = ([regex]::Matches($combined, [regex]::Escape('[ERROR] Could not determine entry point'))).Count + $dieMsgCount = ([regex]::Matches($setupText, [regex]::Escape('[ERROR] Could not determine entry point'))).Count # Both call sites share identical message text (by design, same underlying failure) -- # exactly 2 proves the mechanism reached BOTH :determine_entry invocations, not more - # (a goto loop) and not fewer (the second call silently skipped too). + # (a goto loop) and not fewer (the second call silently skipped too). Counted against + # $setupText alone, not $combined -- see the identical note in the missing_python block. $bothSitesReached = ($dieMsgCount -eq 2) # The intervening dependency-install/pipreqs/warnfix block must NOT have run -- this # marker only appears once dependency installation actually begins. From e2c09e2cb16855bef8352a769451225e7ce7251e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 20:00:00 +0000 Subject: [PATCH 6/8] Address second CodeRabbit review round: header accuracy + step isolation selfapps_die_emit_fallthrough.ps1's own header claimed "Batches 3 and 4/5" covering "three of the sites," but the file documents four scenarios including Batch 2's missing_python -- fixed the summary text and replaced all six remaining bare run_setup.bat line-number citations in the header with subroutine names (:conda_create_done, :ci_skip_entry, :after_env_mode_selection, :after_env_bootstrap), matching this repo's own documentation convention and the citation style already applied elsewhere in this PR. The ci_skip_entry self-test step (batch-check.yml) runs in every lane but had no per-step continue-on-error, unlike its established sibling pattern (the PEP 723 write-back steps, whose own header comment documents -- confirmed via a real prior CI run -- that job-level continue-on-error does not make sibling steps in the same job resilient to an earlier step's failure; a failure silently skips every later step in that job on a non-gating lane). Added the identical per-step flag, scoped to non-gating lanes only so the two gating lanes still fail hard on a genuine failure. --- .github/workflows/batch-check.yml | 8 ++++++++ tests/selfapps_die_emit_fallthrough.ps1 | 15 ++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index a9945534..9fab291d 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -379,8 +379,16 @@ jobs: # call :die ("find_entry helper syntax error") for the same root cause. Cheap # (HP_CI_SKIP_ENV path, no conda/uv needed) so runs in every lane, matching the # HP_CI_SKIP_ENV-based steps directly above. + # derived requirement: per-step continue-on-error, not just the job-level one -- see the + # identical note above the PEP 723 write-back steps (this same file, "REQ-005.11: PEP 723 + # header write-back") for why: without it, a genuine failure here silently skips every + # later step in the SAME job on a non-gating lane. Scoped to non-gating lanes only + # (matches matrix.mode != 'real' && matrix.mode != 'conda-full', the complement of the + # job-level continue-on-error list on the job definition itself) -- a real failure on the + # two gating lanes must still fail the job. - name: "Self-test: die-emit-fallthrough ci_skip_entry (CI-only)" if: ${{ !cancelled() && env.HP_CACHE_CORRUPTED != '1' }} + continue-on-error: ${{ matrix.mode != 'real' && matrix.mode != 'conda-full' }} env: DIE_EMIT_SCENARIO: ci_skip_entry shell: pwsh diff --git a/tests/selfapps_die_emit_fallthrough.ps1 b/tests/selfapps_die_emit_fallthrough.ps1 index 570a05cd..99ab4e62 100644 --- a/tests/selfapps_die_emit_fallthrough.ps1 +++ b/tests/selfapps_die_emit_fallthrough.ps1 @@ -1,9 +1,9 @@ # ASCII only # selfapps_die_emit_fallthrough.ps1 - Regression coverage for CLAUDE.md Active Backlog Item 46 -# Bucket A Batches 3 and 4/5 (docs/plan-die-fatal-remediation.md's "Batch Roadmap" -- the full +# Bucket A Batches 2, 3, and 4/5 (docs/plan-die-fatal-remediation.md's "Batch Roadmap" -- the full # 20-site trace and batch grouping). :die uses "exit /b" (a subroutine return, not a process # halt -- see docs/agent-lessons-learned.md's ":die" entry), so a caller with no goto/halt after -# "call :die" simply continues. This file covers three of the sites the full trace found genuinely +# "call :die" simply continues. This file covers four of the sites the full trace found genuinely # needed a goto -- each reached via a new, narrow test hook rather than a real disk failure: # # New hook: HP_TEST_FORCE_EMIT_FAIL= (run_setup.bat, :emit_from_base64) deterministically @@ -14,7 +14,7 @@ # # Four scenarios (DIE_EMIT_SCENARIO env var): # -# - "missing_python" (Batch 2, site ~1252, the NON-cascade path -- distinct from +# - "missing_python" (Batch 2, :conda_create_done, the NON-cascade path -- distinct from # selfapps_cascade_conda_create_fail.ps1's own "missing_python" scenario, which is specifically # a REQ-009 cascade RE-ENTRY where HP_CASCADE_SAVED_PY is defined and routes through # :cascade_conda_create_failed instead, never reaching this site at all): HP_FORCE_CONDA_ONLY=1, @@ -30,7 +30,8 @@ # selfapps_entrysmoke_no_interpreter.ps1 for a DIFFERENT trigger) gets a chance to catch the # broken interpreter and confirms no PyInstaller build is attempted against it. # -# - "condarc" (Batch 2's neighbor code path, site ~1275): HP_FORCE_CONDA_ONLY=1 drives a REAL, +# - "condarc" (Batch 4, the same :conda_create_done subroutine's own .condarc-staging step, +# right after the "missing_python" site above): HP_FORCE_CONDA_ONLY=1 drives a REAL, # successful conda create (python.exe genuinely produced) so :conda_create_done reaches its own # normal .condarc-staging step. HP_TEST_FORCE_EMIT_FAIL=HP_CONDARC fails that staging; before # this fix, the fall-through then attempted `copy /y "~condarc" ...` (doomed, since the source @@ -43,7 +44,7 @@ # state=error (the correct, honest report -- :die's own centralized HP_BOOTSTRAP_STATE=error set # is not undone by the graceful continuation downstream). # -# - "ci_skip_entry" (Batch 5, site ~2011): HP_CI_SKIP_ENV=1 takes the cheap, no-conda/no-uv system- +# - "ci_skip_entry" (Batch 5, :ci_skip_entry): HP_CI_SKIP_ENV=1 takes the cheap, no-conda/no-uv system- # Python path. HP_TEST_FORCE_EMIT_FAIL=HP_FIND_ENTRY fails :ci_skip_entry's own ~find_entry.py # staging; before this fix, the fall-through continued into :update_find_entry_abs / # :verify_find_entry_helper, which -- since the helper file was never staged -- ALSO failed and @@ -60,9 +61,9 @@ # flagged here so a future reader does not mistake it for something this change broke. Asserted # as the known, unchanged behavior rather than silently ignored. # -# - "determine_entry" (Batch 3, site ~1362): HP_FORCE_CONDA_ONLY=1 (same real-create cost as +# - "determine_entry" (Batch 3, :after_env_mode_selection): HP_FORCE_CONDA_ONLY=1 (same real-create cost as # "condarc"). :determine_entry runs TWICE in a normal bootstrap -- once early inside -# :after_env_mode_selection (site ~1362), once again inside :after_env_bootstrap (site ~2125) -- +# :after_env_mode_selection, once again inside :after_env_bootstrap -- # both via the identical ~find_entry.py staging call, so HP_TEST_FORCE_EMIT_FAIL=HP_FIND_ENTRY # fails BOTH invocations identically. Before this fix, the first failure's fall-through ran the # ENTIRE intervening dependency-install/pipreqs/warnfix/pyvisa block (real work, not just a From 372951e9fb7a3fcd57aeaf3db46fbb7824c9fb20 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 22:09:45 +0000 Subject: [PATCH 7/8] Fix determine_entry scenario's wrong statusState expectation (state=ok -> error) Confirmed against a real conda-full CI run (job 98968925916): every other assertion for this scenario was already correct (bothSitesReached, depPhaseSkipped both true) -- only the statusState check was wrong. :die unconditionally sets HP_BOOTSTRAP_STATE=error as its own first action (same as every other scenario in this file), and nothing downstream resets it once the run falls through to the benign "skip packaging" branch, so the honest, correct report is state=error, not state=ok. The header comment's "run still completes, state=ok" conflated docs/plan-die-fatal-remediation.md Finding 3's classification of this fall-through as "benign" (meaning: doesn't crash or loop) with a claim about the final reported status, which Finding 3 never actually made. Fixed both the assertion and the header comment. --- tests/selfapps_die_emit_fallthrough.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/selfapps_die_emit_fallthrough.ps1 b/tests/selfapps_die_emit_fallthrough.ps1 index 99ab4e62..484f829b 100644 --- a/tests/selfapps_die_emit_fallthrough.ps1 +++ b/tests/selfapps_die_emit_fallthrough.ps1 @@ -72,9 +72,13 @@ # that block outright; the second call still runs and still dies (same message text at both # sites, so this asserts an exact COUNT of 2 rather than distinguishing which site fired), then # falls through to the ALREADY-benign "if HP_ENTRY=="" skip packaging" branch confirmed safe by -# the full trace (docs/plan-die-fatal-remediation.md Finding 3) -- run still completes, -# state=ok. The key assertion is the ABSENCE of a dependency-install-phase marker that would -# only appear if the skipped block had actually run. +# the full trace (docs/plan-die-fatal-remediation.md Finding 3) -- "benign" there means the +# fall-through doesn't crash or loop, not that the final status reads ok: :die already set +# HP_BOOTSTRAP_STATE=error at both call sites (its own centralized, unconditional set, same as +# every other scenario in this file) and nothing downstream resets it, so the honest, correct +# report is state=error, confirmed directly against a real CI run. The key assertion is the +# ABSENCE of a dependency-install-phase marker that would only appear if the skipped block had +# actually run. # # "condarc"/"ci_skip_entry"/"determine_entry" (NOT "missing_python", see above) set # HP_SKIP_ENTRY_SMOKE=1 / HP_SKIP_EXE_SMOKERUN=1 (REQ-012) -- those three are about the setup-flow @@ -304,7 +308,7 @@ switch ($scenario) { # The intervening dependency-install/pipreqs/warnfix block must NOT have run -- this # marker only appears once dependency installation actually begins. $depPhaseSkipped = -not ($combined -match [regex]::Escape('[TRACE] dep install phase: start')) - $pass = $bothSitesReached -and $depPhaseSkipped -and ($statusState -eq 'ok') + $pass = $bothSitesReached -and $depPhaseSkipped -and ($statusState -eq 'error') $details = [ordered]@{ scenario = $scenario dieMsgCount = $dieMsgCount From c83208dbd62228ed22bea0120b46649e04a5e659 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 22:19:15 +0000 Subject: [PATCH 8/8] Validate DIE_EMIT_SCENARIO before use, harden cleanup with -LiteralPath An unrecognized DIE_EMIT_SCENARIO value previously reached the non-Windows platform-skip write unconditionally (reporting pass=true for a typo'd scenario, since neither switch block has a default case) and was used unvalidated to build $workDir for a Remove-Item -Recurse -Force cleanup a few lines later. Added an explicit allow-list check right after $scenario is read, failing loud (pass=false, exit 1) for anything outside the four known scenarios -- before either the platform check or $workDir construction. Also switched the workDir Test-Path/Remove-Item pair to -LiteralPath so a value containing wildcard characters can't affect an unintended path. Smoke-tested locally: DIE_EMIT_SCENARIO=bogus_value now exits 1 with pass=false and a clear error, instead of silently succeeding. --- tests/selfapps_die_emit_fallthrough.ps1 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/selfapps_die_emit_fallthrough.ps1 b/tests/selfapps_die_emit_fallthrough.ps1 index 484f829b..602f59ca 100644 --- a/tests/selfapps_die_emit_fallthrough.ps1 +++ b/tests/selfapps_die_emit_fallthrough.ps1 @@ -112,6 +112,22 @@ function Write-NdjsonRow { $scenario = $env:DIE_EMIT_SCENARIO if (-not $scenario) { $scenario = 'missing_python' } +# derived requirement: validate BEFORE the non-Windows platform-skip write below (which would +# otherwise report pass=true for a typo'd/unknown scenario, since it fires unconditionally and +# neither switch block below has a default case) and before $scenario is used to build $workDir +# for a Remove-Item -Recurse -Force cleanup a few lines down. +$validScenarios = @('missing_python', 'condarc', 'ci_skip_entry', 'determine_entry') +if ($validScenarios -notcontains $scenario) { + Write-NdjsonRow ([ordered]@{ + id = 'self.die_emit_fallthrough' + req = 'CLAUDE.md-Item-46-BucketA' + pass = $false + desc = "die-emit-fallthrough: unknown DIE_EMIT_SCENARIO value" + details = [ordered]@{ scenario = $scenario; error = 'unrecognized DIE_EMIT_SCENARIO value' } + }) + exit 1 +} + # derived requirement: matches selfapps_pyinstaller_fail.ps1's own established multi-scenario # precedent (docs/agent-ndjson.md: "Three scenarios... all emitting the same row id") -- ONE # shared, LITERAL id at every Write-NdjsonRow call site, with details.scenario as the @@ -147,7 +163,7 @@ if (-not (Test-Path $batchPath)) { } $workDir = Join-Path $here "~selftest_die_emit_$scenario" -if (Test-Path $workDir) { Remove-Item -Recurse -Force $workDir } +if (Test-Path -LiteralPath $workDir) { Remove-Item -LiteralPath $workDir -Recurse -Force } New-Item -ItemType Directory -Force -Path $workDir | Out-Null Copy-Item -Path $batchPath -Destination $workDir -Force Set-Content -Path (Join-Path $workDir 'app.py') -Value 'print("should-not-matter")' -Encoding ASCII