Skip to content

chore(prune): add --adopt and --prune-detached, fix NameError in main() - #853

Merged
lmorchard merged 1 commit into
mainfrom
chore/prune-adopt
Sep 16, 2026
Merged

lmorchard merged 1 commit into
mainfrom
chore/prune-adopt

Conversation

@lmorchard

@lmorchard lmorchard commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Follow-up to #852. The prune script deliberately never touched UNLANDED
branches, which left no path for the abandoned-experiment case -- so the last
5 branches and 2 detached worktrees in this clone had nowhere to go.

Two escape hatches, both off by default

--adopt BRANCH prunes one named UNLANDED branch. It takes a branch name
rather than a blanket --include-unlanded switch, so abandoning work stays a
per-branch human decision and cannot happen in bulk by accident. A mistyped or
protected name exits 2 rather than silently pruning nothing.

--prune-detached removes detached worktrees whose HEAD is already an
ancestor of origin/main -- those hold no unique commits by definition.
Detached checkouts with unmerged HEADs are still skipped and reported.

One bug in the original

The detached-worktree report ignored --only, so a filtered run still printed
every detached entry. Fixed here.

(An earlier draft of this description claimed main() shipped a NameError in
#852. That was wrong -- #852 as merged is internally consistent and its
make prune-worktrees run pruned 25 branches successfully. The len(landed)
mismatch was introduced by the landed -> prunable rename in this branch
and fixed before the commit.)

The origin/main ancestor test is now a single in_main() helper shared by the
report and the detached pruner, so the two agree by construction instead of by
duplicated subprocess calls.

Verification

Defaults unchanged -- a plain run still reports 5 UNLANDED / 2 detached and
prunes nothing (0 branch(es) would be pruned). Both guard paths exit 2
(mistyped --adopt pr-80, and --adopt main). --only issue-31-wt now
narrows the detached list from 2 to 1. Dry run with all five adopted correctly
reclassifies them ADOPTED / 0 UNLANDED. ruff check and ruff format clean.

Recovery record

These commits have no remote copy once the branches are deleted, so recording
them here deliberately:

git branch 779-concurrency-durable-input-queues  f12d81cb378fff1b44c362d782d322d905a6606f
git branch feat-788-textual-tui                  cb172f189f4f4d6d67328ba8b3885febca6d0e05
git branch fix-confirmation-widgets              070c570e651f04cca835bebacabb0dc6865955a7
git branch fix/777-managed-tool-output-files     bdb605480ff1cddaf0cb48067cc037a49f7b4be5
git branch pr-807                                4ebed5b14829bfceab95f8227a0e1c33146c512c

Detached worktree HEADs, both already in origin/main: f84907d3
(issue-31-wt), e2dec038 (.Codex/worktrees/fix-779).

Each branch's work was verified present in main via a sibling branch:
#779 -> src/decafclaw/inbox.py, #777 -> tests/test_tool_output_store.py,
#757 -> reps in the eval runner. feat-788-textual-tui is an abandoned
Python-Textual alternative to the ink TUI that shipped in #847, and is also on
origin. fix-confirmation-widgets was pushed to origin in #852's session, so
it survives regardless.

Copilot AI lite review requested due to automatic review settings September 15, 2026 23:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved safety and correctness issues remain in detached-worktree pruning and branch adoption.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds opt-in branch adoption and detached-worktree pruning, while fixing stale variable references and ancestry checks.

Changes:

  • Adds --adopt validation and classification.
  • Adds --prune-detached with --only support.
  • Fixes reporting and centralizes ancestry checks.
File summaries
File Review findings
scripts/prune_agent_worktrees.py Critical (3 votes): Detached pruning can delete uncommitted work. Moderate (3 votes): The current branch is not protected during adoption validation. Nit (3 votes): Dry-run wording incorrectly says entries are pruned. Moderate (1 vote): Abbreviated SHAs can cause ancestry checks to fail.
Review details

Suppressed comments (1)

scripts/prune_agent_worktrees.py:264

  • The detached entries passed here contain only the first eight characters of each HEAD (see scan_worktrees()), which is a display abbreviation rather than a guaranteed object name. If that abbreviation is ambiguous, merge-base --is-ancestor fails and this already-landed detached worktree is incorrectly skipped. Keep the full SHA for ancestry checks and abbreviate it only when printing.
    for path, sha in detached:
        if not in_main(sha):
  • Files reviewed: 1/1 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +267 to +269
out = subprocess.run(
["git", "worktree", "remove", "--force", str(path)],
cwd=REPO_ROOT,
if unknown:
print(f"error: --adopt names no such local branch: {', '.join(unknown)}")
return 2
protected = [b for b in args.adopt if b in PROTECTED]
Comment on lines 215 to +217
if detached:
print(f"\n=== DETACHED worktrees ({len(detached)}) - not pruned, review by hand ===")
note = "pruned below" if prune_detached else "not pruned, review by hand"
print(f"\n=== DETACHED worktrees ({len(detached)}) - {note} ===")
The initial script deliberately never touched UNLANDED branches, which left no
path for the abandoned-experiment case: decafclaw still had 5 branches holding
work that landed via a sibling branch (verified by artifact, not by ancestry)
plus 2 detached worktrees sitting at commits already in origin/main.

- --adopt BRANCH prunes one named UNLANDED branch. It takes a branch name
  rather than a blanket --include-unlanded switch, so abandoning work stays a
  per-branch human decision and cannot happen in bulk by accident. Mistyped or
  protected names exit 2 instead of silently pruning nothing.
- --prune-detached removes detached worktrees whose HEAD is already an ancestor
  of origin/main (no unique commits by definition). Detached checkouts with
  unmerged HEADs are still skipped and reported.

Also fixes one bug in the original: the detached report ignored --only. The
origin/main ancestor test is now one in_main() helper shared by the report and
the detached pruner, so they agree by construction.

Defaults are unchanged: a plain run still reports 5 UNLANDED / 2 detached and
prunes nothing. Verified dry-run and both guard paths (exit 2).
@lmorchard
lmorchard merged commit 9273f73 into main Sep 16, 2026
2 checks passed
@lmorchard
lmorchard deleted the chore/prune-adopt branch September 16, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants