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.
@@ -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).