diff --git a/.gitattributes b/.gitattributes index 1780cbb94b5..7487edb260e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -23,9 +23,13 @@ # wrapper does change. /src/main/__fixtures__/shell-wrapper-snapshots/*.txt linguist-generated=true -# Undecided (STA-4307 follow-up): whether the Windows CLI shim should ship CRLF. -# cmd.exe reads LF-only batch files, but orca.cmd uses `goto` with labels, which is -# the one batch construct with a history of misbehaving without CRLF. Pinning it -# changes a shipped byte, so it is a product call rather than a hygiene fix. -# If we decide yes, it is exactly this one line: -# /resources/win32/bin/orca.cmd text eol=crlf +# The one deliberate CRLF exception, and it is not a hygiene lapse: it is the byte +# that already ships. The release runner has no pin and sets no core.autocrlf, so it +# converted this file on checkout — the committed blob is 644 B with 0 CR, while the +# copy inside v1.4.192's orca-windows-setup.exe is 665 B with 21 CR, the same bytes +# with every LF doubled. Under the blanket rule above it would check out LF instead, +# changing a shipped byte on a batch file that uses `goto` with labels, in a shape +# nothing in CI executes. The pin only changes the working tree; the index stays LF. +# check-line-ending-policy.mjs asserts this pin still resolves, and +# check-windows-launcher-line-endings.ps1 runs the shim both ways on windows-2022. +/resources/win32/bin/orca.cmd text eol=crlf diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 53a14ad9ed3..5cfc56637ea 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -661,6 +661,13 @@ jobs: with: persist-credentials: false + # Why here and not after packaging: this needs nothing but the checkout, runs in + # seconds, and the encoding it asserts is decided by the checkout itself. Placed + # first it reports in about a minute instead of after a 20-minute package. + - name: Check Windows CLI shim line endings + shell: pwsh + run: ./config/scripts/check-windows-launcher-line-endings.ps1 + - name: Cache electron-builder downloads uses: actions/cache@v5 with: diff --git a/AGENTS.md b/AGENTS.md index b19f6aa65f0..2ae36ccd4fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh - **Keyboard shortcuts**: Never hardcode `e.metaKey`. Use a platform check (`navigator.userAgent.includes('Mac')`) to pick `metaKey` on Mac and `ctrlKey` on Linux/Windows. Electron menu accelerators should use `CmdOrCtrl`. - **Shortcut labels in UI**: Display `⌘` / `⇧` on Mac and `Ctrl+` / `Shift+` on other platforms. - **File paths**: Use `path.join` or Electron/Node path utilities — never assume `/` or `\`. -- **Line endings**: the whole tree is LF (`* text=auto eol=lf`), so a `core.autocrlf=true` Windows clone no longer breaks shipped shell scripts or byte-compared artifacts. `pnpm lint` fails if any tracked file with a shebang or the executable bit acquires CRLF. See [`docs/reference/line-endings.md`](./docs/reference/line-endings.md). +- **Line endings**: the tree is LF (`* text=auto eol=lf`), so a `core.autocrlf=true` Windows clone no longer breaks shipped shell scripts or byte-compared artifacts. The one exception is `resources/win32/bin/orca.cmd`, pinned `eol=crlf` because that is the byte the Windows installer already ships. `pnpm lint` fails if any tracked file with a shebang or the executable bit acquires CRLF, or if that pin stops resolving. See [`docs/reference/line-endings.md`](./docs/reference/line-endings.md). - **Windows setup scripts**: the setup/issue-command runner is a `.cmd` batch file unless the script starts with a `#!` line — never derive that from the user's terminal-shell preference, and never launch a `.cmd` runner with a bare `cmd.exe /c` from a Git Bash pane (MSYS rewrites the `/c`). See [`docs/reference/windows-setup-shell.md`](./docs/reference/windows-setup-shell.md). - **Windows child processes**: start them through `runProcess`/`spawnProcess` in `src/shared/child-process/` — never `child_process` directly. It pins `windowsHide`, refuses `shell: true`, and encodes `.cmd`/`.bat` arguments so neither `CommandLineToArgvW` nor `cmd.exe` mangles them. A ratchet test fails on any new direct import. - **Windows process enumeration**: read the table through `src/main/windows/windows-process-table.ts`, never by forking `powershell.exe`. See [`docs/reference/windows-process-enumeration.md`](./docs/reference/windows-process-enumeration.md). diff --git a/config/scripts/check-line-ending-policy.mjs b/config/scripts/check-line-ending-policy.mjs index 7a0f7d90a5f..bbbf8b98795 100644 --- a/config/scripts/check-line-ending-policy.mjs +++ b/config/scripts/check-line-ending-policy.mjs @@ -85,6 +85,57 @@ export function findViolations(entries, attrs) { return violations } +/** + * The one path the blanket LF rule must NOT reclaim. `cmd.exe` is its only consumer: + * it carries no shebang and no executable bit, so the population above never sees it. + * v1.4.192 already shipped it converted — a 644-byte LF blob arrived in the installer + * as 665 bytes with 21 CR, because the release runner pins nothing and sets no + * `core.autocrlf`. Pinning reproduces those bytes on every machine; leaving it to the + * blanket rule would change them. Asserted, not just allowed: an exception nothing + * checks is exactly how this file's encoding became a runner-image accident. + */ +export const CRLF_PINNED_PATHS = ['resources/win32/bin/orca.cmd'] + +/** + * Mirror of {@link findViolations} for the CRLF exception, and deliberately not a + * `!== 'lf'` inversion of it: the blob assertion points the same way for both rules. + * `eol=crlf` converts on checkout, so the stored blob must still be LF — a CRLF blob + * would reach macOS and Linux too, where the exception has no reason to exist. + * + * @param pins {{ path: string, tracked: boolean, crlf: boolean }[]} + * @param attrs Map + */ +export function findPinViolations(pins, attrs) { + const violations = [] + for (const pin of pins) { + // A pin naming a path that no longer exists is a silent hole, not a clean repo. + if (!pin.tracked) { + violations.push({ + path: pin.path, + rule: 'pinned to CRLF but not tracked', + detail: 'the pin no longer names a file; move the .gitattributes line or drop it' + }) + continue + } + if (pin.crlf) { + violations.push({ + path: pin.path, + rule: 'committed blob contains CRLF', + detail: 'eol=crlf already converts on checkout; a CRLF blob ships CRLF everywhere' + }) + } + const eol = attrs.get(pin.path)?.eol + if (eol !== 'crlf') { + violations.push({ + path: pin.path, + rule: `checked out with eol=${eol ?? 'unspecified'}, not crlf`, + detail: 'this is the shipped Windows CLI shim; LF here changes a byte cmd.exe parses' + }) + } + } + return violations +} + /** * Why the explicit status check: `git grep` exits 1 on "no matches", which is * indistinguishable from a failure unless we look. A silently empty result here would @@ -148,7 +199,7 @@ function readIndexBlobs(root, files) { * working-tree scan would fail for everyone on Windows and prove nothing. The committed * blob is the thing that actually ships. */ -export function collectExecutableArtifacts(root) { +function readIndex(root) { const index = new Map() const executableBit = new Set() for (const entry of gitText(root, ['ls-files', '-s', '-z']).split('\0')) { @@ -166,6 +217,11 @@ export function collectExecutableArtifacts(root) { executableBit.add(file) } } + return { index, executableBit } +} + +export function collectExecutableArtifacts(root) { + const { index, executableBit } = readIndex(root) // `git grep` narrows 20k tracked files to ~130 candidates in one C-side pass; the // anchor is per-line, so this is a superset that the blob read below trims exactly. // `-z` because plain `-l` quotes any path git considers unusual. @@ -206,23 +262,42 @@ export function collectExecutableArtifacts(root) { return entries } +export function collectCrlfPins(root) { + const { index } = readIndex(root) + const tracked = CRLF_PINNED_PATHS.filter((file) => index.has(file)) + const blobs = readIndexBlobs( + root, + tracked.map((file) => ({ path: file, sha: index.get(file) })) + ) + return CRLF_PINNED_PATHS.map((file) => ({ + path: file, + tracked: index.has(file), + crlf: containsCrlf(blobs.get(file) ?? Buffer.alloc(0)) + })) +} + export function checkLineEndingPolicy(root) { const entries = collectExecutableArtifacts(root) + const pins = collectCrlfPins(root) + const queried = [...entries.map((e) => e.path), ...pins.map((p) => p.path)] const attrs = parseCheckAttr( - gitText( - root, - ['check-attr', '-z', '--stdin', 'eol'], - `${entries.map((e) => e.path).join('\0')}\0` - ) + gitText(root, ['check-attr', '-z', '--stdin', 'eol'], `${queried.join('\0')}\0`) ) - return { entries, violations: findViolations(entries, attrs) } + return { + entries, + pins, + violations: [...findViolations(entries, attrs), ...findPinViolations(pins, attrs)] + } } function main(root) { - const { entries, violations } = checkLineEndingPolicy(root) - if (entries.length === 0) { + const { entries, pins, violations } = checkLineEndingPolicy(root) + if (entries.length === 0 || pins.length === 0) { // A population of zero means the discovery broke, not that the repo got clean. - console.error('::error::line-ending policy: found no executable artifacts to check.') + console.error( + '::error::line-ending policy: nothing to check' + + ` (${entries.length} executable artifact(s), ${pins.length} CRLF-pinned path(s)).` + ) return 1 } if (violations.length > 0) { @@ -236,7 +311,10 @@ function main(root) { ) return 1 } - console.log(`line-ending policy OK — ${entries.length} executable artifact(s), all LF.`) + console.log( + `line-ending policy OK — ${entries.length} executable artifact(s) LF, ` + + `${pins.length} CRLF-pinned path(s) still pinned.` + ) return 0 } diff --git a/config/scripts/check-line-ending-policy.test.mjs b/config/scripts/check-line-ending-policy.test.mjs index a3f57a67d22..fc3b283f16c 100644 --- a/config/scripts/check-line-ending-policy.test.mjs +++ b/config/scripts/check-line-ending-policy.test.mjs @@ -5,6 +5,8 @@ import { describe, expect, it } from 'vitest' import { checkLineEndingPolicy, containsCrlf, + CRLF_PINNED_PATHS, + findPinViolations, findViolations, hasShebang, parseCheckAttr @@ -86,6 +88,43 @@ describe('findViolations', () => { }) }) +describe('findPinViolations', () => { + const shim = 'resources/win32/bin/orca.cmd' + const pin = (over = {}) => [{ path: shim, tracked: true, crlf: false, ...over }] + const attrs = (eol) => new Map([[shim, eol === undefined ? {} : { eol }]]) + + it('passes the shim when it is LF in the index and CRLF on checkout', () => { + expect(findPinViolations(pin(), attrs('crlf'))).toEqual([]) + }) + + // This is what #17303's blanket rule alone produces, and what flips a shipped byte. + it('flags the shim once the blanket LF rule reclaims it', () => { + const found = findPinViolations(pin(), attrs('lf')) + expect(found).toHaveLength(1) + expect(found[0].rule).toContain('not crlf') + }) + + it('flags an unpinned shim, which is the accident that decided its bytes before', () => { + const found = findPinViolations(pin(), attrs(undefined)) + expect(found).toHaveLength(1) + expect(found[0].rule).toContain('unspecified') + }) + + // eol=crlf converts on checkout, so a CRLF blob would reach macOS and Linux too. + it('still requires the stored blob to be LF', () => { + const found = findPinViolations(pin({ crlf: true }), attrs('crlf')) + expect(found).toHaveLength(1) + expect(found[0].rule).toContain('blob contains CRLF') + }) + + // Otherwise deleting the file would silently empty the rule instead of failing it. + it('flags a pin whose path is no longer tracked instead of skipping it', () => { + const found = findPinViolations(pin({ tracked: false }), attrs('crlf')) + expect(found).toHaveLength(1) + expect(found[0].rule).toContain('not tracked') + }) +}) + describe('the repo itself', () => { const root = new URL('../..', import.meta.url).pathname @@ -101,6 +140,18 @@ describe('the repo itself', () => { expect(() => checkLineEndingPolicy(tmpdir())).toThrow(/git ls-files exited/) }) + it('keeps the Windows CLI shim pinned to CRLF, outside the LF population', () => { + const { entries, pins } = checkLineEndingPolicy(root) + // The literal path, not CRLF_PINNED_PATHS: comparing the result to the constant + // that produced it passes just as happily when someone empties the constant. + expect(CRLF_PINNED_PATHS).toContain('resources/win32/bin/orca.cmd') + expect(pins.map((p) => p.path)).toContain('resources/win32/bin/orca.cmd') + expect(pins.every((p) => p.tracked && !p.crlf)).toBe(true) + // The two rules must not overlap: the shim has no shebang and no executable bit, + // so requiring CRLF here cannot contradict the LF rule above. + expect(entries.map((e) => e.path)).not.toContain('resources/win32/bin/orca.cmd') + }) + it('covers the shipped launchers, the packaging scripts and the commit hook', () => { const covered = new Set(checkLineEndingPolicy(root).entries.map((e) => e.path)) for (const shipped of [ diff --git a/config/scripts/check-windows-launcher-line-endings.ps1 b/config/scripts/check-windows-launcher-line-endings.ps1 new file mode 100644 index 00000000000..c0f882ed018 --- /dev/null +++ b/config/scripts/check-windows-launcher-line-endings.ps1 @@ -0,0 +1,157 @@ +# Behavioural check for the shipped Windows CLI shim, resources/win32/bin/orca.cmd. +# +# The static half lives in check-line-ending-policy.mjs, which asserts the pin still +# resolves. This half asserts the thing the pin exists for: that the shim, in exactly +# the encoding CI checked out, still runs under a real cmd.exe. Nothing else executes +# it — smoke-packaged-cli.mjs resolves the packaged CLI to resources/bin/orca.exe on +# win32, so the .cmd beside it has never been run by any test. +# +# It also measures the LF encoding and reports the outcome. That result is a finding, +# not a policy violation, so it never fails the job on its own: the pin means LF is +# not what ships either way, and a green run must not be read as "LF was fine". +# +# Contract: this script fails or reports explicitly. There is no path on which a +# missing fixture, a broken stub, or an unexecuted arm reads as a pass. +# +# Why PowerShell and not a config/scripts/*.mjs: driving a .cmd file from Node would +# mean child_process with the .cmd argument-encoding hazard AGENTS.md forbids, and the +# repo's runProcess wrapper is TypeScript under src/. A pwsh step also has no MSYS +# layer, so the bare `/c` switch survives (docs/reference/windows-setup-shell.md). + +$ErrorActionPreference = 'Stop' +# Every native call below merges its own streams inside cmd, so PowerShell should never +# see stderr — but on hosts where this preference is on, one stray line would abort the +# run mid-arm and read as a harness crash rather than a result. +if (Test-Path variable:PSNativeCommandUseErrorActionPreference) { + $PSNativeCommandUseErrorActionPreference = $false +} + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$Relative = 'resources/win32/bin/orca.cmd' +$Source = Join-Path $RepoRoot 'resources\win32\bin\orca.cmd' + +function Fail($message) { + Write-Host "::error::$message" + exit 1 +} + +function Get-CrCount([byte[]]$bytes) { + $count = 0 + foreach ($byte in $bytes) { + if ($byte -eq 13) { $count++ } + } + return $count +} + +# `/s /c` so cmd strips exactly the outer quotes and takes the rest literally, instead +# of applying its conditional quote-stripping rule. The `2>&1` is inside the command +# line so cmd merges the streams itself; letting PowerShell merge a native command's +# stderr turns it into an ErrorRecord under some hosts and aborts the run. +function Invoke-Shim([string]$directory, [string]$arguments) { + Push-Location $directory + try { + $output = & cmd.exe /s /c "orca.cmd $arguments 2>&1" + return [pscustomobject]@{ Code = $LASTEXITCODE; Output = ($output -join "`n") } + } finally { + Pop-Location + } +} + +if (-not (Test-Path $Source)) { + Fail "PROBE CANNOT RUN: $Relative is absent. Not a pass." +} + +# --- What the checkout produced ------------------------------------------------ +# Read through git rather than off disk: `i/` is the stored blob and `w/` is the +# working tree, which is the whole distinction the pin turns on. +$eol = (& git -C $RepoRoot ls-files --eol -- $Relative) -join '' +if ($LASTEXITCODE -ne 0 -or -not $eol) { + Fail "PROBE CANNOT RUN: git ls-files --eol returned nothing for $Relative. Not a pass." +} +Write-Host "checkout state: $eol" + +if ($eol -notmatch '(^|\s)i/lf(\s|$)') { + Fail "The committed blob is not LF ($eol). eol=crlf converts on checkout, so a CRLF blob ships CRLF on macOS and Linux too." +} +if ($eol -notmatch '(^|\s)w/crlf(\s|$)') { + Fail "This runner checked $Relative out as LF, not CRLF ($eol). v1.4.192 shipped it as CRLF; the /resources/win32/bin/orca.cmd text eol=crlf pin is missing or was overridden." +} + +$shipped = [IO.File]::ReadAllBytes($Source) +Write-Host ("as checked out: {0} bytes, {1} CR" -f $shipped.Length, (Get-CrCount $shipped)) + +# --- The stub the shim shells out to ------------------------------------------- +# orca.cmd exits 1 at its `if not exist "%LAUNCHER%"` block before reaching any of +# the code under test, so a real orca.exe has to be beside it or both arms measure +# nothing. A copy of cmd.exe is the stub: `orca.cmd /c exit 7` falls through to +# `"%LAUNCHER%" /c exit 7`, making 7 a sentinel that only a shim which parsed to its +# last line can produce. No compiler, and nothing to skip on. +if (-not $env:ComSpec) { + Fail 'PROBE CANNOT RUN: %ComSpec% is unset, so there is no cmd.exe to test against. Not a pass.' +} +$scratch = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP } +if (-not $scratch) { + Fail 'PROBE CANNOT RUN: neither RUNNER_TEMP nor TEMP is set. Not a pass.' +} +$work = Join-Path $scratch 'orca-shim-eol' +Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $work | Out-Null +$stub = Join-Path $work 'orca.exe' +Copy-Item $env:ComSpec $stub -Force + +# Validate the sentinel itself, or a stub that cannot return 7 would read as a shim +# that failed to parse. +& $stub /s /c "exit 7" | Out-Null +if ($LASTEXITCODE -ne 7) { + Fail "PROBE CANNOT RUN: the stub launcher returned $LASTEXITCODE, not the 7 sentinel. Not a pass." +} + +$arms = @{} +foreach ($encoding in @('shipped', 'lf')) { + $directory = Join-Path $work $encoding + New-Item -ItemType Directory -Force -Path $directory | Out-Null + Copy-Item $stub $directory -Force + + # The `shipped` arm is the checked-out bytes verbatim — no rewrite, so it measures + # what this commit actually produces. The `lf` arm strips the CRs from those same + # bytes, which is what the blanket rule alone would have written. + $target = Join-Path $directory 'orca.cmd' + if ($encoding -eq 'shipped') { + [IO.File]::WriteAllBytes($target, $shipped) + } else { + [IO.File]::WriteAllBytes($target, [byte[]]($shipped | Where-Object { $_ -ne 13 })) + } + $written = [IO.File]::ReadAllBytes($target) + Write-Host ("[{0}] {1} bytes, {2} CR" -f $encoding, $written.Length, (Get-CrCount $written)) + + # Arm 1, the guard path: `goto :unsafe_body` must fire. Label seeking is the one + # batch construct with a history of misbehaving without CRLF, so this is the arm + # the encoding question actually rides on. + $guard = Invoke-Shim $directory 'orchestration send --body x' + # Arm 2, the fall-through: no goto taken, the shim reaches `"%LAUNCHER%" %*` and + # propagates its status. Proves the file parses end to end. + $through = Invoke-Shim $directory '/c exit 7' + + $guardOk = ($guard.Code -eq 2) -and + ($guard.Output -match 'cannot safely forward orchestration message bodies') + $throughOk = $through.Code -eq 7 + Write-Host ("[{0}] guard exit={1} want 2, message {2} | fall-through exit={3} want 7" -f + $encoding, $guard.Code, $(if ($guardOk) { 'present' } else { 'MISSING' }), $through.Code) + Write-Host ("[{0}] guard output: {1}" -f $encoding, $guard.Output) + + $arms[$encoding] = [pscustomobject]@{ GuardOk = $guardOk; ThroughOk = $throughOk } +} + +# --- Verdict ------------------------------------------------------------------- +if (-not ($arms['shipped'].GuardOk -and $arms['shipped'].ThroughOk)) { + Fail "The Windows CLI shim does not behave in the encoding this commit ships. Guard path must exit 2 with its own message and the fall-through must propagate 7." +} + +if ($arms['lf'].GuardOk -and $arms['lf'].ThroughOk) { + Write-Host 'FINDING: LF is safe for this shim on this image — both arms matched the shipped encoding. The CRLF pin is a consistency choice (it reproduces the shipped bytes), not a correctness one.' +} else { + Write-Host 'FINDING: LF breaks this shim on this image, while the shipped encoding passes both arms. The CRLF pin is load-bearing.' +} + +Write-Host 'Windows CLI shim OK in the encoding this commit ships.' +exit 0 diff --git a/docs/reference/line-endings.md b/docs/reference/line-endings.md index 49e279e8000..5b17d777ac3 100644 --- a/docs/reference/line-endings.md +++ b/docs/reference/line-endings.md @@ -9,7 +9,8 @@ ``` That means Git stores every text file with LF **and** writes it to disk with LF, on -macOS, Linux and Windows alike. Nothing in the tree is checked out with CRLF. +macOS, Linux and Windows alike. One file is deliberately checked out with CRLF — +the Windows CLI shim, [below](#the-one-crlf-exception). Everything else is LF. ## What this fixes @@ -47,6 +48,48 @@ git reset --hard A fresh clone needs nothing. +## The one CRLF exception + +`resources/win32/bin/orca.cmd` is pinned the other way: + +``` +/resources/win32/bin/orca.cmd text eol=crlf +``` + +This is not a hygiene lapse. It is the byte that already ships. The release Windows +runner pins nothing and sets no `core.autocrlf`, so it converted this file on checkout +long before the blanket rule existed: + +| | committed blob | inside v1.4.192's `orca-windows-setup.exe` | +| --- | ---: | ---: | +| size | 644 B | 665 B | +| CR | 0 | 21 | + +665 − 644 = 21, exactly the file's line count — the same bytes with every `\n` doubled. +The same installer carries its own control: the seven files under `resources/plugins/` +were already pinned to LF and shipped unconverted, byte-identical to their blobs. One +artifact, both directions. + +So the pin **reproduces** today's shipped launcher rather than changing it, and makes it +deterministic instead of dependent on a runner-image default nobody controls. Leaving +the file to the blanket rule is what would flip a shipped byte — on a batch file that +uses `goto` with labels, into an encoding no smoke test executes +(`config/scripts/smoke-packaged-cli.mjs` resolves the packaged CLI to +`resources/bin/orca.exe` on win32, never the `.cmd` beside it). + +Whether LF actually *breaks* `cmd.exe` here is measured, not assumed — +`config/scripts/check-windows-launcher-line-endings.ps1` runs the shim both ways on +`windows-2022` and reports which. The pin stands either way, because reproducing the +shipped bytes is the point. + +The pin changes the working tree only. The stored blob stays LF, so +`git add --renormalize` still stages nothing: + +``` +$ git ls-files --eol -- resources/win32/bin/orca.cmd +i/lf w/crlf attr/text eol=crlf resources/win32/bin/orca.cmd +``` + ## The three exemptions - `config/patches/*.patch` are `-text`: pnpm hashes each patch byte-for-byte, so any @@ -68,6 +111,13 @@ executable bit, 132 files today — it asserts both halves independently: Neither implies the other: a clean blob still breaks Windows under a stray `eol=crlf`, and a correct attribute still ships CRLF if the blob itself carries it. +The same script asserts the CRLF exception with the mirrored rules — the blob must +still be LF, and `eol` must resolve to `crlf` — so the exception cannot be silently +reclaimed by the blanket rule or quietly widened. On Windows, +`check-windows-launcher-line-endings.ps1` adds the empirical half: it reads +`git ls-files --eol` to confirm the runner really wrote CRLF, then executes the shim's +guard path and its fall-through under a real `cmd.exe`. + The population is derived from file content, not hand-listed. A curated list would have had to name 122 files on the day this landed and would silently miss the 123rd — which is exactly how the broken launcher shipped. Equally, the gate is not a repo-wide "no