This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine.
What I Built
Every repository is quietly becoming a graveyard. Files nobody remembers writing. Tests nobody dares delete. TODOs aging like gravestones.
haunt is a ghost for that graveyard — and it lives in git, not in a terminal pasted on top. Two small rituals, entirely offline:
pip install . # then, inside any git repository:
haunt install # the whisper joins your post-commit hook
git commit -m "tomb, again"
# 🕯️ Ah, the echoes of lost brilliance in those dusty files, a symphony of
# potential forever unplayed. The phantom hand of progress reaches out
# to erase... but will it leave behind anything but shadows?
haunt scan # which files are the most haunted, and why
haunt report --html # a narrated HAUNTINGS.md of the worst hauntings
haunt persona ancestor # swap the voice: victorian, poltergeist, ancestor
Three design choices make it a ghost in the machine rather than an AI plugin:
-
The deterministic score comes first. Every tracked file gets a haunting score computed from
git logalone — no model, fully explainable:
dormant = min(age_days/365, 1) × min(size/500, 1) (w 0.35)
restless = min(churn_90d/20, 1) (w 0.25)
orphaned = 1 if every author inactive 180d+ (w 0.25)
debt = min((TODO FIXME HACK XXX)/10, 1) (w 0.15)
Scores name their ghosts: 🫥 Orphan spirit (all authors gone), 🕸️ Sleeper (old, quiet), ⚡ Poltergeist (high churn, many hands, no owner), 🕯️ Revenant (dormant for months, then suddenly touched).
The model only narrates; it never reads your code. Gemma 2 2B (open weights, via Ollama) receives metrics and commit subjects — never source files (
send_snippetsis off by default). A 2B model can't review code, so haunt never asks it to. It asks for what a 2B model is actually good at: voice.The whispers never block a commit. One generate attempt inside a time budget; if the model is cold, busy, or absent, a deterministic template keyed to the commit hash takes over — same commit, same line, forever. No retries, no queues. The hook always exits 0.
The demo (all captured live, CPU-only laptop, today)
haunt scan on pallets/click — a 14-year-old, very-much-alive open-source CLI library — finds 175 haunted files. The top haunting:
score type age churn90 authors debt loc file
0.59 🫥 Orphan spirit 354d 0 0/13 0 782 tests/test_context.py
Thirteen authors. All of them inactive. 354 days without a commit. The file has a funeral attended by no one. The full tally: 84 orphan spirits, 81 sleepers, 10 poltergeists — a repo at rest that nobody would call haunted until you read the blame.
And with the ancestor persona — built for Día de los Muertos, the week's truth that this Halloween stretch is also about honoring the past:
🪔 "Their efforts bring warmth and joy to these old halls. May their spirits find solace in the stories shared through this code."
The poltergeist handles the noisy places:
⚡ "More flickering, more ghosts. Just don't break a fingernail."
Graceful degradation, demonstrated by literally killing Ollama mid-session and committing:
🕯️ A chill passes through the repository. The commit is buried without ceremony.
Same line twice, deterministically keyed to the commit hash — and the hook cost the commit nothing.
And when the report narrates with Gemma — here is haunt's HAUNTINGS.md on that same repo, Victorians mourning real files:
age 354d | churn90 0 | authors 0/13 | TODO/FIXME 0 | 782 loc"The pages flutter, whispering tales of what could have been. An echo of promises left unfulfilled, a spectral symphony in the grand ballroom of code. Time slips away like dust, leaving only emptiness where brilliance once resided. Give this ghost a purpose: delete, add a test, document it, or assign an owner to finally give it meaning, however fleeting."
The model honestly ends with what the metrics justify — every narration closes with a grounded directive, not vibes. On a lonely orphaned doc it got almost tender:
"Dust motes dance in the flickering light, each one a memory of a lost chapter, a discarded dream. So many stories whispered between lines of code, now gathering dust like forgotten souls. A quiet voice asks: 'Shall I erase its echo or preserve its fragile remnants?' Name an owner for this forlorn document; at least grant it some solace."
How I Built It
Python 3.13, typer, httpx, and raw git subprocesses. No GitPython, no LLM framework. The repo is here; MIT licensed; 24 tests pass, including a test that runs a real git commit end-to-end through the installed hook and asserts the whisper appears.
-
One-pass history. A single
git log --format=<sep>%H<sep>%at<sep>%an<sep>%s<sep> --name-onlyfeeds the entire scanner — records joined on\x1e/\x1funit separators. Small saga: Python'sstr.splitlines()will eat your\x1eas a line boundary and silently shred every record;split("\n")keeps the ghosts intact. -
Gemma 2 2B with intent.
num_ctxpinned at 4096 — Ollama's silent default of 2048 clips the persona+stats prompt without telling you.keep_alive=30mand an install-time warm-up so the first whisper fits its budget. -
Idempotent hook surgery.
haunt installwrites a marker block (>>> HAUNT BEGIN … <<< HAUNT END) with an absolute interpreter path and aPYTHONPATHfallback topython -m haunt— the hook works in any repo withoutpip install— and backs up any existing hook it finds. - Classification precedence found by tests, not vibes. My first rule made Sleeper unreachable: any file old enough was already an orphan. Ship order is Orphan → Revenant → Poltergeist → Sleeper, with Sleeper firing at ~90 days quiet while its author is still around — the exact limbo of files everyone assumes someone else maintains.
Three posts from the community shaped these calls: @enjoy_kumawat's install-and-timeout postmortems (hooks that "ran" but never actually run), @lowspecai and @studiomeyer_io on what local inference really costs on CPU, and @bartrozycki on how retries and fallbacks compound into their own failure mode — which is why haunt has exactly one attempt and one fallback, no interaction between them.
Then reality kept the honesty flowing:
- My 8-second whisper budget was a siren song. Measured cold truth: gemma2:2b decodes at ~5.4 tok/s on this CPU-only machine, so a 100-token whisper is ~18 seconds — the whisper path was unreachable, always falling back. Defaults are now 25s/100 tokens, tuned to the lane the benchmarks above predicted.
- The probe gate did more harm than silence. A 2-second "is Ollama alive" check flaked precisely when the server was busy decoding, silently downgrading real whispers to templates. Removed — generation carries its own timeout; the fallback covers everything.
-
Emoji left silently. On Windows' default cp1252 pipes, persona emoji raised
UnicodeEncodeErrorinside the never-fail-a-commit catch-all — the line simply vanished. The CLI now bootstraps UTF-8 at startup. - And while writing this post, a commit ran while the model was busy narrating a report in the background — the whisper fell back to its template and the commit succeeded instantly. That's the contract working, not failing.
Why Open Innovation Matters
Haunt's premise is that an AI reads your private diffs to comment on them. Closed models make that premise toxic: your repo's history would live in someone else's logs forever. With Gemma's open weights on-device, the only witness is a ghost, and it never leaves your machine.
- It works with the lights off. Airplane mode, airgapped CI, a hotel with one dead outlet — haunt doesn't care; nothing in the pipeline calls home.
- Zero marginal cost makes it a habit. A whisper on every commit is viable only when inference is free. Gate every commit behind a $0.002 API bill and the ghost becomes a toy you disable in week one.
-
The haunting can outlive the tool. 1.6 GB — one
ollama pull gemma2:2b— and any other Ollama model slots in via one config line. Personas are TOML files; localize the ancestor voice for your team's history. And the fallback templates are auditable, first-party Python — even "no model" is an inspectable code path here.
That last point is the open story in miniature: when the proprietary link dies, dignity dies with it. When the open link dies, you still get a whisper, just a quieter one.
Honest Limits
- CPU-only whisper takes ~18s; report narrations (~260 tokens) up to a minute each. Fine off to the side, too slow to be chatty.
- The score is a heuristic, not truth — churn isn't ghostliness. Every row prints its own metrics so you can argue with the ghost.
- Shallow clones thin out age and author data; haunt still scores, just blunter.
- Next up: a pre-push "last rites" summary, blame-aware ancestor naming (opt-in), and a GitHub Action.
The repository is never truly empty. It is merely quiet. 🪦
Top comments (0)