DEV Community

Cover image for I held a séance for dead GitHub repos (and gave their ghosts a voice)
Wathsala Gamarachchi
Wathsala Gamarachchi

Posted on Edited on

I held a séance for dead GitHub repos (and gave their ghosts a voice)

Last Commit turns abandoned repos into Día de Muertos ofrendas. Real commit history on one side, a Gemma-written ghost voiced by ElevenLabs on the other.

Every repo dies the same way. Not with a bang, but with one last commit that nobody knew would be the last.

Atom's first commit message was literally try it out. Eleven years and 38,533 commits later, it ended with Merge pull request #25709. That's it. That's the whole funeral.

So for Día de Muertos I built Last Commit: a graveyard where dead GitHub repos get an ofrenda, and their ghosts get to say a few words.

👉 Live: https://lastcommit.wathsala.tech

![The Last Commit graveyard: tombstones for BrowserQuest, three20, Persona and left-pad]

What it does

Each grave is one abandoned repo: Atom, AngularJS, Brackets, BrowserQuest, left-pad, Mozilla Persona, three20, Flux and request. Click a tombstone and you get its ofrenda:

  • Left side: what is true. First commit, last commit, lifespan, how long it's been silent, commits, stars, forks, open issues, archive status. All straight from the GitHub API, with links to the real commits.
  • Right side: the ghost speaks. A short first-person monologue written by Gemma, read out loud by ElevenLabs.

![The left-pad ofrenda: verified facts on the left, the ghost's monologue and audio player on the right]

Here's Atom, unedited:

I am atom, the hackable text editor, once a vibrant sea of JavaScript. I lived for 11.3 years, weaving through the syntax of a thousand dreams before the quiet settled in. I remember the spark of my beginning on 2011-08-19, when I was nothing more than a whisper saying try it out.

And left-pad, which is a bit too self-aware:

I have been silent for 7.6 years, watching from this ofrenda as the world moved toward String.prototype.padStart(). [...] Do not weep for the deprecated, dear developers. Just remember the space I once filled.

The one rule: facts and fiction never mix

A ghost is fiction. A commit date is not. I really didn't want an AI making up stuff about real maintainers, like "the developer gave up" or "they were burned out". Those are real people.

So the design is strict:

  1. Facts never come from the model. The left panel is rendered from the API data, not from anything Gemma wrote. The page labels it Verified facts, and the monologue Fiction.
  2. The ghost only talks about itself. The prompt says it speaks as the code and never describes what developers felt, thought or did.
  3. No real names reach the model. Contributor usernames are stripped out of the prompt, including sneaky ones like Merge pull request #64 from someuser/master.
  4. Check, retry, scrub. If a reply still names a real person, it gets regenerated, and anything that survives gets replaced with "someone".

After generating all nine, I ran a script that pulls every number out of every monologue and checks it against the facts. Zero mismatches, zero names.

How it works

repos.txt ──► seance.py ──► GitHub API   (facts)
                   │
                   ├──► Gemma            (epitaph + monologue)
                   ├──► ElevenLabs       (MP3)
                   ▼
         site/data/*.json + site/audio/*.mp3 ──► static site on Render
Enter fullscreen mode Exit fullscreen mode

The séance happens on my laptop. The graveyard is public.

  • No backend. One Python script writes JSON and MP3 files. The site is plain HTML, CSS and JS that reads them. Nothing can crash while someone's looking at it.
  • No dependencies. The script is Python standard library only (urllib, json, argparse).
  • Hosting: a free Render static site, using a render.yaml blueprint, on a .tech domain.

Holding a séance is one command:

python3 seance.py atom/atom
# 🕯  Summoning atom/atom ...
#    ghost written in 6s (aistudio:gemma-4-26b-a4b-it)
#    EPITAPH: A hackable text editor resting in the digital silence.
#    voice saved to site/audio/atom__atom.mp3
Enter fullscreen mode Exit fullscreen mode

Things that went wrong (the fun part)

My laptop vs. a language model. My plan was to run Gemma locally with Ollama. My laptop has 7 GB of RAM, and with Discord and VS Code open I had about 1.3 GB free and full swap. Gemma 3 4B needs about 4 GB, and the new Gemma 4 small models are 7.5–9.5 GB to download. So I went with Gemma 3 1B, which ran at about 18 tokens/sec and wrote a ghost in about 20 seconds. Not bad!

...but 1B made stuff up. It confused forks with stars ("a paltry 17,167 stars"; Atom has 60k). It wrote about "the pressure of the deadline" and developers' "moments of frustration". That's exactly the thing I didn't want. Tightening the prompt helped a bit, but a 1B model just doesn't follow rules that well.

Gemma 4 via Google AI Studio fixed the writing, then returned nothing. I switched to gemma-4-26b-a4b-it on AI Studio's free tier. Every response came back empty after about 90 seconds. The raw response showed why:

"finishReason": "MAX_TOKENS",
"thoughtsTokenCount": 4094
Enter fullscreen mode Exit fullscreen mode

It had spent all 4,096 tokens thinking: drafting, counting words, re-checking constraints, "wait, one more thing...". It never wrote the actual answer. And my code was concatenating every part, including the thought parts. The fix:

"generationConfig": {
    "maxOutputTokens": 1024,
    "thinkingConfig": {"thinkingLevel": "minimal"},
}
...
return "".join(p["text"] for p in parts if not p.get("thought"))
Enter fullscreen mode Exit fullscreen mode

That took it from 90 seconds and empty to 6 seconds with a great answer. (thinkingBudget: 0 and thinkingLevel: "low" both returned 400 for this model. Only minimal worked.) The 31B model kept returning 500s that day, so I added retries with backoff too.

AngularJS was born in 1970. Its oldest commit is an import from Perforce, dated 1970-01-01T00:00:01Z. Git didn't exist before 2005, so if a first commit claims to be older than that, I fall back to the repo's creation date.

Node v0.x was a stub. nodejs/node-v0.x-archive has a default branch called moved with exactly 2 commits ("Node.js has moved"). Technically correct, emotionally useless. I swapped it for Flux.

The voice step ate my ghosts. The ElevenLabs key I first made was restricted and didn't include Text to Speech, so every voice call failed with a 401. Because the script only saved after the voice step, the freshly written ghosts were lost too. Now it saves the text first, then adds the voice.

One empty-looking DNS record. Render said it couldn't verify the domain. I queried the registrar's nameserver directly with dig @ns1.dns.host and it had nothing, so the record just hadn't been saved. Once it was, verification passed and HTTPS came up in minutes.

The stack

  • Gemma 4 (gemma-4-26b-a4b-it) via Google AI Studio, with local Ollama + Gemma 3 1B as a switchable fallback (PROVIDER=ollama)
  • ElevenLabs eleven_multilingual_v2, with stability turned down a bit so the voices sound a little uneven and ghostly
  • GitHub REST API for every fact
  • Render static site + a .tech domain
  • Plain HTML, CSS, JS; IM Fell English + JetBrains Mono fonts

All nine voices used about 6,000 of ElevenLabs' 10,000 free monthly characters, so the whole thing ran on free tiers.

What I'd add next

  • A "summon your own" box, so you can type any repo and hold a séance (needs a small backend, which I skipped on purpose)
  • Ghosts talking to each other: Atom and Brackets definitely have things to say
  • More graves. Suggestions welcome in the comments 🕯️

Built 31 October – 1 November 2026. Code: https://github.com/wathsalaggg/last-commit

Top comments (0)