Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

loose ends

Your repo is keeping notes you will never read again — and it will not tell you which ones.
loose-ends finds every one of them, including the ones ls and git ls-files cannot see,
explains in plain words why each is loose, and turns the pile into a short list of decisions.

read-only until you say otherwise  ·  no dependencies  ·  no network  ·  no API key  ·  no model in the scan

The loose-ends terminal reviewer: one decision per row, a colour-coded verdict on the left, a looseness meter, and a detail pane explaining exactly why the selected file is loose.

One decision per row. The verdict is already filled in. The pane at the bottom says why.


Contents


Thirty seconds on why this exists

Work gets interrupted. You need information you don't have yet, or the shape of the problem changes, or it's late. The responsible thing is to write down where you got to — handoff.md, plan.md, parked-findings.md, NIGHT-REPORT.md, task-7-brief.md — so the thinking isn't lost.

That's good practice. It is also, exactly, how the pile forms.

Every one of those files was worth writing. Almost none of them are worth keeping forever. None of them announce which kind they are. And the way modern work is organised makes each one multiply:

  • Worktrees overlap. A linked worktree is a whole second checkout. Its docs/ is not your docs/. A note written in one is invisible from the other, and worktrees outside the repo directory are invisible from everywhere.
  • Branches hold notes hostage. The plan you wrote on spike/new-parser is still there, on a branch you moved off in March. It doesn't appear in your editor, your file search, or git ls-files on main.
  • Task lists drift apart. todo.md on the branch and todo.md on main are now different files with the same name and 60% of the same headings, and the merge that reconciles them is one nobody schedules.
  • Notes go stale silently. A plan naming Persistence.swift, AppStore.swift and STATUS.md still reads as authoritative long after two of those files stopped existing. Nothing marks a document as describing a repo that has moved on.
  • Coding agents made all of this an order of magnitude worse. One overnight run leaves thirty briefs and reports in a gitignored directory you never open. Ten runs leave three hundred.

So the problem was never that these files exist. It's that you cannot tell them apart without opening all of them — so nobody opens any of them, and the pile compounds, and the one paragraph you would genuinely want back is somewhere in it.

loose-ends is the thing that opens all of them for you and hands back a short list of decisions.


What one real run found

This is the repo the tool was built in — a Mac + iOS app, four worktrees, an .superpowers/ directory full of agent output, months of plans and reports:

  loose ends — delightful_note_experience on main
  ────────────────────────────────────────────────────
   221 files read across worktree, branches, scratch dirs
    28 want a decision   (fold·14, later·14)
       5 of them cover 76 files at once
    91 filed — something in the repo points at them
     0 already judged   27 not work notes
  ────────────────────────────────────────────────────

221 notes in. 28 decisions out. Every number on this page comes from that run.

Here is the part that makes the case better than any adjective could. Of the 99 files it flagged as loose, this is where they were living:

where it was how many would you have found it?
a gitignored agent scratch directory 76 git ls-files returns zero of these. git status doesn't mention them.
inside a linked worktree, not on HEAD 22 one of those worktrees is a sibling directory outside the repo entirely
untracked in the working tree 1 one git clean from gone, with 8 open items in it

git ls-files '*.md' in that repo returns 125 files. The scan read 221. Ninety-six markdown notes existed that git's own file list did not contain — and the highest-scoring file that wasn't part of an agent run was that single untracked one: nothing in the repo pointed at it, no commit had ever recorded it, and it claimed eight things were still outstanding.

That's the demonstration. Not "your repo is probably messy" — this is what a normal, well-run repository looks like underneath, and no tool you already have shows it to you.


Try it on your own repo — it cannot change anything

The scan reads git objects and files. It never checks out a branch, never touches the index, never writes into your repo. The only file it produces is the one you name with --out. So the honest way to evaluate this is to just point it at something and look:

git clone https://github.com/seyonv/loose-ends ~/.claude/skills/loose-ends

cd /your/repo
python3 ~/.claude/skills/loose-ends/scripts/scan.py --repo . --out /tmp/candidates.json

