fix(code-tidying): parse porcelain by slicing so spaced paths survive discovery - #3140
Conversation
… discovery
audit-comment-residue's default-target router parsed `git status --porcelain`
with `awk '{print $NF}'`. A path containing a space was split on that space and
git's closing quote was kept, so the reconstructed path named no file and
dropped out of the target list.
The failure mode is what makes this worth more than a parse fix: the run
reported `files=0` alongside the note `no code targets`. An audit that errors
prompts a retry; one that confidently reports a clean tree ends the
investigation. The finding was present the whole time — passing the same file
explicitly surfaced it.
detect.sh now slices the path out of the porcelain record, takes the right-hand
side of a rename, and unwraps git's quoting. The rename split is gated on the
`R`/`C` index status letter so an ordinary path that happens to contain " -> "
is left intact. Git's octal escapes for control and non-ASCII bytes are still
not decoded; that limitation is recorded at the parse site instead of silent.
The skill's own `Uncommitted code files` pre-computed context carried the
identical `$NF` parse and would have previewed the same false negative, so it
is fixed the same way.
Regression coverage lands as three separately-rooted fixtures — spaced path,
rename, and spaced rename. They are separate repos on purpose: sharing one lets
a correctly-parsed file hold `files=` above zero and mask the other arm's
disappearance. Six of the eight new assertions fail against the pre-fix parse.
Closes #3126
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaYGqoRKUL6ScK6Yj8xHSN
|
Claude finished @kyle-sexton's task in 1m 8s —— View job Security review complete
No security issues found. This PR changes how
No CRITICAL/IMPORTANT/SUGGESTION findings to report. |
|
Claude finished @kyle-sexton's task in 9m 15s —— View job Claude finished the
|
|
Last security-reviewed head: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6017877ea5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…olumn too
Porcelain's short format is XY: X is the index status, Y the worktree status,
and a rename can be recorded in either. The arrow-split gate inspected X alone,
so an intent-to-add rename — `mv old new && git add -N new`, which emits
" R old -> new" with R in Y and X blank — was left unsplit. The whole
"old -> new" string became the path, resolved to nothing, and the run reported
files=0.
That is a regression this PR introduced: `awk '{print $NF}'` returned new.py for
that record by accident of field order, and it is the exact false-negative shape
the PR exists to remove, so it has to be closed here rather than deferred.
Both columns are now checked, in detect.sh and in the mirrored SKILL.md
pre-computed-context parser. Regression guard added as case 9d; verified it
fails against the X-only gate.
Reported by Codex review on #3140 and reproduced locally against git 2.43.0
before fixing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaYGqoRKUL6ScK6Yj8xHSN
|
Claude finished @kyle-sexton's task in 42s —— View job Security review complete
No security issues found. Skip-gate note: the only commits since the last reviewed head (
|
|
Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
…, and test it The pre-computed-context parser in SKILL.md was fixed only to the quote-stripping half of what detect.sh does: it unwrapped the surrounding quotes but never unescaped the \" and \\ that git emits inside them. A filename C-quoted because it contains a quote or backslash — rather than a space — was therefore still previewed wrong, so "fixed the same way" was an overstatement. Both parsers now handle the same cases identically, verified against real filenames on disk: quote".py, back\-slash.py, plain space.py, and a worktree-column rename all round-trip to the same bytes through either. The deeper problem was that SKILL.md's parser had no automated coverage at all, which is why it could be half-fixed without anything noticing. Case 10 now EXTRACTS the awk program from SKILL.md and executes it, rather than restating it in the test — a restatement would keep passing while the real line rotted. It also asserts every file detect.sh audits appears in the preview, so the model is never shown a tree the audit disagrees with. Verified discriminating: four of its assertions fail against the previous SKILL.md parser. The backslash fixture is named back\-slash.py rather than back\slash.py because the shell-portability gate substring-matches \\s and read the filename as a GNU-only regex class. Renaming avoids a portability-ok suppression that a future reader would have to re-evaluate, and tests the same thing. Reported by the /review:code-review pass on #3140. Its other finding (X-column- only rename gate) was already closed by 92e04c9 — that review ran against the pre-fix head. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LaYGqoRKUL6ScK6Yj8xHSN
|
Both Finding 1 — X-column-only rename gate. Already fixed in Finding 2 — The parser unwrapped the surrounding quotes but never unescaped the
On the missing coverage — that was the root cause, not a side note: Suite is 45/45. One incidental note: the backslash fixture is Thanks for both passes. The extract-and-execute test exists because of finding 2. Generated by Claude Code |
|
Claude finished @kyle-sexton's task in 1m 11s —— View job Security review complete
No security issues found. Scope note: the last reviewed head was
This is consistent with the prior three security-review passes on this PR (no issues found each time); the two open findings on this PR (worktree-column rename gate — now fixed in |
|
Claude finished @kyle-sexton's task in 2m 57s —— View job Claude finished the
|
Resolves two conflicts in docs-hygiene, both from #3142 bumping the plugin to 0.19.0 while this branch carried 0.18.4: - plugin.json: version becomes 0.19.1, this branch's fix on top of 0.19.0. - CHANGELOG.md: this branch's entry re-headed [0.19.1] and ordered above [0.19.0]; both entries kept in full. #3140 also landed on main during this branch's life, so the sibling `code-tidying/audit-comment-residue` parse it ports from is now present. The gate and the two-step unescape here are identical to it, so the two porcelain parsers converge rather than failing in opposite directions on renames. Noted in the changelog entry. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CXYVz59XVYS7NEjMB5UyHq
#3140 fixed the same #3126 false negative on main while this branch was open, so the two implementations collided in detect.sh, detect.test.sh and the changelog, and both claimed 0.13.3. Resolved toward the union rather than either side: * detect.sh keeps this branch's `--porcelain -z` read. Verified a strict superset of the landed v1 slice: it resolves the intent-to-add rename #3140 gated on the worktree status letter (` R dst\0src\0` -> dst), structurally rather than by matching an arrow, and additionally decodes the paths #3140 records as still missing. * detect.test.sh takes #3140's suite verbatim — its spaced-rename, worktree-column-rename and SKILL.md-parity coverage exceeds this branch's — plus a new case 11 for the paths v1 escapes. Two of those five assertions fail against the parse on main, which is the gap this branch closes; the arrow case passes there already. * CHANGELOG keeps #3140's 0.13.3 entry intact and adds 0.13.4 above it, written as a follow-on to the limitation 0.13.3 recorded rather than a restatement of the original bug. Version moves to 0.13.4 since 0.13.3 is published. SKILL.md's preview parser still carries the v1 slice, so it and detect.sh now diverge on escaped paths. The parity assertion still passes because REPO13 holds no such fixture. Left as-is pending a call on whether to move that parser to -z too. Refs #3126, #3140 Co-Authored-By: Claude <noreply@anthropic.com>
#3156 landed the dissolve-comments scope-fallback ladder on main and took 0.14.0, so this branch's 0.13.4 no longer sits above the published version and both the manifest and the changelog collided. * plugin.json takes main's description verbatim — #3156 rewrote the dissolve-comments clause — with the version moved to 0.14.1. * CHANGELOG keeps main's 0.14.0 entry intact and re-headings this branch's entry as 0.14.1 above it. The entry text is unchanged: it documents the audit-comment-residue escape-decode fix, which is untouched by #3156. No code conflict this time; #3156 does not touch audit-comment-residue. Refs #3126, #3140, #3156 Co-Authored-By: Claude <noreply@anthropic.com>
Review on #3151 flagged the divergence this branch had left open: detect.sh reads -z and audits an escaped path, while the `Uncommitted code files` pre-computed context still ran the v1 slice and listed nothing for it. #3140 brought that line to parity deliberately and added a test that extracts and runs it, so leaving it behind reopened the gap that test exists to close, on the surface the model actually reads. The preview now runs the same NUL-delimited read. The parity test was also masking the mismatch. It extracted only the awk program and hardcoded `git status --porcelain` as the input, so a first attempt that hardcoded `-z` instead made the v1 program look correct: -z output carries no quoting for a quote-stripping parse to fail at, and every existing fixture is ASCII. The reader now extracts the porcelain invocation from SKILL.md too, and a non-ASCII fixture joins REPO13 — the only case the two parses do not already agree on. Verified by swapping main's SKILL.md back in: three assertions fail, including #3140's own `SKILL.md preview covers every file detect.sh audits`, which the hardcoded harness had been passing through the divergence. Refs #3126, #3140 Co-Authored-By: Claude <noreply@anthropic.com>
….19.1) (#3171) Closes #3143 ## Summary `audit-noise`'s `git status --porcelain` parse dropped files whose paths git treats specially. Both defects are the silent-false-negative class: the file did not error, it simply disappeared from the target list, so a run over a tree containing one reported clean. ## Fix **Rename split is gated on the status letter, not the path text.** It previously fired on any record whose path contained `" -> "`, so a file literally named `notes -> draft.md` was reduced to `draft.md` — a name that resolves to nothing. It now gates on `[RC]` in *either* column, which is narrower and still catches a rename recorded in the index or the worktree. **`\\` is now unescaped as well as `\"`.** Git C-quotes a path for an embedded backslash too, so `back\-slash.md` stayed escaped and resolved to nothing. Both escapes are undone, `\"` before `\\`. #3140 landed on `main` while this branch was in flight. The gate and the two-step unescape here are **identical** to the ones it gave `code-tidying/audit-comment-residue`, so the two porcelain parsers now converge rather than failing in opposite directions on renames — which is what #3143 asked for. `audit-noise`'s `SKILL.md` does not mirror this parse — it uses a plain `grep '\.md$'` pipeline — so the conditional "mirrored parser" half of the issue does not apply here. `docs-hygiene` 0.19.1. ## Verification - `detect.test.sh`: **all 66 checks pass**, including 5 new cases. - **Discriminator check**: reverting only `detect.sh` to the old parse fails exactly the two new defect cases (`' -> '` path, backslash path) and nothing else — so neither case passes vacuously. The rename cases pass under both implementations by design; they are regression guards showing the new gate does not cost the `old -> new` handling it narrows. - All 7 `docs-hygiene` suites pass, re-run after merging `main`; nothing outside this skill references `audit-noise/scripts/detect.sh`. - `shellcheck`, `shfmt -d`, `markdownlint-cli2`, `typos`, `editorconfig-checker`: clean. - `check-fixture-git-isolation.sh --check`, `check-plugin-manifest-presence.sh`, `check-changelog-parity.sh --check` / `--check-order` / `--check-bump` / `--check-preserved` against `origin/main`: pass. The suite also picks up the `unset GIT_DIR GIT_WORK_TREE GIT_CONFIG` isolation line that 0.18.3's sweep missed on this file. **Known residual, recorded at the parse site rather than left implicit:** git's octal escapes (`\NNN`) for control and non-ASCII bytes are still not decoded, so those paths continue to miss. Converging on `git status --porcelain -z` would close the class outright rather than extending the string parse a third time. ## Related - Refs #3140 — the sibling `code-tidying/audit-comment-residue` parse fix this issue was deferred out of; now landed, and this change converges with it. - Refs #3126 — the original `audit-comment-residue` defect report. - Refs #2872 — the fixture git-isolation convention this suite now satisfies. - Refs #3142 — bumped `docs-hygiene` to 0.19.0 mid-flight; merged in, so this PR ships 0.19.1. Co-authored-by: Claude <noreply@anthropic.com>
#3140 fixed the same #3126 false negative on main while this branch was open, so the two implementations collided in detect.sh, detect.test.sh and the changelog, and both claimed 0.13.3. Resolved toward the union rather than either side: * detect.sh keeps this branch's `--porcelain -z` read. Verified a strict superset of the landed v1 slice: it resolves the intent-to-add rename #3140 gated on the worktree status letter (` R dst\0src\0` -> dst), structurally rather than by matching an arrow, and additionally decodes the paths #3140 records as still missing. * detect.test.sh takes #3140's suite verbatim — its spaced-rename, worktree-column-rename and SKILL.md-parity coverage exceeds this branch's — plus a new case 11 for the paths v1 escapes. Two of those five assertions fail against the parse on main, which is the gap this branch closes; the arrow case passes there already. * CHANGELOG keeps #3140's 0.13.3 entry intact and adds 0.13.4 above it, written as a follow-on to the limitation 0.13.3 recorded rather than a restatement of the original bug. Version moves to 0.13.4 since 0.13.3 is published. SKILL.md's preview parser still carries the v1 slice, so it and detect.sh now diverge on escaped paths. The parity assertion still passes because REPO13 holds no such fixture. Left as-is pending a call on whether to move that parser to -z too. Refs #3126, #3140 Co-Authored-By: Claude <noreply@anthropic.com>
#3156 landed the dissolve-comments scope-fallback ladder on main and took 0.14.0, so this branch's 0.13.4 no longer sits above the published version and both the manifest and the changelog collided. * plugin.json takes main's description verbatim — #3156 rewrote the dissolve-comments clause — with the version moved to 0.14.1. * CHANGELOG keeps main's 0.14.0 entry intact and re-headings this branch's entry as 0.14.1 above it. The entry text is unchanged: it documents the audit-comment-residue escape-decode fix, which is untouched by #3156. No code conflict this time; #3156 does not touch audit-comment-residue. Refs #3126, #3140, #3156 Co-Authored-By: Claude <noreply@anthropic.com>
Review on #3151 flagged the divergence this branch had left open: detect.sh reads -z and audits an escaped path, while the `Uncommitted code files` pre-computed context still ran the v1 slice and listed nothing for it. #3140 brought that line to parity deliberately and added a test that extracts and runs it, so leaving it behind reopened the gap that test exists to close, on the surface the model actually reads. The preview now runs the same NUL-delimited read. The parity test was also masking the mismatch. It extracted only the awk program and hardcoded `git status --porcelain` as the input, so a first attempt that hardcoded `-z` instead made the v1 program look correct: -z output carries no quoting for a quote-stripping parse to fail at, and every existing fixture is ASCII. The reader now extracts the porcelain invocation from SKILL.md too, and a non-ASCII fixture joins REPO13 — the only case the two parses do not already agree on. Verified by swapping main's SKILL.md back in: three assertions fail, including #3140's own `SKILL.md preview covers every file detect.sh audits`, which the hardcoded harness had been passing through the divergence. Refs #3126, #3140 Co-Authored-By: Claude <noreply@anthropic.com>
Close the residual octal-escape / arrow-bearing-name gap in both porcelain parsers by switching to git status --porcelain -z. Refs #3140
…#3151) No related issue: follow-on to the limitation #3140 recorded at its own parse site. #3126 is already closed by that PR, and no issue tracks the residual escape-decode gap. ## Summary #3140 fixed the #3126 false negative by slicing the v1 porcelain record, and recorded the remaining limitation at the parse site: > Git's octal escapes for control and non-ASCII bytes are still not decoded by either, so such a path continues to miss. This PR closes that gap, in both parsers that carry it. **It is a follow-on to #3140, not a competing fix.** | Path | v1 renders as | v1 slice yields | |---|---|---| | `café.py` | `?? "caf\303\251.py"` | literal escape sequence — names nothing | | `tab<TAB>here.py` | `?? "tab\there.py"` | literal `\t` — names nothing | Both confirmed against git 2.55. The file is silently dropped, so the run reports a clean tree — the same false-negative class #3126 described. ## Fix Both porcelain parsers in this skill move to the NUL-delimited `--porcelain -z` form, which git documents as performing no quoting or backslash-escaping, so there is nothing left to decode. - **`detect.sh`** — the audit's target router. - **`SKILL.md`'s `Uncommitted code files:` preview** — the pre-computed context the model reads. #3140 deliberately brought this to parity and added a test that extracts and runs it, so leaving it behind would have reopened the divergence that test exists to prevent: the audit would find `café.py` while the preview listed nothing. Under `-z` a rename emits the **new** path first and the original as a following record — the reverse of v1's display order — and that second record is consumed and dropped. Rename handling is therefore structural, with no arrow matching, which also resolves the intent-to-add rename #3140 gated on the worktree status letter (` R dst\0src\0` → `dst`, verified against git 2.55). ## Verification - **53/53 pass**, including every one of #3140's checks. - Nothing vacuous, checked by reverting each piece: - `main`'s `detect.sh` → fails 2 of case 11's 5 assertions (`non-ASCII`, `tab-bearing`). The arrow assertion passes on `main` — #3140's `[RC]` gating already handles it, **not** claimed here. - `main`'s `SKILL.md` → fails 3 assertions, including #3140's own `SKILL.md preview covers every file detect.sh audits`. - The parity harness was itself masking the defect and is fixed here. It extracted only the awk program and hardcoded the porcelain invocation; feeding `-z` to a v1 program makes the v1 program look correct, because `-z` output carries no quoting for it to fail at decoding. It now reads the invocation from `SKILL.md` too, and `REPO13` gains a non-ASCII fixture. - `mawk 1.3.4` (the runner's default `awk`) confirmed to support `RS = "\0"` before relying on it. - Local gates green: typos, shell-portability (scripts and `SKILL.md`), skill-portability, skill-precompute-compose, changed-skills, leaf-names, count-claims, fixture-git-isolation, orphaned-fixtures, manifest-duplicate-keys, `bash -n`, changelog-parity in all three modes. - `ci-status` (the required aggregate check) reported **success** on head `65a7b2c`; head `d7b2b67` adds only a `main` merge plus the version rebase. ## Current state — read this before continuing Head is **`d7b2b67`**, version **`0.14.2`**. `main` published its own `0.14.0` (#3156) and `0.14.1` mid-review, so the version was rebased twice; the changelog keeps each published entry intact with this PR's entry above them. **One blocker remains, and it is not about the code.** The branch's commits are signed with a key that is not registered on the committing GitHub account, so they report `verified=false, reason=unknown_key`. That trips two ruleset rules: - `required_signatures` - `require_extra_approval_for_unattributed_changes` Everything else is satisfied: all four required checks green, all review threads resolved, no merge conflicts, and `required_approving_review_count` is **0** — so once the signature question is settled this PR needs no human approval. Two ways to settle it: 1. **Register the signing key** on the account (Settings → SSH and GPG keys → New SSH key, type *Signing Key*). No push needed; the existing commits become verified, and the commit history is preserved. 2. **Rebuild the branch through the GitHub API.** Commits created via the API are signed by GitHub and verify automatically — that is why every commit on `main` shows `committer=noreply@github.com, verified=true`. Since the repo is squash-merge only, collapsing this branch's commits loses nothing that would survive the merge anyway. ## Related - Refs #3140 — landed the v1 slice this builds on; its `0.13.3` entry and full test suite are preserved here. - Refs #3156 — bumped `code-tidying` to `0.14.0` mid-review. - Refs #3126 — the original bug, closed by #3140. Not reopened here. - Refs #3164 — `audit-noise` carries the same defect class. #3171 fixed its rename-split half; the octal-escape half is still open and tracked there. **Not fixed here** — separate plugin, separate version bump.
Closes #3126
Summary
audit-comment-residue's default-target router dropped any uncommitted path containing a space, then reported the run asfiles=0with the noteno code targets. An audit that errors prompts a retry; one that confidently reports a clean tree ends the investigation, so this false negative was worse than a crash would have been.Fix
plugins/code-tidying/skills/audit-comment-residue/scripts/detect.shparsedgit status --porcelainwithawk '{print $NF}'. A spaced path was split on its space and git's closing quote was kept, so the reconstructed path named no file and fell out of the target list.The parse now slices the path out of the porcelain record (
${line:3}) instead of splitting on whitespace, takes the right-hand side of a rename, and unwraps git's C-quoting (including the\"and\\escapes inside it).Two deliberate choices beyond a straight port of the sibling
docs-hygiene/audit-noiseblock the issue pointed at:R/Cstatus letter in either column. Porcelain's short format isXY—Xis the index status,Ythe worktree status — and a rename can be recorded in either. Ungated entirely,${path##* -> }would also mangle an ordinary path containing" -> ".The skill's own
Uncommitted code filespre-computed-context line inSKILL.mdcarried the identical$NFparse and would have previewed the same false negative to the model, so it is brought to full parity — same column handling, same unescaping. That is Boy Scout scope: the same defect in the same skill, not a widening of the issue.Review findings, and what they changed
Three commits, each closing a defect the previous one did not. Both review findings were confirmed by local reproduction before being fixed, not taken on assertion.
6017877e$NFsplit92e04c96Y) left unsplit04558340SKILL.mdparser missing\"/\\unescaping, and untested/review:code-review92e04c96was a regression this PR introduced.mv old.py new.py && git add -N new.pyemitsR old.py -> new.py—RinY,Xblank. The X-only gate left the arrow in place, so the record resolved to nothing and the run reportedfiles=0: the exact failure shape this PR exists to remove.awk '{print $NF}'had returnednew.pycorrectly. Fixtures 9b/9c could not have caught it becausegit mvalways stages intoX.04558340fixed the cause, not just the symptom. TheSKILL.mdparser stripped the surrounding quotes but never unescaped what was inside them, so a filename C-quoted for an embedded quote or backslash — rather than a space — was still previewed wrong. It survived because that parser had no automated coverage at all. Verified against real files on disk:quote".py"quote\".py"quote\".pyquote".pyback\-slash.py"back\\-slash.py"back\\-slash.pyback\-slash.pyplain space.py"plain space.py"Verification
Reproduced first, per the bug-investigation rule:
Pre-fix the default-target run found 1 of 2 files; post-fix it finds both. Passing the same spaced file explicitly always found the residue, confirming the finding existed and only discovery missed it.
Regression coverage — sections 9 and 10 of
detect.test.sh, 18 new assertions.Section 9's arms live in separately-rooted fixture repos on purpose: sharing one lets a correctly-parsed file hold
files=above zero and mask another arm's disappearance. That is not hypothetical — the first draft shared a repo and only 1 of 5 assertions caught the bug. Arms: spaced path (9a), index rename (9b), spaced rename (9c), worktree-column rename viagit add -N(9d).Section 10 guards the
SKILL.mdparser by extracting the awk program out ofSKILL.mdand executing it, rather than restating it in the test — a restatement would keep passing while the real line rotted, which is exactly how the divergence survived. It also asserts every filedetect.shaudits appears in the preview, so the two cannot disagree about the tree.Discrimination was measured, not assumed. Each fix was reverted in place and the suite re-run:
$NFsplitThe assertions that pass either way cover the plain-rename path, which resolved correctly under
$NFonly by accident of field order.Gates run locally, all clean:
detect.test.shscripts/affected-tests.sh)shellcheck·shfmt -d -i 2check-shell-portability.sheditorconfig-checker·typos·markdownlint-cli2check-changelog-parity.sh--check/--check-order/--check-bumpcheck-changed-skills.shgitleaksVersion bumped
0.13.2→0.13.3with the matching CHANGELOG entry.One incidental note for reviewers: the backslash fixture is named
back\-slash.pyrather thanback\slash.pybecauseshell-portability-lintsubstring-matches\\sand reads the filename as a GNU-only regex class. Renaming avoids aportability-oksuppression that a future reader would have to re-litigate, and tests the same thing.Related
Refs #2872— the most recent prior change to this suite (fixture git-environment isolation), whoseunset GIT_DIR GIT_WORK_TREE GIT_CONFIGguard the new fixtures rely on.Refs #3143— filed from this PR. The siblingdocs-hygiene/audit-noiseparse has two related defects, found while checking whether the port was faithful. Its rename split is ungated (*" -> "*), so it fails in the opposite direction from this skill's pre-fix parse: it over-splits and mangles an ordinary path literally containing" -> ", where this one under-split. It also unescapes\"but not\\. Deferred rather than fixed here because it is a second plugin, with its own version bump and CHANGELOG.git status --porcelain -zwould close that class outright; fix(docs-hygiene): audit-noise porcelain parse mangles paths containing " -> " and leaves \\ escaped #3143 carries that suggestion, and it is deliberately not attempted here.Generated by Claude Code