docs(audit-git-checkouts): state the submodule removal rules exactly #82

Merged
jercik merged 7 commits from docs/audit-wording-followups into main 2026-09-25 21:52:30 +00:00
Owner

Wording fixes deferred from #81's review rounds 4 and 5; no behavior changes.

The driver's help no longer implies every submodule commit that removal deletes is on a remote-tracking ref, since a commit kept only by an upstream tag also passes. The removal-gates reference now names the <modules> directory, says that only the removal-time scan covers ignored files inside submodules, and says how to restore a listed Git directory's checkout, nested submodules included.

The restore step also keeps a detached HEAD commit that no ref holds. git submodule update --init checks out the recorded commit, which would let the rerun pass and removal delete that commit. So the step records HEAD first and checks it out again afterwards, without creating a ref.

🤖 Generated with Claude Code

Wording fixes deferred from #81's review rounds 4 and 5; no behavior changes. The driver's help no longer implies every submodule commit that removal deletes is on a remote-tracking ref, since a commit kept only by an upstream tag also passes. The removal-gates reference now names the `<modules>` directory, says that only the removal-time scan covers ignored files inside submodules, and says how to restore a listed Git directory's checkout, nested submodules included. The restore step also keeps a detached HEAD commit that no ref holds. `git submodule update --init` checks out the recorded commit, which would let the rerun pass and removal delete that commit. So the step records HEAD first and checks it out again afterwards, without creating a ref. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(audit-git-checkouts): state the submodule removal rules exactly
All checks were successful
commit-msg / commitlint (pull_request) Successful in 16s
Node tests / node:test (pull_request) Successful in 1m8s
Review / Review (pull_request_target) Successful in 3m37s
19ba106e23
The help text no longer implies every deleted submodule commit is on a
remote-tracking ref, since a commit kept only by an upstream tag also
passes. The removal-gates reference now names the modules directory,
says how to find a submodule's checkout path from its name, and says
which ignored-file scan covers submodules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Review 01M3D4QCZWJBXAARR22CWSW3KA — head fb5fb7f1d9507f3f680ec8aeb419efd767ea4fa2

Review — j4k-oss/agent-skills @ 4a3f3699d5

Scope: diff against base tree 19897a019b18
Status: dispatched — coverage complete (3/3 slots terminal)
Facts: current review-wide projection

Computed under:

{
  "abandonment": "abandonment-v1",
  "anchor_recipe": 1,
  "batch_policy": "batch-v1",
  "coverage": "coverage-v3",
  "dispatch_policy": "dispatch-v2",
  "grounder_version": 1,
  "grounding_read_rule": "grounding-read-v1",
  "promotion_policy": "promotion-v1",
  "report": "report-v3",
  "tally": "tally-v1",
  "triage_settle": "triage-settle-v2"
}

Findings (3)

medium — Submodule restore procedure is packed into one table cell with steps out of execution order

  • claim: 01M3D4T8E0DH02WS0614RQYXQ0
  • anchor: skills/audit-git-checkouts/references/removal-gates.md (snippet)
Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it.
  • lens: writing-quality · arm: default
  • verdicts: 1 valid / 0 invalid / 0 uncertain
  • disposition: none

What I examined: the judgment/submodule-local-work row of the "Resolve a kept worktree" table in skills/audit-git-checkouts/references/removal-gates.md. This change added about 300 words to it. SKILL.md tells the agent to read this reference in full before acting on a kept worktree. I also read the driver functions list_unchecked_submodule_git_dirs and list_submodule_git_dirs_without_checkout in scripts/audit-checkouts.sh to confirm what the entries look like.

What the subject says: for a (not checked out) entry, the cell gives these instructions in this order:
(a) choose between deleting and restoring;
(b) the consequence of restoring: "Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it.";
(c) "So first record that HEAD with git --git-dir <dir> rev-parse HEAD ... and after restoring, check it out again in the restored submodule; the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner returns the submodule to the recorded commit.";
(d) an alternative outcome ("Committing the new Gitlink instead ...");
(e) only after that, how to restore: "To restore, find the superproject whose .gitmodules names the submodule. Start in the worktree with <sub> set to the entry ... Otherwise <sub> is <outer>/modules/<rest> ... move into the checkout ... and repeat with <rest>."

What goes wrong: this is a real sequence where order matters. HEAD must be recorded before git submodule update --init runs, or the stranded commit is lost (the cell itself says removal would then delete it). Yet the cell says "after restoring" before it explains how to restore, and the restore procedure is a loop ("repeat with <rest>") written as prose in a Markdown table cell, which cannot hold a list. Clause (c) joins three independent statements with semicolons. It also calls the resulting outcome "a dirty tree" instead of naming the expected/… row it maps to. An agent reading linearly has to rebuild the order from out-of-sequence clauses, and this is the one row where a wrong order destroys the owner's commit. The writing-for-agents skill says to use "numbered steps only for real sequence", to "Order sections by the decisions the reader makes", and to "spend detail where a plausible mistake would derail the task".

Proposed correction: keep one sentence in the table cell pointing to a new subsection (for example "## Restore a submodule Git directory without a checkout"). Put numbered steps there, in execution order:

  1. Show the owner what <dir> holds (the log and stash commands).
  2. If they choose deletion, delete <dir> and rerun.
  3. Otherwise, record git --git-dir <dir> rev-parse HEAD.
  4. Find the Gitlink path with the .gitmodules lookup, including the <outer>/modules/<rest> recursion and why the whole entry is tried first.
  5. Run git submodule update --init -- <path>.
  6. Check out the recorded HEAD in the restored submodule.
  7. Rerun. Expect expected/… (dirty) until the commit is pushed and the submodule is returned to its Gitlink, or judgment/containment-not-proven if a new Gitlink is committed.

This keeps every fact now in the cell: the stranding risk, the lookup rule, and both follow-up outcomes. The only change is that the order of the text matches the order of execution.

Proof status: static reading of the prose; I did not run the procedure. Git behavior (that submodule update detaches to the recorded commit) is as the cell itself states.

low — Same Git directory is named <modules>/<name> in the log/stash commands and <dir> in rev-parse, defined only after first use

  • claim: 01M3D4TKFSGTPJBNEQ56CVK3GZ
  • anchor: skills/audit-git-checkouts/references/removal-gates.md (snippet)
So first record that HEAD with `git --git-dir <dir> rev-parse HEAD`, where `<dir>` is `<modules>/<name>` or the listed path
  • lens: writing-quality · arm: default
  • verdicts: 1 valid / 0 invalid / 0 uncertain
  • disposition: none

What I examined: the judgment/submodule-local-work row in skills/audit-git-checkouts/references/removal-gates.md. This change added the rev-parse sentence and the restore lookup to it. I also read the <name> (not checked out) bullet higher in the same file, which defines two kinds of entry: <modules>-relative names and worktree-relative paths such as dep/.git/modules/inner.

What the subject says: one cell names the same Git directory three different ways. The inspection step reads "show the owner what <modules>/<name> (or the listed worktree-relative path) holds ... git --git-dir <modules>/<name> --work-tree <modules>/<name> log ... and … stash list". Here the commands hard-code <modules>/<name>, and the other form appears only in a parenthetical. Two sentences later a new placeholder, <dir>, is defined for the same directory ("where <dir> is <modules>/<name> or the listed path"), but only the rev-parse command uses it. The restore lookup then adds <sub>, <checkout>, <rest>, and <outer>.

What goes wrong: a literal reader handling a worktree-relative entry (for example dep/.git/modules/inner) finds log and stash list commands written for <modules>/<name>, and has to substitute the parenthetical form on their own. The rev-parse command gets that substitution through <dir>. The same concept under two names, defined after its first use, invites exactly the mismatch the entry types exist to prevent. The writing-for-agents skill says: "use one term for one concept, and define project-local terms on first use."

Proposed correction: define <dir> once, where the cell first introduces the (not checked out) entry: "<dir> is <modules>/<name>, or the listed worktree-relative path for a nested entry". Then use it in all three commands: git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes, git --git-dir <dir> --work-tree <dir> stash list, and git --git-dir <dir> rev-parse HEAD. Spell out the stash list command instead of eliding it with "…", because it needs the same two options. This preserves both entry forms and removes the parenthetical and the late definition.

Proof status: static reading; the git commands behave the same either way once the right path is substituted, so the risk is an agent substituting the wrong path, not a wrong command.

low — Help text says branch refs are never deleted, then that removal deletes submodules' local branches, and drops the condition that gates it

  • claim: 01M3D4V6756SJBB5XE9BC03A55
  • anchor: skills/audit-git-checkouts/scripts/audit-checkouts.sh (snippet)
  echo "worktree deletes its submodules' Git directories, local branches and tags included."
  • lens: writing-quality · arm: default
  • verdicts: 1 valid / 0 invalid / 0 uncertain
  • disposition: none

What I examined: the usage() help text in skills/audit-git-checkouts/scripts/audit-checkouts.sh. SKILL.md tells the agent to run scripts/audit-checkouts.sh --help from the skill directory before first use, so this text is agent-facing. I also read the "Refuses, per repository" paragraph just above it, and submodule_holds_local_work / list_submodules_with_local_work, which walk every submodule's branches, stash, and tags.

What the subject says: the closing paragraph now reads: "The audited repositories' branch refs and stashes are never deleted. Removing a worktree deletes its submodules' Git directories, local branches and tags included." Before this change, the second sentence ended "local branches included, once every commit there is held by a remote-tracking ref." This change dropped that qualifier and added tags.

What goes wrong: the two sentences are adjacent and read as contradicting each other. The first says branch refs are never deleted. The next says local branches are deleted. The driver walks submodules as repositories in their own right, so "the audited repositories" does not clearly exclude them. Without the old qualifier, the second sentence also no longer says the deletion is gated. A reader who remembers that removal is refused while a submodule holds unpushed work has to connect it themselves to the "Refuses" paragraph eight lines up. The writing-for-agents skill says: "attach conditions to the action they govern" and "Make claims verifiable".

Proposed correction: separate the two scopes and restore the condition, for example: "Branch refs and stashes in the audited checkouts themselves are never deleted. Removing a worktree also deletes its submodules' Git directories, including their local branches and tags, once the refusals above find nothing only that submodule holds." This keeps the new fact (tags are deleted too) and the guarantee the old wording carried.

Proof status: static reading of the help text against the refusal logic; the behavior itself is not in question, only whether the text states it unambiguously.

Other claims

  • grounding-pending (0)
  • ungrounded (0)
  • rejected (0)
  • duplicate-of (0)
  • unadjudicated (0)

Coverage

Coverage pass: 01M3D4QD2QVHWQ5AFPGYWWRYAN
Accounting: complete
Slot health: healthy

lens part arm unit status runs loss
general-bug whole default no-claims 1 no
writing-quality whole default claims-emitted 1 no
test-trimming whole default no-claims 1 no
<!-- review:summary --> **Review** `01M3D4QCZWJBXAARR22CWSW3KA` — head `fb5fb7f1d9507f3f680ec8aeb419efd767ea4fa2` # Review — j4k-oss/agent-skills @ 4a3f3699d569 Scope: diff against base tree `19897a019b18` Status: dispatched — coverage complete (3/3 slots terminal) Facts: current review-wide projection Computed under: ```json { "abandonment": "abandonment-v1", "anchor_recipe": 1, "batch_policy": "batch-v1", "coverage": "coverage-v3", "dispatch_policy": "dispatch-v2", "grounder_version": 1, "grounding_read_rule": "grounding-read-v1", "promotion_policy": "promotion-v1", "report": "report-v3", "tally": "tally-v1", "triage_settle": "triage-settle-v2" } ``` ## Findings (3) ### medium — Submodule restore procedure is packed into one table cell with steps out of execution order - claim: `01M3D4T8E0DH02WS0614RQYXQ0` - anchor: `skills/audit-git-checkouts/references/removal-gates.md` (snippet) ``` Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it. ``` - lens: writing-quality · arm: default - verdicts: 1 valid / 0 invalid / 0 uncertain - disposition: none > What I examined: the `judgment/submodule-local-work` row of the "Resolve a kept worktree" table in skills/audit-git-checkouts/references/removal-gates.md. This change added about 300 words to it. SKILL.md tells the agent to read this reference in full before acting on a kept worktree. I also read the driver functions `list_unchecked_submodule_git_dirs` and `list_submodule_git_dirs_without_checkout` in scripts/audit-checkouts.sh to confirm what the entries look like. > > What the subject says: for a `(not checked out)` entry, the cell gives these instructions in this order: > (a) choose between deleting and restoring; > (b) the consequence of restoring: "Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it."; > (c) "So first record that HEAD with `git --git-dir <dir> rev-parse HEAD` ... and after restoring, check it out again in the restored submodule; the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner returns the submodule to the recorded commit."; > (d) an alternative outcome ("Committing the new Gitlink instead ..."); > (e) only after that, how to restore: "To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry ... Otherwise `<sub>` is `<outer>/modules/<rest>` ... move into the checkout ... and repeat with `<rest>`." > > What goes wrong: this is a real sequence where order matters. HEAD must be recorded before `git submodule update --init` runs, or the stranded commit is lost (the cell itself says removal would then delete it). Yet the cell says "after restoring" before it explains how to restore, and the restore procedure is a loop ("repeat with `<rest>`") written as prose in a Markdown table cell, which cannot hold a list. Clause (c) joins three independent statements with semicolons. It also calls the resulting outcome "a dirty tree" instead of naming the `expected/…` row it maps to. An agent reading linearly has to rebuild the order from out-of-sequence clauses, and this is the one row where a wrong order destroys the owner's commit. The writing-for-agents skill says to use "numbered steps only for real sequence", to "Order sections by the decisions the reader makes", and to "spend detail where a plausible mistake would derail the task". > > Proposed correction: keep one sentence in the table cell pointing to a new subsection (for example "## Restore a submodule Git directory without a checkout"). Put numbered steps there, in execution order: > 1. Show the owner what `<dir>` holds (the log and stash commands). > 2. If they choose deletion, delete `<dir>` and rerun. > 3. Otherwise, record `git --git-dir <dir> rev-parse HEAD`. > 4. Find the Gitlink path with the `.gitmodules` lookup, including the `<outer>/modules/<rest>` recursion and why the whole entry is tried first. > 5. Run `git submodule update --init -- <path>`. > 6. Check out the recorded HEAD in the restored submodule. > 7. Rerun. Expect `expected/…` (dirty) until the commit is pushed and the submodule is returned to its Gitlink, or `judgment/containment-not-proven` if a new Gitlink is committed. > > This keeps every fact now in the cell: the stranding risk, the lookup rule, and both follow-up outcomes. The only change is that the order of the text matches the order of execution. > > Proof status: static reading of the prose; I did not run the procedure. Git behavior (that `submodule update` detaches to the recorded commit) is as the cell itself states. ### low — Same Git directory is named `<modules>/<name>` in the log/stash commands and `<dir>` in rev-parse, defined only after first use - claim: `01M3D4TKFSGTPJBNEQ56CVK3GZ` - anchor: `skills/audit-git-checkouts/references/removal-gates.md` (snippet) ``` So first record that HEAD with `git --git-dir <dir> rev-parse HEAD`, where `<dir>` is `<modules>/<name>` or the listed path ``` - lens: writing-quality · arm: default - verdicts: 1 valid / 0 invalid / 0 uncertain - disposition: none > What I examined: the `judgment/submodule-local-work` row in skills/audit-git-checkouts/references/removal-gates.md. This change added the rev-parse sentence and the restore lookup to it. I also read the `<name> (not checked out)` bullet higher in the same file, which defines two kinds of entry: `<modules>`-relative names and worktree-relative paths such as `dep/.git/modules/inner`. > > What the subject says: one cell names the same Git directory three different ways. The inspection step reads "show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds ... `git --git-dir <modules>/<name> --work-tree <modules>/<name> log ...` and `… stash list`". Here the commands hard-code `<modules>/<name>`, and the other form appears only in a parenthetical. Two sentences later a new placeholder, `<dir>`, is defined for the same directory ("where `<dir>` is `<modules>/<name>` or the listed path"), but only the rev-parse command uses it. The restore lookup then adds `<sub>`, `<checkout>`, `<rest>`, and `<outer>`. > > What goes wrong: a literal reader handling a worktree-relative entry (for example `dep/.git/modules/inner`) finds `log` and `stash list` commands written for `<modules>/<name>`, and has to substitute the parenthetical form on their own. The rev-parse command gets that substitution through `<dir>`. The same concept under two names, defined after its first use, invites exactly the mismatch the entry types exist to prevent. The writing-for-agents skill says: "use one term for one concept, and define project-local terms on first use." > > Proposed correction: define `<dir>` once, where the cell first introduces the `(not checked out)` entry: "`<dir>` is `<modules>/<name>`, or the listed worktree-relative path for a nested entry". Then use it in all three commands: `git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes`, `git --git-dir <dir> --work-tree <dir> stash list`, and `git --git-dir <dir> rev-parse HEAD`. Spell out the `stash list` command instead of eliding it with "…", because it needs the same two options. This preserves both entry forms and removes the parenthetical and the late definition. > > Proof status: static reading; the git commands behave the same either way once the right path is substituted, so the risk is an agent substituting the wrong path, not a wrong command. ### low — Help text says branch refs are never deleted, then that removal deletes submodules' local branches, and drops the condition that gates it - claim: `01M3D4V6756SJBB5XE9BC03A55` - anchor: `skills/audit-git-checkouts/scripts/audit-checkouts.sh` (snippet) ``` echo "worktree deletes its submodules' Git directories, local branches and tags included." ``` - lens: writing-quality · arm: default - verdicts: 1 valid / 0 invalid / 0 uncertain - disposition: none > What I examined: the `usage()` help text in skills/audit-git-checkouts/scripts/audit-checkouts.sh. SKILL.md tells the agent to run `scripts/audit-checkouts.sh --help` from the skill directory before first use, so this text is agent-facing. I also read the "Refuses, per repository" paragraph just above it, and `submodule_holds_local_work` / `list_submodules_with_local_work`, which walk every submodule's branches, stash, and tags. > > What the subject says: the closing paragraph now reads: "The audited repositories' branch refs and stashes are never deleted. Removing a worktree deletes its submodules' Git directories, local branches and tags included." Before this change, the second sentence ended "local branches included, once every commit there is held by a remote-tracking ref." This change dropped that qualifier and added tags. > > What goes wrong: the two sentences are adjacent and read as contradicting each other. The first says branch refs are never deleted. The next says local branches are deleted. The driver walks submodules as repositories in their own right, so "the audited repositories" does not clearly exclude them. Without the old qualifier, the second sentence also no longer says the deletion is gated. A reader who remembers that removal is refused while a submodule holds unpushed work has to connect it themselves to the "Refuses" paragraph eight lines up. The writing-for-agents skill says: "attach conditions to the action they govern" and "Make claims verifiable". > > Proposed correction: separate the two scopes and restore the condition, for example: "Branch refs and stashes in the audited checkouts themselves are never deleted. Removing a worktree also deletes its submodules' Git directories, including their local branches and tags, once the refusals above find nothing only that submodule holds." This keeps the new fact (tags are deleted too) and the guarantee the old wording carried. > > Proof status: static reading of the help text against the refusal logic; the behavior itself is not in question, only whether the text states it unambiguously. ## Other claims - grounding-pending (0) - ungrounded (0) - rejected (0) - duplicate-of (0) - unadjudicated (0) ## Coverage Coverage pass: 01M3D4QD2QVHWQ5AFPGYWWRYAN Accounting: complete Slot health: healthy | lens | part | arm | unit status | runs | loss | | --- | --- | --- | --- | --- | --- | | general-bug | whole | default | no-claims | 1 | no | | writing-quality | whole | default | claims-emitted | 1 | no | | test-trimming | whole | default | no-claims | 1 | no |
@ -54,3 +54,3 @@
| `judgment/sparse-checkout` | Keep the cone's `skip-worktree` bits: clearing them turns absent files into apparent deletions. Confirm the cone with `git sparse-checkout list`, verify no file outside it is materialized, then remove manually once every other gate holds. |
| `judgment/precious-ignored-files` | Leave `preciousPaths` in place until the owner disposes of them or authorizes their deletion, then rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, or delete the directory, and rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, where `<path>` is the output of `git config -f .gitmodules submodule.<name>.path`, run inside the enclosing submodule for a nested entry, or delete the directory, and rerun. |