Ten seconds later you get the summary block above, for your repository. If it says zero, your repo is tidy and you've lost ten seconds. If it says forty, you now know something about your project that you did not know a minute ago.

Then review them:

python3 ~/.claude/skills/loose-ends/scripts/tui.py --in /tmp/candidates.json --out /tmp/verdicts.json

Still nothing changed — that writes a list of decisions, not the changes. Applying them is a separate command that is a dry run by default. See Reference for the whole four-line pipeline, and Nothing happens until you say so for why it's built that way.

Using Claude Code or a compatible agent? Cloning into ~/.claude/skills/ is all the installation there is — then just say /loose-ends.


How it works, in one picture

Four stages. GATHER: five sources — working tree, every local branch, agent scratch directories, linked worktrees including ones outside the repo, and .claude/worktrees. SIFT: 221 files read, minus 27 that are not work notes, minus 91 that something in the repo points at, minus 4 below the looseness cutoff, leaving 99 loose files scored with reasons. DECIDE: 76 files from 5 agent runs collapse into 5 rows, leaving 28 decisions, each with one of four verdicts already suggested — delete, fold in, park, later. APPLY: a dry run by default, then one commit, recorded in a content-hash ledger; it never checks out a branch, touches the index, or rewrites history.


How it works, step by step

This is the whole idea, in order. None of it involves a language model. The scan is deterministic: two runs against an unchanged repo produce byte-identical output, which is what makes it safe to wire into a habit.


1 · Gather from five places, two of which you cannot see

