This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new 3099d25  feat(setup-steward): verify check 8c — stale agent-worktrees 
under .claude/worktrees/ (#413)
3099d25 is described below

commit 3099d259bba67c91805be5218cd89c577a217bf5
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sun May 31 13:32:00 2026 +0200

    feat(setup-steward): verify check 8c — stale agent-worktrees under 
.claude/worktrees/ (#413)
    
    Stale worktrees the agent (or a prior session) created under
    .claude/worktrees/ are a real friction source: they hold branches
    (typically `main`, since EnterWorktree defaults to branching from
    main), so a subsequent `git checkout main` from the main checkout
    fails with "main is already used by worktree at …" — silently, in
    the middle of longer command pipelines, producing confusing
    downstream failures. A session that ended without explicit
    `ExitWorktree(action: "remove")` leaves the worktree on disk and
    the next session has no way to know it is abandoned.
    
    Concretely observed on 2026-05-30: a worktree
    `.claude/worktrees/cozy-hugging-lerdorf` from an earlier session
    blocked `git checkout main` for an entire 8-hour session, causing
    multiple downstream commands to silently retain a stale base
    branch. Each `/setup-steward verify` run that session would have
    caught the stale worktree — *if* the check existed.
    
    The new check 8c:
    
    1. Filters `git worktree list --porcelain` to entries under
       `.claude/worktrees/`.
    2. Computes age as the max of directory mtime and most-recent
       commit timestamp (defends against both failure modes — old
       commits + fresh files, fresh commits + old files).
    3. Buckets against a 7-day threshold (overridable via
       `worktree_stale_days` in `<project-config>/setup-steward.md`).
    4. Surfaces ⚠ for stale + clean (propose `git worktree remove
       <path>`) and ✗ for stale + dirty (operator decision required,
       never auto-force).
    
    Main-checkout only — `git worktree list` is the same across the
    family. Read-only — proposes cleanup but never executes it.
    
    The 7-day default is calibrated against the realistic use case:
    agent-worktrees are per-task isolation, designed for open / work /
    close. >7 days is overwhelmingly an unclosed session. Lower
    thresholds hit false-positive on multi-day tasks; higher
    thresholds let the bug class persist long enough to actually break
    a `git checkout main` weeks later.
---
 .claude/skills/setup-steward/verify.md | 85 ++++++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)

diff --git a/.claude/skills/setup-steward/verify.md 
b/.claude/skills/setup-steward/verify.md
index ddbbd8a..21fd1aa 100644
--- a/.claude/skills/setup-steward/verify.md
+++ b/.claude/skills/setup-steward/verify.md
@@ -302,6 +302,79 @@ is layered: `/setup-steward` writes during adopt/upgrade,
 (check 8 there), and this check is the cheap static cross-check
 to surface drift between the two skill families.
 
+### 8c. Stale agent-worktrees under `.claude/worktrees/`
+
+Detect worktrees the agent (or a prior session) created under
+`<repo-root>/.claude/worktrees/` that have been left lying around
+beyond their useful life. **Main-checkout only** — worktrees can
+only be inspected from the checkout that owns them, and the
+`git worktree list` output is the same across the family anyway.
+
+Stale agent-worktrees are a real friction source: they hold
+branches (typically `main`, since `EnterWorktree` defaults to
+branching from `main`), so a subsequent `git checkout main` from
+the main checkout fails with *"main is already used by worktree
+at …"* — silently, in the middle of a longer command pipeline,
+producing confusing downstream failures. A session that ended
+without explicit `ExitWorktree(action: "remove")` leaves the
+worktree on disk; the next session has no way to know it is
+abandoned.
+
+The check:
+
+1. Run `git worktree list --porcelain` and filter to entries
+   whose `worktree` path is under `<repo-root>/.claude/worktrees/`.
+2. For each, compute the **age** — the maximum of:
+   - the worktree directory's `mtime` (file-system signal — how
+     long since anything inside changed); and
+   - `git -C <worktree> log -1 --format=%cI HEAD`'s commit time
+     (git-state signal — how recent the latest commit on the
+     worktree's branch is).
+
+   The max-of-two avoids two failure modes: a worktree whose
+   commits are old but whose files were touched recently (still
+   active) and a worktree whose files are old but whose branch
+   was recently rebased (still in use). Both look fresh to one
+   of the signals alone.
+
+3. Bucket the result against a threshold (default: **7 days**;
+   adopter override via `worktree_stale_days` in
+   `<project-config>/setup-steward.md` — if absent, default
+   stands):
+   - ✓ if age ≤ threshold
+   - ⚠ if age > threshold AND the worktree has zero
+     uncommitted changes (`git -C <worktree> status --porcelain`
+     is empty) — surface the path, age, branch name, and
+     propose `git worktree remove <path>` as the cleanup.
+   - ✗ if age > threshold AND the worktree has uncommitted
+     changes — surface the same info plus an explicit
+     *"uncommitted changes present"* warning, and propose
+     two-step cleanup: first commit-or-stash, then
+     `git worktree remove --force <path>` (or
+     `EnterWorktree(path)` to enter it interactively and
+     decide).
+
+4. The check is **read-only**: it never auto-removes a
+   worktree, never force-anything. The proposal lands in the
+   verify-report and the operator chooses to act.
+
+**Threshold rationale.** Agent-worktrees are designed for
+per-task isolation: open, work, close. A worktree older than
+7 days is overwhelmingly a session that ended without explicit
+cleanup. Lower thresholds (3 days, 1 day) hit false-positive
+on multi-day tasks that legitimately stretch across sessions;
+higher thresholds (14, 30 days) let the bug class persist
+long enough to actually break a `git checkout main` weeks
+later.
+
+**Why this check exists separately from worktree-init.**
+`worktree-init` wires up a newly-created worktree. There is
+no symmetric step for end-of-life: `EnterWorktree(action:
+"remove")` from inside a session removes it cleanly, but
+sessions that crash, get interrupted, or end via context-
+window-exhaustion leak. This check is the periodic cleanup
+sweep that catches the leakage.
+
 ### 9. Project documentation mentions the framework
 
 Two files to check (per
@@ -346,5 +419,17 @@ list, ordered most → least urgent:
 - ✗ on check 4 / SHA-512 mismatch → **investigate first**;
   do not run upgrade until you understand why the
   released zip changed under the same version.
+- ⚠ on check 8c (stale agent-worktree, no uncommitted
+  changes) → `git worktree remove <path>` per the
+  per-worktree proposal in the report. Idempotent; safe to
+  batch across all flagged worktrees in one pass.
+- ✗ on check 8c (stale agent-worktree, **uncommitted
+  changes present**) → operator decision required. The
+  proposal lists each affected worktree with its branch
+  + diff summary; recover via `EnterWorktree(path)` (or
+  `cd <path>` outside the harness) to inspect, then either
+  commit / push or stash, then `git worktree remove --force
+  <path>`. Never propose `--force` without first
+  surfacing the diff.
 - All other ✗ / ⚠ → name the gap, give the one-line
   remediation.

Reply via email to