worktree-janitor-cli 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- worktree_janitor_cli-1.0.0/LICENSE +21 -0
- worktree_janitor_cli-1.0.0/PKG-INFO +511 -0
- worktree_janitor_cli-1.0.0/README.md +485 -0
- worktree_janitor_cli-1.0.0/pyproject.toml +52 -0
- worktree_janitor_cli-1.0.0/setup.cfg +4 -0
- worktree_janitor_cli-1.0.0/src/worktree_janitor.py +1070 -0
- worktree_janitor_cli-1.0.0/src/worktree_janitor_cli.egg-info/PKG-INFO +511 -0
- worktree_janitor_cli-1.0.0/src/worktree_janitor_cli.egg-info/SOURCES.txt +10 -0
- worktree_janitor_cli-1.0.0/src/worktree_janitor_cli.egg-info/dependency_links.txt +1 -0
- worktree_janitor_cli-1.0.0/src/worktree_janitor_cli.egg-info/entry_points.txt +2 -0
- worktree_janitor_cli-1.0.0/src/worktree_janitor_cli.egg-info/top_level.txt +1 -0
- worktree_janitor_cli-1.0.0/tests/test_worktree_janitor.py +739 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jasveer Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: worktree-janitor-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Read-only inventory of a git repo's worktrees and branches — verdicts with reasons, and a cleanup plan it prints but never executes.
|
|
5
|
+
Author: Jasveer Singh
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jasveerai88/worktree-janitor
|
|
8
|
+
Project-URL: Repository, https://github.com/jasveerai88/worktree-janitor
|
|
9
|
+
Project-URL: Issues, https://github.com/jasveerai88/worktree-janitor/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/jasveerai88/worktree-janitor/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: git,worktrees,ai-agents,claude-code,cli,developer-tools
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
<h1 align="center">worktree-janitor</h1>
|
|
28
|
+
|
|
29
|
+
<p align="center"><strong>Stale worktrees pile up. Clean up without the fear — an inventory that judges every one, and a plan that never runs itself.</strong></p>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img alt="tests: 36 passing" src="https://img.shields.io/badge/tests-36%20passing-brightgreen">
|
|
33
|
+
<img alt="python 3.10 | 3.11 | 3.12" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue">
|
|
34
|
+
<img alt="version 1.0.0" src="https://img.shields.io/badge/version-1.0.0-orange">
|
|
35
|
+
<img alt="dependencies: none" src="https://img.shields.io/badge/dependencies-0-brightgreen">
|
|
36
|
+
<img alt="license: MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center"><img alt="worktree-janitor demo: a six-worktree sample repo is scanned and judged, the cleanup plan is printed, and a final git worktree list shows all six worktrees still there" src="docs/demo.gif" width="880"></p>
|
|
40
|
+
|
|
41
|
+
Parallel agents leave worktrees behind like coat
|
|
42
|
+
hangers. Every AI coding session that runs in its own
|
|
43
|
+
git worktree leaves the worktree behind when it ends —
|
|
44
|
+
headless runs leave theirs *locked*. A forgotten
|
|
45
|
+
worktree pins its branch, holds a folder of disk, and
|
|
46
|
+
may contain the only copy of some piece of work. So the
|
|
47
|
+
pile grows: nobody deletes blindly (that loses work),
|
|
48
|
+
nobody keeps track (that was the manual part), and
|
|
49
|
+
`git worktree list` gets longer every week.
|
|
50
|
+
|
|
51
|
+
worktree-janitor is the audit step that makes cleanup
|
|
52
|
+
safe. One read-only pass asks git — and only git — for
|
|
53
|
+
the facts about every worktree and local branch: dirty
|
|
54
|
+
or clean, uncommitted and untracked file counts, merged
|
|
55
|
+
into the default branch or not, unpushed commits,
|
|
56
|
+
last-commit age, locked and prunable state. Each
|
|
57
|
+
worktree gets a verdict with its reasons printed:
|
|
58
|
+
**SAFE_TO_REMOVE**, **REVIEW**, or **KEEP**. Then the
|
|
59
|
+
`plan` subcommand prints the exact cleanup commands —
|
|
60
|
+
and never runs them. There is no code path from the
|
|
61
|
+
plan to a shell; the commands are output text, and
|
|
62
|
+
running them (or not) is your decision.
|
|
63
|
+
|
|
64
|
+
**Read-only, enforced in code — not just promised.**
|
|
65
|
+
Every git invocation funnels through one gate that
|
|
66
|
+
refuses, before git starts, any argv carrying a
|
|
67
|
+
mutating verb or flag (`remove`, `prune`, `checkout`,
|
|
68
|
+
`clean`, `-d`, `--force`, …). The only commands ever
|
|
69
|
+
run are git's read-only queries, and all of them run
|
|
70
|
+
with `GIT_OPTIONAL_LOCKS=0`, so git does not even
|
|
71
|
+
refresh the index. The test suite proves it two ways:
|
|
72
|
+
every subprocess argv of a full scan + plan run is
|
|
73
|
+
recorded and asserted against the allowlist, and the
|
|
74
|
+
fixture repo's full state is byte-identical before and
|
|
75
|
+
after. The demo above ends the way every run does —
|
|
76
|
+
`git worktree list` afterwards, all six worktrees
|
|
77
|
+
still standing.
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pipx install git+https://github.com/jasveerai88/worktree-janitor # or: pip install git+https://github.com/jasveerai88/worktree-janitor
|
|
83
|
+
worktree-janitor scan # judge the repo you're standing in
|
|
84
|
+
worktree-janitor plan # the exact commands — printed, never run
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
That's the whole workflow: one pass, verdicts with
|
|
88
|
+
reasons, an exit code you can gate on — **0** = nothing
|
|
89
|
+
to clean, **1** = cleanup candidates or REVIEW items
|
|
90
|
+
exist, **2** = error. `DIR` can be any folder inside
|
|
91
|
+
any worktree; git resolves the whole repo from it. Add
|
|
92
|
+
`--json` for the same facts as one JSON object, and
|
|
93
|
+
`--stale-days N` to tune when a merged branch counts
|
|
94
|
+
as stale (default 30). From a source checkout,
|
|
95
|
+
`python3 src/worktree_janitor.py scan` does the same
|
|
96
|
+
thing. Python 3.10+, standard library only, no
|
|
97
|
+
dependencies. No repo you dare point it at? The
|
|
98
|
+
bundled sample builds the exact sprawl in the demo:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python3 examples/make_sample.py /tmp/worktree-janitor-demo
|
|
102
|
+
worktree-janitor scan /tmp/worktree-janitor-demo/repo
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## The verdicts — the bundled sample, judged
|
|
106
|
+
|
|
107
|
+
The sample repo (built by `examples/make_sample.py`,
|
|
108
|
+
the same fixture the test suite asserts) holds 6
|
|
109
|
+
worktrees, 8 local branches, and 1 stash entry. One
|
|
110
|
+
scan judges all of it:
|
|
111
|
+
|
|
112
|
+
| Worktree | Verdict | Why — git's facts, printed on the report |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `wt-dirty` (branch `feat-wip`) | **REVIEW** | 1 uncommitted change, 1 untracked file, branch not merged into `main`, 1 unpushed commit — it exists nowhere else |
|
|
115
|
+
| `wt-detached` | **REVIEW** | Detached HEAD — no branch holds its position, so merge and unpushed state cannot be verified |
|
|
116
|
+
| `wt-safe` (branch `feat-done`) | **SAFE_TO_REMOVE** | Clean, branch merged into `main`, no unpushed commits, not locked |
|
|
117
|
+
| `wt-gone` (branch `feat-gone`) | **SAFE_TO_REMOVE** | The directory is already gone and git marks the registration prunable — clearing it cannot lose files |
|
|
118
|
+
| `repo` (branch `main`) | **KEEP** | The main worktree |
|
|
119
|
+
| `wt-locked` (branch `feat-locked`) | **KEEP** | Locked by git — reason recorded by git: "agent session running" |
|
|
120
|
+
|
|
121
|
+
**2 KEEP · 2 SAFE_TO_REMOVE · 2 REVIEW.** The branch
|
|
122
|
+
inventory rides along: `feat-merged-old` (merged, tip
|
|
123
|
+
dated 2020-01-02) is **STALE** and joins the plan as
|
|
124
|
+
`git branch -d` — lowercase, so git itself refuses if
|
|
125
|
+
the branch somehow is not merged. The plan for this
|
|
126
|
+
repo is exactly 3 commands, and the 2 REVIEW
|
|
127
|
+
worktrees are never in it. The full verbatim report
|
|
128
|
+
is [below](#real-runs-verbatim).
|
|
129
|
+
|
|
130
|
+
[Quick start](#quick-start) · [The verdicts](#the-verdicts--the-bundled-sample-judged) ·
|
|
131
|
+
[How it compares](#how-it-compares) · [Reference](#reference)
|
|
132
|
+
|
|
133
|
+
## How it compares
|
|
134
|
+
|
|
135
|
+
| | **worktree-janitor** | `git worktree list` + checking each folder by hand | Deleting as you go (`git worktree remove`, `git branch -D`) | A full worktree manager (e.g. worktrunk) |
|
|
136
|
+
|---|---|---|---|---|
|
|
137
|
+
| The job | Audit before cleanup: inventory, verdicts, a printed plan | The raw list; every judgement in your head | Removal itself | Running the fleet: creating, switching, and removing worktrees all day |
|
|
138
|
+
| Executes deletions | **Never — no code path exists** | You do, one by one | Yes — that is the command | Yes, when you tell it to |
|
|
139
|
+
| Verdicts with reasons | Every worktree, from git's own records | None | None — `remove` refuses a dirty or locked worktree and says nothing about merged or unpushed state | Workflow-first; audit verdicts are not its job |
|
|
140
|
+
| Unverifiable state | Named in the report, and the worktree is REVIEW — never SAFE | Depends on you noticing | `--force` deletes it anyway | — |
|
|
141
|
+
| For agents & scripts | `--json` on both subcommands, exit codes 0 / 1 / 2 | No | No | Varies |
|
|
142
|
+
|
|
143
|
+
Full managers are excellent at their job — if you run
|
|
144
|
+
many parallel agents, a manager will create and switch
|
|
145
|
+
worktrees for you all day. worktree-janitor is the
|
|
146
|
+
step *before* anyone deletes anything: it manages
|
|
147
|
+
nothing, creates nothing, and switches nothing. It
|
|
148
|
+
tells you what the pile actually is.
|
|
149
|
+
|
|
150
|
+
## What it does NOT do
|
|
151
|
+
|
|
152
|
+
- **It never deletes, prunes, or removes anything.**
|
|
153
|
+
There is no `--execute` flag and there will not be
|
|
154
|
+
one — the plan being inert text is the entire design.
|
|
155
|
+
- **It never creates or switches worktrees.** It is
|
|
156
|
+
not a worktree manager; it is the audit in front of
|
|
157
|
+
whoever (or whatever) does the managing.
|
|
158
|
+
- **It does not measure disk sizes.** The report says
|
|
159
|
+
what is safe to remove, not what each removal
|
|
160
|
+
reclaims.
|
|
161
|
+
- **It never fetches.** Unpushed counts use the refs
|
|
162
|
+
on your machine; their freshness is your last
|
|
163
|
+
`git fetch`, which stays your decision.
|
|
164
|
+
- **It cannot see what git cannot see.** Worktree
|
|
165
|
+
stashes an editor or agent runner keeps outside the
|
|
166
|
+
repo, and folders git has no registration for, are
|
|
167
|
+
out of scope — stated, not hidden.
|
|
168
|
+
|
|
169
|
+
## Honest limits (the short version)
|
|
170
|
+
|
|
171
|
+
Git-visible state only; local branches only; a plan
|
|
172
|
+
is advice computed in one pass, not a transaction —
|
|
173
|
+
a worktree can change the second after the scan, so
|
|
174
|
+
re-run `scan` after executing a plan. The full limits
|
|
175
|
+
are [below](#known-limits).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Reference
|
|
180
|
+
|
|
181
|
+
**Contents:** [The read-only promise](#the-read-only-promise-enforced) ·
|
|
182
|
+
[Verdict rules](#verdict-rules) ·
|
|
183
|
+
[What "unpushed" means](#what-unpushed-means-here) ·
|
|
184
|
+
[Usage](#usage) · [For agents](#for-agents) ·
|
|
185
|
+
[Real runs](#real-runs-verbatim) · [Tests](#tests) ·
|
|
186
|
+
[Known limits](#known-limits) · [Privacy](#privacy) ·
|
|
187
|
+
[Related](#related)
|
|
188
|
+
|
|
189
|
+
### The read-only promise, enforced
|
|
190
|
+
|
|
191
|
+
Plenty of tools *say* read-only. This one is built so
|
|
192
|
+
the mutating path does not exist:
|
|
193
|
+
|
|
194
|
+
- The only git commands ever invoked are
|
|
195
|
+
`worktree list --porcelain`, `status --porcelain=v1
|
|
196
|
+
--branch`, `branch --format / --merged`,
|
|
197
|
+
`log` (tip facts and range counts), and `stash list`.
|
|
198
|
+
- Every invocation passes through one function that
|
|
199
|
+
refuses — before git starts — any argv containing a
|
|
200
|
+
mutating verb or flag (`remove`, `prune`, `checkout`,
|
|
201
|
+
`clean`, `-d`, `--force`, ...). The test suite records
|
|
202
|
+
every argv of a full scan + plan run and asserts each
|
|
203
|
+
one passes the gate.
|
|
204
|
+
- The cleanup commands in a `plan` are output *text*,
|
|
205
|
+
assembled as strings. There is no code path from the
|
|
206
|
+
plan to a shell. The suite also snapshots the whole
|
|
207
|
+
fixture repo (git's listings + every working-tree
|
|
208
|
+
file) around a scan and a plan and requires exact
|
|
209
|
+
equality.
|
|
210
|
+
|
|
211
|
+
### Verdict rules
|
|
212
|
+
|
|
213
|
+
- **SAFE_TO_REMOVE** — clean, branch merged into the
|
|
214
|
+
default branch, no unpushed commits, not locked, not
|
|
215
|
+
the main worktree, not the worktree the scan was run
|
|
216
|
+
from. A prunable registration (directory already gone)
|
|
217
|
+
is also SAFE: clearing it cannot lose files.
|
|
218
|
+
- **REVIEW** — something in it exists nowhere else, or
|
|
219
|
+
cannot be verified: uncommitted changes, untracked
|
|
220
|
+
files, an unmerged branch, unpushed commits, a
|
|
221
|
+
detached HEAD, an unreadable status. The exact reason
|
|
222
|
+
is always printed. REVIEW worktrees never appear in
|
|
223
|
+
the plan.
|
|
224
|
+
- **KEEP** — the main worktree, a worktree git reports
|
|
225
|
+
as locked, the worktree the scan was run from, or a
|
|
226
|
+
bare repository.
|
|
227
|
+
|
|
228
|
+
Branches get their own inventory: merged or not, tip
|
|
229
|
+
age, checked out where, upstream and unpushed count. A
|
|
230
|
+
branch that is merged, not checked out anywhere, and
|
|
231
|
+
whose last commit is at least `--stale-days` old
|
|
232
|
+
(default 30) is **STALE** and joins the plan as
|
|
233
|
+
`git branch -d` — lowercase, so git itself refuses if
|
|
234
|
+
the branch somehow is not merged. The plan never prints
|
|
235
|
+
`-D`. One sequencing subtlety is handled: a merged, old
|
|
236
|
+
branch checked out *only* in a dead (prunable)
|
|
237
|
+
registration is flagged `stale_after_prune`, and its
|
|
238
|
+
`git branch -d` is sequenced after the
|
|
239
|
+
`git worktree prune` that frees it.
|
|
240
|
+
|
|
241
|
+
The stash is reported (count + entries) because it is
|
|
242
|
+
shared across all worktrees and survives every worktree
|
|
243
|
+
removal — listed so it is not forgotten, never acted
|
|
244
|
+
on.
|
|
245
|
+
|
|
246
|
+
### What "unpushed" means here
|
|
247
|
+
|
|
248
|
+
With an upstream configured, unpushed = commits in
|
|
249
|
+
`upstream..branch`, from git's own record. With no
|
|
250
|
+
upstream, the count is taken against the default branch
|
|
251
|
+
(`default..branch`) — and the field names its basis, in
|
|
252
|
+
text and in JSON (`unpushed_basis`). If neither is
|
|
253
|
+
possible, the count is `null`, the check is named under
|
|
254
|
+
CHECKS SKIPPED, and the worktree is REVIEW, never SAFE.
|
|
255
|
+
A check that cannot be made is reported, never silently
|
|
256
|
+
passed.
|
|
257
|
+
|
|
258
|
+
Ages are computed from git's committer timestamps
|
|
259
|
+
against the moment the scan ran, and dates print in
|
|
260
|
+
UTC. Nothing is estimated and nothing is invented:
|
|
261
|
+
every number in the report is git's own.
|
|
262
|
+
|
|
263
|
+
The default branch is the main worktree's checked-out
|
|
264
|
+
branch. If the main worktree is detached, the tool
|
|
265
|
+
falls back to a local branch named `main`, then
|
|
266
|
+
`master`, and says which basis it used
|
|
267
|
+
(`default_branch_basis` in JSON); if none exists, merge
|
|
268
|
+
and unpushed checks are skipped — named in the report —
|
|
269
|
+
and affected worktrees are REVIEW, never SAFE.
|
|
270
|
+
|
|
271
|
+
### Usage
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# 0. Build the bundled sample sprawl (or use your own repo)
|
|
275
|
+
python3 examples/make_sample.py /tmp/worktree-janitor-demo
|
|
276
|
+
|
|
277
|
+
# 1. Inventory + verdicts (DIR = any folder in the repo)
|
|
278
|
+
worktree-janitor scan /tmp/worktree-janitor-demo/repo
|
|
279
|
+
|
|
280
|
+
# 2. The cleanup plan — printed, never executed
|
|
281
|
+
worktree-janitor plan /tmp/worktree-janitor-demo/repo
|
|
282
|
+
|
|
283
|
+
# 3. Drive it from a script or an agent
|
|
284
|
+
worktree-janitor scan REPO --json
|
|
285
|
+
worktree-janitor plan REPO --json
|
|
286
|
+
|
|
287
|
+
# 4. Tune staleness (default 30 days)
|
|
288
|
+
worktree-janitor scan REPO --stale-days 90
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Running with no subcommand is `scan` on the current
|
|
292
|
+
folder.
|
|
293
|
+
|
|
294
|
+
### For agents
|
|
295
|
+
|
|
296
|
+
Every run takes `--json`: one JSON object with the same
|
|
297
|
+
facts and exit codes as the text view (errors come back
|
|
298
|
+
as `{"error": ...}`, argparse usage errors included).
|
|
299
|
+
Agents: read [SKILL.md](SKILL.md) first — the verdict
|
|
300
|
+
rules at their exact edges, the JSON schema, exit
|
|
301
|
+
codes, and the honesty rules. The first of those rules
|
|
302
|
+
bears repeating here: a plan is a *suggestion computed
|
|
303
|
+
from git's facts*; running the commands is a human's
|
|
304
|
+
decision, and worktree-janitor will never do it for
|
|
305
|
+
you. An agent that executes the plan "to be helpful"
|
|
306
|
+
defeats the tool's entire design.
|
|
307
|
+
|
|
308
|
+
### Real runs (verbatim)
|
|
309
|
+
|
|
310
|
+
Building the bundled sample and scanning it. The full
|
|
311
|
+
captured outputs also live in
|
|
312
|
+
[examples/](examples/sample-report.txt) (they were
|
|
313
|
+
captured at `/tmp/worktree-janitor-demo` with the
|
|
314
|
+
checkout invocation; the installed command prints the
|
|
315
|
+
identical report):
|
|
316
|
+
|
|
317
|
+
```text
|
|
318
|
+
$ python3 examples/make_sample.py /tmp/worktree-janitor-demo
|
|
319
|
+
Created /tmp/worktree-janitor-demo/repo plus worktrees wt-safe, wt-dirty, wt-locked, wt-detached under /tmp/worktree-janitor-demo (wt-gone's folder was deleted on purpose — its registration is prunable)
|
|
320
|
+
(exit code: 0)
|
|
321
|
+
|
|
322
|
+
$ python3 src/worktree_janitor.py scan /tmp/worktree-janitor-demo/repo
|
|
323
|
+
worktree-janitor v1.0.0 — scan · repo repo
|
|
324
|
+
/tmp/worktree-janitor-demo/repo · default branch main (the main worktree's checked-out branch) · stale threshold 30 day(s)
|
|
325
|
+
6 worktree(s) — 2 KEEP · 2 SAFE_TO_REMOVE · 2 REVIEW · 8 local branch(es) · 1 stale · 1 stash entry
|
|
326
|
+
|
|
327
|
+
WORKTREES:
|
|
328
|
+
[REVIEW] /tmp/worktree-janitor-demo/wt-detached — detached @ 45bb378
|
|
329
|
+
clean · last commit 0 day(s) ago, 2026-11-04 (UTC) — "merge done"
|
|
330
|
+
why: detached HEAD — no branch holds this worktree's position, so its merge and unpushed state cannot be verified
|
|
331
|
+
[REVIEW] /tmp/worktree-janitor-demo/wt-dirty — branch feat-wip @ 46d7d33
|
|
332
|
+
1 uncommitted change(s), 1 untracked file(s) · last commit 0 day(s) ago, 2026-11-04 (UTC) — "wip work"
|
|
333
|
+
why: 1 uncommitted change(s) (staged 0, unstaged 1) — they exist only in this worktree; 1 untracked file(s) — they exist only in this worktree; branch feat-wip is not merged into main (1 commit(s) not in main); 1 unpushed commit(s) (no upstream configured — counted against the default branch main)
|
|
334
|
+
[SAFE_TO_REMOVE] /tmp/worktree-janitor-demo/wt-gone — branch feat-gone @ 45bb378
|
|
335
|
+
directory gone (prunable)
|
|
336
|
+
why: the directory is gone and git marks this worktree prunable (gitdir file points to non-existent location) — clearing the registration cannot lose files; its branch, if any, stays in the branch list below
|
|
337
|
+
[SAFE_TO_REMOVE] /tmp/worktree-janitor-demo/wt-safe — branch feat-done @ e6302bd
|
|
338
|
+
clean · last commit 0 day(s) ago, 2026-11-04 (UTC) — "done work"
|
|
339
|
+
why: clean, branch feat-done merged into main, no unpushed commits, not locked
|
|
340
|
+
[KEEP] /tmp/worktree-janitor-demo/repo — branch main @ 45bb378
|
|
341
|
+
clean · last commit 0 day(s) ago, 2026-11-04 (UTC) — "merge done"
|
|
342
|
+
why: the main worktree
|
|
343
|
+
[KEEP] /tmp/worktree-janitor-demo/wt-locked — branch feat-locked @ 45bb378
|
|
344
|
+
clean · last commit 0 day(s) ago, 2026-11-04 (UTC) — "merge done"
|
|
345
|
+
why: locked by git — reason recorded by git: agent session running
|
|
346
|
+
|
|
347
|
+
BRANCHES:
|
|
348
|
+
feat-done · checked out at /tmp/worktree-janitor-demo/wt-safe · merged into main · last commit 0 day(s) ago, 2026-11-04 (UTC) — "done work"
|
|
349
|
+
feat-gone · checked out at /tmp/worktree-janitor-demo/wt-gone · merged into main · last commit 0 day(s) ago, 2026-11-04 (UTC) — "merge done"
|
|
350
|
+
feat-locked · checked out at /tmp/worktree-janitor-demo/wt-locked · merged into main · last commit 0 day(s) ago, 2026-11-04 (UTC) — "merge done"
|
|
351
|
+
feat-merged-new · merged into main · last commit 0 day(s) ago, 2026-11-04 (UTC) — "new work"
|
|
352
|
+
feat-merged-old · merged into main · last commit 2498 day(s) ago, 2020-01-02 (UTC) — "old work" · STALE
|
|
353
|
+
feat-unmerged · NOT merged into main (1 commit(s) not in it) · last commit 0 day(s) ago, 2026-11-04 (UTC) — "unmerged work"
|
|
354
|
+
feat-wip · checked out at /tmp/worktree-janitor-demo/wt-dirty · NOT merged into main (1 commit(s) not in it) · last commit 0 day(s) ago, 2026-11-04 (UTC) — "wip work"
|
|
355
|
+
main · default · checked out at /tmp/worktree-janitor-demo/repo · merged into main · last commit 0 day(s) ago, 2026-11-04 (UTC) — "merge done"
|
|
356
|
+
|
|
357
|
+
STASH: 1 entry — the stash is shared across all worktrees; removing a worktree never touches it, and neither does this tool:
|
|
358
|
+
stash@{0}: On main: test stash
|
|
359
|
+
|
|
360
|
+
VERDICT: cleanup candidates exist — run the `plan` subcommand for the exact commands, review the REVIEW items by hand first.
|
|
361
|
+
worktree-janitor only reads: every fact above came from git's own read-only queries, nothing was executed, removed or changed — not even an index refresh (GIT_OPTIONAL_LOCKS=0). Verdicts are git's facts (clean / merged / unpushed / locked), never a guess; a check that could not be made is named under CHECKS SKIPPED and its worktree is REVIEW, never SAFE.
|
|
362
|
+
(exit code: 1)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The plan for the same repo — printed, and the sample
|
|
366
|
+
repo is still standing afterwards, untouched:
|
|
367
|
+
|
|
368
|
+
```text
|
|
369
|
+
$ python3 src/worktree_janitor.py plan /tmp/worktree-janitor-demo/repo
|
|
370
|
+
worktree-janitor v1.0.0 — plan · repo repo
|
|
371
|
+
/tmp/worktree-janitor-demo/repo · default branch main (the main worktree's checked-out branch) · stale threshold 30 day(s)
|
|
372
|
+
6 worktree(s) — 2 KEEP · 2 SAFE_TO_REMOVE · 2 REVIEW · 8 local branch(es) · 1 stale · 1 stash entry
|
|
373
|
+
|
|
374
|
+
CLEANUP PLAN — the exact commands, in order. worktree-janitor does NOT run them; copy them yourself after reading the REVIEW items:
|
|
375
|
+
|
|
376
|
+
git worktree remove /tmp/worktree-janitor-demo/wt-safe
|
|
377
|
+
# clean, branch feat-done merged into main, no unpushed commits, not locked
|
|
378
|
+
git worktree prune
|
|
379
|
+
# clears every prunable worktree registration whose directory is already gone
|
|
380
|
+
git branch -d feat-merged-old
|
|
381
|
+
# merged into main, last commit at least 30 day(s) old, not checked out anywhere — lowercase -d, so git itself refuses if the branch is somehow unmerged
|
|
382
|
+
|
|
383
|
+
After running them, re-run `python3 src/worktree_janitor.py scan` to confirm the repo is CLEAN. Branch deletions use lowercase -d: git itself refuses if a branch is somehow unmerged. There is no -D anywhere in this plan, deliberately.
|
|
384
|
+
|
|
385
|
+
NOT IN THE PLAN: 2 worktree(s) marked REVIEW — each holds something that exists nowhere else. See the scan report.
|
|
386
|
+
|
|
387
|
+
worktree-janitor only reads: this plan was computed from git's own read-only queries and printed as text. Nothing was executed, removed or changed.
|
|
388
|
+
(exit code: 1)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
And the two clean failures — not a repo, in text and in
|
|
392
|
+
`--json` (never a traceback):
|
|
393
|
+
|
|
394
|
+
```text
|
|
395
|
+
$ python3 src/worktree_janitor.py scan /tmp/wtj-plain
|
|
396
|
+
ERROR: could not read the worktree list (is this a git repository?): fatal: not a git repository (or any parent up to mount point /)
|
|
397
|
+
(exit code: 2)
|
|
398
|
+
|
|
399
|
+
$ python3 src/worktree_janitor.py scan /tmp/wtj-plain --json
|
|
400
|
+
{"error": "could not read the worktree list (is this a git repository?): fatal: not a git repository (or any parent up to mount point /)"}
|
|
401
|
+
(exit code: 2)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Tests
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
python3 -m unittest discover -s tests -v
|
|
408
|
+
# Ran 36 tests — OK
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The suite builds REAL git repositories with the git CLI
|
|
412
|
+
in temp dirs — several worktrees, dirty files, merged
|
|
413
|
+
and unmerged branches, a locked worktree, a deleted
|
|
414
|
+
(prunable) one, a stash, and a second fixture with a
|
|
415
|
+
real local upstream — and asserts the verdicts that were
|
|
416
|
+
worked out by hand in the test-suite header (6 worktrees
|
|
417
|
+
= 2 KEEP + 2 SAFE + 2 REVIEW; exactly 1 stale branch;
|
|
418
|
+
the plan is exactly 3 commands). Both parsers are pinned
|
|
419
|
+
on synthetic porcelain (locked/prunable reasons with
|
|
420
|
+
spaces, dotted and slashed branch names, ahead/behind
|
|
421
|
+
headers, detached and unborn states). Edge repos behave:
|
|
422
|
+
empty (no commits), bare, not-a-repo and git-missing
|
|
423
|
+
all end in clean exits, never tracebacks. And the
|
|
424
|
+
read-only promise is proven two ways — every subprocess
|
|
425
|
+
argv of a scan + plan is recorded and checked against
|
|
426
|
+
the allowlist, and the fixture's full state (git
|
|
427
|
+
listings + every working-tree file) is identical before
|
|
428
|
+
and after.
|
|
429
|
+
|
|
430
|
+
### Known limits
|
|
431
|
+
|
|
432
|
+
- **Git-visible state only.** Tools that keep their own
|
|
433
|
+
worktree stashes outside the repo (some editors and
|
|
434
|
+
agent runners do) are invisible to git and therefore
|
|
435
|
+
to this tool. Orphaned directories that git no longer
|
|
436
|
+
has any registration for are likewise out of scope —
|
|
437
|
+
v1.0.0 inventories what git itself records.
|
|
438
|
+
- **No disk sizes.** A worktree's on-disk weight
|
|
439
|
+
(especially `node_modules`) is not measured in
|
|
440
|
+
v1.0.0; the report tells you what is safe to remove,
|
|
441
|
+
not what each removal reclaims.
|
|
442
|
+
- **Local branches only.** Remote-tracking branches are
|
|
443
|
+
read only as upstreams; the inventory and the plan
|
|
444
|
+
concern local worktrees and local branches.
|
|
445
|
+
- **The default branch is the main worktree's branch.**
|
|
446
|
+
If the main worktree is detached, the tool falls back
|
|
447
|
+
to a local branch named `main`, then `master`, and
|
|
448
|
+
says which basis it used; if none exists, merge and
|
|
449
|
+
unpushed checks are skipped — named in the report —
|
|
450
|
+
and affected worktrees are REVIEW, never SAFE.
|
|
451
|
+
- **A plan is advice, not a transaction.** Facts are
|
|
452
|
+
gathered in one pass; a worktree can change the
|
|
453
|
+
second after the scan. Re-run `scan` after executing
|
|
454
|
+
a plan, and eyeball REVIEW items yourself — that is
|
|
455
|
+
what they are for.
|
|
456
|
+
|
|
457
|
+
### Privacy
|
|
458
|
+
|
|
459
|
+
What it touches: the git metadata and working-tree
|
|
460
|
+
status of the one repository you name (via git's own
|
|
461
|
+
read-only queries).
|
|
462
|
+
What it sends: nothing — there is no network code in
|
|
463
|
+
this tool, and the git commands it runs perform no
|
|
464
|
+
network operations (no fetch, ever).
|
|
465
|
+
What it changes: nothing — see "The read-only promise,
|
|
466
|
+
enforced" above.
|
|
467
|
+
|
|
468
|
+
## Contributing
|
|
469
|
+
|
|
470
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The ground
|
|
471
|
+
rules are the product: standard library only, local
|
|
472
|
+
only, and read-only is never weakened — a change that
|
|
473
|
+
adds an execution path for the plan will not be
|
|
474
|
+
merged. Verdicts derive from git's own records and
|
|
475
|
+
fail toward REVIEW; skipped checks are named, never
|
|
476
|
+
silent.
|
|
477
|
+
|
|
478
|
+
## Security
|
|
479
|
+
|
|
480
|
+
worktree-janitor is itself a safety tool, so its own
|
|
481
|
+
posture is simple: it cannot delete anything, its only
|
|
482
|
+
subprocess use is the gated git runner, and it has no
|
|
483
|
+
network code. See [SECURITY.md](SECURITY.md) for how
|
|
484
|
+
to report a problem.
|
|
485
|
+
|
|
486
|
+
## Related
|
|
487
|
+
|
|
488
|
+
The agent-safety trio — three read-only audits for
|
|
489
|
+
agent-heavy setups, each judging a different layer:
|
|
490
|
+
|
|
491
|
+
- [skill-vet](https://github.com/jasveerai88/skill-vet)
|
|
492
|
+
— vets a whole agent skill pack before you install
|
|
493
|
+
it, blocking only on proven stealing / spying /
|
|
494
|
+
hidden-sending combinations.
|
|
495
|
+
- [permission-audit](https://github.com/jasveerai88/permission-audit)
|
|
496
|
+
— audits a Claude Code permission allowlist chain
|
|
497
|
+
for wildcard grants, dangerous modes committed to
|
|
498
|
+
shared settings, and rules that can never fire.
|
|
499
|
+
- **worktree-janitor** (this repo) — audits what the
|
|
500
|
+
agents leave behind in git, and prints the cleanup
|
|
501
|
+
plan it will never run.
|
|
502
|
+
|
|
503
|
+
## Support
|
|
504
|
+
|
|
505
|
+
If this saves you one worktree you were about to delete
|
|
506
|
+
— or one afternoon of manual checking — consider
|
|
507
|
+
sponsoring: **https://github.com/sponsors/jasveerai88**
|
|
508
|
+
|
|
509
|
+
## Licence
|
|
510
|
+
|
|
511
|
+
MIT — see [LICENSE](LICENSE).
|