This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady.
What I Built
sloth-tidy — a local file organizer that tidies a messy folder overnight, files it the way you file it (not the way a generic tool guesses), and gets better every time you tell it it's wrong.
It is deliberately not the first "point a local LLM at Downloads" tool. Plenty shipped this month (@tahary's daemon-style SlothTidy, @sufian_khan's Messimo, and more). In every one of them the same failure shows up after a few nights: the model files your invoices the way an average user would, and it commits the same mistake on night 14 that it made on night 1. You unfile it, life moves on, the tool learns nothing.
sloth-tidy is built around three mechanisms that close that loop:
-
Habit profiling. Before classifying anything, a read-only scanner walks the folders you already organize yourself —
~/Documents, whatever you keep tidy — and learns your taxonomy, your naming style, and your filename habits. If your folder is calledWork & Career, the model is never even offered the choiceWork; the allowed enum in the prompt is built from your real folder names. -
Undo-as-feedback.
sloth-tidy undodoesn't just reverse a run — every reversal is recorded as a correction, and the next run's classify prompt carries your recent corrections: "the user rejected this filing; do not repeat it." Fix it once, never again. -
Two-model consensus. Any file the primary model (
gemma3:4b) tags below 0.7 confidence also gets a second opinion (qwen2.5:3b). Agree → file it with the weaker vote's confidence. Disagree → one richer reconsider pass with both opinions in context; still shaky → honestUnsorted.
And because this thing is going to move files while you sleep, safety isn't an afterthought — it's the architecture:
-
Dry-run default. Nothing moves unless you explicitly pass
--apply. The preview is computed by the exact same destination math as apply, so the table you approve is what happens. -
Move-only.
shutil.moveis the only filesystem verb in the codebase. No delete, no overwrite, anywhere. -
Journal-first. Every move is recorded
pendingin an SQLite journal before the move and markeddone(with the final destination) after. A crash can only leave a pending row — undo refuses those. -
Never overwrites. Collisions get
-2,-3, … suffixes — during apply, and during undo. -
Undo forever.
sloth-tidy undoreverses the last run (or any run by ID) newest-first. A vanished destination is logged honestly asskipped_missing; a re-filled source spot gets the file restored beside it, never through it.
Code
Repo: https://github.com/Abhinav412/sloth-tidy
The fastest repro is three commands — a generated messy folder, previewed, then applied:
python tests/make_samples.py demo/messy-inbox # 16 messy files incl. a dupe pair
sloth-tidy run --config demo/config.yaml # dry-run plan table — nothing moved
sloth-tidy run --config demo/config.yaml --apply
…and sloth-tidy digest in the morning, sloth-tidy undo when the weekend-you disagrees with the overnight-you (that disagreement becomes the training data the next run consumes).
The part I'm most convinced by is the journal contract. A planned move is two-phase:
# mover.py — journal-first move: pending BEFORE the filesystem feels it
for fi, decision, _meta in plan.decisions:
planned = target_for(fi, decision, settings, now=now)
move_id = journal.record_pending(run_id, fi, decision, planned)
# ... then the move, then mark_done(move_id, final_dst)
Undo lives on the other side of the same coin, and writes the correction
memory the next run consumes:
# mover.py — undo_run: never combine a restoration and a lesson
for m in moves: # newest-first
if not m.dst.exists():
journal.mark_skipped_missing(m.id) # honest, not a crash
continue
restored = unique_destination(m.src) # src free -> src, occupied -> src-2
shutil.move(str(m.dst), str(restored))
journal.mark_undone(m.id)
journal.record_correction(run_id, restored, m.dst, m.category)
The category enum is enforced at inference time with Ollama's format=<json — schema-constrained decoding, not "please return JSON" hoping:
schema>
# classify.py — the model physically cannot invent a category
category_schema = properties.get("category")
if category_schema is not None and categories:
category_schema["enum"] = list(categories)
Safety has its own test file (tests/test_safety.py): never-overwrite, journal
round-trip, undo reversibility, corrections dedupe, consensus ladders.
How I Built It
Everything runs on open-weight models through Ollama,
on your laptop, with design choices tuned for small open-weight models:
-
Schema-constrained prompts. The pydantic
Decisionmodel (flat, 4 fields — category, new_name, summary, confidence) is compiled to a JSON schema with your category enum injected, passed as Ollama'sformat. Temperature 0. Invalid or off-enum output gets exactly one corrective retry that carries the real validation error, then a deterministicUnsortedfallback. Hard cap. -
First profile, then classify.
sloth-tidy profilewalks your organized directories once, caches aHabitProfile(categories, naming examples, style rules), and it merges into the prompt ahead of any manualconfig.yamllist. -
Consensus ladder, bounded. File → model A. If A is confident, no second
call — obvious files shouldn't pay for two votes. Anywhere else, model B votes;
agreement takes the minimum confidence (honest number), disagreement triggers
one reconsider pass with both opinions in context, and a persistent
disagreement lands in
Unsorted/— original filename untouched, because if we don't trust the category we shouldn't trust the invented name either. - Digest from journal, not from disk. The 2-line morning summary is built from what the journal recorded, so what you read is exactly what happened — even if users touched files since.
- No surprises in memory: corrections dedupe by
(src, dst, wrong_category), so repetitive undo cycles never balloon the prompt.
Why Does Open Innovation Matter?
For this project, open is the whole trust story:
- Nothing leaves the laptop. File contents — invoices, contracts, personal scans — are snippets sent to a localhost daemon. A cloud API means shipping your private papers to someone else's quota-policy. Here there is no network path at all in the classification loop.
- It costs nothing to run forever. An overnight organizer only creates change if you leave it running — with 4B/3B open models on Ollama, that's electricity, not a subscription.
- The habit profile is the product, and it's only possible locally. A closed service can't build "your" taxonomy, because they'd have to walk your folders. Mine reads them once, per cache day, and keeps the walk fully offline.
-
Swappable models. The config lists models, not an account: put any two
local models in
models:— tonight's consensus pair changes with two lines.
My Agent Session
The full three-day build — design decisions, the journal-first safety contract,
the consensus ladder, every test fix — is saved and embeddable:
The Lazy Payoff
The stall: my Downloads folder is the eternal triage list — every few days, ten
minutes of poking, re-filing and "where does this even belong". The payoff:
- ~16 files nightly, and after two undo cycles the same mistake never comes back
- One command, scheduled, no UI, no paid API
- The real payoff is qualitative: it's the first organizer I trust unattended, precisely because a bad filing costs one undo command and zero future nights — the undo handles it, the memory remembers it.
Prize Categories
-
Best Use of Gemma (featured, $200) —
gemma3:4bis the primary classifier in the consensus pair: schema-constrained by habit profile, corrected by user memory, second-opinioned by a smaller model.
Top comments (1)
🫤