fix: adversarial review, pass 5 (rule-23 grant, one finding)

bin/gstack-worktree-audit: git status runs with --untracked-files=normal
--ignore-submodules=none, so a user's status.showUntrackedFiles=no config
cannot hide untracked files and let a worktree holding them read clean and
safe. Test: the fresh worktree under that config via GIT_CONFIG_* still
reads scratch:1 / review (red before the fix).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 22:15:38 +00:00
1 parent 96927f2d4c
commit 0d372b7349
3 files changed
+15 -2

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
### Tests
- `test/decision-trail.test.ts`: header once (an empty file gets one, a second append does not), the formula guard on every leading byte it covers, flattened tabs and line breaks, the start row's text and its idempotent rerun, a second run's start row, the `--run` refusal and admission, a `-h` cell appended as data, a missing final newline, CRLF rows and a repeated header, a blank file and a BOM, run ids with whitespace or quotes refused, usage errors.
- `test/worktree-audit.test.ts`: a bare origin, a clone and twelve worktrees (merged, wip with an open PR, scratch with a chat today, stale, closed PR, a merged one whose path has a space, a merged one with only an untracked file, a registered one whose directory is gone, a locked one whose directory is gone, a broken one inside the clone with a precious file, one whose branch shares its name with a tag, one with only a gitignored `.env`) classified into every bucket through a fake `gh` (which records its arguments and runs the audit's own `--jq` over canned JSON with a fork PR when `jq` is installed) and a `CLAUDE_CONFIG_DIR` transcript directory; the fixture, its worktree registry included, is byte-identical after the audit; `-b`, `--no-fetch`, an unpushed commit on a closed PR reading `review` and an open PR on the reused name winning, no `gh` on PATH (git resolved from `git --exec-path`, so a distribution's wrapper script does not matter), a failing `gh`, a missing base and a non-repo path.
- `test/worktree-audit.test.ts`: a bare origin, a clone and twelve worktrees (merged, wip with an open PR, scratch with a chat today, stale, closed PR, a merged one whose path has a space, a merged one with only an untracked file, a registered one whose directory is gone, a locked one whose directory is gone, a broken one inside the clone with a precious file, one whose branch shares its name with a tag, one with only a gitignored `.env`) classified into every bucket (also under `status.showUntrackedFiles=no`, which must not hide untracked files) through a fake `gh` (which records its arguments and runs the audit's own `--jq` over canned JSON with a fork PR when `jq` is installed) and a `CLAUDE_CONFIG_DIR` transcript directory; the fixture, its worktree registry included, is byte-identical after the audit; `-b`, `--no-fetch`, an unpushed commit on a closed PR reading `review` and an open PR on the reused name winning, no `gh` on PATH (git resolved from `git --exec-path`, so a distribution's wrapper script does not matter), a failing `gh`, a missing base and a non-repo path.
### Gate integrity
- No threshold, timeout, trial count or case kind changed. No pinned skill changed; `landing-report/SKILL.md` grows by one line and is unpinned. `test/autoplan-driver.test.ts`'s line cap on `autoplan/DRIVER.md` moves 160 → 162 (measured 162) for the decision-trail paragraph at the end of the coordinator contract section; the file was at the cap before.
+3 -1
View File
@@ -154,7 +154,9 @@ git worktree list --porcelain | awk 'BEGIN { RS=""; FS="\n" } {
# Tracked edits are work in progress; untracked files alone are scratch. A
# clean tree still names its ignored entries (.env, build output): they do
# not change the bucket, but they are what removing the worktree would lose.
if porcelain=$(git -C "$wt" status --porcelain 2>/dev/null); then
# The flags pin what status reports: a status.showUntrackedFiles=no config
# would otherwise hide untracked files and let a worktree holding them read clean.
if porcelain=$(git -C "$wt" status --porcelain --untracked-files=normal --ignore-submodules=none 2>/dev/null); then
if [ -z "$porcelain" ]; then
ignored=$(git -C "$wt" ls-files --others --ignored --exclude-standard --directory 2>/dev/null | grep -c .)
[ "$ignored" -gt 0 ] && dirty="clean,ignored:$ignored" || dirty=clean
+11
View File
@@ -206,6 +206,17 @@ describe("gstack-worktree-audit", () => {
expect(after).toBe(before);
});
test("status.showUntrackedFiles=no in the user's config does not hide untracked files", () => {
const r = spawnSync("bash", [BIN, "--no-fetch"], {
cwd: repo, encoding: "utf8", timeout: 60_000,
env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH}`, CLAUDE_CONFIG_DIR: claudeDir, HOME: tmp, GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "status.showUntrackedFiles", GIT_CONFIG_VALUE_0: "no" },
});
expect(r.status).toBe(0);
const row = r.stdout.split("\n").find(l => l.endsWith("\t" + wts.fresh))!;
expect(row.split("\t").slice(3, 4)).toEqual(["scratch:1"]);
expect(row.split("\t")[7]).toBe("review");
});
test("an unpushed commit on a branch with a closed PR is review, an open PR on the same name wins and holds it, and -b picks the base", () => {
git(wts.closed, "commit", "-q", "--allow-empty", "-m", "one more");
const r1 = audit("-b", "origin/main", "--no-fetch", repo);