Stop gstack-uninstall deleting nested worktrees, locked worktrees and a .gstack repo (GST-2268) (#4267)

* Stop gstack-uninstall deleting nested worktrees, locked worktrees and a .gstack repo (GST-2268)

The worktree check now walks every checkout under .gstack-worktrees/,
nested ones included, and keeps a submodule of a clean lane covered by
that lane's status. A locked worktree stops the uninstall even with
--force-worktrees. A .gstack/ that holds its own git repository is kept
with a message. The check runs again right before .gstack-worktrees/ is
deleted, so work saved while the prompt waited is kept (exit 2). The
manual steps in docs/uninstall.md no longer rm -rf the per-project
folders.

test/uninstall.test.ts is already excluded from the Windows subset by
scripts/lib/windows-curation.ts (it spawns a bin/ shebang script).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep a lane's submodule when it holds commits no remote-tracking branch has (GST-2268)

The submodule exemption trusted the lane's status, which only compares the submodule's HEAD to the recorded gitlink. Commits on the submodule's own branches or stash live in the lane's git dir and were deleted with exit 0. Uninstall now refuses such a submodule, and matches the gitlink by exact path instead of a prefix or glob pathspec.

Review-Finding: s11/outside-voice#1 reader=outside-voice
Review-Finding: s11/adversarial-native#1 reader=adversarial-native
Review-Finding: p1/specialist-batched#1 reader=testing
Review-Finding: p1/red-team#3 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Explain the submodule rule in the uninstall docs (GST-2268)

docs/uninstall.md now says a submodule inside a clean lane goes only when its commits are on a remote-tracking branch, and the changelog entry drops a mid-sentence colon.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Make every gstack-uninstall keep check run before anything is deleted (GST-2268)

The post-prompt worktree check now runs right after confirmation, before
the install root or anything else is removed, so running uninstall from
inside the gstack install no longer deletes a worktree edited during the
prompt. A .gstack/ holding its own git repository inside the install being
removed now stops the uninstall. A failing git worktree list stops it too,
even with --force-worktrees. The manual cleanup in docs/uninstall.md now
matches worktree paths by prefix, so it never removes a worktree outside
the project whose path merely contains it.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Also stop gstack-uninstall when a .gstack repo sits inside a Cursor, OpenCode, Copilot, Antigravity or Gemini install (GST-2268)

Review-Finding: p1/specialist-batched#1 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Split a run-on sentence in the uninstall release note (GST-2268)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-10 07:15:05 +00:00
1 parent 4685fa9233
commit 6c23638d6d
4 files changed
+417 -33

No files matched your search

+25 -3
View File
@@ -10,7 +10,7 @@ If gstack is installed on your machine:
This handles skills, symlinks, harness registrations (the files `gstack-harness` wrote into OpenClaw, Hermes, a Dot or another harness, from their ledgers, plus the opt-in [start hook](hosts.md#start-hook), which it turns off through the harness's own CLI), global state (`~/.gstack/`), project-local state, browse daemons, and temp files. Use `--keep-state` to preserve config and analytics; it also keeps the repo's `.gstack/` and `.gstack-worktrees/`. Use `--force` to skip confirmation.
A plain uninstall keeps the git-tracked files under `.gstack/` (such as ship policy) and removes only the ones git ignores. It stops with exit 2, before removing anything, when a checkout under `.gstack-worktrees/` has uncommitted changes, has commits no branch or tag holds, or is not a worktree this repo registered. Pass `--force-worktrees` to delete those checkouts anyway. When `.gstack-worktrees` is a symlink, uninstall removes only the link and never follows it, so the folder it points at and the worktrees there are kept. It checks only the top-level checkouts: a worktree created inside another checkout under `.gstack-worktrees/` is not checked and is deleted with its parent, so commit or stash its work before you uninstall.
A plain uninstall keeps the git-tracked files under `.gstack/` (such as ship policy) and removes only the ones git ignores. It stops with exit 2, before removing anything, when a checkout under `.gstack-worktrees/` has uncommitted changes, has commits no branch or tag holds, or is not a worktree this repo registered. Pass `--force-worktrees` to delete those checkouts anyway. Every checkout is checked, including a worktree created inside another checkout under `.gstack-worktrees/`. A git submodule inside a clean checkout is deleted with it only when every commit it holds is on a remote-tracking branch; otherwise it stops the uninstall. A locked worktree (`git worktree lock`) always stops the uninstall, even with `--force-worktrees`; unlock it with `git worktree unlock <path>` if you want it gone. If `git worktree list` fails, the uninstall stops too, even with `--force-worktrees`. The check runs again after you confirm, before anything is removed, so a checkout you edit while the confirmation prompt waits is kept: uninstall exits 2 and removes nothing. Run from inside the gstack install itself, a `.gstack/` that holds its own git repository also stops the uninstall, because deleting the install would take it along. A `.gstack/` that is its own git repository is kept, with a message saying so, because its commits may exist nowhere else. When `.gstack-worktrees` is a symlink, uninstall removes only the link and never follows it, so the folder it points at and the worktrees there are kept.
## Option 2: Manual removal (no local repo)
@@ -62,10 +62,32 @@ rm -rf ~/.gemini/skills/gstack* 2>/dev/null
rm -f /tmp/gstack-* 2>/dev/null
# 7. Per-project cleanup (run from each project root)
rm -rf .gstack .gstack-worktrees .claude/skills/gstack 2>/dev/null
rm -rf .agents/skills/gstack* .factory/skills/gstack* 2>/dev/null
rm -rf .claude/skills/gstack .agents/skills/gstack* .factory/skills/gstack* 2>/dev/null
# Browse state: removes only the files git ignores, so tracked files such as
# ship policy, and a .gstack/ that is its own git repo, stay.
git clean -fdXq -- .gstack 2>/dev/null
# Worktrees, deepest first: each goes only when it has no uncommitted changes,
# no commits a branch or tag lacks, and no checkout inside it. git refuses a
# locked one. Anything printed as kept still holds work: save it, then re-run.
root=$(git rev-parse --show-toplevel)
git worktree list --porcelain | sed -n 's/^worktree //p' | sort -r |
while IFS= read -r wt; do
case "$wt" in "$root/.gstack-worktrees/"*) ;; *) continue ;; esac
if [ -z "$(git -C "$wt" status --porcelain --untracked-files=all)" ] &&
[ -z "$(git -C "$wt" log --oneline -1 HEAD --not --branches --tags)" ] &&
[ -z "$(find "$wt" -mindepth 2 -name .git -print -quit)" ]; then
git worktree remove "$wt" || echo "kept $wt"
else
echo "kept $wt: it holds work or another checkout"
fi
done
find .gstack-worktrees -depth -type d -empty -delete 2>/dev/null || true
```
Never `rm -rf .gstack-worktrees` by hand: a checkout there can hold uncommitted
work, commits no branch has, or another worktree nested inside it, and `rm -rf`
deletes all of it. Anything the loop above keeps, check and remove yourself.
Manual removal leaves gstack's hook entries behind in `~/.claude/settings.json`
(the uninstall script removes all of them for you, including entries whose
`_gstack_source` tag was stripped). Edit that file and delete every hook whose