A plain ls or find misses most of the pile. loose-ends reads:

  1. The working tree — tracked and untracked.
  2. Every local branch, looking for files that exist there but not on HEAD. This is the note you wrote on a branch you never merged. It is read as a git blob: no branch is ever checked out, and the index is never touched.
  3. Agent scratch directories.superpowers/, .agent/, .cursor/, .aider/. These are usually gitignored, which means git ls-files cannot see them at all, so they are walked directly from disk.
  4. Every linked worktree in git worktree list, including ones outside the repo directory entirely.
  5. .claude/worktrees/* — where overnight and parallel-lane agent runs leave their reports.

Note

A trap worth naming, because the first version fell into it. A linked worktree is a complete second checkout of the repo. Walking into one naïvely reports every file in your project twice. Version one did exactly that: 442 candidates, 324 of them duplicates of each other, which is worse than useless because it teaches you to distrust the list. Worktree prefixes are now skipped during the main walk and handled separately, contributing only the files that are not already on HEAD — which is precisely the set that is invisible from where you're standing.


2 · Is this even a work note?

Plenty of markdown isn't a loose end. A README is documentation. A marketing page is a deliverable. A changelog is a record. None of them is waiting on a decision.

So each file earns a work signal from what it contains, not merely where it sits: unticked checkboxes; phrases like next steps, deferred, follow-ups, working notes, handoff, where we left off, acceptance criteria, phase 2; a date-stamped filename; the shape of a plan, spec, report or queue. Anything scoring below the threshold is set aside as a document, not a candidate. On the real run that removed 27 files before any question was asked.

Files whose names a project depends on — README.md, CLAUDE.md, CHANGELOG.md, LICENSE, CONTRIBUTING.md — are never candidates, whatever they contain.


3 · The load-bearing question: is anything reading this file?

This is the part that makes the tool work, and it is worth stating plainly, because it is the opposite of what tools in this space normally do.

Most "find your TODOs" tools grep for the string TODO. That is why they hand you five hundred results and you close the window. Searching for unfinished-looking text finds every file that has ever been unfinished — which, in a working repo, is nearly all of them. The signal is drowned by the thing it's made of.

The better question isn't about a file's contents at all. It's about its position:

Does anything else in this repository refer to this file?

A plan that STATUS.md links to is filed. It has a reader, a place in some structure, and a reason to still exist — someone will arrive at it by walking the thing that points to it. A plan that nothing mentions is an orphan. It is not part of any structure. It survives purely because deleting files feels risky.

Mechanically: every tracked text file in the repo is concatenated into one string, and each candidate is checked for mentions of its own path and filename — with its self-references subtracted, so a file quoting its own name cannot vouch for itself.

That single test did most of the work. It moved 91 of 221 notes out of the way before a single question was asked. Those 91 are not deleted or hidden — --all brings them back — they are simply not what you're being asked about, because something in the repo already looks after them.


4 · How loose is it, exactly?

Orphanhood is the strongest signal but not the only one, so looseness is scored, and the reasons are shown to you in plain words rather than as a number you're asked to trust. Each signal adds:

signal weight what it tells you
not on HEAD +5 it isn't part of the current repo at all
exists only on another branch +5 it was parked with a branch you moved off
untracked +4 never committed; one git clean from gone
nothing points at it +4 no reader, no place — see step 3
only inside a linked worktree +4 invisible from the main checkout
in an agent scratch directory +3 machine-written paperwork, rarely re-read
near-duplicate of a newer file +3 the same content already exists, fresher
has unticked checkboxes +2 it claims work is outstanding
names 3+ files that no longer exist +2 it describes a repo that moved on
carries an unreviewed Parked banner +2 parked once, never revisited
45 days cold and unreferenced +1 time alone, but only when nobody reads it

Default cutoff is 5. --min-loose N moves it; --all shows everything.

Two files from the real run, to make this concrete.

tasks/2026-08-05-seven-lanes.md — not on HEAD (+5), untracked (+4), nothing points at it (+4), 8 unticked boxes (+2) = 15. Top of the list, and correctly so: it is one git clean away from vanishing and it claims eight things are outstanding.

docs/superpowers/plans/2026-07-24-phase-1-editor-core.md — on HEAD, tracked, and STATUS.md links to it = 0. Never shown. It is an archived plan sitting exactly where the index says it is. Nothing is wrong with it, and nobody needs to be asked about it.

Same kind of file. Completely different situations. Only the second question tells them apart.


5 · Group what is really one decision

An agent run that produced fourteen task briefs and fourteen reports is one thing you have an opinion about, not twenty-eight. Any scratch-run directory with four or more sibling notes collapses into a single row carrying all of them, labelled with what's inside (8 report, 3 queue, 1 plan). Judge the run; the verdict applies to every file in it.

On the real repo, 5 grouped rows covered 76 files. That is the difference between a review you finish in four minutes and one you abandon on row nine.


6 · Catch the near-duplicates

The same note often exists twice — written once, copied to a branch, edited slightly on one side. Comparing full text won't catch it, because the words drift as soon as anyone edits either copy.

Headings drift much less. So each pair of candidates is compared on the set of their headings: how many they share, over how many they have between them. At 55% or higher they are flagged as near-duplicates, and the older one is recommended for folding into the newer. The card names its twin, so you can see the pairing and judge it rather than take it on faith.


7 · Recommend something defensible

Every row arrives with a suggested verdict and a sentence saying why, so a fast pass is mostly pressing Enter. The rules are checked in this order, first match wins:

# if the file… why that default
1 already carries a Parked banner park it's a decision someone already made; don't quietly re-litigate it
2 lives in an agent scratch directory later that directory belongs to another tool — deleting there is a real deletion, not repo tidying, and should be deliberate
3 is a near-duplicate of something newer fold the fresher copy is the one to keep
4 has every box ticked and is 21+ days cold delete the work is behind it
5 is a report or night report, 21+ days cold delete it described a moment that has passed
6 names 3+ files that are gone, most of what it names delete it describes a repo that no longer exists
7 is a plan or spec, 21+ days old, boxes still unticked park see below — this one matters
8 has unticked boxes fold something in it is still live
9 exists only on a branch fold nothing on HEAD leads to it
10 is 60+ days old with nothing outstanding park park it or lose it
11 — anything else — later no strong signal, and pretending otherwise would be worse than saying so

Rule 7 is the one that earns its keep. An old plan with 71 open boxes almost always describes work that shipped and was never re-ticked. Folding those 71 lines into your standing queue would be vandalism — you'd resurrect finished work as a backlog. So the default is to park it, not fold it, and the case for reading the file before folding it is made in Working with an agent.

A suggestion is only a suggestion. Every row takes any of the four verdicts.


The four verdicts

means what actually happens
Delete spent git rm if tracked, plain remove if not
Fold in the live part belongs on a standing list appends the unticked lines — under the headings they sat below — into a destination file, beneath a dated ## Folded in from … heading, then deletes the source
Park keep it, but say so out loud writes > Parked 2026-08-08 — <reason> under the title, so next time it reads as a decision instead of a loose end
Later not now nothing, and — importantly — no ledger entry, so it comes back next run

Fold destinations are discovered, not configured. Files matching todo / status / backlog / queue / roadmap / lessons are ranked by how many checkboxes they already carry, on the theory that a file which collects work looks like a file full of checkboxes. Press t to cycle between them. A destination file is never itself offered as a candidate — a standing list is where loose ends go, so it is never one.

A fold with nothing live in it is refused, with the reason printed. A file with no unticked lines is a delete or a park; silently folding an empty block would hide that fact from you and quietly delete the source.


Nothing is judged twice

.loose-ends/ledger.json records the path, a content hash, the verdict, the date and the reason. The next scan skips anything already judged at that same hash.

The hash is what makes this behave the way you'd want. Edit a parked file and it returns for a fresh look, because it is genuinely a different file now. Leave it alone and it stays quiet forever. (Parking writes a banner, which changes the file — so the ledger records the hash of the file as it will be, not as it was, or the tool would immediately hand you back the note it had just annotated.)

The ledger is committed on purpose. A fresh clone, a teammate, and any agent working in the repo should all be able to find out what has already been decided, rather than proposing it again.


Two front ends, same verdicts

The terminal reviewer at the top of this page is the fast one — built for a pass where most rows are Enter. The browser page is the one for actually reading the notes before you decide, which is the part that matters when the decision is "delete".

The two-pane review page: on the left a queue of 28 rows with verdict chips, on the right the full text of the selected note, rendered, with the four verdict buttons beneath it and the suggestion shown as a quiet line under them.

Left is the queue, one line per decision, which you fly down with j/k. Right is the note itself — the whole thing, rendered: headings, checkboxes, tables, code. For a grouped agent run, the strip of filenames across the top flips between the twelve files inside it without leaving the row. The header of each note carries the facts that put it in the list (0 references, 24 open boxes, never committed) as tags, so why is this even here never requires a second window.

Nothing arrives pre-decided. The suggestion sits as one quiet line under the buttons — suggested Delete — a report of work already landed, 43d old — and the suggested button is outlined in dashes. Enter takes it, a takes every remaining one in the current view (and asks first). But the loud thing on the screen is always your call, and until you make it, that line reads — undecided —. A verdict you didn't choose should never look like one you did.

And you can leave in the middle. Press s and everything decided so far goes back to the terminal and is written to verdicts.json immediately; those rows lock, and the queue carries on. The footer keeps a running count — 12 sent · 6 ready to send · 10 to judge — and the terminal prints each batch as it lands. A 40-note pile does not have to be one sitting, and quitting halfway through never costs you the half you did.

  ←   6 verdicts   6 rows, 31 files so far → verdicts.json
  ←   4 verdicts  10 rows, 47 files so far → verdicts.json

--serve opens the page and waits for Finish & apply before handing back to the terminal. With no server listening the page downloads verdicts.json instead, so you can build the page, mail it to yourself, and decide on a train. It is one self-contained HTML file, light and dark, no network of any kind.

The same review page in dark mode


Nothing happens until you say so

apply.py is a dry run unless you pass --apply. It prints every step it would take, and separately, everything it cannot do.

The apply dry run listing every delete, fold and park it would perform, ending with 'Nothing changed. Re-run with --apply to do it.'

  • It never rewrites a branch's history. A file living only on a dead branch is reported under needs you, with the one command to run there. It does not check out fourteen branches in order to be helpful.
  • It never switches branches or touches the index while scanning. Branch content is read as blobs.
  • Applying is one commit, listing what was deleted, folded and parked, with the ledger updated in the same change — so the whole thing is one git revert away from never having happened.
  • Folding is destructive to the source. It appends, then deletes. If you are not sure the append captured what mattered, park it instead; park is free.

Reference

Install

git clone https://github.com/seyonv/loose-ends ~/.claude/skills/loose-ends

Then in any repo, /loose-ends — or run it by hand:

S=~/.claude/skills/loose-ends/scripts
W=$(mktemp -d)

python3 $S/scan.py  --repo . --out $W/candidates.json               # read-only
python3 $S/tui.py   --in $W/candidates.json --out $W/verdicts.json  # your judgment
python3 $S/apply.py --repo . --verdicts $W/verdicts.json            # dry run
python3 $S/apply.py --repo . --verdicts $W/verdicts.json --apply --commit

Browser instead of terminal:

python3 $S/build_page.py --in $W/candidates.json --out $W/loose-ends.html \
                         --verdicts $W/verdicts.json --serve

Tip

Work in a scratch directory, not in the repo. candidates.json carries an excerpt of every file it found — that is your own writing, and it belongs nowhere near a commit.

Keys — the same letters in both front ends

terminal page
j k move move
d f p l delete · fold · park · later same
Enter accept the suggestion same
a accept every remaining suggestion same, but asks first
t cycle the fold destination same
u undo the last verdict
s send what is decided so far
[ ] previous / next file inside a run
14 to judge · ready · sent · all
/ filter the queue by path
? the key list, on screen
v open the file in your pager — (the note is already open)
w write the verdicts and quit
q quit without writing a thing

Flags

--min-loose N move the looseness cutoff (default 5)
--min-signal N move the work-note threshold (default 3)
--all include the filed files too
--remotes also scan origin/* branches
--no-ledger re-ask about everything, ignoring past verdicts

Python 3.8+, standard library only.


Working with an agent

Run as a skill, the split is clean: the mechanical parts stay mechanical, and the agent does the part that requires actually reading — checking that each fold's extracted lines are really what should move.

That check is the whole reason rule 7 exists. A file with 71 unticked boxes can be 71 live commitments or 71 lines of finished work nobody re-ticked, and no amount of pattern matching can tell those apart — but something that reads the plan and looks at the repo can. The skill's instruction is explicit about it: never apply a fold of more than about twenty lines without having read the source, and when the extraction is wrong, say so and recommend park or delete instead.

Everything before --apply is read-only, which is exactly what makes it safe to hand an agent a repository you have not looked at in a month.


Questions people actually ask

Is this just grep -r TODO? No, and that difference is the entire design. Grepping for unfinished-looking text finds every file that has ever been unfinished. This asks whether anything in the repo is reading the file — a question about position, not contents. On the real run that one test set aside 91 of 221 files as already looked-after. See step 3.

Will it delete something I wanted? It cannot delete anything without --apply, and it deletes nothing you did not personally give a verdict to. The dry run prints every step first, and the applied change is a single commit.

Do I have to review all of them? No — that's what later is for, and later is deliberately not recorded, so those files come back next run. A pass where you judge the obvious ten and defer the rest is a successful pass; the ten are gone for good.

Can I stop halfway through? Yes, and it costs nothing. In the browser page, s sends everything decided so far and the terminal writes it to disk on arrival, batch by batch. Close the tab at row twelve and the twelve are still there. Even Ctrl-C in the waiting terminal keeps whatever already landed.

What if my notes live somewhere unusual? The five sources cover the common cases including gitignored agent directories and worktrees outside the repo. Anything in the working tree is read regardless of where it sits, tracked or not.

Does it phone home / call a model / need a key? None of the three. The scan is pure Python over git and the filesystem, and it is deterministic — same repo, same output, every time.

Is it safe on a repo with uncommitted work? Yes. Scanning never switches branches and never touches the index; branch content is read as blobs. Applying only ever touches the files you gave verdicts to.


Why the colours

Amber, sage, bark. Nothing turns red, because none of this is an error. It's just work you haven't decided about yet — and a tool that shouts at you about your own honest notes is a tool you will run exactly once.


MIT.

About

Every repository accumulates notes nobody reads again. loose-ends finds all of them - across branches, worktrees and agent scratch dirs - explains why each is loose, and turns the pile into a short list of decisions.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages