DEV Community

Abhinav Sankarshana Dasu
Abhinav Sankarshana Dasu

Posted on

sloth-tidy: the file organizer that files like you — and never repeats a mistake

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

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:

  1. 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 called Work & Career, the model is never even offered the choice Work; the allowed enum in the prompt is built from your real folder names.
  2. Undo-as-feedback. sloth-tidy undo doesn'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.
  3. 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 → honest Unsorted.

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.move is the only filesystem verb in the codebase. No delete, no overwrite, anywhere.
  • Journal-first. Every move is recorded pending in an SQLite journal before the move and marked done (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 undo reverses the last run (or any run by ID) newest-first. A vanished destination is logged honestly as skipped_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
Enter fullscreen mode Exit fullscreen mode

…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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The category enum is enforced at inference time with Ollama's format=<json
schema>
— schema-constrained decoding, not "please return JSON" hoping:

# 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)
Enter fullscreen mode Exit fullscreen mode

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 Decision model (flat, 4 fields — category, new_name, summary, confidence) is compiled to a JSON schema with your category enum injected, passed as Ollama's format. Temperature 0. Invalid or off-enum output gets exactly one corrective retry that carries the real validation error, then a deterministic Unsorted fallback. Hard cap.
  • First profile, then classify. sloth-tidy profile walks your organized directories once, caches a HabitProfile (categories, naming examples, style rules), and it merges into the prompt ahead of any manual config.yaml list.
  • 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:

sloth-tidy: building a habit-mirroring local file organizer (3-day build)
You

Build the sloth-tidy project per ARCHITECTURE.md: a local Ollama-powered overnight file organizer with habit-mirroring, corrections memory, and two-model consensus for the Hacktoberfest Week 3 'Slow and Steady' challenge (due Oct 25).

Agent

Research note: the Week 3 field already has 15+ entries including a same-named SlothTidy daemon. The open lane is habit-mirroring: profile what folders/naming the user already maintains, feed that into the classify prompt, and make every wrong filing a correction the model never repeats.

Agent
You

go

Agent

Day 2 = the safety layer. Journal-first contract: every planned move is recorded 'pending' in SQLite BEFORE the filesystem sees it, marked 'done' after. Undo only touches 'done' rows, newest-first.

Agent
Agent

Found and fixed during the loop: dead model B initially punished the file with a fabricated disagreement — now B's death degrades to solo-with-A. Undo consolidated into mover.undo_run so CLI and tests share one code path.

You

go ahead with day 3

Agent

Day 3: README (lead with habit-mirroring + corrections memory, not file-tidying), MIT license, .gitignore, demo walkthrough, and the #hf26challenge submission draft following the challenge template.

You

repo url set, publish the draft along with saving the agent session

Agent

Submitting this session to DEV, embedding it into the draft's 'My Agent Session' section, and publishing on the user's explicit go-ahead.

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:4b is 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)

Collapse
 
tahary profile image
taha •

🫤