Git worktrees as leases that expire, with unmerged work surfaced and archived recoverably instead of stranded. Use when worktrees have accumulated, when a ticket close is blocked by the worktree retention audit, when work may have been left unmerged in a worktree, or to schedule automatic reclamation. Also owns scoped landing: commit and push ONLY named files onto origin/main from any dirty checkout, worktree, or branch via `run.sh land`.
Scanned 9/11/2026
Install to Claude Code
npx -y skills add grahama1970/agent-skills --skill ops-worktrees --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ops Worktrees?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/grahama1970-ops-worktrees)More formats (shields.io, HTML) on the badges page.
---
name: ops-worktrees
description: >
Git worktrees as leases that expire, with unmerged work surfaced and archived
recoverably instead of stranded. Use when worktrees have accumulated, when a
ticket close is blocked by the worktree retention audit, when work may have
been left unmerged in a worktree, or to schedule automatic reclamation. Also
owns scoped landing: commit and push ONLY named files onto origin/main from
any dirty checkout, worktree, or branch via `run.sh land`.
triggers:
- commit and push
- commit and push only relevant files
- land these files on main
- push to main from a worktree
- scoped commit
- git commit push
- gcp
- worktree sprawl
- clean up worktrees
- too many worktrees
- worktree audit failed
- did I forget to merge
- unmerged work
- stranded commits
- archive worktree
- reap worktrees
allowed-tools: Bash
metadata:
short-description: Worktree leases, unmerged-work detection, recoverable archive
provides:
- ops-worktrees
composes:
- cleanup
- agentic-evals
disciplines:
- developer-tooling
- observability-operations
---
# ops-worktrees
Worktrees accumulate because creation is automated and reclamation is not. In
this repo three skills run `git worktree add` and none has ever run
`git worktree remove`; `cleanup_worktree.py` and `audit-worktrees.sh` both
classify and explicitly delete nothing, and no cron ran either. The count could
only go up: 183 registered, 8 created in a single day.
Two distinct losses come out of that, and they need different fixes.
**Sprawl** is worktrees nobody reclaims. **Stranding** is work that was never
merged — commits sitting in a worktree that no branch name leads back to. The
second is the expensive one, and refusing to delete it does not fix it: the
work is not deleted, it is simply never found again.
## A dirty tree is not a finding
Stop re-deriving this. In `~/workspace/experiments/agent-skills`, the primary
checkout, `git status` is expected to be dirty and local `main` is expected to
be diverged. Measured 2026-08-14: 192 ahead / 332 behind `origin/main`, 375
files differing from local `HEAD`. That is the normal steady state, not damage
and not something to reconcile.
The reason: work lands on `origin/main` by plumbing (`read-tree` ->
`update-index` -> `write-tree` -> `commit-tree` -> `push <sha>:main`) because
several cron lanes write tracked files in this same checkout mid-run. Local
`HEAD` is therefore stale by design, and every file that moved on the remote
shows as modified or deleted locally.
So a bare `git status` answers a question nobody asked. The only question that
matters is whether the files YOU touched differ from the remote:
```bash
git fetch -q origin main
git diff --stat origin/main -- <the paths you edited>
git log --oneline origin/main..HEAD -- <the paths you edited> # must be empty
```
Rules:
- Do not `git add -A`, `git commit -a`, `git stash`, `git checkout .`, or
`git reset --hard` in this checkout. Another lane's uncommitted state is in
there, and a running job may be mid-write.
- Do not "clean up" untracked files. `sparta_metrics.py` was untracked, a
cleanup swept it away, and ArangoDB health checks failed for five days.
- Do not report tree dirtiness as a problem, a blocker, or a finding. Report it
only if a file you were asked to change is unexpectedly modified by something
else.
- Generated bulk is ignored, not deleted: `skills/dogpile/local/search-runs/`
(62,265 files), `skills.pre-symlink-*/`, `site/.next.stale-*/`. That cut the
untracked count from 67,807 to 475. An agent reading a 67,807-line status
learns nothing and starts guessing.
If the tree looks alarming, run the audit and read its verdict instead of
inferring one:
```bash
skills/ops-worktrees/run.sh audit --json
```
## Agent dispatch: worktrees are allowed again, via `wt` only
The worktree ban existed because agents forget the finish steps (merge, remove,
branch delete) 100% of the time. The fix is not remembering — it is making
cleanup nobody's job but the cron's.
- **Create/switch/finish through [worktrunk](https://github.com/max-sixty/worktrunk)**
(`wt`, installed at `~/.local/bin/wt`, v0.75.0). `wt switch -c <branch>` to
dispatch, `wt merge main` or `wt remove` to finish. Raw `git worktree add`
stays banned.
- **Agents are not trusted to clean up.** The scheduler job `worktree-reap`
runs `scripts/reap_worktrees.sh` hourly (`15 * * * *`) with
`WORKTREE_REAP_APPLY=1 WORKTREE_GRACE_DAYS=3`: landed worktrees are removed,
unmerged work is bundle-archived, active/dirty/in-TTL trees are kept. An
agent that forgets its worktree loses nothing and strands nothing.
- **Merged-ness caveat in this repo:** local `main` is permanently diverged
(plumbing pushes), so `wt list`'s `main_state` reads `would_conflict` for
everything. The reaper's landed check uses `@{upstream}..HEAD` /
`origin/main..HEAD`, not `wt`'s local-main comparison. Trust the reaper's
classification, not `wt list` merged-ness, here.
## Commands
```bash
skills/ops-worktrees/run.sh land -m "msg" <path>... # scoped landing on origin/main
skills/ops-worktrees/run.sh unmerged # what work never reached origin/main
skills/ops-worktrees/run.sh reap # preview: remove / archive / keep
skills/ops-worktrees/run.sh reap --apply # act on it
skills/ops-worktrees/run.sh archive <path> # archive one worktree
skills/ops-worktrees/run.sh backlog # classify pre-lease worktrees
skills/ops-worktrees/run.sh register <path> --purpose <why>
```
## Scoped landing: `land`
`run.sh land -m "message" <paths...>` is the ONLY sanctioned way for an agent
to commit-and-push. It lands exactly the named paths onto `origin/main` by
plumbing (`read-tree origin/main` -> scoped `add` -> `write-tree` ->
`commit-tree -p origin/main` -> `push <sha>:main`), from any dirty checkout,
worktree, or branch, without switching branches, stashing, or staging anything
else. It refuses repo-wide pathspecs (`.`, `-A`, `*`), retries push races, and
verifies the commit is an ancestor of `origin/main` before reporting success.
After landing from a worktree, do NOT merge or delete the worktree yourself —
the hourly reaper classifies it as landed and removes it. There is no separate
"git commit push" skill; this command plus the reaper is the whole contract.
## Three dispositions, never two
A two-way remove/keep split sends everything uncertain into `keep`, which
preserves nothing — the work stays unfindable and the sprawl stays.
| disposition | when |
| --- | --- |
| `remove` | provably clean, landed, lease expired, owner process gone |
| `archive` | dirty, unmerged, unprovable, or unregistered past the grace period |
| `keep` | owner PID alive, or inside its TTL / grace |
## Archiving preserves; moving does not
A moved directory is not preservation — whoever deletes that folder later
deletes the work. Archiving:
1. WIP-commits dirty files, so uncommitted work is preserved **in git** rather
than in a tarball nobody will open.
2. Bundles the unmerged commits with `git bundle`, restorable into any clone
independently of the directory.
3. **Verifies the bundle**, then unregisters. Verifying afterwards means
discovering the bundle was empty once the tree is already gone.
4. Writes `MANIFEST.json` carrying the exact restore command.
Bundle the **branch**, never `origin/main..HEAD`: a range ending in HEAD records
no named ref, so the bundle verifies and then cannot be fetched by name. A
verified archive nobody can restore is worse than no archive.
Restore:
```bash
git -C <repo> fetch /mnt/storage12tb/worktrees/deprecated/<name>/<name>.bundle \
<branch>:recovered/<branch>
```
## Every rule is a refusal
Deleting an unpushed day of work is unrecoverable in a way that leaving a
directory is not, so the reaper defaults toward keeping:
- a live owner PID is never touched — work in flight
- a dirty tree is never removed — uncommitted changes are the work
- commits not reachable from the remote are never removed — `git status` cannot
see committed-but-unpushed work
- inside the TTL is never touched — a job between steps looks identical to one
that finished
- unregistered inside the grace period is never touched — no owner is recorded,
and something created two days ago may still be in use
This matters concretely: of 181 pre-lease worktrees, 48 were dirty and 9 held
unpushed commits. An age-based "delete old worktrees" script would have
destroyed all 57.
## Leases
Whoever creates a worktree records owner PID, purpose and TTL, so a later
reaper knows whether the work is abandoned. `project-watchdog` and
`orchestrate` register automatically. Registration failure never fails the work
that needed the worktree.
## Scheduling
`scripts/reap_worktrees.sh` previews by default; set `WORKTREE_REAP_APPLY=1` to
act. Without a schedule the count only grows — that absence is the original bug,
not an oversight to repeat.
## Proof boundary
Tests run against a real repository with a real remote, because unpushed work
is undetectable without one. The archive tests assert the work comes **back**:
a committed file and a never-committed file are both fetched out of the bundle.
Preserving is only real if the work returns.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!