Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 6 min read

Gravedigger: I Built a Graveyard for Every File Your Repo Ever Deleted

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine What I Built Every repo has ghosts: files that lived, mattered, and were deleted. Gravedigger digs through a gi

Gravedigger: I Built a Graveyard for Every File Your Repo Ever Deleted

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

Every repo has ghosts: files that lived, mattered, and were deleted. Gravedigger digs through a git repo's history, finds them, and lays them to rest in a single offline HTML page: a moonlit graveyard of tombstones, one per notable dead file. Click a stone and a card opens with an epitaph, the file's last words, and the real facts engraved from git: birth, death, lifespan, lines at death, the commit that killed it, and the last snippet it ever held.

I pointed it at pallets/flask (5,562 commits). It found 207 deaths, ranked them, and buried the top 30: 4,910 lines, one file that lived 13 years, and 2019 as the deadliest year. There's also a Noche de Muertos theme toggle, because ghosts deserve marigolds too.

A local model (Qwen 3.5 2B through Ollama) writes the epitaphs. For example, the epitaph for docs/deploying/fastcgi.rst is "FastCGI deployment guide deleted mid-explanation", and its last words are "Still trying to convince me it's worth reading." The LICENSE file's last words are "Did not expect the end to be so brief."

Demo

Code

Gravedigger

Where deleted files rest in peace.

Gravedigger digs through a git repository's history, finds every file that was deleted, and turns the most notable ones into a tombstone graveyard: a single, self-contained HTML page. A small local LLM (Qwen via Ollama) writes each epitaph. Every number on the page is engraved from real git data, never written by the model.

Built for the Hacktoberfest Open-Source AI Challenge, Week 4: Ghost in the Machine.

What it does Exhumes deleted files from git log (one bulk pass, rename-aware) Filters noise (lockfiles, build artifacts, binaries) and files that were later resurrected Ranks the dead by notability: lifespan_days * log1p(lines_at_death), with a bonus for .py files and for files born near the first commit Groups mass graves (5+ files deleted in one commit) and caps each top-level directory so one folder can't crowd the page Writes epitaphs with a local model (qwen3.5:2b by…






Run it yourself:

pip install -e .
gravedigger path/to/any/repo -o graveyard.html --limit 30

no model installed? add --no-llm

How I Built It

How It Works
Exhume. One bulk git log pass finds every deleted path, handling renames and filenames with spaces. Files that were deleted and later resurrected are excluded.
Rank. Notability is lifespan_days * log1p(lines_at_death). The log keeps huge files from dominating, with a bonus for .py files and for files born within 30 days of the first commit.
Group. Five or more files deleted in one commit become a mass grave. Each top-level directory is capped at 3 stones so one folder can't take over the page.
Epitaphs. Qwen writes a short epitaph and last words. A quality gate rejects anything with digits, clichΓ©s, copied commit messages, duplicates, or foreign file paths. Results are retried once, then fall back to a deterministic epitaph, and are cached on disk.
Render. One self-contained HTML file with no external requests. Every repo-derived string is escaped, tombstones are real buttons, and prefers-reduced-motion is respected.
What the Ghost in the Machine Taught Me

The model should write the joke, not the facts. My first run had the 2B model write the whole epitaph. About 10 of 30 outputs had invented numbers or last words that contradicted the death. One LICENSE file supposedly lived 135,000 days. I redesigned it: the model is forbidden from writing digits, and the page engraves every real fact from git data. The jokes got a bit blander, but the page stopped lying.

One misplaced argument made everything 5x slower. With think=False inside options, Ollama silently ignored it. The model kept "thinking", and calls took about 13s instead of 2.4s. As a top-level argument to ollama.chat, it just works. Nothing errors in this case, so you only find it by measuring.

My coding agent's tool layer mangled special strings. It rewrote a literal thinking tag into placeholder text, and later turned HTML entities in my tests into raw characters. The fix both times was the same: build the string programmatically (chr(60)/chr(62), or compare against html.escape()) instead of typing it.

The phoenix file. README.md in Flask was deleted in 2013 by a revert, brought back, rewritten in 2024, and given SVG logos in 2025. It died 5+ times and always came back. It's the only file Gravedigger deliberately refuses to bury.

AI Usage
Qwen 3.5 2B (local, via Ollama): writes the epitaphs and last words at runtime.
Nemotron 3 Ultra through OpenCode: the coding agent that implemented most of the code, driven by short, scoped prompts and a project_progress.md file it read each session.

I reviewed and ran everything, and the test suite (132 tests, mocked LLM client) passes.

Why Does Open Innovation Matter?

Gravedigger only works the way it does because the model is open and local.

Private history stays private. A git history is the most honest record of a codebase, including the files nobody wants to talk about. Gravedigger runs Qwen through Ollama on your own machine, so a company repo's commit messages and code snippets never leave it. A closed API would mean sending that history to someone else's server.
I could change how the model behaves. The biggest bug in the project was a single misplaced argument: think=False inside options was silently ignored, and every call was about 5x slower (about 13s instead of 2.4s). I could find that by measuring token counts and timings on a model I control, then switch reasoning off entirely. With a hosted model, I'd have been stuck with whatever it does.
No keys, no meter. It runs offline with no API key and no per-call cost. Ranking 30 graves and writing 30 epitaphs costs nothing, so I could iterate on the prompt as many times as I needed.
Swappable and reproducible. The model is one flag (--model), so anyone can try a different local model, and a pinned model plus an on-disk cache means the same repo gives the same graveyard.
Open all the way down. The demo data is Flask's public history, the agent ran through OpenCode, and the whole thing is a small repo anyone can read, fork and point at their own project.

My Agent Session

I built Gravedigger with a coding agent (Nemotron 3 Ultra through OpenCode) over three days, driven by short, scoped prompts. The workflow that made it work:

A project_progress.md file the agent read at the start of every session: what works, what's broken, decisions made, and what to do next.
An AGENTS.md with the rules of the project.
One small task per prompt, with explicit "do not touch anything else" limits.

The agent also thrashed at times: it created pile after pile of scratch scripts while fighting a regex and some test strings. Two of its mistakes became part of the story above: its tool layer mangled a literal thinking tag and later HTML entities, so the fixes were to build those strings in code or compare against html.escape().

The Spirit of It

A deleted file is a small ghost: it lived, it was part of something, and then one commit made it disappear. Most of the time nobody notices. Gravedigger gives a few of them a stone.

Some of the stones stuck with me. setup.py in Flask's history lived from 2010 to 2023 and was only 17 lines long. The LICENSE file's last words, written by a 2B model that had no idea it was being funny, were "Did not expect the end to be so brief." And the one file the tool will never bury is README.md, which was deleted in 2013, came back, and kept dying and returning for more than a decade.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.