docs: add detached-HEAD refspec recovery and the zsh status gotcha to audit-git-checkouts #49

Merged
jercik merged 4 commits from align/audit-git-checkouts into main 2026-08-07 09:16:25 +00:00
Owner

Relocates the two verified facts from setup-atlas's sync-repos skill ahead of that skill's deletion: recovering origin/<default> when a narrow fetch refspec (e.g. tag-only) hides it while moving a detached checkout onto its default branch, and zsh's read-only status parameter breaking ad hoc sweep loops. The setup-atlas deletion PR depends on this landing first.

Relocates the two verified facts from setup-atlas's `sync-repos` skill ahead of that skill's deletion: recovering `origin/<default>` when a narrow fetch refspec (e.g. tag-only) hides it while moving a detached checkout onto its default branch, and zsh's read-only `status` parameter breaking ad hoc sweep loops. The setup-atlas deletion PR depends on this landing first.
docs: add detached-HEAD refspec recovery and the zsh status gotcha to audit-git-checkouts
Some checks failed
Node tests / node:test (pull_request) Successful in 9s
commit-msg / commitlint (pull_request) Successful in 18s
PR Review / Prepare immutable review tools (pull_request_target) Successful in 1m29s
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Failing after 42s
PR Review / forgejo-review-approach-3 generator (pull_request_target) Failing after 46s
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Failing after 47s
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Successful in 2m22s
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Successful in 4m6s
PR Review / Dispatch and observe exact review writers (pull_request_target) Failing after 4m26s
dca3139902
forgejo-actions left a comment

Approach review: The overall approach is sound. Relocating two verified facts into audit-git-checkouts ahead of deleting the sync-repos skill is the right sequencing, the change adds no new machinery, and each fact lands next to the material it qualifies (checkoutComparison/detached-reference handling; the jq reporting recipes).

