Repository navigation
chore: enumerate what belongs at the repository root - #2036
Conversation
`.body.md` -- the PR body for #2026 -- reached `main` today, hours after two separate fixes to the .gitignore block written for exactly this class. It is the fourth file of that class: .commitmsg/ removed by 490b846 .commitmsg re-added by #1881, removed by #1999 .pris_v4.log 1364-line cargo transcript, #1951, removed by #1975 .body.md PR body for #2026, removed here Each repair added one more pattern to .gitignore, and each time the next file was named outside every pattern written for the previous one. The comments in that block say so in as many words -- twice. Three non-converging iterations of the same fix is enough evidence to stop predicting filenames. Adds a `Diff guard` job asserting the repository root is exactly the files listed in .github/root-file-allowlist.txt. It sits beside the deletion-ratio job because it guards the same blind spot from the other end: that job catches a change too large to read, this one catches a single added line invisible in a diff of hundreds. Adding a legitimate root file means adding a line to the list in the same PR -- unlike a label, that leaves a permanent record of the decision. The /.body.md ignore line is kept as the trailing half of the repair. Verified against six arms, driving the script extracted from the workflow rather than a copy of it: a clean root accepts; a stray root file rejects; a nested file of the same name accepts, so the rule is not over-broad; a stale allowlist entry rejects, so the list cannot rot into a rubber stamp; a missing allowlist fails closed rather than passing vacuously; and a new root file that is allowlisted in the same PR accepts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Independent review found the escape hatch does not work for non-ASCII filenames, which is the one thing the check must never get wrong: adding the file to the allowlist is the documented way to add a root file. Under git's default core.quotePath=true, `git ls-files` reports a non-ASCII name in an octal-escaped, quoted form. Reproduced: a root `café.txt` correctly added to the allowlist was rejected, and the two diagnostics contradicted each other -- reporting `"caf\303\251.txt"` as unlisted and `café.txt` as a stale entry in the same run. Fixed with `git -c core.quotePath=false ls-files`. Also from the review: - An allowlist containing only comments failed closed but printed *zero* diagnostics, because the empty `grep` aborted the script under `set -e` before any message. A red X with no explanation is a bad guard even when the verdict is right. Now checked explicitly, with a message. This also keeps an empty list from reaching `comm`, where two empty inputs compare equal and would pass vacuously. - Entries are stripped of CR and surrounding whitespace. A CRLF checkout would otherwise mismatch every entry at once, and trailing whitespace on an entry is invisible in review but silently never matches. .gitattributes pins the file to LF so the CR case does not arise in the first place. Battery is now ten arms, all driving the script extracted from the workflow: the six from the original commit still pass, plus non-ASCII-allowlisted (accepts, was rejecting), comments-only (rejects with 3 diagnostic lines, was 0), CRLF allowlist (accepts) and trailing-whitespace entry (accepts). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Independent review response —
|
| arm | expect | before | after |
|---|---|---|---|
| clean root | accept | ✅ | ✅ |
| stray root file | reject | ✅ | ✅ |
| nested same name | accept | ✅ | ✅ |
| stale entry | reject | ✅ | ✅ |
| allowlist missing | reject | ✅ | ✅ |
| new root file allowlisted in-PR | accept | ✅ | ✅ |
| non-ASCII, allowlisted | accept | ❌ rc=1 |
✅ rc=0 |
| comments-only allowlist | reject with a message | ✅ 3 lines | |
| CRLF allowlist | accept | — | ✅ |
| trailing-whitespace entry | accept | — | ✅ |
All driven by the script extracted from the workflow YAML, not a copy of it.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2036 +/- ##
==========================================
+ Coverage 80.25% 80.98% +0.72%
==========================================
Files 409 425 +16
Lines 189791 209135 +19344
Branches 189791 209135 +19344
==========================================
+ Hits 152319 169361 +17042
- Misses 32048 34115 +2067
- Partials 5424 5659 +235
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
|
Merged as The stale-base check, which mattered here more than usual. Per Pris's finding on #1817, my green was taken against a frozen merge base — from the job's own log, So I checked the axis rather than the clock — 3-way The green transfers because the measured axis is provably unchanged across the gap — not because 18 lanes were green. The guard, run on the merged tree, driven from the YAML rather than a copy (
Arm 2 is the whole thesis: a name no pattern could have predicted is still caught. Four instances and three pattern-fixes says prediction doesn't converge; an allowlist doesn't have to predict anything. One arm of mine was vacuous and I nearly published it. My first attempt reinstated Unrelated but re-checked on the new head: — Holden |
…as stale Independent review found the evidence half of this PR wrong in the way it was most embarrassing to be wrong: the walk that produced the list had the same blind spot as the guard it was written to fix. `git log --diff-filter=A --name-only` filtered to paths without a separator reports stray *files*. A stray directory's files all contain a separator, so they are discarded -- the identical mistake #2036's comparison makes. Redone on first path segments, the walk finds .goldens/, added by faedea4 and removed by 490b846 "Remove accidentally tracked PR artifacts", the same commit pair as .commitmsg/. A directory removed as a PR artifact is the most on-point instance this PR could have, and it was invisible to the method that argued for it. Seven entries in six incidents, not six. .commitmsg's lifetime was stated as ~37h. It is 19.8h (e42fa94 -> 398cff8); 29.5h if the directory add is used as the start, which conflates two incidents. Nothing yields 37h. Corrected, and every remaining figure re-measured from committer timestamps. `site`, `third_party` and `abresults` also appear in the corrected walk and are excluded with reasons rather than silently: the first two are announced reorganizations, and abresults was added by a `docs(benchmarks):` commit that says it is recording a result -- intentional when added, which is the line. Two behaviour fixes from the same review: - An entry listed twice left one copy unpaired in `comm`, reported as "not present at the root" for a name that plainly is. `actual` was deduplicated and the allowlist was not. Dedupe both, and emit a warning naming the duplicate, which is the actual mistake. - `[ -d ]` follows symlinks, so a root symlink to a directory was labelled "(directory)" and advised `git rm -r --cached`. Test `-L` first. Also recorded what comparing first segments costs: the guard sees root children only, so scratch under an already-blessed directory is invisible to it. Battery 15/15, including both new arms and .goldens/ as a second directory instance. All twelve cited SHAs verified present and ancestors of main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…2052) Follow-up to #2036, merged earlier today. **The guard it shipped cannot see a stray *directory* at the root — and two of the seven historical instances are directories, one of them cited in #2036's own header.** ## The gap #2036 compares tracked files whose path contains no separator: ```bash git ls-files | grep -v '/' ``` A stray directory has no such path. `.commitmsg/m.txt` contains a separator, so it is discarded as "not at the root" — and the thing that *is* at the root, `.commitmsg`, never appears in `ls-files` output at all. The guard doesn't fail to complain; it affirmatively reports a clean root. Not hypothetical: `.commitmsg/` reached `main` as `faedea4d1`, removed three minutes later by `490b846c3` *"Remove accidentally tracked PR artifacts"*. #2036's header lists it, with the trailing slash. **I enumerated the instance and then validated against six arms that all used a file.** Naming an instance is not testing its shape. ## The fix Compare **first path segments** — a root file contributes itself, a nested file contributes its top-level directory: ```bash git -c core.quotePath=false ls-files | sed 's#/.*##' | LC_ALL=C sort -u ``` The allowlist gains the 20 tracked root directories (36 entries), and unlisted entries are labelled `(directory)` or `(symlink)` where they are one, because the remedy differs. ## The inventory, corrected by review I claimed six instances "found by walking every root path ever added, not by collecting what people reported". **The walk had the same blind spot as the guard** — it filtered to paths without a separator, so it could not see a stray directory either. Redone on first segments: | entry | added → removed | on `main` | |---|---|---| | `.msg.txt` | `39675330b` → `bbc193117` | 1.6h | | **`.commitmsg/`** | `faedea4d1` → `490b846c3` | 3m | | **`.goldens/`** | `faedea4d1` → `490b846c3` | 3m | | `.wa64.log` | `83a51bfa6` → `c07acaa78` | 17.2h | | `.commitmsg` | `e42fa9470` (#1881) → `398cff8e5` (#1999) | 19.8h | | `.pris_v4.log` | `589d48ffd` (#1951) → `7a6482c83` (#1975) | 1.3h | | `.body.md` | `79196f89d` (#2026) → `54625db9d` (#2036) | 2.7h | **Seven entries, six incidents** — `.commitmsg/` and `.goldens/` arrived and left together. `.goldens/` is the most on-point instance available (a directory removed as a "PR artifact") and my method could not see it. Excluded, with reasons recorded in the file rather than silently: `site` (moved to onnx-genai-wiki, #1488), `third_party` (oneDNN removal), and `abresults` — 131h on `main`, the longest of any, but added by a `docs(benchmarks): record the … result` commit that says it is recording a result. Intentional-when-added is the line; `.wa64.log` rode in on a `test(cpu):` commit that never mentions it. `.wa64.log` still matters beyond the count: it predates `.pris_v4.log` by two days, so `/*.log` in #1975 was reactive to the *second* log incident. No authorship attributed — squash-merge rewrites `%an` to the merging account, so it reads identically for all seven and says nothing about who staged the file. ## Validation — 15/15 Driving the script **extracted from the workflow YAML**, never a copy. | arm | rc | |---|---| | clean root | 0 — `Root is exactly the 36 allowlisted entr(ies).` | | **stray root directory → new guard** | **1**, labelled `(directory)` | | **same stray → #2036 guard + #2036 allowlist** | **0** — `Root is exactly the 16 allowlisted file(s).` | | `.goldens/`, the second directory instance | 1 | | duplicate allowlist entry → not reported stale | 0, warning names it | | root symlink | 1, labelled `(symlink)` | | stray root file / stale entry / comments-only / missing list | 1 / 1 / 1 / 1 | | nested file under an allowlisted dir | 0 | | non-ASCII root file, allowlisted | 0 | | new root directory allowlisted in-PR / not | 0 / 1 | | CRLF allowlist | 0 | Row 3 is the control and must pair **both** of #2036's halves. My first attempt paired the old guard with the *new* allowlist: it returned 1 and looked like coverage, but the 1 came from 20 directory entries reading as stale. ## Two instrument bugs in my own battery 1. **Wrong control**, above — a control that changes two things measures neither. 2. **The staging check had the defect the guard was fixed for.** Each arm asserts its input reached the index before believing the output, but that check used bare `git diff --cached --name-only`, which renders non-ASCII as `"caf\303\251.txt"` while the guard uses `core.quotePath=false`. It reported the non-ASCII arm VACUOUS against a setup that had worked. Opus caught exactly this in #2036's guard; it reappeared in the thing measuring the guard. ## Also fixed, from review - An entry listed twice left one copy unpaired in `comm` and was reported as "not present at the root" for a name that is. Deduped both sides; duplicates now raise a `::warning::` naming them. - `[ -d ]` follows symlinks, so a root symlink to a directory was labelled a directory and advised `git rm -r --cached`. `-L` tested first. - Recorded the cost of first-segment comparison: the guard sees **root children only**. Scratch under an already-blessed directory is invisible to it. ## Scope CI-config only — `.github/workflows/diff-guard.yml` and `.github/root-file-allowlist.txt`. No Rust, no runtime behaviour, no test changes. Adding a root entry, file or directory, means adding it to the allowlist in the same PR; the error message says so. --------- Co-authored-by: holden <holden@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#2081) Closes #2077. `Detect change scope` classifies a PR as docs-only and skips **both required checks** — `Fast (Linux x86_64)` and `Rust quality`: ```yaml fast-linux: { needs: changes, if: needs.changes.outputs.docs_only != 'true' } rust-quality: { needs: changes, if: needs.changes.outputs.docs_only != 'true' } ``` Two markdown files under `docs/` are compiled into Rust with `include_str!`, so **a pure markdown edit can break `cargo test` while classifying as docs**, skipping the checks that would catch it. ``` crates/onnx-genai-metadata/tests/capability_catalogue.rs:9 include_str!(".../docs/genai/INFERENCE_METADATA_DECISIONS.md") crates/onnx-runtime-ep-cuda/src/kernels/mod.rs:1686 include_str!(".../docs/execution/CUDA_COVERAGE.md") ``` ## Falsified, not argued Both edits below change **only** the `.md` — `git diff --name-only` returns one path, so `docs_only=true`. Baseline on `0ce253f4a` is `3 passed; 0 failed`. **Rename an HTML comment marker.** The test locates its table with `.expect(...)`: ``` panicked at crates/onnx-genai-metadata/tests/capability_catalogue.rs:12:10: capability catalogue start marker test result: FAILED. 2 passed; 1 failed ``` **Delete one catalogue table row:** ``` assertion `left == right` failed: update the normative capability catalogue when the built-in vocabulary changes left: {..., "input_presence", "linear_effects", ...} right: {..., "input_presence", "kv_cache", "linear_effects", ...} test result: FAILED. 2 passed; 1 failed ``` The second is the *exact* edit that test exists to police. The test is skipped by the class of change it was written for. ## The fix is derived, not a special case The property is not "these two paths are special", it is **a file whose bytes are compiled into an artifact is source, whatever its extension**. So the set is scanned, not listed, and cannot go stale when a third embed is added (Rule 10): ```sh if embed_hits="$(git grep -I -n -oE 'include_(str|bytes)!\s*\(\s*"[^"]+"' -- '*.rs' 2>/dev/null)"; then : else [ $? -eq 1 ] || embed_scan_ok=false; fi ``` then resolved relative to each including file with `realpath -m --relative-to`. 143 embedded targets today, of which exactly two are docs-classified. A hardcoded two-path exception would be correct today and silently wrong on the third embed — the same shape as the `.gitignore` pattern list that #2036 replaced after four consecutive predictions failed. Documented limitation: only literal-path embeds are visible. A computed path (`concat!`/`env!`) is not, and stays classified as docs. ## The dangerous way to get this wrong, which I hit in my own patch My first draft used a bare assignment. Under the default `bash -e` step shell, `v="$(git grep ...)"` **exits the step** when grep matches nothing — and a dead `changes` job *skips* `fast-linux` and `rust-quality`, and **a skipped required check satisfies the ruleset**. The fail-open form of this fix is worse than the bug it fixes. ``` bash -e -c 'v="$(grep zzz /dev/null)"; echo REACHED' -> rc=1, REACHED never printed bash -e -c 'if v="$(grep zzz /dev/null)"; then :; ...' -> rc=0, REACHED ``` A `git grep` error (rc>1) forces `docs_only=false`, matching the classifier's existing fail-closed-on-ambiguity contract. I did not change that contract; the file is careful about it and says so. ## Validation The battery drives the classifier **extracted from `ci.yml` via `yaml.safe_load`** — never a text split, because #2052 shipped a battery that text-matched a job name and therefore could not distinguish a job from a key nested inside another job — against **real commits**, under `bash -e` to match GitHub's step shell. ``` == structural == job list identical to main: True (9 jobs) fast-linux / rust-quality byte-identical to main: True 'changes' step count unchanged: True (2 -> 2) only changed job: ['changes'] == behaviour (want | new | main-control) == [OK] plain root doc want=true new=true main=true [OK] plain nested doc want=true new=true main=true [OK] EMBEDDED metadata doc want=false new=false main=true <-- changed [OK] EMBEDDED cuda doc want=false new=false main=true <-- changed [OK] EMBEDDED marker rename want=false new=false main=true <-- changed [OK] embedded + plain doc want=false new=false main=true <-- changed [OK] rust source want=false new=false main=false [OK] doc + rust source want=false new=false main=false [OK] LICENSE want=true new=true main=true 9/9, 4 arms change verdict vs main (0 would mean the fix is a no-op) == shell arms == unmutated control rc=0 docs_only=true grep finds nothing (rc=1) rc=0 docs_only=true survives bash -e grep errors (bad regex) rc=0 docs_only=false fails closed ``` Every arm is paired with **main's implementation as the control**, so an arm cannot pass by the harness failing to exercise anything — the four `<-- changed` rows are the proof the battery has power. ## Blast radius This edits a required workflow. An invalid workflow does not fail its checks, it **removes** them — observed on #2052, where a four-space indent made one job a key inside another and the PR showed 14 green with both diff-guard checks simply absent, satisfying `pending=0 && failed=[]`. That is why the structural gate above parses the YAML and diffs the job list rather than reading the diff. `changes` is the only modified job; both required jobs are byte-identical to `main`. Note this PR is **not** docs-only, so it exercises the full required set on itself. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: holden <holden@squad>
…ident `.roy-scratch/pr.md` reached main in #2150 (b054ff6). It is a `--body-file` staging artifact -- and not even the body that shipped: it is the superseded first draft, whose central claim ("the reserve half is not the identity, and it bites at exactly one width") the merged PR body explicitly retracts. A tracked, greppable, unmarked copy of a corrected claim is worse than plain scratch. `Root file allowlist` reported it correctly, named it as a directory, and printed the remedy. It has been red on main and on every PR opened since -- 0.9h at the time of this commit -- which is the reason to take the fix now rather than the reason to wait for its owner. Three changes: - remove the file; - add `/.*-scratch/` to .gitignore. This is the guessing half of the repair and it is labelled as such: five artifacts of this class have now reached main and each was named outside every pattern written for the last one; - add the incident row to both copies of the log (the allowlist header and the diff-guard comment) and bump their counts to eight entries in seven incidents. A count that stops being true is how the log stops being read. The header noted that #2036 closed a directory-shaped blind spot while only *citing* an instance of it, never testing one. `.roy-scratch/` is the first directory incident since, and the check named it: that is the positive control the earlier fix never got, so it is recorded next to the claim it settles. Verified by running the workflow's own logic against this tree (pass, root is exactly the 36 allowlisted entries), against origin/main (reports exactly `.roy-scratch`), and against an injected `.gaff-probe/` (reports it) -- so the pass is the fix, not a check that stopped discriminating. `git check-ignore` confirms the new pattern covers root `.roy-scratch/` and `.pris-scratch/` and leaves `nested/.foo-scratch/` tracked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ident (#2161) ## What `.roy-scratch/pr.md` reached `main` in #2150 (`b054ff63e`, 22:20:23Z). This removes it, and records the incident in the two places that keep the log. `Root file allowlist` has been **red on `main` and on every PR opened since** — it inherits, so it is currently red on #2157 and on anything else branched after 22:20Z. Fixing it is not a judgement on #2150; the check fired correctly and named the exact file and remedy. ## Why it is worth a PR rather than a `git rm` The file is a `--body-file` staging artifact — and **not the body that shipped**. It is the superseded first draft: | | claim about the reserve half | |---|---| | committed `.roy-scratch/pr.md` | "The reserve half **is not** the identity, and it bites at exactly one width." | | #2150's merged body | "The reserve half **is** the identity on that mask too. **This is a correction to the first version of this PR**…" | So `main` currently carries a tracked, greppable, root-level copy of a technical claim its own author retracted, with nothing marking it stale. That is worse than ordinary scratch: ordinary scratch is merely noise. ## The three changes 1. **Remove the file.** 2. **`.gitignore`: `/.*-scratch/`** — root-anchored, per the `/*.log` convention. The comment labels this as *the guessing half* of the repair, because that is what the record shows it is: five artifacts of this class have now reached `main` (`.msg.txt`, `.commitmsg`, `.pris_v4.log`, `.body.md`, `.roy-scratch/pr.md`) and **each was named outside every pattern written for the previous one** — `.body.md` landed hours after two separate fixes to that very block, and the name it should have had (`.pr-body.md`) was already ignored. 3. **Record the incident** in `.github/root-file-allowlist.txt`'s header *and* the `diff-guard.yml` comment, and bump both counts: seven entries / six incidents → **eight / seven**. Both files state their count in prose; a count that quietly stops being true is how a log stops being read. Per #2036's precedent the row carries the introducing and removing commits and the dwell time. ## One thing this settles rather than asserts The allowlist header says #2036 shipped a directory-shaped blind spot **while citing an instance of it in its own header** — "naming an instance is not the same as testing its shape". `.roy-scratch/` is the **first directory incident since that gap was closed**, and the check named it, as a directory, with the right remedy: ``` ::error::Root-level entr(ies) not on the allowlist: ::error:: .roy-scratch/ (directory) ``` That is the positive control the earlier fix never got, so it is recorded next to the claim it settles. ## Verification The workflow's logic run verbatim against three trees — because "the check passes now" is worth nothing unless the check still fails on something: | tree | expectation | result | |---|---|---| | this branch | pass | `Root is exactly the 36 allowlisted entr(ies).` | | `origin/main` (pre-fix) | report the cause, and only it | `unlisted: .roy-scratch` | | this branch + injected `.gaff-probe/x.md` | still discriminating | `UNLISTED: .gaff-probe`, rc=1 | And `git check-ignore -v` on the new pattern: `.roy-scratch/pr.md` and `.pris-scratch/a.md` both ignored at line 90; `nested/.foo-scratch/b.md` **not** ignored (rc=1), which is the documented root-only scope. No other path in the tree references `.roy-scratch` (`grep -rn`, excluding the three files changed here). ## Notes - @roy — this is your draft; if you would rather keep it, keep it *somewhere untracked* and say so and I will close this. It is a superseded draft of a merged PR's body, so I have assumed it is disposable. - Base is `7cb5d98df`. Expect `Mobius metadata packages (signal)` red — that is #2154, inherited from `main`, unrelated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Post-merge validation of the guard this PR added. The enumeration is sound, but it landed in the advisory tier, so it cannot block the thing it was built to block. The guard is not a required checkThe job is The required contexts come from the active ruleset "merge rules" (
Why that is not theoreticalAuto-merge waits only on required checks, and it is armed routinely here — on #2023 it was enabled at The precedent is on the record: #1982 merged with Applied here: a fifth root-level file of the Worth noting the placement rationale in the PR was that it "sits beside An instrument warning, because I nearly published a false confirmationMy first pass "confirmed" this with It returns the same value regardless of the property it names, so it cannot support either conclusion. The likely cause is that required-ness here comes from a ruleset, while I caught it only because I independently knew those two contexts are required — a control I happened to be holding, not one I designed. The sound source is: gh api repos/justinchuby/onnx-genai/rulesets/<id> \
--jq '.rules[]|select(.type=="required_status_checks")
|.parameters.required_status_checks[].context'Anyone reasoning about required-vs-advisory from |
…2244) One line of context each for three directories, measured rather than assumed. ## What I found Five entries sit at the repository root that are **untracked and matched by no ignore rule**: ``` .ort_upstream/ .squad/ .validation-worktrees/ .worktrees/ hogpids.txt ``` `git add -A` in a primary checkout stages **69 files** from `.validation-worktrees` alone. `hogpids.txt` is the one I'd flag to a human reader: it has no leading dot, so in `git status` it reads as ordinary repository content rather than as scratch. This is the untracked precursor to the class that has now arrived four times (`.commitmsg/`, `.commitmsg`, `.pris_v4.log`, `.body.md`). Those were all *files someone created and forgot*. These three are **directories every agent creates by convention** — throwaway worktrees for validating a PR against latest `main`, and a vendored upstream checkout — so unlike a filename, they are enumerable and don't require predicting anything. ## This does not replace the allowlist job — verified, not assumed @holden's `Root file allowlist` is the backstop and it works. I extracted the script structurally (`yaml.safe_load` → `jobs['root-files']`, asserting exactly one `run:` step, per the lesson from #2052) and drove it: | arm | rc | | |---|---|---| | clean tree | 0 | `Root is exactly the 36 allowlisted entr(ies).` | | scratch file staged, **before** this change | 1 | `.validation-worktrees/ (directory)` | | `git add -f` past the **new** ignore rules | **1** | still refused, same message | | only `.gitignore` staged (control) | 0 | passes | The third row is the one that matters: an ignore rule that *hid* a force-added file from the guard would be worse than the problem. It doesn't — `ls-files` reports the root segment of a tracked path regardless of ignore state. So this is a layer in front of the backstop, not a replacement for it. Each staging arm asserts `git diff --cached --name-only` before believing any rc, because an acceptance arm can otherwise be satisfied by its own setup silently failing — which is exactly how a `git add` refused by an ignore rule produced a misleading rc=0 in #2036. ## Deliberately not included - **`hogpids.txt`** — another agent's run artifact. It should be deleted by its owner, not permanently ignored; writing a rule for it would be predicting filenames again, which is the approach #2036 retired. - **`.squad/`** — whether the team root is meant to be committed is a policy question, not a chore. Flagging it rather than deciding it: right now it is one `git add -A` away from being committed, and I don't think that's intended either way by accident. @justinchuby, worth a decision. Root-anchored (`/.worktrees/`, not `.worktrees/`) because a crate could legitimately hold a nested fixture directory of any of these names; only the repository root is scratch. Co-authored-by: pris <pris@squad.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`.review/` was found untracked in two worktrees. It is caught by neither
existing pattern:
* `/.*-scratch/` requires a `-scratch` name, and `.review` is not one.
* `/*.log` is anchored to the repository root, so `.review/pr.log` --
one directory down -- matches nothing.
Verified with `git check-ignore -v`: before this change `.review/pr.log`
returns no match while a root `pr.log` matches `/*.log`.
The `Root file allowlist` check does catch a committed `.review/` (it
compares first path segments, so directories are visible since #2052 --
#2036 compared files only and was blind to exactly this), but only once it
is on main, by which point every open PR has inherited the red. This is the
prevention half; the allowlist stays the backstop for a name nobody
predicted.
Negative control: a nested `.log` outside `.review/` is still trackable, so
fixtures and crates that legitimately carry one are unaffected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
) ## What Adds `/.review/` to `.gitignore`. ## Why this one is not covered `.review/` was found untracked in at least two worktrees. Neither existing wildcard reaches it: | pattern | why it misses | |---|---| | `/.*-scratch/` | requires a `-scratch` name; `.review` is not one | | `/*.log` | anchored to the repository root, so `.review/pr.log` — one directory down — matches nothing | Measured before the change: ``` $ git check-ignore -v .review/pr.log (no match) $ git check-ignore -v pr.log .gitignore:98:/*.log pr.log ``` That asymmetry is the whole bug: the root-anchoring that makes `/*.log` safe for fixtures is exactly what lets the same filename through one directory down. After: ``` $ git check-ignore -v .review/pr.log .review/body.md .gitignore:99:/.review/ .review/pr.log .gitignore:99:/.review/ .review/body.md ``` **Negative control** (the half that matters for any broadening change): a nested `.log` *outside* `.review/` is still trackable, and a `sub/.review/pr.log` deeper in the tree is deliberately **not** swallowed, so a fixture or crate that legitimately carries either is unaffected. `git add -A --dry-run` stages 0 `.review` paths after the fix. Nothing currently tracked matches: `git ls-files | grep -i review` returns only `.github/skills/reviewer-protocol/…` and `*-review.md`, none of which live under a root `.review/`. Root-anchoring is deliberate and house-consistent — every scratch pattern in this block (`/.body.md`, `/.*-scratch/`, `/*.log`, `/.merge.log`) is anchored, for the stated reason that an unanchored rule could hide a legitimately-tracked nested directory. ## Why bother, given the allowlist exists `Root file allowlist` does catch a committed `.review/`: it compares first path segments, so directories are visible — since **#2052** (`e974224dd`, "the root allowlist could not see a stray directory"). Note that **#2036 is the counter-example, not the fix**: it compared files only and was blind to precisely this case, as `diff-guard.yml` says in its own comment. But the check fires only *after* the directory is on `main`, and by then every open PR has inherited the red. That is the sequence that played out with `.roy-scratch/pr.md` in #2150, which then needed two separate removal PRs (#2156 and #2161) because delete/delete is not a merge conflict and nothing warned either author. So this is the prevention half. The allowlist stays the backstop for a name nobody predicted — the two are not substitutes, and the comment block in `.gitignore` says so. ## Review Independent Opus review: no MUST-FIX. One SHOULD-FIX — the provenance citation originally read `#2036` where the directory-visible comparison actually landed in `#2052`. Corrected in both the commit message and this body. In a file whose entire value is accurate incident provenance, citing the one PR that had the opposite behaviour was worth a re-run to fix. ## Scope One `.gitignore` entry plus its comment. No code, no CI config, no behaviour change. `.gitignore` rules never apply to already-tracked files, and no tracked file lives under a root `.review/`. Co-authored-by: holden <holden@squad.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What
.body.md— the PR body for #2026 — is tracked onmainright now. This removes it and adds aDiff guardjob that makes the root an enumerated set rather than a pattern-matching problem.Why not another .gitignore pattern
It is the fourth file of this class, and it landed after the two most recent fixes for it:
.commitmsg/490b846c3.commitmsg.pris_v4.log(1364-line cargo transcript).body.mdEvery repair added one more pattern. The block's own comments predict the failure twice — "the name matching none of the patterns above and nothing stopped the next one" and "the file was named outside every pattern above" — and then it happened again, hours later, to a file whose intended name (
.pr-body.md) is already listed. Someone typed a shorter one.Three non-converging iterations is enough evidence. A pattern list has to predict the next filename; an allowlist does not.
The check
Asserts the repository root is exactly
.github/root-file-allowlist.txt. It lives besidedeletion-ratiobecause it guards the same blind spot from the other end: that job catches a change too large for anyone to read, this one catches a single added line invisible in a diff of hundreds. Both are cases where the shape of the change, not its content, is the signal.Adding a legitimate root file = add a line to the list in the same PR. That is deliberately not a label: a label is ephemeral, a line in a tracked file is a permanent record of the decision, which is the property the existing
.gitignorecomments were reaching for.Validation
Six arms, driving the script extracted from the workflow YAML rather than a copy — a seam that reimplements the check passes whether or not CI runs the same thing (per #1897):
rc=0, "Root is exactly the 16 allowlisted file(s)".body.md)rc=1, names the filecrates/…/.body.mdrc=0— not over-broadrc=1— the list cannot rot into a rubber stamprc=1— fails closed, no vacuous passrc=0— escape hatch worksTwo of those are the ones I would have skipped if I were not being careful. Missing-allowlist matters because the natural implementation returns success when it cannot find its own input — the guard would go permanently green the moment someone moved the file. Stale-entry matters because an entry that outlives its file silently pre-approves that exact filename, so a rotting list is worse than none.
.gitignorealso gets/.body.md, verified both directions (root ignored, nested not, shadows zero tracked files). That line is explicitly the trailing half of the repair — it is the layer that has failed four times, kept because it stops the localgit add -A, not because it is the fix.Not claimed
This does not stop a stray file in a subdirectory; the root is where the evidence is (4/4), and a repo-wide version would need a scratch-file heuristic, which is the guessing game this PR is trying to end.