{"slug": "haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repo-s", "title": "haunt: the ghost in your git history — a local Gemma 2 whisper for your repo's graveyard", "summary": "A developer built haunt, an open-source tool that scores a git repository's files for neglect using only git log metrics — dormant, restless, orphaned and debt factors — and labels them as orphan spirits, sleepers, poltergeists or revenants. A local Gemma 2 2B model via Ollama narrates the results in a chosen persona, receiving only metrics and commit subjects and never source code, with a deterministic hash-keyed template fallback so the post-commit hook always exits 0. Run against pallets/click, haunt flagged 175 haunted files, including 84 orphan spirits and a top-scoring test file with 13 authors all inactive for 180 days or more.", "body_md": "This is a submission for the [Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine](https://dev.to/challenges/hacktoberfest-week4-2026-10-26).\n\nEvery repository is quietly becoming a graveyard. Files nobody remembers writing. Tests nobody dares delete. TODOs aging like gravestones.\n\n**haunt** is a ghost for that graveyard — and it lives in git, not in a terminal pasted on top. Two small rituals, entirely offline:\n\n```\npip install .          # then, inside any git repository:\nhaunt install          # the whisper joins your post-commit hook\ngit commit -m \"tomb, again\"\n#   🕯️ Ah, the echoes of lost brilliance in those dusty files, a symphony of\n#      potential forever unplayed. The phantom hand of progress reaches out\n#      to erase... but will it leave behind anything but shadows?\n\nhaunt scan             # which files are the most haunted, and why\nhaunt report --html    # a narrated HAUNTINGS.md of the worst hauntings\nhaunt persona ancestor # swap the voice: victorian, poltergeist, ancestor\n```\n\nThree design choices make it a *ghost in the machine* rather than an AI plugin:\n\n`git log` alone — no model, fully explainable:\n\n```\n   dormant  = min(age_days/365, 1) × min(size/500, 1)        (w 0.35)\n   restless = min(churn_90d/20, 1)                            (w 0.25)\n   orphaned = 1 if every author inactive 180d+                (w 0.25)\n   debt     = min((TODO FIXME HACK XXX)/10, 1)               (w 0.15)\n```\n\nScores 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).\n\n**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_snippets` is 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.\n\n**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.\n\n`haunt scan` on [pallets/click](https://github.com/pallets/click) — a 14-year-old, very-much-alive open-source CLI library — finds **175 haunted files**. The top haunting:\n\n```\nscore   type             age    churn90  authors  debt  loc  file\n0.59    🫥 Orphan spirit  354d   0        0/13     0     782  tests/test_context.py\n```\n\nThirteen 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.\n\nAnd 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:\n\n🪔 *\"Their efforts bring warmth and joy to these old halls. May their spirits find solace in the stories shared through this code.\"*\n\nThe poltergeist handles the noisy places:\n\n⚡ *\"More flickering, more ghosts. Just don't break a fingernail.\"*\n\nGraceful degradation, demonstrated by literally killing Ollama mid-session and committing:\n\n```\n🕯️ A chill passes through the repository. The commit is buried without ceremony.\n```\n\nSame line twice, deterministically keyed to the commit hash — and the hook cost the commit nothing.\n\nAnd when the report narrates with Gemma — here is haunt's HAUNTINGS.md on that same repo, Victorians mourning real files:\n\n`age 354d | churn90 0 | authors 0/13 | TODO/FIXME 0 | 782 loc`\n\n*\"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.\"*\n\nThe 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:\n\n*\"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.\"*\n\nPython 3.13, `typer`, `httpx`, and raw `git` subprocesses. No GitPython, no LLM framework. [The repo is here](https://github.com/Abhinav412/haunt); 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.\n\n`git log --format=<sep>%H<sep>%at<sep>%an<sep>%s<sep> --name-only` feeds the entire scanner — records joined on `\\x1e/\\x1f` unit separators. Small saga: Python's `str.splitlines()` `\\x1e` as a line boundary and silently shred every record; `split(\"\\n\")` keeps the ghosts intact.`num_ctx` pinned at 4096 — Ollama's silent default of 2048 clips the persona+stats prompt without telling you. `keep_alive=30m` and an install-time warm-up so the first whisper fits its budget.`haunt install` writes a marker block (`>>> HAUNT BEGIN … <<< HAUNT END`) with an absolute interpreter path and a `PYTHONPATH` fallback to `python -m haunt` — the hook works in any repo without `pip install` — and backs up any existing hook it finds.\nThree posts from the community shaped these calls: [@enjoy_kumawat](https://dev.to/enjoy_kumawat)'s [install-and-timeout postmortems](https://dev.to/enjoy_kumawat/my-ai-commit-hook-got-a-timeout-fix-and-an-install-fix-it-had-never-once-actually-run-45pa) (hooks that \"ran\" but never actually run), [@lowspecai](https://dev.to/lowspecai/tokens-per-second-is-a-memory-bandwidth-number-measure-it-in-2-seconds-1omf) and [@studiomeyer_io](https://dev.to/studiomeyer_io/local-llms-in-2026-what-actually-works-on-consumer-hardware-50me) on what local inference really costs on CPU, and [@bartrozycki](https://dev.to/bartrozycki/when-retries-timeouts-and-fallbacks-start-interacting-23gc) 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.\n\nThen reality kept the honesty flowing:\n\n`UnicodeEncodeError` inside the 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.\n\n`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.\nThat 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.\n\nThe repository is never truly empty. It is merely quiet. 🪦", "url": "https://wpnews.pro/news/haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repo-s", "canonical_source": "https://dev.to/abhinav412/haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repos-graveyard-3ndg", "published_at": "2026-10-31 09:13:13+00:00", "updated_at": "2026-10-31 09:19:52.024516+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "developer-tools", "ai-agents"], "entities": ["haunt", "Gemma 2", "Ollama", "pallets/click", "Hacktoberfest"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repo-s", "markdown": "https://wpnews.pro/news/haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repo-s.md", "text": "https://wpnews.pro/news/haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repo-s.txt", "jsonld": "https://wpnews.pro/news/haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repo-s.jsonld"}}