Repository navigation
Description
Follow-up from Flight's git status reports clean while the file on disk is still CRLF, so the original defect (dead suite, broken shebang) survives with no remaining signal.
Current text (scripts/check-shebang-eol.mjs, merged dev)
console.error(` ${unpinned.length > 0 ? '2' : '1'}. Run: git add --renormalize -- <path> (a rule alone does not rewrite committed blobs)`);That repairs the index. It does not repair the working tree. Result: i/lf w/crlf — the #1793 condition.
Measured, not reasoned
Scratch repos reproducing the exact production condition (CRLF blob committed with no rule, then *.js text eol=lf added in a second commit, pre-existing working tree). Blob state read as raw bytes in Node — never pipe git cat-file through PowerShell, it re-encodes line endings and gives a confidently wrong answer.
| # | Sequence | HEAD | index | worktree | git status |
|
|---|---|---|---|---|---|---|
| A | git add --renormalize ← what we ship |
CRLF | LF | CRLF | M |
partial |
| B | A + git checkout -- <path> |
CRLF | LF | CRLF | M |
silent no-op |
| C | A + commit + git checkout -- <path> |
LF | LF | CRLF | (clean) |
silently wrong |
| H | A + commit + git checkout --force -- <path> |
LF | LF | CRLF | (clean) |
--force does not force |
| E | A + commit + delete file + git checkout |
LF | LF | LF | (clean) |
✅ repaired |
| F | A + commit + git rm --cached -r . + git reset --hard |
LF | LF | LF | (clean) |
✅ repaired |
| G | A + delete file + git checkout (no commit) |
CRLF | LF | LF | M |
✅ repaired, uncommitted |
Three findings, in increasing order of nastiness
git checkout -- <path>is a no-op here (row B). It looks like the obvious second step. Git skips the write because the stat cache says the file is current.--forcedoes not force it either (row H). This is the one nobody would guess, and it is the natural escalation after B fails.- Once the blob is LF,
git statusis structurally incapable of reporting a CRLF worktree (rows C/H). The clean filter normalizes CRLF→LF on the way in, so git compares LF-to-LF and sees no difference. The remediation we ship converts a visible, permanently-dirty file into an invisible broken one. That is a strict downgrade — the churn was annoying but at least it was a signal.
Only deleting the file first works. That is the single non-obvious step, and it is absent from the message.
Proposed replacement
console.error(` ${unpinned.length > 0 ? '2' : '1'}. Repair the committed blob:`);
console.error(' git add --renormalize -- <path>');
console.error(` ${unpinned.length > 0 ? '3' : '2'}. Force the working tree to be rewritten -- the step above fixes`);
console.error(' the index only, and the working tree keeps its CRLF copy:');
console.error(' rm <path> && git checkout -- <path>');
console.error(' Deleting first is REQUIRED. `git checkout` alone is a silent no-op (stat');
console.error(' cache), and `--force` does not override it. Once the blob is LF, `git');
console.error(' status` reports clean whether the working tree is CRLF or not -- the clean');
console.error(' filter normalizes on read -- so skipping this step leaves a broken file');
console.error(' with no remaining signal. Verify with a byte check, not `git status`.');Verification that is actually valid (PowerShell-safe):
node -e "const b=require('fs').readFileSync(process.argv[1]);console.log([...b].filter((c,i)=>c===10&&b[i-1]===13).length+' CRLF')" <path>
Why this belongs on the record beyond the text fix
This is tooling reproducing the defect it was built to catch — third occurrence in this family (the CRLF-shebang lint authored with CRLF; check-shebang-eol's enumeration blind to its own churn class, #1804; now its remediation manufacturing the invisible variant). It is also the sixth instance of a silent no-op masquerading as success in this area, alongside --renormalize staging nothing for the LF-blob/CRLF-worktree mirror case (*.snap).
The generalization worth keeping: for text eol=lf files, git status is not a valid oracle for working-tree line endings in either direction. Any runbook step, gate message, or repair block in this repo that verifies with git status is verifying nothing. Byte-count or blob-SHA only.
Related: #1793 (same condition, reached a different way), #1804, #1805, #1790.
Activity
github-actions commentedon Aug 21, 2026
📋 Assigned to Booster (CI/CD Engineer)
Issue: #1809 — check-shebang-eol's remediation message repairs the index only — and lands the developer at
git statusclean with a still-CRLF fileBooster will pick this up in the next Copilot session.
bradygaster commentedon Aug 21, 2026
Scale of the invisible class, measured on a real checkout
Follow-up to the point above that
git statuscannot see a CRLF working tree once the blob is LF. I measured how many files are actually in that state right now, on this worktree, immediately after tonight''s merges.At the same moment:
git status --porcelaincheck-shebang-eol.mjsmjs-shebang-loadable+check-shebang-eolsuitesEvery oracle we have reports success while 34 files are in the bad state, 16 of them shebanged — including
cli.jsandindex.cjs, which are shipped entrypoints. A publish from a working tree in this state ships CRLF shebangs.Two of the 34 are
packages/squad-cli/src/remote-ui/app.jsanddocs/src/pages/rss.xml.js— the exact two files #1805 renormalized tonight. The blob fix landed correctly and the working tree kept its CRLF copy, which is this issue''s failure mode reproducing on the change that introduced the rule. Consistent with the probe table: renormalize repairs the index, and nothing repairs the working tree.The actual gap
check-shebang-eol.mjsreads the index — deliberately, and that is right for a CI gate, since a fresh CI checkout has index == HEAD == correct. But it means nothing in this repo checks working-tree EOL state, andgit statusstructurally cannot. That is why 34 files accumulated silently.Working detector, ~40 lines, exits non-zero (this is what produced the numbers above):
Suggested shape: a
--worktreeflag on the existing gate. Local/pre-commit only, never in CI — a fresh CI checkout is correct by construction, so a CI-side worktree check would only ever produce false confidence.Consequence for how the repair gets verified
Any runbook confirming an EOL repair with
git status, or with a test suite, is sampling. The suites cover 26 of 173 pinned files; the 34 currently-CRLF files include zeroscripts/*.mjs, which is exactly why they pass. A suite-based check is valid for the narrow claim "the dead suites load again" and invalid for "the repair worked."bradygaster commentedon Aug 21, 2026
Correction to my previous comment — git ships the oracle already
I wrote that "nothing in this repo checks working-tree EOL state, and
git statusstructurally cannot" and supplied a 40-line byte detector. The first half of that is misleading and the detector is redundant.git statusis genuinely blind — that part is verified and stands. Butgit ls-files --eolreads the working tree directly and reports it accurately:Ground truth by raw byte count on the same five files:
CRLF(13),CRLF(1),CRLF(1937),CRLF(334),LF. Every field matches, includingw/mixedfor the file with exactly one CRLF line.At full-repo scale the two methods agree exactly:
git ls-files --eolfiltered toattr/text eol=lf+w/(crlf|mixed)So the complete detector is one command, not forty lines:
w/mixedmust be included —docs/src/pages/rss.xml.jshas a single CRLF line and reportsmixed, notcrlf. Filtering onw/crlfalone silently misses it, which is the same class of near-miss this issue is about.What this changes
--worktreeflag becomes trivial — parse onegit ls-files --eolcall. No byte reading, nocheck-attrbatching, and none of the Windows argv-limit handling that needed. Still local/pre-commit only; a fresh CI checkout is correct by construction.rm→git checkout --→ re-run the same command → expect empty. That is a real post-condition, which neithergit statusnor a test suite can provide.git status, which cannot answer the question, and nobody checked whether a better instrument existed. That is a worse finding than "there is a gap," and a more useful one.The byte detector above is still fine as an independent cross-check — it is how I caught this — but it should not be what gets built.
bradygaster commentedon Aug 21, 2026
Validated against the real specimen, not a scratch repo
Three things confirmed on this branch''s 34-file working tree.
1. The repair procedure works on a real file
Byte delta is exactly 200 — one byte per CR removed.
git statusreported clean both before and after, which is the blindness demonstrated end to end on a real file rather than a fixture.2. The blindness is structural, not a stale stat cache — verified
I suspected the clean
git statusmight just be git skipping the comparison on a cached stat. It isn't. Touchingcli.js''s mtime with byte-identical content forces git to re-read and fully compare — and it still reports clean:The clean filter normalizes CRLF→LF on read, so git compares LF to LF. The original mechanism claim in this issue stands, now with a controlled test behind it rather than an inference.
3. New gotcha —
git statuscan reportMwith an emptygit diffRewriting a file''s line endings in place produced:
Status and diff disagree, and diff is right. This is index staleness, not a change.
git update-index --refreshclears it with zero content modification — verified byte-identical afterwards (6731 / CRLF=200 /w/crlf/ clean).Worth knowing for anyone running the repair: if you see
Mwith an empty diff, do notgit addit. Refresh the index instead. Staging it would commit a line-ending flip that no diff was showing you. The repair sequence itself does not produce this state — only in-place rewriting does.The specimen is reproducible, so it does not need protecting
I destroyed and rebuilt one of the 34 in place and restored it byte-identically. So this branch is not a precious artifact and no "do not run the sweep here" convention is needed — a social protection that would fail silently anyway.
Post-condition after rebuild:
git ls-files --eolreportsw/crlf,git statusreports clean, repo-wide count returns to 34. Anyone implementing--worktreecan recreate the state from scratch rather than depending on this checkout surviving.docs(e2e): post-condition must match w/mixed, not just w/crlf
docs(e2e): make the renormalize selector attribute-driven, not glob-s…
github-actions commentedon Sep 10, 2026
🔄 Ralph — Auto-Triage
Assigned to: INCO
Reason: Matched routing keyword(s): copy
Source: routing-rule
github-actions commentedon Sep 10, 2026
🔄 Ralph — Auto-Triage
Assigned to: INCO
Reason: Matched routing keyword(s): copy
Source: routing-rule
github-actions commentedon Sep 11, 2026
🔄 Ralph — Auto-Triage
Assigned to: INCO
Reason: Matched routing keyword(s): copy
Source: routing-rule
github-actions commentedon Sep 11, 2026
🔄 Ralph — Auto-Triage
Assigned to: INCO
Reason: Matched routing keyword(s): copy
Source: routing-rule
github-actions commentedon Sep 13, 2026
🔄 Ralph — Auto-Triage
Assigned to: architect
Reason: No module, routing, or role keyword match — routed to Lead/Architect
Source: lead-fallback
github-actions commentedon Sep 24, 2026
📋 Assigned to release-engineer (Release Engineer)
Issue: #1809 — check-shebang-eol's remediation message repairs the index only — and lands the developer at
git statusclean with a still-CRLF filerelease-engineer will pick this up in the next Copilot session.