One material alternative on the recovery procedure: the prescribed fix persists a widened remote.<name>.fetch in the owner's repository, when a one-shot refspec on the fetch command achieves the same recovery without mutating owner-configured state. scripts/audit-checkouts.sh:801 already takes exactly that route — it fetches '+refs/heads/*:refs/remotes/origin/*' explicitly rather than trusting or editing the configured refspec — so the skill would be advising a config edit for a problem its own driver routes around. A narrow refspec is often deliberate (a tag-only mirror kept cheap on purpose), and widening it changes every future fetch in that checkout, including ones unrelated to this skill. Details inline.

Minor, non-blocking: the new paragraph is the one place in the skill that authorizes switching branches in an audited checkout, and it sits in the read-only "Audit workflow" section while "Safety boundary" states the automatic update "never switches branches" and "Cleanup when explicitly authorized" owns owner-authorized mutations. Worth a pointer from that section if the mutation guidance is meant to be discoverable from there.

Approach review by Claude Code Opus (opus)

**Approach review:** The overall approach is sound. Relocating two verified facts into `audit-git-checkouts` ahead of deleting the `sync-repos` skill is the right sequencing, the change adds no new machinery, and each fact lands next to the material it qualifies (`checkoutComparison`/detached-reference handling; the jq reporting recipes). One material alternative on the recovery procedure: the prescribed fix persists a widened `remote.<name>.fetch` in the owner's repository, when a one-shot refspec on the fetch command achieves the same recovery without mutating owner-configured state. `scripts/audit-checkouts.sh:801` already takes exactly that route — it fetches `'+refs/heads/*:refs/remotes/origin/*'` explicitly rather than trusting or editing the configured refspec — so the skill would be advising a config edit for a problem its own driver routes around. A narrow refspec is often deliberate (a tag-only mirror kept cheap on purpose), and widening it changes every future fetch in that checkout, including ones unrelated to this skill. Details inline. Minor, non-blocking: the new paragraph is the one place in the skill that authorizes switching branches in an audited checkout, and it sits in the read-only "Audit workflow" section while "Safety boundary" states the automatic update "never switches branches" and "Cleanup when explicitly authorized" owns owner-authorized mutations. Worth a pointer from that section if the mutation guidance is meant to be discoverable from there. _Approach review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL2FnZW50LXNraWxscyIsIm51bWJlciI6IjQ5IiwiaGVhZFNoYSI6ImRjYTMxMzk5MDI2ZTM3Njk2M2M1MmY0ZmJlZjM4YTZhZmRiNzJkOGUiLCJzbG90IjoiZm9yZ2Vqby1yZXZpZXctYXBwcm9hY2gtc21hcnQtMSIsInNvdXJjZVdvcmtmbG93UnVuSWQiOiIxNzk3MiIsInNvdXJjZUdlbmVyYXRvckF0dGVtcHQiOiIxIiwicmVzdWx0SWQiOiJiZDczYmIzYi0wMTY3LTQzZmQtODFlNi1hZTJiMGRkNjU5ZGYifQ== -->
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then a
For every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.
When the owner explicitly asks to move a detached checkout onto its default branch, switch only after confirming the working tree is clean, and verify the remote-tracking ref exists first with `git show-ref --verify refs/remotes/<remote>/<default>`: a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it. If the ref is missing, inspect `remote.<name>.fetch`, add `+refs/heads/*:refs/remotes/<remote>/*`, fetch again, then create the tracking branch and update with `git pull --ff-only`.

Consider recovering with a one-shot refspec instead of persisting a config change. git fetch <remote> '+refs/heads/*:refs/remotes/<remote>/*' (optionally narrowed to +refs/heads/<default>:refs/remotes/<remote>/<default>) populates the missing ref without touching remote.<name>.fetch, which is the same technique the bundled driver already uses at scripts/audit-checkouts.sh:801 — it passes the heads refspec explicitly rather than relying on the configured one. Two reasons to prefer it here: a narrow refspec is frequently intentional (a tag-only mirror), so widening it silently changes the cost and result of every later fetch in that checkout; and because the driver already fetches heads explicitly for origin, a missing refs/remotes/origin/<default> after a successful audit run points at a failed fetch (defaultComparison.fresh != true, per the --no-fetch paragraph above) rather than at the refspec — so config editing would be treating the wrong cause for the origin case, and only a non-origin remote reaches this path with a narrow refspec as the real explanation.

Adopting the one-shot fetch also simplifies the tail of the procedure: with the ref freshly fetched, git switch <default> (DWIM off the remote-tracking ref) already lands on the fetched tip, so the trailing git pull --ff-only is only needed to work around the still-narrow configured refspec — the step that motivated the config edit in the first place.

Consider recovering with a one-shot refspec instead of persisting a config change. `git fetch <remote> '+refs/heads/*:refs/remotes/<remote>/*'` (optionally narrowed to `+refs/heads/<default>:refs/remotes/<remote>/<default>`) populates the missing ref without touching `remote.<name>.fetch`, which is the same technique the bundled driver already uses at `scripts/audit-checkouts.sh:801` — it passes the heads refspec explicitly rather than relying on the configured one. Two reasons to prefer it here: a narrow refspec is frequently intentional (a tag-only mirror), so widening it silently changes the cost and result of every later fetch in that checkout; and because the driver already fetches heads explicitly for `origin`, a missing `refs/remotes/origin/<default>` after a successful audit run points at a failed fetch (`defaultComparison.fresh != true`, per the `--no-fetch` paragraph above) rather than at the refspec — so config editing would be treating the wrong cause for the `origin` case, and only a non-`origin` remote reaches this path with a narrow refspec as the real explanation. Adopting the one-shot fetch also simplifies the tail of the procedure: with the ref freshly fetched, `git switch <default>` (DWIM off the remote-tracking ref) already lands on the fetched tip, so the trailing `git pull --ff-only` is only needed to work around the still-narrow configured refspec — the step that motivated the config edit in the first place.
jercik marked this conversation as resolved
forgejo-actions left a comment

Summary: Docs-only change adding two paragraphs to skills/audit-git-checkouts/SKILL.md. Found 2 medium issues, both in the new detached-HEAD recovery paragraph (line 61); one carries an additional low-severity note.

I reproduced the paragraph's premise in a scratch repo: with remote.origin.fetch narrowed to +refs/tags/*:refs/tags/*, git fetch origin leaves refs/remotes/origin/main absent while git ls-remote --heads origin still reports the branch, and git show-ref --verify refs/remotes/origin/main exits 128. The documented failure mode is real. The problems are with the two remedies prescribed around it: the clean-tree precondition does not cover commits made on the detached HEAD, and the refspec fix mutates owner config where a one-shot fetch (already what scripts/audit-checkouts.sh:801 does) suffices.

The zsh status note (line 135) could not be verified empirically — no zsh in this environment and no Perplexity credential for askpplx — so I am not asserting anything about it either way. It matches the documented zsh special-parameter behavior and I found no reason to doubt it.

Code review by Claude Code Opus (opus)

**Summary:** Docs-only change adding two paragraphs to `skills/audit-git-checkouts/SKILL.md`. Found 2 medium issues, both in the new detached-HEAD recovery paragraph (line 61); one carries an additional low-severity note. I reproduced the paragraph's premise in a scratch repo: with `remote.origin.fetch` narrowed to `+refs/tags/*:refs/tags/*`, `git fetch origin` leaves `refs/remotes/origin/main` absent while `git ls-remote --heads origin` still reports the branch, and `git show-ref --verify refs/remotes/origin/main` exits 128. The documented failure mode is real. The problems are with the two remedies prescribed around it: the clean-tree precondition does not cover commits made on the detached HEAD, and the refspec fix mutates owner config where a one-shot fetch (already what `scripts/audit-checkouts.sh:801` does) suffices. The zsh `status` note (line 135) could not be verified empirically — no zsh in this environment and no Perplexity credential for `askpplx` — so I am not asserting anything about it either way. It matches the documented zsh special-parameter behavior and I found no reason to doubt it. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL2FnZW50LXNraWxscyIsIm51bWJlciI6IjQ5IiwiaGVhZFNoYSI6ImRjYTMxMzk5MDI2ZTM3Njk2M2M1MmY0ZmJlZjM4YTZhZmRiNzJkOGUiLCJzbG90IjoiZm9yZ2Vqby1yZXZpZXctY29kZS1zbWFydC0xIiwic291cmNlV29ya2Zsb3dSdW5JZCI6IjE3OTcyIiwic291cmNlR2VuZXJhdG9yQXR0ZW1wdCI6IjEiLCJyZXN1bHRJZCI6IjMxMzk4NmQzLTYxYjItNGNhMy05YzlmLWIxYTA3ZjgyYmNlOCJ9 -->
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then a
For every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.
When the owner explicitly asks to move a detached checkout onto its default branch, switch only after confirming the working tree is clean, and verify the remote-tracking ref exists first with `git show-ref --verify refs/remotes/<remote>/<default>`: a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it. If the ref is missing, inspect `remote.<name>.fetch`, add `+refs/heads/*:refs/remotes/<remote>/*`, fetch again, then create the tracking branch and update with `git pull --ff-only`.

🟡 Medium: The precondition — "switch only after confirming the working tree is clean" — does not cover the case this skill treats as most precious. Line 18 states "Treat every dirty file, local commit, and stash as user work," but a clean git status --porcelain says nothing about commits made on the detached HEAD, which are reachable from no ref at all.

Reproduced: committed on a detached HEAD, confirmed git status --porcelain empty and git for-each-ref --contains <sha> empty, then git switch main succeeded and left the commit reflog-only (subject to gc.reflogExpireUnreachable, 30 days by default). Git's "you are leaving 1 commit behind" advice prints only after the switch, so an agent following the documented gate has already moved HEAD before seeing it.

Add a reachability gate alongside the cleanliness one: refuse the switch unless the detached tip is named by something, e.g. git for-each-ref --contains HEAD is non-empty, or git merge-base --is-ancestor HEAD refs/remotes/<remote>/<default> succeeds. Otherwise report the unique commits to the owner rather than switching — the same standard the containment ladder applies everywhere else in this skill.

🟡 **Medium:** The precondition — "switch only after confirming the working tree is clean" — does not cover the case this skill treats as most precious. Line 18 states "Treat every dirty file, local commit, and stash as user work," but a clean `git status --porcelain` says nothing about commits made *on* the detached HEAD, which are reachable from no ref at all. Reproduced: committed on a detached HEAD, confirmed `git status --porcelain` empty and `git for-each-ref --contains <sha>` empty, then `git switch main` succeeded and left the commit reflog-only (subject to `gc.reflogExpireUnreachable`, 30 days by default). Git's "you are leaving 1 commit behind" advice prints only *after* the switch, so an agent following the documented gate has already moved HEAD before seeing it. Add a reachability gate alongside the cleanliness one: refuse the switch unless the detached tip is named by something, e.g. `git for-each-ref --contains HEAD` is non-empty, or `git merge-base --is-ancestor HEAD refs/remotes/<remote>/<default>` succeeds. Otherwise report the unique commits to the owner rather than switching — the same standard the containment ladder applies everywhere else in this skill.

🟡 Medium: The recovery step — "add +refs/heads/*:refs/remotes/<remote>/*" to remote.<name>.fetch — makes a permanent change to the owner's repository configuration to fix a one-time visibility problem, and a narrow refspec is usually deliberate (a --single-branch clone, a tag-only mirror). Widening it silently changes what every future git fetch in that repository pulls down and prunes.

A one-shot fetch with an explicit refspec recovers the ref without touching config. Verified in a scratch repo with remote.origin.fetch set to +refs/tags/*:refs/tags/* only:

$ git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'
 * [new branch]      main       -> origin/main
$ git config --get-all remote.origin.fetch
+refs/tags/*:refs/tags/*        # unchanged

This is exactly what the driver already does at scripts/audit-checkouts.sh:801, so the persistent edit is not needed for the audit path either. Suggest recommending the explicit-refspec fetch, and mentioning the config edit only as an owner-authorized option when they want the widening to persist.

🟢 Low: In the same paragraph, prefer git show-ref --verify --quiet refs/remotes/<remote>/<default> for the existence check. Without --quiet a missing ref prints fatal: 'refs/remotes/origin/main' - not a valid ref and exits 128; with it, exit 1 and no output. In a skill that trains the agent to triage operational/ outcomes from stderr, an expected-negative check emitting fatal: invites misreading it as a repository-level failure.

🟡 **Medium:** The recovery step — "add `+refs/heads/*:refs/remotes/<remote>/*`" to `remote.<name>.fetch` — makes a permanent change to the owner's repository configuration to fix a one-time visibility problem, and a narrow refspec is usually deliberate (a `--single-branch` clone, a tag-only mirror). Widening it silently changes what every future `git fetch` in that repository pulls down and prunes. A one-shot fetch with an explicit refspec recovers the ref without touching config. Verified in a scratch repo with `remote.origin.fetch` set to `+refs/tags/*:refs/tags/*` only: ``` $ git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' * [new branch] main -> origin/main $ git config --get-all remote.origin.fetch +refs/tags/*:refs/tags/* # unchanged ``` This is exactly what the driver already does at `scripts/audit-checkouts.sh:801`, so the persistent edit is not needed for the audit path either. Suggest recommending the explicit-refspec fetch, and mentioning the config edit only as an owner-authorized option when they want the widening to persist. 🟢 **Low:** In the same paragraph, prefer `git show-ref --verify --quiet refs/remotes/<remote>/<default>` for the existence check. Without `--quiet` a missing ref prints `fatal: 'refs/remotes/origin/main' - not a valid ref` and exits 128; with it, exit 1 and no output. In a skill that trains the agent to triage `operational/` outcomes from stderr, an expected-negative check emitting `fatal:` invites misreading it as a repository-level failure.
jercik marked this conversation as resolved
docs: gate the detached-branch move on reachability and fetch with a one-shot refspec
Some checks failed
Node tests / node:test (pull_request) Successful in 12s
commit-msg / commitlint (pull_request) Successful in 24s
PR Review / Prepare immutable review tools (pull_request_target) Successful in 1m29s
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-3 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Failing after 43s
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Successful in 4m30s
PR Review / Dispatch and observe exact review writers (pull_request_target) Failing after 4m49s
3f9690f8e4
Author
Owner

Replying to approach review #10165 (comment #44024) and code review #10167 (comments #44032, #44033)

All three findings are incorporated in 3f9690f; the paragraph at line 61 is rewritten:

  • Reachability gate (44032): the switch now requires both a clean tree and git for-each-ref --contains HEAD printing at least one line, with the explicit warning that a clean porcelain says nothing about detached-HEAD commits and that git's "leaving commits behind" advice only prints after HEAD has moved. An unreachable tip is reported to the owner instead of switched away from.
  • One-shot refspec instead of config edit (44024, 44033): recovery is now git fetch --prune <remote> '+refs/heads/*:refs/remotes/<remote>/*', with the rationale that a narrow refspec is often deliberate and widening it changes every future fetch; git config --get-all remote.<name>.fetch stays as the diagnostic, and persisting a wider refspec is framed as an owner-choice only. The trailing git pull --ff-only is gone — with the ref fetched, git switch <default> lands on the fetched tip, as the approach review noted.
  • --quiet on the existence check (44033 low): adopted, with the stderr-triage rationale inline.

On the approach review's non-blocking placement note: leaving the paragraph in the audit-workflow section is deliberate — it qualifies the detached-checkout reporting guidance directly above it, and its opening clause ("When the owner explicitly asks…") is the same authorization language the cleanup section uses, so the mutation gate travels with the text rather than through a cross-reference. The cleanup section governs removals, which this is not.

On the unverified zsh note (review body): confirmed live on this workstation before the original commit — zsh -c 'status=5' exits 1 with zsh:1: read-only variable: status.

> Replying to approach review #10165 (comment #44024) and code review #10167 (comments #44032, #44033) All three findings are incorporated in 3f9690f; the paragraph at line 61 is rewritten: - **Reachability gate (44032):** the switch now requires both a clean tree and `git for-each-ref --contains HEAD` printing at least one line, with the explicit warning that a clean porcelain says nothing about detached-HEAD commits and that git's "leaving commits behind" advice only prints after HEAD has moved. An unreachable tip is reported to the owner instead of switched away from. - **One-shot refspec instead of config edit (44024, 44033):** recovery is now `git fetch --prune <remote> '+refs/heads/*:refs/remotes/<remote>/*'`, with the rationale that a narrow refspec is often deliberate and widening it changes every future fetch; `git config --get-all remote.<name>.fetch` stays as the diagnostic, and persisting a wider refspec is framed as an owner-choice only. The trailing `git pull --ff-only` is gone — with the ref fetched, `git switch <default>` lands on the fetched tip, as the approach review noted. - **`--quiet` on the existence check (44033 low):** adopted, with the stderr-triage rationale inline. On the approach review's non-blocking placement note: leaving the paragraph in the audit-workflow section is deliberate — it qualifies the detached-checkout reporting guidance directly above it, and its opening clause ("When the owner explicitly asks…") is the same authorization language the cleanup section uses, so the mutation gate travels with the text rather than through a cross-reference. The cleanup section governs removals, which this is not. On the unverified zsh note (review body): confirmed live on this workstation before the original commit — `zsh -c 'status=5'` exits 1 with `zsh:1: read-only variable: status`.
forgejo-actions left a comment

Summary: Found 1 high and 1 medium issue, both in the new detached-HEAD recovery paragraph in skills/audit-git-checkouts/SKILL.md.

I reproduced the documented scenario (tag-only remote.origin.fetch, no local default branch, detached at a tag) against git 2.39.5. The reachability gate (git for-each-ref --contains HEAD prints nothing for commits made on a detached HEAD, exit 0 either way), the git show-ref --verify quiet/non-quiet split (fatal: + exit 128 without --quiet, silent exit 1 with it), and the ordering of git's "leaving N commits behind" advice all check out exactly as written. The two problems are in the recovery steps that follow: git switch <default> does not work under a narrow refspec, and --prune on the one-shot fetch deletes other remote-tracking refs the narrow refspec maintained.

The zsh status note was not machine-verified (no zsh in this environment), but matches zsh's documented read-only special parameter.

Code review by Claude Code Opus (opus)

**Summary:** Found 1 high and 1 medium issue, both in the new detached-HEAD recovery paragraph in `skills/audit-git-checkouts/SKILL.md`. I reproduced the documented scenario (tag-only `remote.origin.fetch`, no local default branch, detached at a tag) against git 2.39.5. The reachability gate (`git for-each-ref --contains HEAD` prints nothing for commits made on a detached HEAD, exit 0 either way), the `git show-ref --verify` quiet/non-quiet split (`fatal:` + exit 128 without `--quiet`, silent exit 1 with it), and the ordering of git's "leaving N commits behind" advice all check out exactly as written. The two problems are in the recovery steps that follow: `git switch <default>` does not work under a narrow refspec, and `--prune` on the one-shot fetch deletes other remote-tracking refs the narrow refspec maintained. The zsh `status` note was not machine-verified (no zsh in this environment), but matches zsh's documented read-only special parameter. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL2FnZW50LXNraWxscyIsIm51bWJlciI6IjQ5IiwiaGVhZFNoYSI6IjNmOTY5MGY4ZTRhZmJkZmY4YjQ3NDA2MzEyZDE2Y2VjZjc2NTNhMGYiLCJzbG90IjoiZm9yZ2Vqby1yZXZpZXctY29kZS1zbWFydC0xIiwic291cmNlV29ya2Zsb3dSdW5JZCI6IjE4MDUxIiwic291cmNlR2VuZXJhdG9yQXR0ZW1wdCI6IjEiLCJyZXN1bHRJZCI6Ijc2MWIzNTY5LWU1YzctNDQ4Yi04Mjc3LTVhMTg0NGUzMDg2OCJ9 -->
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then a
For every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.
When the owner explicitly asks to move a detached checkout onto its default branch, gate the switch on two checks, not one: the working tree is clean, and the detached tip is reachable from some ref — `git for-each-ref --contains HEAD` prints at least one line. A clean tree says nothing about commits made on the detached HEAD; switching away leaves them reflog-only, and git's "leaving commits behind" advice prints only after HEAD has moved. If the tip is unreachable, report its unique commits to the owner instead of switching. Verify the remote-tracking ref exists with `git show-ref --verify --quiet refs/remotes/<remote>/<default>` (without `--quiet` the expected-negative case prints `fatal:` to stderr, inviting misreading as a repository failure): a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it — confirm the cause with `git config --get-all remote.<name>.fetch`. Recover with a one-shot explicit refspec, `git fetch --prune <remote> '+refs/heads/*:refs/remotes/<remote>/*'`, rather than editing the configured refspec: a narrow refspec is often deliberate, and widening it changes every future fetch in that checkout. With the ref fetched, `git switch <default>` creates the tracking branch at the fetched tip. Persist a wider `remote.<name>.fetch` only when the owner wants the widening to stay.

🟠 High: git switch <default> does not work in the situation this paragraph describes, so an agent following the recipe hits a fatal error at the last step.

The DWIM that creates a local branch from <remote>/<default> resolves the remote-tracking ref through the remote's configured fetch refspec, not by looking for the ref on disk. With the refspec left narrow, the mapping fails even though the one-shot fetch just created the ref. Reproduced on git 2.39.5 (tag-only remote.origin.fetch, no local main, refs/remotes/origin/main present):

$ git switch main
fatal: invalid reference: main
$ git switch -c main --track origin/main
fatal: cannot set up tracking information; starting point 'origin/main' is not a branch
$ git branch --set-upstream-to=origin/main main
fatal: cannot set up tracking information; starting point 'origin/main' is not a branch
$ git switch -c main origin/main
Switched to a new branch 'main'      # works, but no upstream configured

Only after git config --add remote.<name>.fetch '+refs/heads/*:refs/remotes/<name>/*' does git switch main succeed and print branch 'main' set up to track 'origin/main'.

This also inverts the closing sentence. Leaving the refspec narrow does not just skip a nicety: the branch gets no upstream, and a later plain git fetch --prune <remote> never refreshes refs/remotes/<remote>/<default> again — I advanced the upstream and confirmed origin/main stayed at the old SHA — so this skill's own defaultComparison for that checkout is stale from the next audit onward.

Suggested correction: say that the switch itself needs git switch -c <default> <remote>/<default> (no upstream, and --track/--set-upstream-to both refuse), and that a checkout that should keep tracking the default branch needs the heads refspec persisted — the one-shot fetch recovers the ref for a single inspection, not a self-maintaining tracking branch.

🟠 **High:** `git switch <default>` does not work in the situation this paragraph describes, so an agent following the recipe hits a fatal error at the last step. The DWIM that creates a local branch from `<remote>/<default>` resolves the remote-tracking ref through the remote's **configured** fetch refspec, not by looking for the ref on disk. With the refspec left narrow, the mapping fails even though the one-shot fetch just created the ref. Reproduced on git 2.39.5 (tag-only `remote.origin.fetch`, no local `main`, `refs/remotes/origin/main` present): ``` $ git switch main fatal: invalid reference: main $ git switch -c main --track origin/main fatal: cannot set up tracking information; starting point 'origin/main' is not a branch $ git branch --set-upstream-to=origin/main main fatal: cannot set up tracking information; starting point 'origin/main' is not a branch $ git switch -c main origin/main Switched to a new branch 'main' # works, but no upstream configured ``` Only after `git config --add remote.<name>.fetch '+refs/heads/*:refs/remotes/<name>/*'` does `git switch main` succeed and print `branch 'main' set up to track 'origin/main'`. This also inverts the closing sentence. Leaving the refspec narrow does not just skip a nicety: the branch gets no upstream, and a later plain `git fetch --prune <remote>` never refreshes `refs/remotes/<remote>/<default>` again — I advanced the upstream and confirmed `origin/main` stayed at the old SHA — so this skill's own `defaultComparison` for that checkout is stale from the next audit onward. Suggested correction: say that the switch itself needs `git switch -c <default> <remote>/<default>` (no upstream, and `--track`/`--set-upstream-to` both refuse), and that a checkout that should keep tracking the default branch needs the heads refspec persisted — the one-shot fetch recovers the ref for a single inspection, not a self-maintaining tracking branch.

🟡 Medium: --prune on the one-shot fetch is not as side-effect-free as "rather than editing the configured refspec" implies. Prune scope comes from the command-line refspec, so +refs/heads/*:refs/remotes/<remote>/* prunes everything under refs/remotes/<remote>/ that has no counterpart under refs/heads/ upstream — including refs the narrow refspec deliberately maintains. Reproduced on git 2.39.5 with remote.origin.fetch = +refs/tags/*:refs/remotes/origin/tags/*:

$ git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'
 - [deleted]         (none)     -> origin/tags/v1.0

Since this paragraph exists precisely for checkouts with non-default refspecs, that is the population most likely to be hit. There is also nothing to prune on a first fetch of a ref that was missing. Dropping --prune, or narrowing to the one ref actually needed (git fetch <remote> '+refs/heads/<default>:refs/remotes/<remote>/<default>'), keeps the recovery genuinely read-mostly.

🟡 **Medium:** `--prune` on the one-shot fetch is not as side-effect-free as "rather than editing the configured refspec" implies. Prune scope comes from the command-line refspec, so `+refs/heads/*:refs/remotes/<remote>/*` prunes *everything* under `refs/remotes/<remote>/` that has no counterpart under `refs/heads/` upstream — including refs the narrow refspec deliberately maintains. Reproduced on git 2.39.5 with `remote.origin.fetch = +refs/tags/*:refs/remotes/origin/tags/*`: ``` $ git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' - [deleted] (none) -> origin/tags/v1.0 ``` Since this paragraph exists precisely for checkouts with non-default refspecs, that is the population most likely to be hit. There is also nothing to prune on a first fetch of a ref that was missing. Dropping `--prune`, or narrowing to the one ref actually needed (`git fetch <remote> '+refs/heads/<default>:refs/remotes/<remote>/<default>'`), keeps the recovery genuinely read-mostly.
jercik marked this conversation as resolved
docs: the default-branch switch needs the refspec mapping persisted, not a one-shot fetch
Some checks failed
Node tests / node:test (pull_request) Successful in 8s
commit-msg / commitlint (pull_request) Successful in 11s
PR Review / Prepare immutable review tools (pull_request_target) Successful in 1m29s
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-3 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Failing after 40s
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Successful in 5m59s
PR Review / Dispatch and observe exact review writers (pull_request_target) Failing after 6m21s
2b0764ab34
Author
Owner

Replying to code review #10208 (comments #44140, #44141)

Both findings reproduced here on git 2.55.0, matching your 2.39.5 results, and both are incorporated in 2b0764a:

  • Switch under a narrow refspec (44140): confirmed — git switch main dies with fatal: invalid reference: main even with refs/remotes/origin/main freshly on disk, --track refuses, and a branch created bare gets no upstream. The paragraph now states the mapping requirement outright and prescribes git remote set-branches --add <remote> <default>, which appends exactly the single-branch refspec beside the narrow one (verified: after it, git switch main creates the branch and prints branch 'main' set up to track 'origin/main'). That also un-inverts the closing sentence: the persistent mapping is required for the ask this paragraph covers — a checkout living on its default branch that stays fetchable — and the all-heads widening stays owner-choice. The staleness consequence (a bare branch never refreshed, defaultComparison stale from the next run) is stated inline.
  • Prune blast radius (44141): confirmed — the glob one-shot with --prune deleted origin/tags/v1.0, the ref the narrow refspec maintained. The one-shot form survives only as the inspect-without-adopting option, narrowed to the single ref and with the prune-scope warning attached.

The one-shot-recovers-everything shape from the previous round is gone; thanks for running it to ground.

> Replying to code review #10208 (comments #44140, #44141) Both findings reproduced here on git 2.55.0, matching your 2.39.5 results, and both are incorporated in 2b0764a: - **Switch under a narrow refspec (44140):** confirmed — `git switch main` dies with `fatal: invalid reference: main` even with `refs/remotes/origin/main` freshly on disk, `--track` refuses, and a branch created bare gets no upstream. The paragraph now states the mapping requirement outright and prescribes `git remote set-branches --add <remote> <default>`, which appends exactly the single-branch refspec beside the narrow one (verified: after it, `git switch main` creates the branch and prints `branch 'main' set up to track 'origin/main'`). That also un-inverts the closing sentence: the persistent mapping is required for the ask this paragraph covers — a checkout living on its default branch that stays fetchable — and the all-heads widening stays owner-choice. The staleness consequence (a bare branch never refreshed, `defaultComparison` stale from the next run) is stated inline. - **Prune blast radius (44141):** confirmed — the glob one-shot with `--prune` deleted `origin/tags/v1.0`, the ref the narrow refspec maintained. The one-shot form survives only as the inspect-without-adopting option, narrowed to the single ref and with the prune-scope warning attached. The one-shot-recovers-everything shape from the previous round is gone; thanks for running it to ground.
forgejo-actions left a comment

Summary: Docs-only change to skills/audit-git-checkouts/SKILL.md. I reproduced every git claim in the new detached-HEAD paragraph on git 2.39.5 against a purpose-built narrow-refspec fixture, and they hold exactly as written: git for-each-ref --contains HEAD prints zero lines for an unreachable detached tip and the “leaving commits behind” advice only prints after the switch; git show-ref --verify without --quiet emits fatal: 'refs/remotes/origin/main' - not a valid ref; git switch main dies with fatal: invalid reference: main even after the ref is on disk; --track refuses with starting point 'origin/main' is not a branch; git remote set-branches --add appends precisely +refs/heads/main:refs/remotes/origin/main and the subsequent git switch creates the branch with its upstream set; and the --prune glob form does delete a narrow-refspec-maintained refs/remotes/origin/pinned. The zsh status note matches documented zsh behaviour (zsh was not installed here, so that one is unverified locally).

Found 2 medium issues, both where the new text contradicts this skill’s own scripts/audit-checkouts.sh: the driver's command-line fetch refspec keeps origin/<default> fresh regardless of the configured narrow refspec (so the stated defaultComparison staleness does not occur), and the driver itself runs the very --prune glob form the paragraph says to never use.

Code review by Claude Code Opus (opus)

**Summary:** Docs-only change to `skills/audit-git-checkouts/SKILL.md`. I reproduced every git claim in the new detached-HEAD paragraph on git 2.39.5 against a purpose-built narrow-refspec fixture, and they hold exactly as written: `git for-each-ref --contains HEAD` prints zero lines for an unreachable detached tip and the “leaving commits behind” advice only prints after the switch; `git show-ref --verify` without `--quiet` emits `fatal: 'refs/remotes/origin/main' - not a valid ref`; `git switch main` dies with `fatal: invalid reference: main` even after the ref is on disk; `--track` refuses with `starting point 'origin/main' is not a branch`; `git remote set-branches --add` appends precisely `+refs/heads/main:refs/remotes/origin/main` and the subsequent `git switch` creates the branch with its upstream set; and the `--prune` glob form does delete a narrow-refspec-maintained `refs/remotes/origin/pinned`. The zsh `status` note matches documented zsh behaviour (zsh was not installed here, so that one is unverified locally). Found 2 medium issues, both where the new text contradicts this skill’s own `scripts/audit-checkouts.sh`: the driver's command-line fetch refspec keeps `origin/<default>` fresh regardless of the configured narrow refspec (so the stated `defaultComparison` staleness does not occur), and the driver itself runs the very `--prune` glob form the paragraph says to never use. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL2FnZW50LXNraWxscyIsIm51bWJlciI6IjQ5IiwiaGVhZFNoYSI6IjJiMDc2NGFiMzRlZDVmYjQ2MWM1MDEwZWY5YWQ3OTFhYWZlOGJjMmQiLCJzbG90IjoiZm9yZ2Vqby1yZXZpZXctY29kZS1zbWFydC0xIiwic291cmNlV29ya2Zsb3dSdW5JZCI6IjE4MTExIiwic291cmNlR2VuZXJhdG9yQXR0ZW1wdCI6IjEiLCJyZXN1bHRJZCI6ImQ4NmUxZjFmLTlkZmUtNDlkNy1hMGY1LTRjNDBmNzBlZTczNyJ9 -->
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then a
For every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.
When the owner explicitly asks to move a detached checkout onto its default branch, gate the switch on two checks, not one: the working tree is clean, and the detached tip is reachable from some ref — `git for-each-ref --contains HEAD` prints at least one line. A clean tree says nothing about commits made on the detached HEAD; switching away leaves them reflog-only, and git's "leaving commits behind" advice prints only after HEAD has moved. If the tip is unreachable, report its unique commits to the owner instead of switching. Verify the remote-tracking ref exists with `git show-ref --verify --quiet refs/remotes/<remote>/<default>` (without `--quiet` the expected-negative case prints `fatal:` to stderr, inviting misreading as a repository failure): a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it — confirm the cause with `git config --get-all remote.<name>.fetch`. The switch cannot ride a one-shot fetch: `git switch <default>` maps the branch through the remote's configured refspec, not through refs on disk, so under the still-narrow refspec it dies with `fatal: invalid reference` even after the ref is fetched, `--track` refuses (`starting point '<remote>/<default>' is not a branch`), and a branch created without an upstream is never refreshed by later fetches — this skill's own `defaultComparison` for that checkout goes stale from the next run. The owner asked for a checkout living on its default branch, so persist the minimal mapping: `git remote set-branches --add <remote> <default>` appends `+refs/heads/<default>:refs/remotes/<remote>/<default>` beside the narrow refspec it deliberately leaves alone; then `git fetch <remote>`, and `git switch <default>` creates the branch with its upstream set and future fetches covering it. Widen to all heads only when the owner wants that. To merely inspect the branch without adopting it, a one-shot `git fetch <remote> '+refs/heads/<default>:refs/remotes/<remote>/<default>'` recovers just that ref — never the `--prune` glob form, whose prune scope is the command-line refspec, deleting every ref under `refs/remotes/<remote>/` that the narrow refspec maintains elsewhere.

🟡 Medium: The clause “this skill’s own defaultComparison for that checkout goes stale from the next run” does not hold for this driver, so it is a false justification for a persistent config mutation.

scripts/audit-checkouts.sh:801 fetches with an explicit command-line refspec — git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' — which overrides the configured narrow refspec, so refs/remotes/origin/<default> is refreshed on every fetching run no matter what remote.origin.fetch says. And both consumers read that ref directly rather than @{upstream}: defaultComparison is git rev-list --left-right --count "HEAD...refs/remotes/origin/$default_branch" (line 615) and the fast-forward is git merge --ff-only "refs/remotes/origin/$default_branch" (line 691). A local branch created without an upstream therefore still gets a fresh defaultComparison and is still eligible for the fast-forward.

Verified on git 2.39.5: in a checkout whose only configured refspec was +refs/tags/v1:refs/remotes/origin/pinned, the driver’s exact fetch command advanced origin/main from 6f0a8aa to the new remote tip.

The git remote set-branches --add recommendation still stands on its other grounds (the owner’s own git fetch/git pull need the mapping); drop or correct just this consequence clause.

🟡 **Medium:** The clause “this skill’s own `defaultComparison` for that checkout goes stale from the next run” does not hold for this driver, so it is a false justification for a persistent config mutation. `scripts/audit-checkouts.sh:801` fetches with an explicit command-line refspec — `git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'` — which overrides the configured narrow refspec, so `refs/remotes/origin/<default>` is refreshed on every fetching run no matter what `remote.origin.fetch` says. And both consumers read that ref directly rather than `@{upstream}`: `defaultComparison` is `git rev-list --left-right --count "HEAD...refs/remotes/origin/$default_branch"` (line 615) and the fast-forward is `git merge --ff-only "refs/remotes/origin/$default_branch"` (line 691). A local branch created without an upstream therefore still gets a fresh `defaultComparison` and is still eligible for the fast-forward. Verified on git 2.39.5: in a checkout whose only configured refspec was `+refs/tags/v1:refs/remotes/origin/pinned`, the driver’s exact fetch command advanced `origin/main` from `6f0a8aa` to the new remote tip. The `git remote set-branches --add` recommendation still stands on its other grounds (the owner’s own `git fetch`/`git pull` need the mapping); drop or correct just this consequence clause.

🟡 Medium: “never the --prune glob form” contradicts this skill’s own driver, which runs exactly that form unconditionally: scripts/audit-checkouts.sh:801 is git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*', executed against every repository on every run that is not --no-fetch.

The hazard described is real — verified on git 2.39.5 with remote.origin.fetch = +refs/tags/v1:refs/remotes/origin/pinned, that command printed - [deleted] (none) -> origin/pinned — but it means the deletion has already happened during the audit fetch, before an agent ever reaches this paragraph, and an agent following the instruction cannot reconcile it with the tool it was told to run. (The loss is recoverable: a later plain git fetch origin restores the ref from the configured refspec.)

Either scope the warning to the ad hoc case and say the driver’s own fetch already prunes this way, or make the driver’s prune scope match the configured refspec.

🟡 **Medium:** “never the `--prune` glob form” contradicts this skill’s own driver, which runs exactly that form unconditionally: `scripts/audit-checkouts.sh:801` is `git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'`, executed against every repository on every run that is not `--no-fetch`. The hazard described is real — verified on git 2.39.5 with `remote.origin.fetch = +refs/tags/v1:refs/remotes/origin/pinned`, that command printed `- [deleted] (none) -> origin/pinned` — but it means the deletion has already happened during the audit fetch, before an agent ever reaches this paragraph, and an agent following the instruction cannot reconcile it with the tool it was told to run. (The loss is recoverable: a later plain `git fetch origin` restores the ref from the configured refspec.) Either scope the warning to the ad hoc case and say the driver’s own fetch already prunes this way, or make the driver’s prune scope match the configured refspec.
jercik marked this conversation as resolved
fix: credit the audit fetch and scope the prune warning to ad hoc use
Some checks failed
Node tests / node:test (pull_request) Successful in 15s
commit-msg / commitlint (pull_request) Successful in 27s
PR Review / Prepare immutable review tools (pull_request_target) Successful in 2m57s
PR Review / forgejo-review-approach-smart-1 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-smart-2 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-approach-3 generator (pull_request_target) Has been skipped
PR Review / forgejo-review-code-smart-2 generator (pull_request_target) Failing after 37s
PR Review / forgejo-review-code-smart-1 generator (pull_request_target) Successful in 3m56s
PR Review / Dispatch and observe exact review writers (pull_request_target) Failing after 4m13s
1b1e6ebc79
Author
Owner

Both findings adopted in 1b1e6eb — thank you for checking the prose against the driver.

  • 44235: the staleness clause is gone; the bare-branch consequence now names what actually breaks (the owner's own git fetch/git pull stay blind to the branch) and credits the audit fetch's command-line glob refspec with keeping origin/<default> fresh regardless.
  • 44236: the --prune warning is scoped to ad hoc glob fetches, and the paragraph now discloses that the driver's own fetch is exactly that pruning glob — so on a narrow-refspec repository those refs are already gone by report time, recoverable with a plain git fetch.

The driver's glob+prune fetch stays as is: the audit deliberately wants complete, prune-fresh origin/* state, and the prose now matches it instead of contradicting it.

Both findings adopted in 1b1e6eb — thank you for checking the prose against the driver. - 44235: the staleness clause is gone; the bare-branch consequence now names what actually breaks (the owner's own `git fetch`/`git pull` stay blind to the branch) and credits the audit fetch's command-line glob refspec with keeping `origin/<default>` fresh regardless. - 44236: the `--prune` warning is scoped to ad hoc glob fetches, and the paragraph now discloses that the driver's own fetch is exactly that pruning glob — so on a narrow-refspec repository those refs are already gone by report time, recoverable with a plain `git fetch`. The driver's glob+prune fetch stays as is: the audit deliberately wants complete, prune-fresh `origin/*` state, and the prose now matches it instead of contradicting it.
forgejo-actions left a comment

Summary: No actionable issues found; 1 informational note.

Docs-only change adding two paragraphs to skills/audit-git-checkouts/SKILL.md. I reproduced every runtime claim in scratch repositories with a tag-only fetch refspec on git 2.39.5, and all of them hold:

  • git for-each-ref --contains HEAD prints nothing for an unreachable detached tip while still exiting 0 — the paragraph correctly gates on "prints at least one line" rather than exit status, and HEAD itself is not listed by default, so the check cannot satisfy itself.
  • git show-ref --verify refs/remotes/origin/main without --quiet writes fatal: 'refs/remotes/origin/main' - not a valid ref and exits 128; with --quiet it exits 1 silently.
  • Under a tag-only refspec, git switch main dies fatal: invalid reference: main even after a one-shot fetch created refs/remotes/origin/main, and --track refuses with cannot set up tracking information; starting point 'origin/main' is not a branch.
  • git remote set-branches --add origin main appends exactly +refs/heads/main:refs/remotes/origin/main beside the untouched tag refspec; a plain git fetch origin then makes git switch main create the branch with its upstream set.
  • A branch created bare from origin/main has no upstream: git pull fails with fatal: no upstream configured.
  • git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' deleted refs/remotes/origin/pr/7 that the configured narrow refspec maintained, and a plain git fetch origin restored it. That glob is exactly the driver's fetch (skills/audit-git-checkouts/scripts/audit-checkouts.sh:801), so the "already gone by report time" claim is accurate.

zsh is not installable in this sandbox, so the read-only status parameter claim could not be executed, but it matches zsh's documented special parameters and the quoted error text.

Code review by Claude Code Opus (opus)

**Summary:** No actionable issues found; 1 informational note. Docs-only change adding two paragraphs to `skills/audit-git-checkouts/SKILL.md`. I reproduced every runtime claim in scratch repositories with a tag-only fetch refspec on git 2.39.5, and all of them hold: - `git for-each-ref --contains HEAD` prints nothing for an unreachable detached tip while still exiting 0 — the paragraph correctly gates on "prints at least one line" rather than exit status, and `HEAD` itself is not listed by default, so the check cannot satisfy itself. - `git show-ref --verify refs/remotes/origin/main` without `--quiet` writes `fatal: 'refs/remotes/origin/main' - not a valid ref` and exits 128; with `--quiet` it exits 1 silently. - Under a tag-only refspec, `git switch main` dies `fatal: invalid reference: main` even after a one-shot fetch created `refs/remotes/origin/main`, and `--track` refuses with `cannot set up tracking information; starting point 'origin/main' is not a branch`. - `git remote set-branches --add origin main` appends exactly `+refs/heads/main:refs/remotes/origin/main` beside the untouched tag refspec; a plain `git fetch origin` then makes `git switch main` create the branch with its upstream set. - A branch created bare from `origin/main` has no upstream: `git pull` fails with `fatal: no upstream configured`. - `git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'` deleted `refs/remotes/origin/pr/7` that the configured narrow refspec maintained, and a plain `git fetch origin` restored it. That glob is exactly the driver's fetch (`skills/audit-git-checkouts/scripts/audit-checkouts.sh:801`), so the "already gone by report time" claim is accurate. zsh is not installable in this sandbox, so the read-only `status` parameter claim could not be executed, but it matches zsh's documented special parameters and the quoted error text. _Code review by Claude Code Opus (opus)_ <!-- axrecipe-review:v1:eyJzY2hlbWFWZXJzaW9uIjoxLCJzdGF0ZSI6InB1Ymxpc2hlZCIsInJlcG9zaXRvcnkiOiJqNGstb3NzL2FnZW50LXNraWxscyIsIm51bWJlciI6IjQ5IiwiaGVhZFNoYSI6IjFiMWU2ZWJjNzlhZTdmODk4MTQ0YzM2YTAzYTg3NDE0NjYxODRiOTAiLCJzbG90IjoiZm9yZ2Vqby1yZXZpZXctY29kZS1zbWFydC0xIiwic291cmNlV29ya2Zsb3dSdW5JZCI6IjE4MTYwIiwic291cmNlR2VuZXJhdG9yQXR0ZW1wdCI6IjEiLCJyZXN1bHRJZCI6IjM2ZGRmZmE5LTZjMDEtNDBiYi1iODA1LWU5OTIyYmY1ODVhMSJ9 -->
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then a
For every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.
When the owner explicitly asks to move a detached checkout onto its default branch, gate the switch on two checks, not one: the working tree is clean, and the detached tip is reachable from some ref — `git for-each-ref --contains HEAD` prints at least one line. A clean tree says nothing about commits made on the detached HEAD; switching away leaves them reflog-only, and git's "leaving commits behind" advice prints only after HEAD has moved. If the tip is unreachable, report its unique commits to the owner instead of switching. Verify the remote-tracking ref exists with `git show-ref --verify --quiet refs/remotes/<remote>/<default>` (without `--quiet` the expected-negative case prints `fatal:` to stderr, inviting misreading as a repository failure): a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it — confirm the cause with `git config --get-all remote.<name>.fetch`. The switch cannot ride a one-shot fetch: `git switch <default>` maps the branch through the remote's configured refspec, not through refs on disk, so under the still-narrow refspec it dies with `fatal: invalid reference` even after the ref is fetched, `--track` refuses (`starting point '<remote>/<default>' is not a branch`), and a branch created bare, without an upstream, leaves the owner's own `git fetch` and `git pull` blind to the branch — only this skill's audit fetch, whose command-line glob refspec overrides the configured one, keeps `origin/<default>` fresh there. The owner asked for a checkout living on its default branch, so persist the minimal mapping: `git remote set-branches --add <remote> <default>` appends `+refs/heads/<default>:refs/remotes/<remote>/<default>` beside the narrow refspec it deliberately leaves alone; then `git fetch <remote>`, and `git switch <default>` creates the branch with its upstream set and future fetches covering it. Widen to all heads only when the owner wants that. To merely inspect the branch without adopting it, a one-shot `git fetch <remote> '+refs/heads/<default>:refs/remotes/<remote>/<default>'` recovers just that ref. Don't add `--prune` to an ad hoc glob fetch: prune scope follows the command-line refspec, deleting every ref under `refs/remotes/<remote>/` that the narrow refspec maintains elsewhere. The audit driver's own fetch is exactly that pruning glob, so on such a repository those refs are already gone by report time — a plain `git fetch <remote>` restores whatever the configured refspec maintains.

ℹ️ Info: The rationale "git's "leaving commits behind" advice prints only after HEAD has moved" is slightly narrower than observed behavior. On git 2.39.5, git switch --track origin/<default> under a narrow refspec printed the full Warning: you are leaving 1 commit behind block and then died with fatal: cannot set up tracking information, leaving HEAD still detached at the original tip. So the advice can also appear on a switch that never moves HEAD.

The operational guidance is unaffected — the advice still arrives too late to serve as a pre-switch gate, which is exactly why the paragraph requires the for-each-ref --contains HEAD check first. Worth noting only so a reader doesn't treat the appearance of that warning as proof HEAD already moved.

ℹ️ **Info:** The rationale "git's \"leaving commits behind\" advice prints only after HEAD has moved" is slightly narrower than observed behavior. On git 2.39.5, `git switch --track origin/<default>` under a narrow refspec printed the full `Warning: you are leaving 1 commit behind` block and *then* died with `fatal: cannot set up tracking information`, leaving HEAD still detached at the original tip. So the advice can also appear on a switch that never moves HEAD. The operational guidance is unaffected — the advice still arrives too late to serve as a pre-switch gate, which is exactly why the paragraph requires the `for-each-ref --contains HEAD` check first. Worth noting only so a reader doesn't treat the appearance of that warning as proof HEAD already moved.
jercik merged commit 26adfb2675 into main 2026-08-07 09:16:25 +00:00
jercik deleted branch align/audit-git-checkouts 2026-08-07 09:16:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
j4k-oss/agent-skills!49
No description provided.