low — Recovery lookup submodule.<name>.path finds nothing for a nested submodule Git directory absorbed under <modules>, because the driver lists it as dep/modules/inner
lens general-bug · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the new judgment/submodule-local-work recovery text in references/removal-gates.md, the <name> (not checked out) bullet above it ("<name> is its path relative to <modules>, which is the submodule's name"), and the driver functions that produce these entries in scripts/audit-checkouts.sh: list_unchecked_submodule_git_dirs and list_submodule_git_dirs_without_checkout. The second function runs find "$modules_path" -name HEAD -type f and prints ${candidate#"$modules_path"/} for every Git directory that no populated submodule uses. It walks the whole tree under <modules>, not just its top level. list_unchecked_submodule_git_dirs skips the per-populated-submodule pass for Git directories under <modules> ("$physical_modules"/*) ... continue), so the recursive find output is the only report.

What goes wrong: a nested submodule whose Git directory was absorbed (the normal layout after git submodule update --init --recursive) and then deinitialized inside its parent is stored at <modules>/dep/modules/inner. The driver reports it as dep/modules/inner (not checked out). That <name> is not a submodule name at any level. The new instruction's git config -f .gitmodules submodule.<name>.path returns nothing either at the worktree root or inside dep, the enclosing submodule the text points to. The real lookup is submodule.inner.path inside dep, and nothing in the doc says to strip the dep/modules/ prefix. The phrase "for a nested entry" suggests the doc handles nesting, but it only fits the non-absorbed dep/.git/modules/inner form. Someone following the doc cannot derive <path> for git submodule update --init -- <path>, so they are left with the other option the table offers: deleting the directory.

Observed reproduction with git 2.47.3: I built a superproject sup with submodule dep, which has its own submodule inner. Then I ran git worktree add ../wt, git submodule update --init --recursive in wt, and git -C dep submodule deinit -f inner. find showed .../worktrees/wt/modules/dep/modules/inner/HEAD. I extracted the driver's list_populated_submodule_git_dirs, list_unchecked_submodule_git_dirs, and list_submodule_git_dirs_without_checkout functions and ran them on wt. They printed dep/modules/inner. git config -f .gitmodules submodule.dep/modules/inner.path exited 1 at the root and inside dep. git -C dep config -f .gitmodules submodule.inner.path printed inner. I did not run the full driver end to end. The existing tests only cover dependency (not checked out) and the non-absorbed dependency/.git/modules/inner form.

Fix: have the doc say that for an entry of the form <a>/modules/<b>, <b> is the name to look up inside the submodule at <a>'s path. Alternatively, have the driver report nested absorbed entries in a form that names the enclosing submodule's checkout path.

claim 01M3CZ2VNGVHG3KNG2SZ6K27XK of review 01M3CZ037RYN4N2VBVBYGFJVAP

<!-- review:claim:01M3CZ2VNGVHG3KNG2SZ6K27XK --> **low** — Recovery lookup `submodule.<name>.path` finds nothing for a nested submodule Git directory absorbed under `<modules>`, because the driver lists it as `dep/modules/inner` lens `general-bug` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the new `judgment/submodule-local-work` recovery text in references/removal-gates.md, the `<name> (not checked out)` bullet above it ("`<name>` is its path relative to `<modules>`, which is the submodule's name"), and the driver functions that produce these entries in scripts/audit-checkouts.sh: `list_unchecked_submodule_git_dirs` and `list_submodule_git_dirs_without_checkout`. The second function runs `find "$modules_path" -name HEAD -type f` and prints `${candidate#"$modules_path"/}` for every Git directory that no populated submodule uses. It walks the whole tree under `<modules>`, not just its top level. `list_unchecked_submodule_git_dirs` skips the per-populated-submodule pass for Git directories under `<modules>` (`"$physical_modules"/*) ... continue`), so the recursive `find` output is the only report. > > What goes wrong: a nested submodule whose Git directory was absorbed (the normal layout after `git submodule update --init --recursive`) and then deinitialized inside its parent is stored at `<modules>/dep/modules/inner`. The driver reports it as `dep/modules/inner (not checked out)`. That `<name>` is not a submodule name at any level. The new instruction's `git config -f .gitmodules submodule.<name>.path` returns nothing either at the worktree root or inside `dep`, the enclosing submodule the text points to. The real lookup is `submodule.inner.path` inside `dep`, and nothing in the doc says to strip the `dep/modules/` prefix. The phrase "for a nested entry" suggests the doc handles nesting, but it only fits the non-absorbed `dep/.git/modules/inner` form. Someone following the doc cannot derive `<path>` for `git submodule update --init -- <path>`, so they are left with the other option the table offers: deleting the directory. > > Observed reproduction with git 2.47.3: I built a superproject `sup` with submodule `dep`, which has its own submodule `inner`. Then I ran `git worktree add ../wt`, `git submodule update --init --recursive` in wt, and `git -C dep submodule deinit -f inner`. `find` showed `.../worktrees/wt/modules/dep/modules/inner/HEAD`. I extracted the driver's `list_populated_submodule_git_dirs`, `list_unchecked_submodule_git_dirs`, and `list_submodule_git_dirs_without_checkout` functions and ran them on wt. They printed `dep/modules/inner`. `git config -f .gitmodules submodule.dep/modules/inner.path` exited 1 at the root and inside `dep`. `git -C dep config -f .gitmodules submodule.inner.path` printed `inner`. I did not run the full driver end to end. The existing tests only cover `dependency (not checked out)` and the non-absorbed `dependency/.git/modules/inner` form. > > Fix: have the doc say that for an entry of the form `<a>/modules/<b>`, `<b>` is the name to look up inside the submodule at `<a>`'s path. Alternatively, have the driver report nested absorbed entries in a form that names the enclosing submodule's checkout path. claim `01M3CZ2VNGVHG3KNG2SZ6K27XK` of review `01M3CZ037RYN4N2VBVBYGFJVAP`

low — Restore step for a (not checked out) entry buries its either/or choice and leaves unclear which command runs inside the enclosing submodule
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the judgment/submodule-local-work row of the "Resolve a kept worktree" table in skills/audit-git-checkouts/references/removal-gates.md. This change extends its last sentence. It now reads: "Then, on their decision, restore the checkout with git submodule update --init -- <path> while a Gitlink remains, where <path> is the output of git config -f .gitmodules submodule.<name>.path, run inside the enclosing submodule for a nested entry, or delete the directory, and rerun." I also read the entry forms defined in the same file (<path>, <path> (files without a checkout), <name> (not checked out)) and the SKILL.md pointer that sends agents to this file before they act.

What goes wrong: one sentence now holds a two-way decision (restore or delete), a precondition ("while a Gitlink remains"), a placeholder definition, and a location condition. Two readings are plausible:

  1. "run inside the enclosing submodule for a nested entry" sits right after the git config command, so a literal reader can apply it only to that lookup. They then run git submodule update --init -- <path> from the worktree root, using a path that is relative to the enclosing submodule.
  2. "or delete the directory" comes after two subordinate clauses, so it is hard to see as the alternative to "restore the checkout".
    The row also reuses <path> for a new meaning: a checkout path relative to the enclosing superproject. A few lines earlier, the same file defines <path> as a worktree-relative path of a populated submodule. The skill's guidance is to attach conditions to the action they govern, use one term for one concept, and use numbered steps for real sequences. This row is a real sequence that acts on user work.

Proposed correction: split the tail into steps, with the choice stated first, for example: "Then act on the owner's decision and rerun: to keep the work while a Gitlink remains, restore its checkout from the superproject that records it (the enclosing submodule for a nested entry): look up its checkout path with git config -f .gitmodules submodule.<name>.path, then run git submodule update --init -- <that path>. To discard it, delete the directory." This keeps every fact in the current text: the Gitlink precondition, the .gitmodules lookup, the nested-entry location, and both outcomes. It removes the attachment ambiguity and the reused placeholder.

Evidence status: static reading only. I did not run an agent against the passage. The misreading is plausible, not observed.

claim 01M3CZ3RTNFMJB1FYEA0H268H1 of review 01M3CZ037RYN4N2VBVBYGFJVAP

<!-- review:claim:01M3CZ3RTNFMJB1FYEA0H268H1 --> **low** — Restore step for a `(not checked out)` entry buries its either/or choice and leaves unclear which command runs inside the enclosing submodule lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the `judgment/submodule-local-work` row of the "Resolve a kept worktree" table in skills/audit-git-checkouts/references/removal-gates.md. This change extends its last sentence. It now reads: "Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, where `<path>` is the output of `git config -f .gitmodules submodule.<name>.path`, run inside the enclosing submodule for a nested entry, or delete the directory, and rerun." I also read the entry forms defined in the same file (`<path>`, `<path> (files without a checkout)`, `<name> (not checked out)`) and the SKILL.md pointer that sends agents to this file before they act. > > What goes wrong: one sentence now holds a two-way decision (restore or delete), a precondition ("while a Gitlink remains"), a placeholder definition, and a location condition. Two readings are plausible: > 1. "run inside the enclosing submodule for a nested entry" sits right after the `git config` command, so a literal reader can apply it only to that lookup. They then run `git submodule update --init -- <path>` from the worktree root, using a path that is relative to the enclosing submodule. > 2. "or delete the directory" comes after two subordinate clauses, so it is hard to see as the alternative to "restore the checkout". > The row also reuses `<path>` for a new meaning: a checkout path relative to the enclosing superproject. A few lines earlier, the same file defines `<path>` as a worktree-relative path of a populated submodule. The skill's guidance is to attach conditions to the action they govern, use one term for one concept, and use numbered steps for real sequences. This row is a real sequence that acts on user work. > > Proposed correction: split the tail into steps, with the choice stated first, for example: "Then act on the owner's decision and rerun: to keep the work while a Gitlink remains, restore its checkout from the superproject that records it (the enclosing submodule for a nested entry): look up its checkout path with `git config -f .gitmodules submodule.<name>.path`, then run `git submodule update --init -- <that path>`. To discard it, delete the directory." This keeps every fact in the current text: the Gitlink precondition, the `.gitmodules` lookup, the nested-entry location, and both outcomes. It removes the attachment ambiguity and the reused placeholder. > > Evidence status: static reading only. I did not run an agent against the passage. The misreading is plausible, not observed. claim `01M3CZ3RTNFMJB1FYEA0H268H1` of review `01M3CZ037RYN4N2VBVBYGFJVAP`
Author
Owner

Fixed in 3f5cc8c. I reproduced it: a deinitialized nested submodule is listed as dep/modules/inner, and the .gitmodules lookup fails for that name. The bullet now says a nested absorbed entry is listed under its parent, and the restore step looks up the text after the last /modules/, run in the enclosing submodule. Following those steps on the reproduction restored inner onto its existing Git directory.

<!-- gh-feedback:reply-to:89367 --> Fixed in 3f5cc8c. I reproduced it: a deinitialized nested submodule is listed as `dep/modules/inner`, and the `.gitmodules` lookup fails for that name. The bullet now says a nested absorbed entry is listed under its parent, and the restore step looks up the text after the last `/modules/`, run in the enclosing submodule. Following those steps on the reproduction restored `inner` onto its existing Git directory.
Author
Owner

Fixed in 3f5cc8c. The row now states the choice first (delete, or restore while a Gitlink remains). It then says both commands run in the superproject whose .gitmodules names the submodule. The lookup uses its own <sub> placeholder instead of reusing <path>.

<!-- gh-feedback:reply-to:89368 --> Fixed in 3f5cc8c. The row now states the choice first (delete, or restore while a Gitlink remains). It then says both commands run in the superproject whose `.gitmodules` names the submodule. The lookup uses its own `<sub>` placeholder instead of reusing `<path>`.
jercik marked this conversation as resolved
Lines 511-512
@ -514,0 +508,5 @@
# submodule must have no uncommitted files, no stash, no linked
# worktree, no branch commit that no remote-tracking ref holds, and
# no tag on a non-commit or on a commit no remote-tracking ref holds
# unless origin has that tag on the same object, or, for a
# lightweight tag, on a tag that peels to the same commit.

low — Header comment for list_submodules_with_local_work says origin has the tag "on a tag that peels", which misstates the lightweight-tag exception
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the rewritten header comment above list_submodules_with_local_work in skills/audit-git-checkouts/scripts/audit-checkouts.sh. I checked it against the code in submodule_holds_local_work and the matching prose in references/removal-gates.md.

What the subject says: "no tag on a non-commit or on a commit no remote-tracking ref holds unless origin has that tag on the same object, or, for a lightweight tag, on a tag that peels to the same commit." Grammatically the elided verb gives "origin has that tag ... on a tag that peels to the same commit". That describes origin's tag pointing at another tag object, which is not the rule.

What the code does: [ "$tag_type" = commit ] && grep -F -x -q -- "$tag_object $tag_ref^{}" <<<"$remote_tags" && continue. A local lightweight tag passes when origin's tag of the same name (an annotated tag) peels to that commit. The inline comment at that line says it correctly: "A lightweight tag holds nothing beyond its commit, which origin's tag of that name may peel to." removal-gates.md also says it correctly: "for a lightweight tag, a tag of that name on its commit."

Why it matters: this header is the summary a maintainer reads to learn which submodule tags block removal. The garbled clause invites a wrong mental model, for example that origin needs a tag-of-a-tag. It also disagrees in wording with the two other statements of the same rule. Skill guidance: make claims precise and keep one term for one concept.

Proposed correction: "... unless origin has that tag on the same object or, for a lightweight tag, has a tag of that name that peels to its commit." This keeps both exceptions and matches the code and the reference doc.

claim 01M3CZ4DJ200HNW38FDBT9H6GK of review 01M3CZ037RYN4N2VBVBYGFJVAP

<!-- review:claim:01M3CZ4DJ200HNW38FDBT9H6GK --> **low** — Header comment for `list_submodules_with_local_work` says origin has the tag "on a tag that peels", which misstates the lightweight-tag exception lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the rewritten header comment above `list_submodules_with_local_work` in skills/audit-git-checkouts/scripts/audit-checkouts.sh. I checked it against the code in `submodule_holds_local_work` and the matching prose in references/removal-gates.md. > > What the subject says: "no tag on a non-commit or on a commit no remote-tracking ref holds unless origin has that tag on the same object, or, for a lightweight tag, on a tag that peels to the same commit." Grammatically the elided verb gives "origin has that tag ... on a tag that peels to the same commit". That describes origin's tag pointing at another tag object, which is not the rule. > > What the code does: `[ "$tag_type" = commit ] && grep -F -x -q -- "$tag_object $tag_ref^{}" <<<"$remote_tags" && continue`. A local lightweight tag passes when origin's tag of the same name (an annotated tag) peels to that commit. The inline comment at that line says it correctly: "A lightweight tag holds nothing beyond its commit, which origin's tag of that name may peel to." removal-gates.md also says it correctly: "for a lightweight tag, a tag of that name on its commit." > > Why it matters: this header is the summary a maintainer reads to learn which submodule tags block removal. The garbled clause invites a wrong mental model, for example that origin needs a tag-of-a-tag. It also disagrees in wording with the two other statements of the same rule. Skill guidance: make claims precise and keep one term for one concept. > > Proposed correction: "... unless origin has that tag on the same object or, for a lightweight tag, has a tag of that name that peels to its commit." This keeps both exceptions and matches the code and the reference doc. claim `01M3CZ4DJ200HNW38FDBT9H6GK` of review `01M3CZ037RYN4N2VBVBYGFJVAP`
Author
Owner

Fixed in 3f5cc8c. The header now reads "unless origin has that tag on the same object or, for a lightweight tag, has a tag of that name that peels to its commit", which matches the ^{} check in submodule_holds_local_work.

<!-- gh-feedback:reply-to:89369 --> Fixed in 3f5cc8c. The header now reads "unless origin has that tag on the same object or, for a lightweight tag, has a tag of that name that peels to its commit", which matches the `^{}` check in `submodule_holds_local_work`.
jercik marked this conversation as resolved
docs(audit-git-checkouts): name nested unchecked submodules correctly
All checks were successful
commit-msg / commitlint (pull_request) Successful in 23s
Node tests / node:test (pull_request) Successful in 1m47s
Review / Review (pull_request_target) Successful in 3m31s
3f5cc8cad0
A nested submodule absorbed into its parent is listed as
`dep/modules/inner`, so the restore lookup now takes the name after the
last `/modules/` and runs in the enclosing submodule. The removal header
now states the lightweight-tag exception the way the check applies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -54,3 +54,3 @@
| `judgment/sparse-checkout` | Keep the cone's `skip-worktree` bits: clearing them turns absent files into apparent deletions. Confirm the cone with `git sparse-checkout list`, verify no file outside it is materialized, then remove manually once every other gate holds. |
| `judgment/precious-ignored-files` | Leave `preciousPaths` in place until the owner disposes of them or authorizes their deletion, then rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, or delete the directory, and rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. To restore, work in the superproject whose `.gitmodules` names the submodule: the worktree for a top-level entry, the enclosing submodule's checkout for a nested one. There, `git config -f .gitmodules submodule.<sub>.path` prints the checkout path to pass to `git submodule update --init --`. `<sub>` is the entry's text after its last `/modules/`, or the whole entry when it has none. |

low — Restore rule for (not checked out) entries misparses top-level submodule names that contain /modules/
lens general-bug · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the new judgment/submodule-local-work restore procedure in references/removal-gates.md, and the code that produces the entries it parses: list_unchecked_submodule_git_dirs / list_submodule_git_dirs_without_checkout in scripts/audit-checkouts.sh.

Mechanism: list_submodule_git_dirs_without_checkout runs find "$modules_path" -name HEAD -type f, keeps each parent directory that has objects/, and prints ${candidate#"$modules_path"/}. A top-level submodule's Git directory lives at <modules>/<name>, and Git's default submodule name is its path. So any top-level submodule whose path contains a modules component, e.g. web/modules/contrib/foo (Drupal layout) or infra/modules/vpc (Terraform layout), is listed as web/modules/contrib/foo (not checked out). The new doc text tells the reader to treat an entry with /modules/ as nested ("the enclosing submodule's checkout for a nested one") and to take <sub> as the text after the last /modules/, which gives contrib/foo. There is no enclosing web submodule, and .gitmodules has no submodule.contrib/foo section.

Reproduction (git, run in /tmp): I created a superproject, ran git submodule add ../up web/modules/contrib/foo, committed, and then ran git submodule deinit -f web/modules/contrib/foo. .gitmodules contains [submodule "web/modules/contrib/foo"]. find .git/modules -name HEAD -type f lists .git/modules/web/modules/contrib/foo/HEAD, which is the path the driver prints relative to modules. git config -f .gitmodules submodule.contrib/foo.path printed nothing and exited 1.

Impact: for these repositories the documented restore step fails, so the operator cannot restore the checkout by following the procedure. It fails loudly (empty output, exit 1) rather than touching the wrong submodule, so nothing is lost; that is why I rated it low. The underlying cause is that the entry format cannot tell a nested absorbed submodule (dep/modules/inner) from a top-level name containing /modules/. Two possible fixes: first look up the whole entry as a name in the worktree's .gitmodules and fall back to splitting only if that finds nothing, or have the driver print the checkout path or name alongside each entry. I did not run the full driver end to end; the listed entry text is inferred from the find/strip code above together with the observed Git directory layout.

claim 01M3CZFXT1DXEPSTTEP19JRA14 of review 01M3CZC2E4JPWJ8ZW154C54E3W

<!-- review:claim:01M3CZFXT1DXEPSTTEP19JRA14 --> **low** — Restore rule for `(not checked out)` entries misparses top-level submodule names that contain `/modules/` lens `general-bug` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the new `judgment/submodule-local-work` restore procedure in references/removal-gates.md, and the code that produces the entries it parses: `list_unchecked_submodule_git_dirs` / `list_submodule_git_dirs_without_checkout` in scripts/audit-checkouts.sh. > > Mechanism: `list_submodule_git_dirs_without_checkout` runs `find "$modules_path" -name HEAD -type f`, keeps each parent directory that has `objects/`, and prints `${candidate#"$modules_path"/}`. A top-level submodule's Git directory lives at `<modules>/<name>`, and Git's default submodule name is its path. So any top-level submodule whose path contains a `modules` component, e.g. `web/modules/contrib/foo` (Drupal layout) or `infra/modules/vpc` (Terraform layout), is listed as `web/modules/contrib/foo (not checked out)`. The new doc text tells the reader to treat an entry with `/modules/` as nested ("the enclosing submodule's checkout for a nested one") and to take `<sub>` as the text after the last `/modules/`, which gives `contrib/foo`. There is no enclosing `web` submodule, and `.gitmodules` has no `submodule.contrib/foo` section. > > Reproduction (git, run in /tmp): I created a superproject, ran `git submodule add ../up web/modules/contrib/foo`, committed, and then ran `git submodule deinit -f web/modules/contrib/foo`. `.gitmodules` contains `[submodule "web/modules/contrib/foo"]`. `find .git/modules -name HEAD -type f` lists `.git/modules/web/modules/contrib/foo/HEAD`, which is the path the driver prints relative to `modules`. `git config -f .gitmodules submodule.contrib/foo.path` printed nothing and exited 1. > > Impact: for these repositories the documented restore step fails, so the operator cannot restore the checkout by following the procedure. It fails loudly (empty output, exit 1) rather than touching the wrong submodule, so nothing is lost; that is why I rated it low. The underlying cause is that the entry format cannot tell a nested absorbed submodule (`dep/modules/inner`) from a top-level name containing `/modules/`. Two possible fixes: first look up the whole entry as a name in the worktree's `.gitmodules` and fall back to splitting only if that finds nothing, or have the driver print the checkout path or name alongside each entry. I did not run the full driver end to end; the listed entry text is inferred from the `find`/strip code above together with the observed Git directory layout. claim `01M3CZFXT1DXEPSTTEP19JRA14` of review `01M3CZC2E4JPWJ8ZW154C54E3W`
Author
Owner

Fixed in 7f1aba5. I reproduced it with web/modules/contrib/foo, which the driver lists under that whole name. The restore step now looks up the whole entry in the worktree's .gitmodules first, and splits only at a /modules/ whose left part the current .gitmodules names, repeating inside that checkout. Walking the text on a worktree holding both web/modules/contrib/foo and dep/modules/inner restored each one.

<!-- gh-feedback:reply-to:89395 --> Fixed in 7f1aba5. I reproduced it with `web/modules/contrib/foo`, which the driver lists under that whole name. The restore step now looks up the whole entry in the worktree's `.gitmodules` first, and splits only at a `/modules/` whose left part the current `.gitmodules` names, repeating inside that checkout. Walking the text on a worktree holding both `web/modules/contrib/foo` and `dep/modules/inner` restored each one.
jercik marked this conversation as resolved
docs(audit-git-checkouts): look up a whole unchecked entry before splitting it
Some checks failed
commit-msg / commitlint (pull_request) Successful in 23s
Node tests / node:test (pull_request) Successful in 1m50s
Review / Review (pull_request_target) Failing after 2m20s
7f1aba515c
A top-level submodule name can contain `/modules/`, such as
`web/modules/contrib/foo`, so splitting at the last `/modules/`
misread it. The restore step now looks up the whole entry first and
splits only at a `/modules/` whose left part `.gitmodules` names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -54,3 +54,3 @@
| `judgment/sparse-checkout` | Keep the cone's `skip-worktree` bits: clearing them turns absent files into apparent deletions. Confirm the cone with `git sparse-checkout list`, verify no file outside it is materialized, then remove manually once every other gate holds. |
| `judgment/precious-ignored-files` | Leave `preciousPaths` in place until the owner disposes of them or authorizes their deletion, then rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, or delete the directory, and rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry, or, for a worktree-relative `<checkout>/.git/modules/<rest>` entry, in `<checkout>` with `<rest>`. If `git config -f .gitmodules submodule.<sub>.path` prints a path, run `git submodule update --init -- <that path>` there. Otherwise `<sub>` is `<outer>/modules/<rest>` for a submodule `<outer>` that this `.gitmodules` names: move into the checkout its `submodule.<outer>.path` names and repeat with `<rest>`. A top-level name can itself contain `/modules/`, which is why the whole entry is looked up first. |

high — Restoring a deinitialized submodule can strand its unique HEAD commit
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

I read the judgment/submodule-local-work recovery instructions, the driver's unchecked-Git-directory gate, and the submodule removal tests. The new recovery text tells the reader to inspect <modules>/<name> and then restore it with git submodule update --init -- <path> when the owner wants the checkout back. In an isolated local Git fixture, I made an unreferenced commit on detached HEAD in a submodule, ran git submodule deinit --force, then ran that update command. The Git directory's HEAD held the unique commit before update; afterward HEAD pointed at the recorded Gitlink commit, and git for-each-ref --contains <unique> returned no ref. The commit remained reachable only through recovery mechanisms such as a reflog and could later be pruned. The guide's log ... HEAD --branches --tags --not --remotes inspection can reveal this case, but it gives no step to preserve the commit before update. State that a unique HEAD must first be put on a durable ref or pushed according to the owner's decision, then restore the checkout and rerun. This preserves the guide's inspect-and-decide workflow while preventing a keep-work decision from silently stranding the work. A case where Git preserves the detached HEAD as an ordinary ref during this update would refute the finding; the local reproduction showed no such ref.

claim 01M3D39C3EA0J25PMG97MRRFPD of review 01M3CZN1ZY9NEJB79N679PPV0A

<!-- review:claim:01M3D39C3EA0J25PMG97MRRFPD --> **high** — Restoring a deinitialized submodule can strand its unique HEAD commit lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > I read the `judgment/submodule-local-work` recovery instructions, the driver's unchecked-Git-directory gate, and the submodule removal tests. The new recovery text tells the reader to inspect `<modules>/<name>` and then restore it with `git submodule update --init -- <path>` when the owner wants the checkout back. In an isolated local Git fixture, I made an unreferenced commit on detached HEAD in a submodule, ran `git submodule deinit --force`, then ran that update command. The Git directory's HEAD held the unique commit before update; afterward HEAD pointed at the recorded Gitlink commit, and `git for-each-ref --contains <unique>` returned no ref. The commit remained reachable only through recovery mechanisms such as a reflog and could later be pruned. The guide's `log ... HEAD --branches --tags --not --remotes` inspection can reveal this case, but it gives no step to preserve the commit before update. State that a unique HEAD must first be put on a durable ref or pushed according to the owner's decision, then restore the checkout and rerun. This preserves the guide's inspect-and-decide workflow while preventing a keep-work decision from silently stranding the work. A case where Git preserves the detached HEAD as an ordinary ref during this update would refute the finding; the local reproduction showed no such ref. claim `01M3D39C3EA0J25PMG97MRRFPD` of review `01M3CZN1ZY9NEJB79N679PPV0A`

low — Nested submodule restore recipe fails when its parent is deinitialized
lens general-bug · arm default · tally 1 valid / 0 invalid / 0 uncertain

I examined the recovery recipe and list_unchecked_submodule_git_dirs. In a local Git fixture with outer_custom at deps/outer and inner_custom beneath it, I deinitialized both. The detector reported outer_custom and outer_custom/modules/inner_custom, so the nested entry is a real output of this code. Applying the new recipe to that nested entry resolves outer_custom in the top-level .gitmodules and moves into deps/outer, but that checkout has no .git or .gitmodules after deinit; git -C deps/outer config -f .gitmodules submodule.inner_custom.path exited 1. The stated next lookup cannot restore inner_custom in this state. Instruct the reader to restore an absent parent checkout at its recorded Gitlink before descending. The only unresolved case would be a way to perform that nested lookup without first restoring the parent; the documented commands do not provide one.

claim 01M3D3EWCPE7ES7S1JBEZCEXMS of review 01M3CZN1ZY9NEJB79N679PPV0A

<!-- review:claim:01M3D3EWCPE7ES7S1JBEZCEXMS --> **low** — Nested submodule restore recipe fails when its parent is deinitialized lens `general-bug` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > I examined the recovery recipe and list_unchecked_submodule_git_dirs. In a local Git fixture with outer_custom at deps/outer and inner_custom beneath it, I deinitialized both. The detector reported outer_custom and outer_custom/modules/inner_custom, so the nested entry is a real output of this code. Applying the new recipe to that nested entry resolves outer_custom in the top-level .gitmodules and moves into deps/outer, but that checkout has no .git or .gitmodules after deinit; `git -C deps/outer config -f .gitmodules submodule.inner_custom.path` exited 1. The stated next lookup cannot restore inner_custom in this state. Instruct the reader to restore an absent parent checkout at its recorded Gitlink before descending. The only unresolved case would be a way to perform that nested lookup without first restoring the parent; the documented commands do not provide one. claim `01M3D3EWCPE7ES7S1JBEZCEXMS` of review `01M3CZN1ZY9NEJB79N679PPV0A`
Author
Owner

Fixed in fae7bf6. I reproduced it: after deinit and git submodule update --init, no ref held the detached commit, and the gate's log HEAD --branches --tags --not --remotes check came back empty, so a rerun would have allowed removal. The row now says to put such a commit on a branch with git --git-dir <modules>/<name> branch <branch> HEAD before restoring. With that step, the restored submodule still lists the commit until it is pushed.

<!-- gh-feedback:reply-to:90128 --> Fixed in fae7bf6. I reproduced it: after `deinit` and `git submodule update --init`, no ref held the detached commit, and the gate's `log HEAD --branches --tags --not --remotes` check came back empty, so a rerun would have allowed removal. The row now says to put such a commit on a branch with `git --git-dir <modules>/<name> branch <branch> HEAD` before restoring. With that step, the restored submodule still lists the commit until it is pushed.
jercik marked this conversation as resolved
docs(audit-git-checkouts): keep a detached HEAD before restoring a submodule checkout
All checks were successful
commit-msg / commitlint (pull_request) Successful in 23s
Node tests / node:test (pull_request) Successful in 1m19s
Review / Review (pull_request_target) Successful in 4m22s
fae7bf6122
`git submodule update --init` checks out the recorded commit, so a HEAD
commit that no ref holds became unreachable and the rerun passed
without seeing it. The restore step now puts that commit on a branch
first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Replying to review comment #90129

This is real, and I'm deferring it to a follow-up PR. The user asked for the round gate from round 3 on, and this is a wording gap rather than a severe bug: the recipe fails loudly (git config exits 1) instead of touching the wrong submodule.

Follow-up fix, in skills/audit-git-checkouts/references/removal-gates.md, judgment/submodule-local-work row: before moving into <outer>'s checkout, restore that checkout first when it is absent (git submodule update --init -- <outer path>).

> Replying to review comment #90129 This is real, and I'm deferring it to a follow-up PR. The user asked for the round gate from round 3 on, and this is a wording gap rather than a severe bug: the recipe fails loudly (`git config` exits 1) instead of touching the wrong submodule. Follow-up fix, in `skills/audit-git-checkouts/references/removal-gates.md`, `judgment/submodule-local-work` row: before moving into `<outer>`'s checkout, restore that checkout first when it is absent (`git submodule update --init -- <outer path>`).
@ -54,3 +54,3 @@
| `judgment/sparse-checkout` | Keep the cone's `skip-worktree` bits: clearing them turns absent files into apparent deletions. Confirm the cone with `git sparse-checkout list`, verify no file outside it is materialized, then remove manually once every other gate holds. |
| `judgment/precious-ignored-files` | Leave `preciousPaths` in place until the owner disposes of them or authorizes their deletion, then rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, or delete the directory, and rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, so first put a HEAD commit that no ref holds on a branch, or the rerun no longer sees it: `git --git-dir <modules>/<name> branch <branch> HEAD`. To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry, or, for a worktree-relative `<checkout>/.git/modules/<rest>` entry, in `<checkout>` with `<rest>`. If `git config -f .gitmodules submodule.<sub>.path` prints a path, run `git submodule update --init -- <that path>` there. Otherwise `<sub>` is `<outer>/modules/<rest>` for a submodule `<outer>` that this `.gitmodules` names: move into the checkout its `submodule.<outer>.path` names and repeat with `<rest>`. A top-level name can itself contain `/modules/`, which is why the whole entry is looked up first. |

medium — The data-loss-sensitive restore procedure for (not checked out) entries is a recursive, ordered procedure packed into one table cell, with its steps out of order and placeholders that break for worktree-relative entries
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the judgment/submodule-local-work row of the "Resolve a kept worktree" table in removal-gates.md, which this change grew from one restore sentence into a full procedure; the <name> (not checked out) bullet higher in the same file; and list_unchecked_submodule_git_dirs and the removal gate in scripts/audit-checkouts.sh, which emit these entries.

What the cell says now, in one prose paragraph inside a table cell: show the owner the log and stash; "Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, so first put a HEAD commit that no ref holds on a branch, or the rerun no longer sees it: git --git-dir <modules>/<name> branch <branch> HEAD. To restore, find the superproject whose .gitmodules names the submodule. Start in the worktree with <sub> set to the entry, or, for a worktree-relative <checkout>/.git/modules/<rest> entry, in <checkout> with <rest>. If git config -f .gitmodules submodule.<sub>.path prints a path, run git submodule update --init -- <that path> there. Otherwise <sub> is <outer>/modules/<rest> ... move into the checkout its submodule.<outer>.path names and repeat with <rest>."

What goes wrong for the reader (an agent recovering a submodule's local work):

  1. The order is scrambled. The text says "act ... and rerun", then goes back with "first" to a branch step, then gives the lookup, and ends with the actual git submodule update. The branch step is what keeps a commit from being lost, and it sits in the middle of a clause. The skill's standard calls for numbered steps where the sequence is real, and for keeping steps whose order affects the result. A table cell can't hold a numbered list, so this procedure needs its own subsection.
  2. The stated reason understates what's at stake. "the rerun no longer sees it" names a symptom. The consequence is that git submodule update detaches HEAD at the recorded commit, the rerun's submodule gate passes, and removal then deletes the unreferenced commit. The standard says to give a reason when it tells the agent what's at stake.
  3. The placeholders break for one of the entry forms. Earlier, the cell says to inspect "<modules>/<name> (or the listed worktree-relative path)". The new git --git-dir <modules>/<name> branch command drops that alternative. An agent handling an entry like dep/.git/modules/inner would build <modules>/dep/.git/modules/inner, which is a path that doesn't exist. The cell also brings in <sub>, <rest> (bound two different ways), <outer>, <checkout>, <that path> and <branch>. "<sub> set to the entry" doesn't say whether the (not checked out) suffix is part of the entry. The standard asks for one term per concept.

Proposed correction: keep one line in the table: "For a (not checked out) entry, follow Restore or delete an unchecked submodule Git directory below." Then add that subsection:
"Let <dir> be <modules>/<name>, or the listed worktree-relative path; drop the (not checked out) suffix.

  1. Before anything checks it out, show the owner git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes and … stash list (--work-tree overrides the deleted checkout its config names).
  2. If the owner chooses deletion, delete <dir> and rerun.
  3. Restoring (only while a Gitlink remains) detaches HEAD at the recorded commit, so a HEAD commit that no ref holds would pass the rerun's gate and then be deleted with the worktree. Branch it first: git --git-dir <dir> branch <branch> HEAD.
  4. Set <sub> to <name> and start in the worktree, or, for <checkout>/.git/modules/<rest>, start in <checkout> with <sub> set to <rest>. If git config -f .gitmodules submodule.<sub>.path prints a path, run git submodule update --init -- <that path> there. Otherwise split <sub> into <outer>/modules/<inner>, where this .gitmodules names <outer>, move into submodule.<outer>.path, set <sub> to <inner>, and repeat this step. Look up the whole name first, because a top-level name can contain /modules/.
  5. Rerun the driver."
    The rewrite keeps every command, the --work-tree note, the /modules/ rationale and the Gitlink precondition. It puts the steps in execution order, defines one directory placeholder that works for both entry forms, and states the real risk.

Evidence and gaps: this is based on reading the text and the code, not on running the procedure. The loss scenario in point 2 follows from the cell's own statement that restoring checks out the recorded commit, combined with the gate in submodule_holds_local_work, which checks only HEAD, branches, tags, stash, status and worktrees, not reflogs.

claim 01M3D3SPGHYGD4X822NGTQGQW9 of review 01M3D3MJBNFP4640GZQBAME1FD

<!-- review:claim:01M3D3SPGHYGD4X822NGTQGQW9 --> **medium** — The data-loss-sensitive restore procedure for `(not checked out)` entries is a recursive, ordered procedure packed into one table cell, with its steps out of order and placeholders that break for worktree-relative entries lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the `judgment/submodule-local-work` row of the "Resolve a kept worktree" table in removal-gates.md, which this change grew from one restore sentence into a full procedure; the `<name> (not checked out)` bullet higher in the same file; and `list_unchecked_submodule_git_dirs` and the removal gate in scripts/audit-checkouts.sh, which emit these entries. > > What the cell says now, in one prose paragraph inside a table cell: show the owner the log and stash; "Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, so first put a HEAD commit that no ref holds on a branch, or the rerun no longer sees it: `git --git-dir <modules>/<name> branch <branch> HEAD`. To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry, or, for a worktree-relative `<checkout>/.git/modules/<rest>` entry, in `<checkout>` with `<rest>`. If `git config -f .gitmodules submodule.<sub>.path` prints a path, run `git submodule update --init -- <that path>` there. Otherwise `<sub>` is `<outer>/modules/<rest>` ... move into the checkout its `submodule.<outer>.path` names and repeat with `<rest>`." > > What goes wrong for the reader (an agent recovering a submodule's local work): > 1. The order is scrambled. The text says "act ... and rerun", then goes back with "first" to a branch step, then gives the lookup, and ends with the actual `git submodule update`. The branch step is what keeps a commit from being lost, and it sits in the middle of a clause. The skill's standard calls for numbered steps where the sequence is real, and for keeping steps whose order affects the result. A table cell can't hold a numbered list, so this procedure needs its own subsection. > 2. The stated reason understates what's at stake. "the rerun no longer sees it" names a symptom. The consequence is that `git submodule update` detaches HEAD at the recorded commit, the rerun's submodule gate passes, and removal then deletes the unreferenced commit. The standard says to give a reason when it tells the agent what's at stake. > 3. The placeholders break for one of the entry forms. Earlier, the cell says to inspect "`<modules>/<name>` (or the listed worktree-relative path)". The new `git --git-dir <modules>/<name> branch` command drops that alternative. An agent handling an entry like `dep/.git/modules/inner` would build `<modules>/dep/.git/modules/inner`, which is a path that doesn't exist. The cell also brings in `<sub>`, `<rest>` (bound two different ways), `<outer>`, `<checkout>`, `<that path>` and `<branch>`. "`<sub>` set to the entry" doesn't say whether the ` (not checked out)` suffix is part of the entry. The standard asks for one term per concept. > > Proposed correction: keep one line in the table: "For a `(not checked out)` entry, follow [Restore or delete an unchecked submodule Git directory](#...) below." Then add that subsection: > "Let `<dir>` be `<modules>/<name>`, or the listed worktree-relative path; drop the ` (not checked out)` suffix. > 1. Before anything checks it out, show the owner `git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). > 2. If the owner chooses deletion, delete `<dir>` and rerun. > 3. Restoring (only while a Gitlink remains) detaches HEAD at the recorded commit, so a HEAD commit that no ref holds would pass the rerun's gate and then be deleted with the worktree. Branch it first: `git --git-dir <dir> branch <branch> HEAD`. > 4. Set `<sub>` to `<name>` and start in the worktree, or, for `<checkout>/.git/modules/<rest>`, start in `<checkout>` with `<sub>` set to `<rest>`. If `git config -f .gitmodules submodule.<sub>.path` prints a path, run `git submodule update --init -- <that path>` there. Otherwise split `<sub>` into `<outer>/modules/<inner>`, where this `.gitmodules` names `<outer>`, move into `submodule.<outer>.path`, set `<sub>` to `<inner>`, and repeat this step. Look up the whole name first, because a top-level name can contain `/modules/`. > 5. Rerun the driver." > The rewrite keeps every command, the `--work-tree` note, the `/modules/` rationale and the Gitlink precondition. It puts the steps in execution order, defines one directory placeholder that works for both entry forms, and states the real risk. > > Evidence and gaps: this is based on reading the text and the code, not on running the procedure. The loss scenario in point 2 follows from the cell's own statement that restoring checks out the recorded commit, combined with the gate in `submodule_holds_local_work`, which checks only HEAD, branches, tags, stash, status and worktrees, not reflogs. claim `01M3D3SPGHYGD4X822NGTQGQW9` of review `01M3D3MJBNFP4640GZQBAME1FD`

low — New restore step tells the agent to create a branch, which SKILL.md's user-work rule forbids outright
lens general-bug · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the rewritten judgment/submodule-local-work row in references/removal-gates.md; the top-level rules in skills/audit-git-checkouts/SKILL.md; and the removal gate in scripts/audit-checkouts.sh (submodule_holds_local_work, list_unchecked_submodule_git_dirs).

What the subject says: this change adds a step to the (not checked out) remediation. It tells the agent to create a new branch in the submodule Git directory before restoring the checkout: git --git-dir <modules>/<name> branch <branch> HEAD. SKILL.md's "Treat user work as untouchable" section says, with no exception: "Never create branches or refs to "preserve" a checkout". SKILL.md is the entry point, and it sends the agent to removal-gates.md for exactly this case. So the agent now gets two contradictory instructions for the same situation.

Why it matters (static reasoning, not run): the new step is what keeps the commit safe. git submodule update --init detaches the submodule at the recorded Gitlink. After that, the old HEAD commit is reachable only from the reflog. On the rerun, submodule_holds_local_work checks for-each-ref --contains HEAD refs/remotes, rev-list --branches --not --remotes and the tags, but not the reflog. So if the recorded commit is on a remote-tracking ref, every gate passes. The driver then runs git worktree remove --force, which deletes the submodule Git directory and the stranded commit with it. An agent that follows SKILL.md's absolute rule and skips the branch step therefore gets the commit silently deleted on the rerun. An agent that follows the reference breaks a rule SKILL.md states as non-negotiable.

Fix: add an explicit exception to the SKILL.md rule for this case (for example, "except the branch removal-gates.md asks for before restoring a submodule checkout, which keeps the commit visible to the gate rather than clearing it"). Or reword the reference step so it doesn't create a new ref.

What would refute this: a reading of SKILL.md's "preserve" (in quotes) as covering only refs made to clear a gate. The branch made here does not clear the gate, because an unpushed branch commit still blocks removal. Even so, the SKILL.md rule states no such limit. I did not run git to reproduce the post-restore gate pass; jq is not installed in this sandbox, so I could not run the driver.

claim 01M3D3TKE2M790D15FHW7P5HD6 of review 01M3D3MJBNFP4640GZQBAME1FD

<!-- review:claim:01M3D3TKE2M790D15FHW7P5HD6 --> **low** — New restore step tells the agent to create a branch, which SKILL.md's user-work rule forbids outright lens `general-bug` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the rewritten `judgment/submodule-local-work` row in references/removal-gates.md; the top-level rules in skills/audit-git-checkouts/SKILL.md; and the removal gate in scripts/audit-checkouts.sh (`submodule_holds_local_work`, `list_unchecked_submodule_git_dirs`). > > What the subject says: this change adds a step to the `(not checked out)` remediation. It tells the agent to create a new branch in the submodule Git directory before restoring the checkout: `git --git-dir <modules>/<name> branch <branch> HEAD`. SKILL.md's "Treat user work as untouchable" section says, with no exception: "Never create branches or refs to \"preserve\" a checkout". SKILL.md is the entry point, and it sends the agent to removal-gates.md for exactly this case. So the agent now gets two contradictory instructions for the same situation. > > Why it matters (static reasoning, not run): the new step is what keeps the commit safe. `git submodule update --init` detaches the submodule at the recorded Gitlink. After that, the old HEAD commit is reachable only from the reflog. On the rerun, `submodule_holds_local_work` checks `for-each-ref --contains HEAD refs/remotes`, `rev-list --branches --not --remotes` and the tags, but not the reflog. So if the recorded commit is on a remote-tracking ref, every gate passes. The driver then runs `git worktree remove --force`, which deletes the submodule Git directory and the stranded commit with it. An agent that follows SKILL.md's absolute rule and skips the branch step therefore gets the commit silently deleted on the rerun. An agent that follows the reference breaks a rule SKILL.md states as non-negotiable. > > Fix: add an explicit exception to the SKILL.md rule for this case (for example, "except the branch removal-gates.md asks for before restoring a submodule checkout, which keeps the commit visible to the gate rather than clearing it"). Or reword the reference step so it doesn't create a new ref. > > What would refute this: a reading of SKILL.md's "preserve" (in quotes) as covering only refs made to clear a gate. The branch made here does not clear the gate, because an unpushed branch commit still blocks removal. Even so, the SKILL.md rule states no such limit. I did not run git to reproduce the post-restore gate pass; jq is not installed in this sandbox, so I could not run the driver. claim `01M3D3TKE2M790D15FHW7P5HD6` of review `01M3D3MJBNFP4640GZQBAME1FD`
Author
Owner

Fixed in 972ab44. The conflict was real: an agent obeying SKILL.md's "Never create branches or refs" rule would skip the branch step and lose the commit. The restore step no longer creates a ref. It records HEAD with git --git-dir <dir> rev-parse HEAD and checks that commit out again after git submodule update --init. On a reproduction, the restored submodule sat on the unpushed commit, no ref held it, and the superproject showed M dep, so the rerun keeps the worktree.

<!-- gh-feedback:reply-to:90255 --> Fixed in 972ab44. The conflict was real: an agent obeying SKILL.md's "Never create branches or refs" rule would skip the branch step and lose the commit. The restore step no longer creates a ref. It records HEAD with `git --git-dir <dir> rev-parse HEAD` and checks that commit out again after `git submodule update --init`. On a reproduction, the restored submodule sat on the unpushed commit, no ref held it, and the superproject showed ` M dep`, so the rerun keeps the worktree.
jercik marked this conversation as resolved
docs(audit-git-checkouts): restore a detached submodule HEAD without creating a ref
All checks were successful
commit-msg / commitlint (pull_request) Successful in 23s
Node tests / node:test (pull_request) Successful in 1m19s
Review / Review (pull_request_target) Successful in 4m7s
972ab4481c
The branch step contradicted SKILL.md's rule against creating refs to
preserve a checkout, and an agent obeying that rule would skip it and
lose the commit. The restore step now records HEAD first and checks it
out again after `git submodule update --init`, which keeps the
worktree blocked without a new ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Replying to review comment #90254

Round 4 of this PR only takes clear, severe bugs, so I'm deferring the restructure to a follow-up PR. Two of its three points are already covered by 972ab44. The step now states the stakes ("removal would then delete it"). Its command also uses <dir>, defined as <modules>/<name> or the listed path, so it works for worktree-relative entries.

Follow-up fix, in skills/audit-git-checkouts/references/removal-gates.md: shrink the judgment/submodule-local-work row's (not checked out) text to a link, and move the procedure into its own numbered subsection. Order it: define <dir> without the (not checked out) suffix, inspect, delete or record HEAD, restore by the .gitmodules lookup, check the recorded HEAD out again, rerun. That same subsection should carry #90129's fix (restore an absent parent checkout before descending).

> Replying to review comment #90254 Round 4 of this PR only takes clear, severe bugs, so I'm deferring the restructure to a follow-up PR. Two of its three points are already covered by 972ab44. The step now states the stakes ("removal would then delete it"). Its command also uses `<dir>`, defined as `<modules>/<name>` or the listed path, so it works for worktree-relative entries. Follow-up fix, in `skills/audit-git-checkouts/references/removal-gates.md`: shrink the `judgment/submodule-local-work` row's `(not checked out)` text to a link, and move the procedure into its own numbered subsection. Order it: define `<dir>` without the ` (not checked out)` suffix, inspect, delete or record HEAD, restore by the `.gitmodules` lookup, check the recorded HEAD out again, rerun. That same subsection should carry #90129's fix (restore an absent parent checkout before descending).
@ -54,3 +54,3 @@
| `judgment/sparse-checkout` | Keep the cone's `skip-worktree` bits: clearing them turns absent files into apparent deletions. Confirm the cone with `git sparse-checkout list`, verify no file outside it is materialized, then remove manually once every other gate holds. |
| `judgment/precious-ignored-files` | Leave `preciousPaths` in place until the owner disposes of them or authorizes their deletion, then rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, or delete the directory, and rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it. So first record that HEAD with `git --git-dir <dir> rev-parse HEAD`, where `<dir>` is `<modules>/<name>` or the listed path, and after restoring, check it out again in the restored submodule; the rerun then keeps the worktree until that commit is pushed. To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry, or, for a worktree-relative `<checkout>/.git/modules/<rest>` entry, in `<checkout>` with `<rest>`. If `git config -f .gitmodules submodule.<sub>.path` prints a path, run `git submodule update --init -- <that path>` there. Otherwise `<sub>` is `<outer>/modules/<rest>` for a submodule `<outer>` that this `.gitmodules` names: move into the checkout its `submodule.<outer>.path` names and repeat with `<rest>`. A top-level name can itself contain `/modules/`, which is why the whole entry is looked up first. |

medium — The steps that prevent data loss when restoring a (not checked out) submodule are written out of order in one table cell
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

Examined: the judgment/submodule-local-work row of the "Resolve a kept worktree" table in references/removal-gates.md, which this change expanded, and the surrounding section.

What the prose does: one table cell now holds a multi-step procedure whose order decides whether a commit is lost. The steps appear in this order: (1) "act on their decision and rerun: delete the directory, or … restore the checkout"; (2) the hazard: restoring "leaves a HEAD commit that no ref holds unreachable, and removal would then delete it"; (3) "So first record that HEAD with git --git-dir <dir> rev-parse HEAD"; (4) "after restoring, check it out again"; (5) only then "To restore, find the superproject whose .gitmodules names the submodule…", followed by a recursive lookup with <sub>, <rest>, and <outer>. The step that must happen first, recording HEAD, comes third. The restore mechanics come after the instruction to re-checkout what they produce. "Rerun" appears at the start, although it is the last action.

Why it matters: under the installed writing-for-agents standard, numbered steps are for "real sequence", and a writer should "Make the decisive constraint prominent, and spend detail where a plausible mistake would derail the task." Here the sequence is real and the mistake is irreversible. An agent that reads "restore the checkout" and jumps to the git submodule update --init recipe at the end of the cell loses the detached HEAD. A dense table cell also cannot hold a numbered list, so the ordering is carried only by "first" and "after".

Proposed correction: shorten the cell to the decision and a pointer, for example: "For a (not checked out) entry, follow Restore or delete an unchecked submodule Git directory below." Then add that subsection after the table as numbered steps: 1. Show the owner what <dir> holds (log and stash commands). 2. To delete, remove <dir> and rerun. 3. To restore while a Gitlink remains, record git --git-dir <dir> rev-parse HEAD first. 4. Find the owning superproject (the <sub>/<outer> lookup) and run git submodule update --init -- <path>. 5. Check out the recorded HEAD in the restored submodule. 6. Rerun. This keeps every fact the cell has now, including the note on /modules/ in names, and puts them in execution order.

This finding is based on reading the text only. I did not observe an agent misapplying it.

claim 01M3D44DWGPYAXVH9PFHCBAYX3 of review 01M3D3ZR53XHPZDB0WKN1BF301

<!-- review:claim:01M3D44DWGPYAXVH9PFHCBAYX3 --> **medium** — The steps that prevent data loss when restoring a `(not checked out)` submodule are written out of order in one table cell lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > Examined: the `judgment/submodule-local-work` row of the "Resolve a kept worktree" table in references/removal-gates.md, which this change expanded, and the surrounding section. > > What the prose does: one table cell now holds a multi-step procedure whose order decides whether a commit is lost. The steps appear in this order: (1) "act on their decision and rerun: delete the directory, or … restore the checkout"; (2) the hazard: restoring "leaves a HEAD commit that no ref holds unreachable, and removal would then delete it"; (3) "So first record that HEAD with `git --git-dir <dir> rev-parse HEAD`"; (4) "after restoring, check it out again"; (5) only then "To restore, find the superproject whose `.gitmodules` names the submodule…", followed by a recursive lookup with `<sub>`, `<rest>`, and `<outer>`. The step that must happen first, recording HEAD, comes third. The restore mechanics come after the instruction to re-checkout what they produce. "Rerun" appears at the start, although it is the last action. > > Why it matters: under the installed writing-for-agents standard, numbered steps are for "real sequence", and a writer should "Make the decisive constraint prominent, and spend detail where a plausible mistake would derail the task." Here the sequence is real and the mistake is irreversible. An agent that reads "restore the checkout" and jumps to the `git submodule update --init` recipe at the end of the cell loses the detached HEAD. A dense table cell also cannot hold a numbered list, so the ordering is carried only by "first" and "after". > > Proposed correction: shorten the cell to the decision and a pointer, for example: "For a `(not checked out)` entry, follow [Restore or delete an unchecked submodule Git directory](#…) below." Then add that subsection after the table as numbered steps: 1. Show the owner what `<dir>` holds (log and stash commands). 2. To delete, remove `<dir>` and rerun. 3. To restore while a Gitlink remains, record `git --git-dir <dir> rev-parse HEAD` first. 4. Find the owning superproject (the `<sub>`/`<outer>` lookup) and run `git submodule update --init -- <path>`. 5. Check out the recorded HEAD in the restored submodule. 6. Rerun. This keeps every fact the cell has now, including the note on `/modules/` in names, and puts them in execution order. > > This finding is based on reading the text only. I did not observe an agent misapplying it. claim `01M3D44DWGPYAXVH9PFHCBAYX3` of review `01M3D3ZR53XHPZDB0WKN1BF301`

low — Restore guidance says the worktree is kept only "until that commit is pushed", but moving the restored submodule off its Gitlink makes the superproject dirty, so pushing never makes it removable
lens general-bug · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the new judgment/submodule-local-work resolution text in references/removal-gates.md, and decide_removal_outcome in scripts/audit-checkouts.sh.

The doc says to record the orphaned HEAD, run git submodule update --init (which checks out the recorded Gitlink commit), then check the saved HEAD out again in the restored submodule, and that "the rerun then keeps the worktree until that commit is pushed." That suggests the worktree is kept by the submodule gate (list_submodules_with_local_work in removal mode, where HEAD is not held by a remote-tracking ref), and that pushing the commit clears it.

In practice, once the submodule HEAD differs from the Gitlink recorded in the superproject's index, the superproject's git status --porcelain --untracked-files=normal lists the submodule as modified. decide_removal_outcome runs that live status check early and returns expected/dirty-working-tree whenever the output is non-empty, well before the submodule walk. I checked this with git 2.47.3: a superproject with submodule dep, plus one extra commit in dep, prints M dep from git status --porcelain --untracked-files=normal.

Impact: the worktree is still kept, so nothing is lost. But the stated exit condition is wrong. After the owner pushes the commit, every rerun still reports expected/dirty-working-tree, which the table describes as "Nothing to do ... Dirty trees are user work". The worktree is never removed until someone also moves the submodule back to the recorded commit or records the new Gitlink. An operator following the doc would expect the rerun to remove the worktree after the push, and would get a different, misleading outcome.

This is static tracing plus the one git status reproduction above. I did not run the full driver because jq is not installed in this sandbox. It would not hold if submodule.<name>.ignore or diff.ignoreSubmodules suppresses the submodule change in status. Suggested fix: say the rerun keeps the worktree as a dirty tree (the submodule no longer matches its Gitlink), and list what must happen before removal: push the commit and either commit the new Gitlink or return the submodule to the recorded commit.

claim 01M3D43RXWPNH0CXAP51XXY85J of review 01M3D3ZR53XHPZDB0WKN1BF301

<!-- review:claim:01M3D43RXWPNH0CXAP51XXY85J --> **low** — Restore guidance says the worktree is kept only "until that commit is pushed", but moving the restored submodule off its Gitlink makes the superproject dirty, so pushing never makes it removable lens `general-bug` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the new `judgment/submodule-local-work` resolution text in references/removal-gates.md, and `decide_removal_outcome` in scripts/audit-checkouts.sh. > > The doc says to record the orphaned HEAD, run `git submodule update --init` (which checks out the recorded Gitlink commit), then check the saved HEAD out again in the restored submodule, and that "the rerun then keeps the worktree until that commit is pushed." That suggests the worktree is kept by the submodule gate (`list_submodules_with_local_work` in removal mode, where HEAD is not held by a remote-tracking ref), and that pushing the commit clears it. > > In practice, once the submodule HEAD differs from the Gitlink recorded in the superproject's index, the superproject's `git status --porcelain --untracked-files=normal` lists the submodule as modified. `decide_removal_outcome` runs that live status check early and returns `expected/dirty-working-tree` whenever the output is non-empty, well before the submodule walk. I checked this with git 2.47.3: a superproject with submodule `dep`, plus one extra commit in `dep`, prints ` M dep` from `git status --porcelain --untracked-files=normal`. > > Impact: the worktree is still kept, so nothing is lost. But the stated exit condition is wrong. After the owner pushes the commit, every rerun still reports `expected/dirty-working-tree`, which the table describes as "Nothing to do ... Dirty trees are user work". The worktree is never removed until someone also moves the submodule back to the recorded commit or records the new Gitlink. An operator following the doc would expect the rerun to remove the worktree after the push, and would get a different, misleading outcome. > > This is static tracing plus the one git status reproduction above. I did not run the full driver because `jq` is not installed in this sandbox. It would not hold if `submodule.<name>.ignore` or `diff.ignoreSubmodules` suppresses the submodule change in status. Suggested fix: say the rerun keeps the worktree as a dirty tree (the submodule no longer matches its Gitlink), and list what must happen before removal: push the commit and either commit the new Gitlink or return the submodule to the recorded commit. claim `01M3D43RXWPNH0CXAP51XXY85J` of review `01M3D3ZR53XHPZDB0WKN1BF301`

low — The unchecked submodule Git directory has two placeholder names in one cell, and the inspection commands cover only one entry form
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

Examined: the judgment/submodule-local-work resolution cell in references/removal-gates.md, and the <name> (not checked out) bullet above it. That bullet says an entry is either a name relative to <modules> or a worktree-relative path such as dep/.git/modules/inner.

What the prose says: the cell first calls the directory "<modules>/<name> (or the listed worktree-relative path)". Its inspection commands then hard-code only the first form: git --git-dir <modules>/<name> --work-tree <modules>/<name> log … and … stash list. A few sentences later the change introduces a second placeholder for the same directory: "<dir>, where <dir> is <modules>/<name> or the listed path".

Why it matters: the writing-for-agents standard asks writers to "use one term for one concept". Two names for one directory make a reader check whether they differ. Also, for a worktree-relative entry like dep/.git/modules/inner, the literal log and stash commands point at a nonexistent <modules>/dep/.git/modules/inner. The agent has to work out the substitution that <dir> already expresses. Those commands are the inspection the owner decides from.

Proposed correction: define <dir> once, where the cell first names the directory. For example: "For a (not checked out) entry, let <dir> be <modules>/<name>, or the listed worktree-relative path; before anything checks it out, show the owner what it holds: git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes and … stash list." Then use <dir> in the later rev-parse HEAD step without redefining it. This covers both entry forms and removes the duplicate definition.

claim 01M3D4551HKYT8DTSCB980PKN0 of review 01M3D3ZR53XHPZDB0WKN1BF301

<!-- review:claim:01M3D4551HKYT8DTSCB980PKN0 --> **low** — The unchecked submodule Git directory has two placeholder names in one cell, and the inspection commands cover only one entry form lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > Examined: the `judgment/submodule-local-work` resolution cell in references/removal-gates.md, and the `<name> (not checked out)` bullet above it. That bullet says an entry is either a name relative to `<modules>` or a worktree-relative path such as `dep/.git/modules/inner`. > > What the prose says: the cell first calls the directory "`<modules>/<name>` (or the listed worktree-relative path)". Its inspection commands then hard-code only the first form: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log …` and `… stash list`. A few sentences later the change introduces a second placeholder for the same directory: "`<dir>`, where `<dir>` is `<modules>/<name>` or the listed path". > > Why it matters: the writing-for-agents standard asks writers to "use one term for one concept". Two names for one directory make a reader check whether they differ. Also, for a worktree-relative entry like `dep/.git/modules/inner`, the literal log and stash commands point at a nonexistent `<modules>/dep/.git/modules/inner`. The agent has to work out the substitution that `<dir>` already expresses. Those commands are the inspection the owner decides from. > > Proposed correction: define `<dir>` once, where the cell first names the directory. For example: "For a `(not checked out)` entry, let `<dir>` be `<modules>/<name>`, or the listed worktree-relative path; before anything checks it out, show the owner what it holds: `git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes` and `… stash list`." Then use `<dir>` in the later `rev-parse HEAD` step without redefining it. This covers both entry forms and removes the duplicate definition. claim `01M3D4551HKYT8DTSCB980PKN0` of review `01M3D3ZR53XHPZDB0WKN1BF301`
Author
Owner

Fixed in 34ca85c, superseding the deferral in #90332. The row now says the rerun keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink. It becomes removable once the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit. That matches decide_removal_outcome, whose live-status check returns expected/dirty-working-tree before the submodule gate runs.

<!-- gh-feedback:reply-to:90314 --> Fixed in 34ca85c, superseding the deferral in #90332. The row now says the rerun keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink. It becomes removable once the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit. That matches `decide_removal_outcome`, whose live-status check returns `expected/dirty-working-tree` before the submodule gate runs.
jercik marked this conversation as resolved
@ -36,3 +36,2 @@
echo "The audited repositories' branch refs and stashes are never deleted. Removing a"
echo "worktree deletes its submodules' Git directories, local branches included, once every"
echo "commit there is held by a remote-tracking ref."
echo "worktree deletes its submodules' Git directories, local branches and tags included."

low — --help and removal-gates.md list different refs as deleted with a submodule's Git directory ("branches and tags" vs "branches and stash")
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

Examined: the rewritten usage sentence in scripts/audit-checkouts.sh, the sentence before it, and the matching statement in references/removal-gates.md.

What the prose says: --help now says "Removing a worktree deletes its submodules' Git directories, local branches and tags included." The reference states the same fact as "Removal also deletes each submodule's Git directory, local branches and stash included". One lists tags and the other lists the stash. The tags are what this change makes relevant: the gate now lets a submodule tag be deleted when origin has it, including a lightweight tag whose name peels to the same commit at origin. The usage sentence also comes right after "The audited repositories' branch refs and stashes are never deleted." A reader has to notice that the first sentence covers the audited repositories and the second covers their submodules. Otherwise "branch refs … never deleted" and "local branches … included" look contradictory.

Why it matters: the skill standard asks for "one term for one concept" and verifiable claims. An agent that reads --help (SKILL.md tells it to run --help before first use) and then the reference gets two different lists of what is lost. When it explains removal consequences to the owner, it may leave out tags or the stash.

Proposed correction: use one list in both places, for example "deletes its submodules' Git directories, including their local branches, tags, and stash". Optionally mark the scope: "Removing a worktree deletes its submodules' Git directories…" after "The audited repositories' own branch refs and stashes are never deleted." This preserves the new point about tags and makes the two files agree.

claim 01M3D455TTQB9RBR76RRRJM5J4 of review 01M3D3ZR53XHPZDB0WKN1BF301

<!-- review:claim:01M3D455TTQB9RBR76RRRJM5J4 --> **low** — `--help` and removal-gates.md list different refs as deleted with a submodule's Git directory ("branches and tags" vs "branches and stash") lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > Examined: the rewritten usage sentence in scripts/audit-checkouts.sh, the sentence before it, and the matching statement in references/removal-gates.md. > > What the prose says: `--help` now says "Removing a worktree deletes its submodules' Git directories, local branches and tags included." The reference states the same fact as "Removal also deletes each submodule's Git directory, local branches and stash included". One lists tags and the other lists the stash. The tags are what this change makes relevant: the gate now lets a submodule tag be deleted when origin has it, including a lightweight tag whose name peels to the same commit at origin. The usage sentence also comes right after "The audited repositories' branch refs and stashes are never deleted." A reader has to notice that the first sentence covers the audited repositories and the second covers their submodules. Otherwise "branch refs … never deleted" and "local branches … included" look contradictory. > > Why it matters: the skill standard asks for "one term for one concept" and verifiable claims. An agent that reads `--help` (SKILL.md tells it to run `--help` before first use) and then the reference gets two different lists of what is lost. When it explains removal consequences to the owner, it may leave out tags or the stash. > > Proposed correction: use one list in both places, for example "deletes its submodules' Git directories, including their local branches, tags, and stash". Optionally mark the scope: "Removing a worktree deletes its submodules' Git directories…" after "The audited repositories' own branch refs and stashes are never deleted." This preserves the new point about tags and makes the two files agree. claim `01M3D455TTQB9RBR76RRRJM5J4` of review `01M3D3ZR53XHPZDB0WKN1BF301`
jercik marked this conversation as resolved
Author
Owner

Replying to review comment #90313

This is the same restructure as #90254, deferred there to a follow-up PR: move the (not checked out) procedure out of the judgment/submodule-local-work table cell into a numbered subsection of skills/audit-git-checkouts/references/removal-gates.md, in execution order. Round 5 of this PR only takes clear, severe bugs.

> Replying to review comment #90313 This is the same restructure as #90254, deferred there to a follow-up PR: move the `(not checked out)` procedure out of the `judgment/submodule-local-work` table cell into a numbered subsection of `skills/audit-git-checkouts/references/removal-gates.md`, in execution order. Round 5 of this PR only takes clear, severe bugs.
Author
Owner

Replying to review comment #90314

This is real, and it's wrong wording I added in 972ab44. After the re-checkout, the superproject lists M dep, and decide_removal_outcome returns expected/dirty-working-tree at its live-status check before the submodule gate runs, so pushing the commit alone never makes the worktree removable. Nothing is lost, because the worktree stays kept. Round 5 only takes clear, severe bugs, so I'm deferring it to a follow-up PR.

Follow-up fix, in skills/audit-git-checkouts/references/removal-gates.md, judgment/submodule-local-work row: replace "the rerun then keeps the worktree until that commit is pushed" with "the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit."

> Replying to review comment #90314 This is real, and it's wrong wording I added in 972ab44. After the re-checkout, the superproject lists ` M dep`, and `decide_removal_outcome` returns `expected/dirty-working-tree` at its live-status check before the submodule gate runs, so pushing the commit alone never makes the worktree removable. Nothing is lost, because the worktree stays kept. Round 5 only takes clear, severe bugs, so I'm deferring it to a follow-up PR. Follow-up fix, in `skills/audit-git-checkouts/references/removal-gates.md`, `judgment/submodule-local-work` row: replace "the rerun then keeps the worktree until that commit is pushed" with "the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit."
Author
Owner

Replying to review comment #90315

This is real, and I'm deferring it to a follow-up PR because round 5 only takes clear, severe bugs. The inspection commands predate this PR (#81), and the extra placeholder came in with 972ab44.

Follow-up fix, in skills/audit-git-checkouts/references/removal-gates.md, judgment/submodule-local-work row: define <dir> once where the cell first names the directory (<modules>/<name> or the listed worktree-relative path), use it in the log and stash list commands, and drop the later redefinition.

> Replying to review comment #90315 This is real, and I'm deferring it to a follow-up PR because round 5 only takes clear, severe bugs. The inspection commands predate this PR (#81), and the extra placeholder came in with 972ab44. Follow-up fix, in `skills/audit-git-checkouts/references/removal-gates.md`, `judgment/submodule-local-work` row: define `<dir>` once where the cell first names the directory (`<modules>/<name>` or the listed worktree-relative path), use it in the `log` and `stash list` commands, and drop the later redefinition.
Author
Owner

Replying to review comment #90316

This is real, and I'm deferring it to a follow-up PR because round 5 only takes clear, severe bugs. Neither list is false, since each is a subset of what the deletion removes, but they should match.

Follow-up fix: use one list in both places, "including their local branches, tags, and stash". That means the usage() closing sentence in skills/audit-git-checkouts/scripts/audit-checkouts.sh and the "Removal also deletes each submodule's Git directory" sentence in skills/audit-git-checkouts/references/removal-gates.md. Also say "The audited repositories' own branch refs and stashes are never deleted" in usage().

> Replying to review comment #90316 This is real, and I'm deferring it to a follow-up PR because round 5 only takes clear, severe bugs. Neither list is false, since each is a subset of what the deletion removes, but they should match. Follow-up fix: use one list in both places, "including their local branches, tags, and stash". That means the `usage()` closing sentence in `skills/audit-git-checkouts/scripts/audit-checkouts.sh` and the "Removal also deletes each submodule's Git directory" sentence in `skills/audit-git-checkouts/references/removal-gates.md`. Also say "The audited repositories' own branch refs and stashes are never deleted" in `usage()`.
Author
Owner

Replying to review summary comment #89366

Round-5 note for review 01M3D3ZR53XHPZDB0WKN1BF301 (head 972ab44). Claim 01M3D455E3SRRMM7ZTT4MS2HAN appears only in the report, because it anchors on unchanged lines. It is real: the list_unchecked_submodule_git_dirs header comment says entries under modules/ print "as its name there", but a nested absorbed entry prints as dep/modules/inner. Round 5 only takes clear, severe bugs, so I'm deferring it.

Follow-up fix, in skills/audit-git-checkouts/scripts/audit-checkouts.sh: have the comment say those entries print as their path relative to modules/, which is a top-level submodule's name, or <parent>/modules/<name> for a nested absorbed one.

> Replying to review summary comment #89366 Round-5 note for review `01M3D3ZR53XHPZDB0WKN1BF301` (head 972ab44). Claim `01M3D455E3SRRMM7ZTT4MS2HAN` appears only in the report, because it anchors on unchanged lines. It is real: the `list_unchecked_submodule_git_dirs` header comment says entries under `modules/` print "as its name there", but a nested absorbed entry prints as `dep/modules/inner`. Round 5 only takes clear, severe bugs, so I'm deferring it. Follow-up fix, in `skills/audit-git-checkouts/scripts/audit-checkouts.sh`: have the comment say those entries print as their path relative to `modules/`, which is a top-level submodule's name, or `<parent>/modules/<name>` for a nested absorbed one.
docs(audit-git-checkouts): say a restored submodule keeps its worktree as a dirty tree
All checks were successful
commit-msg / commitlint (pull_request) Successful in 23s
Node tests / node:test (pull_request) Successful in 1m23s
Review / Review (pull_request_target) Successful in 3m52s
34ca85c5de
After the restore steps the superproject no longer matches its Gitlink, so
the driver keeps the worktree as dirty; pushing the commit alone never made
it removable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -54,3 +54,3 @@
| `judgment/sparse-checkout` | Keep the cone's `skip-worktree` bits: clearing them turns absent files into apparent deletions. Confirm the cone with `git sparse-checkout list`, verify no file outside it is materialized, then remove manually once every other gate holds. |
| `judgment/precious-ignored-files` | Leave `preciousPaths` in place until the owner disposes of them or authorizes their deletion, then rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, or delete the directory, and rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it. So first record that HEAD with `git --git-dir <dir> rev-parse HEAD`, where `<dir>` is `<modules>/<name>` or the listed path, and after restoring, check it out again in the restored submodule; the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit. To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry, or, for a worktree-relative `<checkout>/.git/modules/<rest>` entry, in `<checkout>` with `<rest>`. If `git config -f .gitmodules submodule.<sub>.path` prints a path, run `git submodule update --init -- <that path>` there. Otherwise `<sub>` is `<outer>/modules/<rest>` for a submodule `<outer>` that this `.gitmodules` names: move into the checkout its `submodule.<outer>.path` names and repeat with `<rest>`. A top-level name can itself contain `/modules/`, which is why the whole entry is looked up first. |

medium — (not checked out) restore procedure is an out-of-order run-on inside a table cell, so the step that saves HEAD reads after the step that loses it
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the judgment/submodule-local-work row of the "Resolve a kept worktree" table in references/removal-gates.md, which this change rewrote. I also read list_submodules_with_local_work and the removal gate in scripts/audit-checkouts.sh to confirm the mechanism the prose describes.

What the text does: the row is now a single table cell of roughly 300 words. It holds a real ordered procedure: inspect, decide, record HEAD, restore, re-check-out HEAD, rerun, then push and commit or reset. That procedure is written as prose in the wrong order. The cell first says "Then act on their decision and rerun: delete the directory, or ... restore the checkout." Only after that does it say "So first record that HEAD ...". The instructions for how to restore ("To restore, find the superproject whose .gitmodules names the submodule ...") come after both. The key sentence runs three clauses together with semicolons: record HEAD and re-check it out; "the rerun then keeps the worktree as a dirty tree"; "it becomes removable once ...". In "check it out again in the restored submodule", the "it" refers back past two nouns to the recorded HEAD. The cell also names the same directory two ways: the inspection commands use <modules>/<name> ("or the listed worktree-relative path"), and <dir> is defined only midway through.

Why it matters: the cell's own rationale says ordering here prevents data loss. A literal agent reads "Then act on their decision and rerun: ... restore the checkout" and runs git submodule update --init, which moves HEAD to the recorded commit. Only then does it reach "So first record that HEAD". By that point the unrecorded commit is reachable only through the reflog. If the agent then reruns, the gate sees a HEAD held by a remote-tracking ref and removes the worktree. The Git directory goes with it, including the commit this passage exists to protect. The writing-for-agents skill says to use "numbered steps only for real sequence" and to "spend detail where a plausible mistake would derail the task". It also says to "use one term for one concept". This passage is a real sequence where a plausible misreading destroys work.

Proposed correction: shorten the table cell to "... For a (not checked out) entry, follow Restore or delete an unchecked submodule Git directory below." Move the procedure into its own subsection as numbered steps:

  1. Set <dir> to <modules>/<name>, or to the listed worktree-relative path. Before anything checks it out, show the owner git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes and git --git-dir <dir> --work-tree <dir> stash list. (--work-tree overrides the deleted checkout its config names.)
  2. If the owner chooses deletion, delete <dir> and rerun.
  3. To restore while a Gitlink remains, first record git --git-dir <dir> rev-parse HEAD. Restoring checks out the recorded commit, so an unrecorded HEAD would become unreachable and removal would delete it.
  4. Run git submodule update --init -- <path> (path lookup below).
  5. In the restored submodule, check out the HEAD recorded in step 3.
  6. Rerun. The worktree stays kept as dirty until the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit.

What this preserves: every fact in the current cell, including the rationale for recording HEAD and the end condition for removability. What it improves: the protective step comes before the destructive one, each step has one sentence, and one placeholder names the directory throughout.

Evidence basis: static reading of the text plus the gate code. I did not run an agent against the passage, so the misordering failure is inferred from the wording, not observed.

claim 01M3D4FWW42MD71ZJXTS74ARYA of review 01M3D4CN7QPC5CMD03VTX5NFRH

<!-- review:claim:01M3D4FWW42MD71ZJXTS74ARYA --> **medium** — `(not checked out)` restore procedure is an out-of-order run-on inside a table cell, so the step that saves HEAD reads after the step that loses it lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the `judgment/submodule-local-work` row of the "Resolve a kept worktree" table in `references/removal-gates.md`, which this change rewrote. I also read `list_submodules_with_local_work` and the removal gate in `scripts/audit-checkouts.sh` to confirm the mechanism the prose describes. > > What the text does: the row is now a single table cell of roughly 300 words. It holds a real ordered procedure: inspect, decide, record HEAD, restore, re-check-out HEAD, rerun, then push and commit or reset. That procedure is written as prose in the wrong order. The cell first says "Then act on their decision and rerun: delete the directory, or ... restore the checkout." Only after that does it say "So first record that HEAD ...". The instructions for how to restore ("To restore, find the superproject whose `.gitmodules` names the submodule ...") come after both. The key sentence runs three clauses together with semicolons: record HEAD and re-check it out; "the rerun then keeps the worktree as a dirty tree"; "it becomes removable once ...". In "check it out again in the restored submodule", the "it" refers back past two nouns to the recorded HEAD. The cell also names the same directory two ways: the inspection commands use `<modules>/<name>` ("or the listed worktree-relative path"), and `<dir>` is defined only midway through. > > Why it matters: the cell's own rationale says ordering here prevents data loss. A literal agent reads "Then act on their decision and rerun: ... restore the checkout" and runs `git submodule update --init`, which moves HEAD to the recorded commit. Only then does it reach "So first record that HEAD". By that point the unrecorded commit is reachable only through the reflog. If the agent then reruns, the gate sees a HEAD held by a remote-tracking ref and removes the worktree. The Git directory goes with it, including the commit this passage exists to protect. The writing-for-agents skill says to use "numbered steps only for real sequence" and to "spend detail where a plausible mistake would derail the task". It also says to "use one term for one concept". This passage is a real sequence where a plausible misreading destroys work. > > Proposed correction: shorten the table cell to "... For a `(not checked out)` entry, follow [Restore or delete an unchecked submodule Git directory](#...) below." Move the procedure into its own subsection as numbered steps: > 1. Set `<dir>` to `<modules>/<name>`, or to the listed worktree-relative path. Before anything checks it out, show the owner `git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes` and `git --git-dir <dir> --work-tree <dir> stash list`. (`--work-tree` overrides the deleted checkout its config names.) > 2. If the owner chooses deletion, delete `<dir>` and rerun. > 3. To restore while a Gitlink remains, first record `git --git-dir <dir> rev-parse HEAD`. Restoring checks out the recorded commit, so an unrecorded HEAD would become unreachable and removal would delete it. > 4. Run `git submodule update --init -- <path>` (path lookup below). > 5. In the restored submodule, check out the HEAD recorded in step 3. > 6. Rerun. The worktree stays kept as dirty until the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit. > > What this preserves: every fact in the current cell, including the rationale for recording HEAD and the end condition for removability. What it improves: the protective step comes before the destructive one, each step has one sentence, and one placeholder names the directory throughout. > > Evidence basis: static reading of the text plus the gate code. I did not run an agent against the passage, so the misordering failure is inferred from the wording, not observed. claim `01M3D4FWW42MD71ZJXTS74ARYA` of review `01M3D4CN7QPC5CMD03VTX5NFRH`

low — Restore guidance says committing the new Gitlink makes the worktree removable, but the containment gate then keeps it
lens general-bug · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the new judgment/submodule-local-work resolution text in references/removal-gates.md, and the gate order in decide_removal_outcome in scripts/audit-checkouts.sh. I traced the code; I did not run it.

What the doc says: after restoring a (not checked out) submodule and checking its old HEAD out again, "it becomes removable once the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit."

What the code does: before any submodule gate, decide_removal_outcome requires containment of the worktree's HEAD in refs/remotes/origin/<default>. It runs prove-branch-contained.sh --target "$target_ref" --branch "$branch_ref", and on status 1 it prints judgment/containment-not-proven. The same file's gate list says containment in origin/<default> is required. Committing the new Gitlink in the linked worktree creates a new superproject commit on its branch, or on its detached HEAD, and origin/<default> does not contain that commit. So the rerun stops at judgment/containment-not-proven, not at removal. Only the other branch of the sentence, returning the submodule to the recorded commit, leads to removal once the submodule commit is pushed.

Impact: an operator or agent following the guidance commits the Gitlink and expects the next run to remove the worktree. It stays with a new blocker, and the table sends them to containment rungs 4–5 for a commit they just made. Nothing is lost, because the gate fails safe, but the documented path is wrong.

Fix: say that a committed Gitlink must also be pushed and merged into origin/<default>, or recommend returning the submodule to the recorded commit as the way to make the worktree removable.

What would refute this: a containment rung that treats a superproject commit changing only a Gitlink as contained. Rungs 1–3 in containment.md would decide that; I did not re-verify each rung.

claim 01M3D4GAYBEB0VN9YKQCQ9JHRZ of review 01M3D4CN7QPC5CMD03VTX5NFRH

<!-- review:claim:01M3D4GAYBEB0VN9YKQCQ9JHRZ --> **low** — Restore guidance says committing the new Gitlink makes the worktree removable, but the containment gate then keeps it lens `general-bug` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the new `judgment/submodule-local-work` resolution text in references/removal-gates.md, and the gate order in `decide_removal_outcome` in scripts/audit-checkouts.sh. I traced the code; I did not run it. > > What the doc says: after restoring a `(not checked out)` submodule and checking its old HEAD out again, "it becomes removable once the commit is pushed and the owner either commits the new Gitlink or returns the submodule to the recorded commit." > > What the code does: before any submodule gate, `decide_removal_outcome` requires containment of the worktree's HEAD in `refs/remotes/origin/<default>`. It runs `prove-branch-contained.sh --target "$target_ref" --branch "$branch_ref"`, and on status 1 it prints `judgment/containment-not-proven`. The same file's gate list says containment in `origin/<default>` is required. Committing the new Gitlink in the linked worktree creates a new superproject commit on its branch, or on its detached HEAD, and `origin/<default>` does not contain that commit. So the rerun stops at `judgment/containment-not-proven`, not at removal. Only the other branch of the sentence, returning the submodule to the recorded commit, leads to removal once the submodule commit is pushed. > > Impact: an operator or agent following the guidance commits the Gitlink and expects the next run to remove the worktree. It stays with a new blocker, and the table sends them to containment rungs 4–5 for a commit they just made. Nothing is lost, because the gate fails safe, but the documented path is wrong. > > Fix: say that a committed Gitlink must also be pushed and merged into `origin/<default>`, or recommend returning the submodule to the recorded commit as the way to make the worktree removable. > > What would refute this: a containment rung that treats a superproject commit changing only a Gitlink as contained. Rungs 1–3 in containment.md would decide that; I did not re-verify each rung. claim `01M3D4GAYBEB0VN9YKQCQ9JHRZ` of review `01M3D4CN7QPC5CMD03VTX5NFRH`

low — The recursive name-to-checkout-path lookup for (not checked out) entries is a deterministic algorithm written as prose rather than emitted by the driver
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the new restore instructions in the judgment/submodule-local-work row of references/removal-gates.md. I also read the code that produces these entries in scripts/audit-checkouts.sh: list_unchecked_submodule_git_dirs and list_submodule_git_dirs_without_checkout, plus the removal gate that appends (not checked out).

What the text does: to restore a checkout, the agent has to turn a listed entry such as dep/modules/inner back into a checkout path. The prose defines four placeholders (<sub>, <rest>, <outer>, <checkout>) and a recursion: look up submodule.<sub>.path; if nothing prints, split <sub> as <outer>/modules/<rest>, move into <outer>'s checkout, and repeat. It ends with an edge-case warning: "A top-level name can itself contain /modules/, which is why the whole entry is looked up first."

What goes wrong: this is a fixed, mechanical procedure. It has a known trap (names that contain /modules/), and an agent has to carry it out by hand from a dense table cell. The writing-for-agents packaging reference says: "Move deterministic operations into scripts/: fixed command sequences, mechanical checks ... Keep judgment in prose." The driver already walks these directories while it builds the entry, so it can resolve the checkout path, or report that no Gitlink remains, far more reliably than an agent re-deriving it. With the algorithm in prose, a wrong split (for example, splitting at the first /modules/ in a top-level name) sends git submodule update --init to the wrong submodule or to none. The warning sentence is also a rationale that could be removed if the script handled the case.

Proposed correction: have the driver report the restorable checkout path with each (not checked out) entry, for example <name> (not checked out; restore with: git -C <superproject> submodule update --init -- <path>), or say that no Gitlink remains. Then replace the four-placeholder lookup in the doc with one sentence: "Restore it with the command the entry names." This keeps the outcome (restoring the correct submodule) and removes a hand-executed recursive algorithm and its edge-case caveat from the agent's reading path.

Proof gap: this is a packaging recommendation based on the skill's guidance and static reading. I did not test whether agents misapply the current lookup. The cost of leaving it as is depends on how often (not checked out) entries with nested or /modules/-containing names occur.

claim 01M3D4GKAJ4JKHD23KNHZ8PFHX of review 01M3D4CN7QPC5CMD03VTX5NFRH

<!-- review:claim:01M3D4GKAJ4JKHD23KNHZ8PFHX --> **low** — The recursive name-to-checkout-path lookup for `(not checked out)` entries is a deterministic algorithm written as prose rather than emitted by the driver lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the new restore instructions in the `judgment/submodule-local-work` row of `references/removal-gates.md`. I also read the code that produces these entries in `scripts/audit-checkouts.sh`: `list_unchecked_submodule_git_dirs` and `list_submodule_git_dirs_without_checkout`, plus the removal gate that appends ` (not checked out)`. > > What the text does: to restore a checkout, the agent has to turn a listed entry such as `dep/modules/inner` back into a checkout path. The prose defines four placeholders (`<sub>`, `<rest>`, `<outer>`, `<checkout>`) and a recursion: look up `submodule.<sub>.path`; if nothing prints, split `<sub>` as `<outer>/modules/<rest>`, move into `<outer>`'s checkout, and repeat. It ends with an edge-case warning: "A top-level name can itself contain `/modules/`, which is why the whole entry is looked up first." > > What goes wrong: this is a fixed, mechanical procedure. It has a known trap (names that contain `/modules/`), and an agent has to carry it out by hand from a dense table cell. The writing-for-agents packaging reference says: "Move deterministic operations into `scripts/`: fixed command sequences, mechanical checks ... Keep judgment in prose." The driver already walks these directories while it builds the entry, so it can resolve the checkout path, or report that no Gitlink remains, far more reliably than an agent re-deriving it. With the algorithm in prose, a wrong split (for example, splitting at the first `/modules/` in a top-level name) sends `git submodule update --init` to the wrong submodule or to none. The warning sentence is also a rationale that could be removed if the script handled the case. > > Proposed correction: have the driver report the restorable checkout path with each `(not checked out)` entry, for example `<name> (not checked out; restore with: git -C <superproject> submodule update --init -- <path>)`, or say that no Gitlink remains. Then replace the four-placeholder lookup in the doc with one sentence: "Restore it with the command the entry names." This keeps the outcome (restoring the correct submodule) and removes a hand-executed recursive algorithm and its edge-case caveat from the agent's reading path. > > Proof gap: this is a packaging recommendation based on the skill's guidance and static reading. I did not test whether agents misapply the current lookup. The cost of leaving it as is depends on how often `(not checked out)` entries with nested or `/modules/`-containing names occur. claim `01M3D4GKAJ4JKHD23KNHZ8PFHX` of review `01M3D4CN7QPC5CMD03VTX5NFRH`
Author
Owner

Fixed in fb5fb7f. I confirmed it with the real prover: a worktree branch whose only extra commit records the new Gitlink gets not-proven from prove-branch-contained.sh (exit 1, no rung), so the driver keeps it as judgment/containment-not-proven. The row now says returning the submodule to the recorded commit is what makes the worktree removable. Committing the Gitlink keeps it kept until that superproject commit lands in origin/<default>.

<!-- gh-feedback:reply-to:90379 --> Fixed in fb5fb7f. I confirmed it with the real prover: a worktree branch whose only extra commit records the new Gitlink gets `not-proven` from `prove-branch-contained.sh` (exit 1, no rung), so the driver keeps it as `judgment/containment-not-proven`. The row now says returning the submodule to the recorded commit is what makes the worktree removable. Committing the Gitlink keeps it kept until that superproject commit lands in `origin/<default>`.
jercik marked this conversation as resolved
docs(audit-git-checkouts): a committed Gitlink keeps the worktree until it lands
All checks were successful
commit-msg / commitlint (pull_request) Successful in 17s
Node tests / node:test (pull_request) Successful in 1m45s
Review / Review (pull_request_target) Successful in 3m38s
fb5fb7f1d9
Committing the restored submodule's new Gitlink adds a superproject
commit that `origin/<default>` lacks, so the containment gate keeps the
worktree. Only returning the submodule to the recorded commit makes it
removable once the submodule commit is pushed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Replying to review comment #90378

This is the same restructure deferred in #90254 and #90313. It's a style change rather than a false statement: the cell already says to record HEAD "first", before restoring. This PR now only takes fixes for false statements it introduced or for data loss, so it stays with the follow-up PR: move the (not checked out) procedure in skills/audit-git-checkouts/references/removal-gates.md into a numbered subsection in execution order.

> Replying to review comment #90378 This is the same restructure deferred in #90254 and #90313. It's a style change rather than a false statement: the cell already says to record HEAD "first", before restoring. This PR now only takes fixes for false statements it introduced or for data loss, so it stays with the follow-up PR: move the `(not checked out)` procedure in `skills/audit-git-checkouts/references/removal-gates.md` into a numbered subsection in execution order.
Author
Owner

Replying to review comment #90380

Acknowledged, not taken here. Having the driver print each entry's restore command would change behavior, and this PR is wording-only. This PR now only takes fixes for false statements it introduced or for data loss.

Follow-up: in skills/audit-git-checkouts/scripts/audit-checkouts.sh, have list_unchecked_submodule_git_dirs resolve each entry's owning superproject and checkout path (or report that no Gitlink remains) and print it with the entry. Then replace the lookup prose in the judgment/submodule-local-work row of references/removal-gates.md with "restore it with the command the entry names".

> Replying to review comment #90380 Acknowledged, not taken here. Having the driver print each entry's restore command would change behavior, and this PR is wording-only. This PR now only takes fixes for false statements it introduced or for data loss. Follow-up: in `skills/audit-git-checkouts/scripts/audit-checkouts.sh`, have `list_unchecked_submodule_git_dirs` resolve each entry's owning superproject and checkout path (or report that no Gitlink remains) and print it with the entry. Then replace the lookup prose in the `judgment/submodule-local-work` row of `references/removal-gates.md` with "restore it with the command the entry names".
@ -54,3 +54,3 @@
| `judgment/sparse-checkout` | Keep the cone's `skip-worktree` bits: clearing them turns absent files into apparent deletions. Confirm the cone with `git sparse-checkout list`, verify no file outside it is materialized, then remove manually once every other gate holds. |
| `judgment/precious-ignored-files` | Leave `preciousPaths` in place until the owner disposes of them or authorizes their deletion, then rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then, on their decision, restore the checkout with `git submodule update --init -- <path>` while a Gitlink remains, or delete the directory, and rerun. |
| `judgment/submodule-local-work` | The listed submodules hold work only this worktree has, or content the driver could not inspect. Show the owner what each holds; rerun once the work is pushed. If the owner authorizes discarding it instead, discard it in the submodule first, then rerun. For a `(not checked out)` entry, first show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds, before anything checks it out: `git --git-dir <modules>/<name> --work-tree <modules>/<name> log --oneline HEAD --branches --tags --not --remotes` and `… stash list` (`--work-tree` overrides the deleted checkout its config names). Then act on their decision and rerun: delete the directory, or, while a Gitlink remains, restore the checkout. Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it. So first record that HEAD with `git --git-dir <dir> rev-parse HEAD`, where `<dir>` is `<modules>/<name>` or the listed path, and after restoring, check it out again in the restored submodule; the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner returns the submodule to the recorded commit. Committing the new Gitlink instead leaves the worktree kept as `judgment/containment-not-proven` until that superproject commit lands in `origin/<default>`. To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry, or, for a worktree-relative `<checkout>/.git/modules/<rest>` entry, in `<checkout>` with `<rest>`. If `git config -f .gitmodules submodule.<sub>.path` prints a path, run `git submodule update --init -- <that path>` there. Otherwise `<sub>` is `<outer>/modules/<rest>` for a submodule `<outer>` that this `.gitmodules` names: move into the checkout its `submodule.<outer>.path` names and repeat with `<rest>`. A top-level name can itself contain `/modules/`, which is why the whole entry is looked up first. |

medium — Submodule restore procedure is packed into one table cell with steps out of execution order
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the judgment/submodule-local-work row of the "Resolve a kept worktree" table in skills/audit-git-checkouts/references/removal-gates.md. This change added about 300 words to it. SKILL.md tells the agent to read this reference in full before acting on a kept worktree. I also read the driver functions list_unchecked_submodule_git_dirs and list_submodule_git_dirs_without_checkout in scripts/audit-checkouts.sh to confirm what the entries look like.

What the subject says: for a (not checked out) entry, the cell gives these instructions in this order:
(a) choose between deleting and restoring;
(b) the consequence of restoring: "Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it.";
(c) "So first record that HEAD with git --git-dir <dir> rev-parse HEAD ... and after restoring, check it out again in the restored submodule; the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner returns the submodule to the recorded commit.";
(d) an alternative outcome ("Committing the new Gitlink instead ...");
(e) only after that, how to restore: "To restore, find the superproject whose .gitmodules names the submodule. Start in the worktree with <sub> set to the entry ... Otherwise <sub> is <outer>/modules/<rest> ... move into the checkout ... and repeat with <rest>."

What goes wrong: this is a real sequence where order matters. HEAD must be recorded before git submodule update --init runs, or the stranded commit is lost (the cell itself says removal would then delete it). Yet the cell says "after restoring" before it explains how to restore, and the restore procedure is a loop ("repeat with <rest>") written as prose in a Markdown table cell, which cannot hold a list. Clause (c) joins three independent statements with semicolons. It also calls the resulting outcome "a dirty tree" instead of naming the expected/… row it maps to. An agent reading linearly has to rebuild the order from out-of-sequence clauses, and this is the one row where a wrong order destroys the owner's commit. The writing-for-agents skill says to use "numbered steps only for real sequence", to "Order sections by the decisions the reader makes", and to "spend detail where a plausible mistake would derail the task".

Proposed correction: keep one sentence in the table cell pointing to a new subsection (for example "## Restore a submodule Git directory without a checkout"). Put numbered steps there, in execution order:

  1. Show the owner what <dir> holds (the log and stash commands).
  2. If they choose deletion, delete <dir> and rerun.
  3. Otherwise, record git --git-dir <dir> rev-parse HEAD.
  4. Find the Gitlink path with the .gitmodules lookup, including the <outer>/modules/<rest> recursion and why the whole entry is tried first.
  5. Run git submodule update --init -- <path>.
  6. Check out the recorded HEAD in the restored submodule.
  7. Rerun. Expect expected/… (dirty) until the commit is pushed and the submodule is returned to its Gitlink, or judgment/containment-not-proven if a new Gitlink is committed.

This keeps every fact now in the cell: the stranding risk, the lookup rule, and both follow-up outcomes. The only change is that the order of the text matches the order of execution.

Proof status: static reading of the prose; I did not run the procedure. Git behavior (that submodule update detaches to the recorded commit) is as the cell itself states.

claim 01M3D4T8E0DH02WS0614RQYXQ0 of review 01M3D4QCZWJBXAARR22CWSW3KA

<!-- review:claim:01M3D4T8E0DH02WS0614RQYXQ0 --> **medium** — Submodule restore procedure is packed into one table cell with steps out of execution order lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the `judgment/submodule-local-work` row of the "Resolve a kept worktree" table in skills/audit-git-checkouts/references/removal-gates.md. This change added about 300 words to it. SKILL.md tells the agent to read this reference in full before acting on a kept worktree. I also read the driver functions `list_unchecked_submodule_git_dirs` and `list_submodule_git_dirs_without_checkout` in scripts/audit-checkouts.sh to confirm what the entries look like. > > What the subject says: for a `(not checked out)` entry, the cell gives these instructions in this order: > (a) choose between deleting and restoring; > (b) the consequence of restoring: "Restoring checks out the recorded commit, which leaves a HEAD commit that no ref holds unreachable, and removal would then delete it."; > (c) "So first record that HEAD with `git --git-dir <dir> rev-parse HEAD` ... and after restoring, check it out again in the restored submodule; the rerun then keeps the worktree as a dirty tree, because the submodule no longer matches its Gitlink; it becomes removable once the commit is pushed and the owner returns the submodule to the recorded commit."; > (d) an alternative outcome ("Committing the new Gitlink instead ..."); > (e) only after that, how to restore: "To restore, find the superproject whose `.gitmodules` names the submodule. Start in the worktree with `<sub>` set to the entry ... Otherwise `<sub>` is `<outer>/modules/<rest>` ... move into the checkout ... and repeat with `<rest>`." > > What goes wrong: this is a real sequence where order matters. HEAD must be recorded before `git submodule update --init` runs, or the stranded commit is lost (the cell itself says removal would then delete it). Yet the cell says "after restoring" before it explains how to restore, and the restore procedure is a loop ("repeat with `<rest>`") written as prose in a Markdown table cell, which cannot hold a list. Clause (c) joins three independent statements with semicolons. It also calls the resulting outcome "a dirty tree" instead of naming the `expected/…` row it maps to. An agent reading linearly has to rebuild the order from out-of-sequence clauses, and this is the one row where a wrong order destroys the owner's commit. The writing-for-agents skill says to use "numbered steps only for real sequence", to "Order sections by the decisions the reader makes", and to "spend detail where a plausible mistake would derail the task". > > Proposed correction: keep one sentence in the table cell pointing to a new subsection (for example "## Restore a submodule Git directory without a checkout"). Put numbered steps there, in execution order: > 1. Show the owner what `<dir>` holds (the log and stash commands). > 2. If they choose deletion, delete `<dir>` and rerun. > 3. Otherwise, record `git --git-dir <dir> rev-parse HEAD`. > 4. Find the Gitlink path with the `.gitmodules` lookup, including the `<outer>/modules/<rest>` recursion and why the whole entry is tried first. > 5. Run `git submodule update --init -- <path>`. > 6. Check out the recorded HEAD in the restored submodule. > 7. Rerun. Expect `expected/…` (dirty) until the commit is pushed and the submodule is returned to its Gitlink, or `judgment/containment-not-proven` if a new Gitlink is committed. > > This keeps every fact now in the cell: the stranding risk, the lookup rule, and both follow-up outcomes. The only change is that the order of the text matches the order of execution. > > Proof status: static reading of the prose; I did not run the procedure. Git behavior (that `submodule update` detaches to the recorded commit) is as the cell itself states. claim `01M3D4T8E0DH02WS0614RQYXQ0` of review `01M3D4QCZWJBXAARR22CWSW3KA`

low — Same Git directory is named <modules>/<name> in the log/stash commands and <dir> in rev-parse, defined only after first use
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the judgment/submodule-local-work row in skills/audit-git-checkouts/references/removal-gates.md. This change added the rev-parse sentence and the restore lookup to it. I also read the <name> (not checked out) bullet higher in the same file, which defines two kinds of entry: <modules>-relative names and worktree-relative paths such as dep/.git/modules/inner.

What the subject says: one cell names the same Git directory three different ways. The inspection step reads "show the owner what <modules>/<name> (or the listed worktree-relative path) holds ... git --git-dir <modules>/<name> --work-tree <modules>/<name> log ... and … stash list". Here the commands hard-code <modules>/<name>, and the other form appears only in a parenthetical. Two sentences later a new placeholder, <dir>, is defined for the same directory ("where <dir> is <modules>/<name> or the listed path"), but only the rev-parse command uses it. The restore lookup then adds <sub>, <checkout>, <rest>, and <outer>.

What goes wrong: a literal reader handling a worktree-relative entry (for example dep/.git/modules/inner) finds log and stash list commands written for <modules>/<name>, and has to substitute the parenthetical form on their own. The rev-parse command gets that substitution through <dir>. The same concept under two names, defined after its first use, invites exactly the mismatch the entry types exist to prevent. The writing-for-agents skill says: "use one term for one concept, and define project-local terms on first use."

Proposed correction: define <dir> once, where the cell first introduces the (not checked out) entry: "<dir> is <modules>/<name>, or the listed worktree-relative path for a nested entry". Then use it in all three commands: git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes, git --git-dir <dir> --work-tree <dir> stash list, and git --git-dir <dir> rev-parse HEAD. Spell out the stash list command instead of eliding it with "…", because it needs the same two options. This preserves both entry forms and removes the parenthetical and the late definition.

Proof status: static reading; the git commands behave the same either way once the right path is substituted, so the risk is an agent substituting the wrong path, not a wrong command.

claim 01M3D4TKFSGTPJBNEQ56CVK3GZ of review 01M3D4QCZWJBXAARR22CWSW3KA

<!-- review:claim:01M3D4TKFSGTPJBNEQ56CVK3GZ --> **low** — Same Git directory is named `<modules>/<name>` in the log/stash commands and `<dir>` in rev-parse, defined only after first use lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the `judgment/submodule-local-work` row in skills/audit-git-checkouts/references/removal-gates.md. This change added the rev-parse sentence and the restore lookup to it. I also read the `<name> (not checked out)` bullet higher in the same file, which defines two kinds of entry: `<modules>`-relative names and worktree-relative paths such as `dep/.git/modules/inner`. > > What the subject says: one cell names the same Git directory three different ways. The inspection step reads "show the owner what `<modules>/<name>` (or the listed worktree-relative path) holds ... `git --git-dir <modules>/<name> --work-tree <modules>/<name> log ...` and `… stash list`". Here the commands hard-code `<modules>/<name>`, and the other form appears only in a parenthetical. Two sentences later a new placeholder, `<dir>`, is defined for the same directory ("where `<dir>` is `<modules>/<name>` or the listed path"), but only the rev-parse command uses it. The restore lookup then adds `<sub>`, `<checkout>`, `<rest>`, and `<outer>`. > > What goes wrong: a literal reader handling a worktree-relative entry (for example `dep/.git/modules/inner`) finds `log` and `stash list` commands written for `<modules>/<name>`, and has to substitute the parenthetical form on their own. The rev-parse command gets that substitution through `<dir>`. The same concept under two names, defined after its first use, invites exactly the mismatch the entry types exist to prevent. The writing-for-agents skill says: "use one term for one concept, and define project-local terms on first use." > > Proposed correction: define `<dir>` once, where the cell first introduces the `(not checked out)` entry: "`<dir>` is `<modules>/<name>`, or the listed worktree-relative path for a nested entry". Then use it in all three commands: `git --git-dir <dir> --work-tree <dir> log --oneline HEAD --branches --tags --not --remotes`, `git --git-dir <dir> --work-tree <dir> stash list`, and `git --git-dir <dir> rev-parse HEAD`. Spell out the `stash list` command instead of eliding it with "…", because it needs the same two options. This preserves both entry forms and removes the parenthetical and the late definition. > > Proof status: static reading; the git commands behave the same either way once the right path is substituted, so the risk is an agent substituting the wrong path, not a wrong command. claim `01M3D4TKFSGTPJBNEQ56CVK3GZ` of review `01M3D4QCZWJBXAARR22CWSW3KA`
jercik marked this conversation as resolved
@ -36,3 +36,2 @@
echo "The audited repositories' branch refs and stashes are never deleted. Removing a"
echo "worktree deletes its submodules' Git directories, local branches included, once every"
echo "commit there is held by a remote-tracking ref."
echo "worktree deletes its submodules' Git directories, local branches and tags included."

low — Help text says branch refs are never deleted, then that removal deletes submodules' local branches, and drops the condition that gates it
lens writing-quality · arm default · tally 1 valid / 0 invalid / 0 uncertain

What I examined: the usage() help text in skills/audit-git-checkouts/scripts/audit-checkouts.sh. SKILL.md tells the agent to run scripts/audit-checkouts.sh --help from the skill directory before first use, so this text is agent-facing. I also read the "Refuses, per repository" paragraph just above it, and submodule_holds_local_work / list_submodules_with_local_work, which walk every submodule's branches, stash, and tags.

What the subject says: the closing paragraph now reads: "The audited repositories' branch refs and stashes are never deleted. Removing a worktree deletes its submodules' Git directories, local branches and tags included." Before this change, the second sentence ended "local branches included, once every commit there is held by a remote-tracking ref." This change dropped that qualifier and added tags.

What goes wrong: the two sentences are adjacent and read as contradicting each other. The first says branch refs are never deleted. The next says local branches are deleted. The driver walks submodules as repositories in their own right, so "the audited repositories" does not clearly exclude them. Without the old qualifier, the second sentence also no longer says the deletion is gated. A reader who remembers that removal is refused while a submodule holds unpushed work has to connect it themselves to the "Refuses" paragraph eight lines up. The writing-for-agents skill says: "attach conditions to the action they govern" and "Make claims verifiable".

Proposed correction: separate the two scopes and restore the condition, for example: "Branch refs and stashes in the audited checkouts themselves are never deleted. Removing a worktree also deletes its submodules' Git directories, including their local branches and tags, once the refusals above find nothing only that submodule holds." This keeps the new fact (tags are deleted too) and the guarantee the old wording carried.

Proof status: static reading of the help text against the refusal logic; the behavior itself is not in question, only whether the text states it unambiguously.

claim 01M3D4V6756SJBB5XE9BC03A55 of review 01M3D4QCZWJBXAARR22CWSW3KA

<!-- review:claim:01M3D4V6756SJBB5XE9BC03A55 --> **low** — Help text says branch refs are never deleted, then that removal deletes submodules' local branches, and drops the condition that gates it lens `writing-quality` · arm `default` · tally 1 valid / 0 invalid / 0 uncertain > What I examined: the `usage()` help text in skills/audit-git-checkouts/scripts/audit-checkouts.sh. SKILL.md tells the agent to run `scripts/audit-checkouts.sh --help` from the skill directory before first use, so this text is agent-facing. I also read the "Refuses, per repository" paragraph just above it, and `submodule_holds_local_work` / `list_submodules_with_local_work`, which walk every submodule's branches, stash, and tags. > > What the subject says: the closing paragraph now reads: "The audited repositories' branch refs and stashes are never deleted. Removing a worktree deletes its submodules' Git directories, local branches and tags included." Before this change, the second sentence ended "local branches included, once every commit there is held by a remote-tracking ref." This change dropped that qualifier and added tags. > > What goes wrong: the two sentences are adjacent and read as contradicting each other. The first says branch refs are never deleted. The next says local branches are deleted. The driver walks submodules as repositories in their own right, so "the audited repositories" does not clearly exclude them. Without the old qualifier, the second sentence also no longer says the deletion is gated. A reader who remembers that removal is refused while a submodule holds unpushed work has to connect it themselves to the "Refuses" paragraph eight lines up. The writing-for-agents skill says: "attach conditions to the action they govern" and "Make claims verifiable". > > Proposed correction: separate the two scopes and restore the condition, for example: "Branch refs and stashes in the audited checkouts themselves are never deleted. Removing a worktree also deletes its submodules' Git directories, including their local branches and tags, once the refusals above find nothing only that submodule holds." This keeps the new fact (tags are deleted too) and the guarantee the old wording carried. > > Proof status: static reading of the help text against the refusal logic; the behavior itself is not in question, only whether the text states it unambiguously. claim `01M3D4V6756SJBB5XE9BC03A55` of review `01M3D4QCZWJBXAARR22CWSW3KA`
jercik marked this conversation as resolved
Author
Owner

Replying to review comment #90399

Same restructure as #90254, #90313 and #90378, and it stays with the follow-up PR. The cell already says to record HEAD "first", so this is ordering and packaging rather than a false statement or data loss, which is all this PR now takes. The follow-up's numbered subsection should also name the rerun outcomes this finding lists: expected/dirty-working-tree, or judgment/containment-not-proven after a committed Gitlink.

> Replying to review comment #90399 Same restructure as #90254, #90313 and #90378, and it stays with the follow-up PR. The cell already says to record HEAD "first", so this is ordering and packaging rather than a false statement or data loss, which is all this PR now takes. The follow-up's numbered subsection should also name the rerun outcomes this finding lists: `expected/dirty-working-tree`, or `judgment/containment-not-proven` after a committed Gitlink.
Author
Owner

Replying to review comment #90400

Same as #90315, deferred to the follow-up PR: in skills/audit-git-checkouts/references/removal-gates.md, define <dir> once where the (not checked out) entry is introduced, and use it in the log, a spelled-out stash list, and rev-parse commands. It's a naming inconsistency, not a false statement, and this PR now only takes those or data-loss fixes.

> Replying to review comment #90400 Same as #90315, deferred to the follow-up PR: in `skills/audit-git-checkouts/references/removal-gates.md`, define `<dir>` once where the `(not checked out)` entry is introduced, and use it in the `log`, a spelled-out `stash list`, and `rev-parse` commands. It's a naming inconsistency, not a false statement, and this PR now only takes those or data-loss fixes.
Author
Owner

Replying to review comment #90401

Overlaps #90316, deferred to the follow-up PR. As you note, the behavior isn't in question; neither sentence is false, and this PR now only takes fixes for false statements or data loss. Follow-up, in usage() of skills/audit-git-checkouts/scripts/audit-checkouts.sh: "Branch refs and stashes in the audited checkouts themselves are never deleted. Removing a worktree also deletes its submodules' Git directories, including their local branches, tags, and stash, once the refusals above find nothing only that submodule holds." Use the same list in references/removal-gates.md.

> Replying to review comment #90401 Overlaps #90316, deferred to the follow-up PR. As you note, the behavior isn't in question; neither sentence is false, and this PR now only takes fixes for false statements or data loss. Follow-up, in `usage()` of `skills/audit-git-checkouts/scripts/audit-checkouts.sh`: "Branch refs and stashes in the audited checkouts themselves are never deleted. Removing a worktree also deletes its submodules' Git directories, including their local branches, tags, and stash, once the refusals above find nothing only that submodule holds." Use the same list in `references/removal-gates.md`.
jercik merged commit 3629cec6bc into main 2026-09-25 21:52:30 +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!82
No description provided.