This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine.
The scene was a dusty attic of code, cobwebs of forgotten files, and the air thick with the metallic tang of a failed attempt.
A local model wrote that. For the record: there was no attic. There were no cobwebs. There was a four-line function called summon_ghost (I wrote it for testing purposes ๐) that divided 1 by a candle count of 0, and then the process was gone in 0.022 seconds.
The model did the atmosphere. Python did the ZeroDivisionError. Both landed in the same file, which is the whole trick.
What I Built
Tales is the project. haunt is the command, and it does exactly what the name says. It wraps another process, waits for the thing to die, and then writes up the death as a ghost story.
haunt run -- python app.py
If your program exits 0, nothing gets narrated and nothing gets written. No death, no story. If it raises, segfaults, gets OOM error, or takes a signal, haunt collects whatever is lying around: the exit code, the signal, how long it lasted, the tail of stdout and stderr, the traceback, your git branch, the files you edited and never committed, and the Python and platform versions. That pile goes to Gemma through Ollama, and Gemma reports back in five panels:
- The Crash Site: where and how it died
- Last Words: the final stderr, translated into something a normal human can read
- Prime Suspects: the traceback, the commits, the files you touched and didn't commit
- The Epitaph: one line for the tombstone
- How to Prevent the Haunting: what to fix
The panels show up in your terminal. A GHOST_REPORT.md shows up on disk (pls bare with the name ๐ฅฒ). Story first, then the raw evidence as JSON, appended and unabridged, so you can check the ghost's work.
Demo
One real run, one take, gemma2:2b, roughly 3.6 seconds from launch to finished report:
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ๐ป The Crash Site โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ The scene was a dusty attic of code, cobwebs of forgotten files, and the air โ
โ thick with the metallic tang of a failed attempt. A single terminal, โ
โ blinking with the last echo of a dying process, held the remnants of a โ
โ tragic downfall. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ๐ป Last Words โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ "Lighting the candles...," a single line from a file named โ
โ haunt/demo_crash.py whispered from the void. A ghostly echo of an aborted โ
โ invocation of a spectral function called summon_ghost. The code, a tapestry โ
โ of haunting lines, was broken. The execution was halted, a victim of a โ
โ simple, yet devastating error. A fatal "ZeroDivisionError" had silenced the โ
โ process, like a stifled whisper in the digital graveyard. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ๐ป Prime Suspects โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ The culprit was a line of code, a single function called summon_ghost, where โ
โ a candle's value was being divided by one. The scriptโs last line. A simple โ
โ error, a missing value, an attempt to summon a spectral entity. A specter โ
โ of a program. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ๐ป The Epitaph โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ "Lighting the candles" became a haunting refrain, a final flicker of an โ
โ ambitious soul. The code, once a promise of the supernatural, was now a โ
โ ghost story of an unfinished project. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโ ๐ป How to Prevent the Haunting โโโโโโโโโโโโโโโโโโโโโโโโฎ
โ To prevent such haunting crashes, a rigorous code review is needed. A โ
โ comprehensive test suite should be used to catch errors early, and proper โ
โ logging should ensure that debugging is faster and more efficient. This โ
โ process is the key to preventing spectral apparitions from haunting the โ
โ lives of developers, and the digital realm of their creations. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Now go back and read the Prime Suspects panel.
a candle's value was being divided by one
That is wrong. The code divides 1 by the candle count, which happens to be 0. The model misread a single operator. It is a 2B model doing its best, and its best is mostly vibes.
Nothing breaks, because the evidence sits in the same file, directly underneath the prose:
"exit_code": 1,
"signal": null,
"duration_s": 0.022,
"stderr_tail": "Lighting the candles...\nThe room grows cold.\nTraceback (most recent call last):\n File \"demo_crash.py\", line 12, in summon_ghost\n return 1 / candles\nZeroDivisionError: division by zero",
"git": { "branch": "main", "uncommitted": ["M haunt/cli.py"] },
"env": { "python": "3.12.5" }
The ghost is allowed to be wrong. The appendix is not. Switch to gemma2:9b and the prose gets better, but the appendix comes out byte for byte the same, because the model never touches it.
Run It
All local, no account, no key:
git clone https://github.com/Ewan-Dkhar/Tales
cd Tales
uv venv && uv pip install -e .
ollama pull gemma2:2b
.venv/bin/haunt demo --model gemma2:2b
Then aim it at your own disaster:
.venv/bin/haunt run -- python app.py # wrap a process, narrate the failure
.venv/bin/haunt run --no-narrate -- ./bin # collect evidence, skip the story
.venv/bin/haunt analyze crash.log # narrate a log you already kept
Put your command after --. This is not optional. Without it, haunt parses your program's flags as its own and gets confused, which is fair, because so do I.
Code
MIT licensed, at github.com/Ewan-Dkhar/Tales.
The landing page argues the same thing without words: recorded evidence in monospace next to narrated prose in serif, so you can tell which half you are reading from across the room. It is at ewan-dkhar.github.io/Tales, plain HTML and CSS, no build step, served straight from the repo.
The manual is at ewan-dkhar.github.io/Tales/guide.html. Every flag, every field the tool records, the exit codes it passes through, config, model choice, and the parts it does not pretend to handle.
Inside, five small modules share one object:
@dataclass
class Evidence:
command: list[str]
cwd: str
timestamp: str
exit_code: int | None
signal: int | None
duration_s: float
stderr_tail: str
stdout_tail: str
traceback: str | None = None
git: GitContext | None = None
env: dict[str, Any] = field(default_factory=dict)
recent_files: list[str] = field(default_factory=list)
source: str = "run"
capture.py runs the process and records how it ended. gather.py fills in the traceback, git, and environment. prompt.py turns that into a prompt. narrator.py streams Gemma. render.py draws the panels and writes the report. Every stage takes Evidence and returns either Evidence or a string, so each one is testable by itself. 21 tests, no network, no model.
How I Built It
Language used is Python btw. Typer for the CLI, Rich for the panels, Pydantic for config, the Ollama client for inference, Sentry for tracing.
The interesting part was keeping the model honest without sanding off what makes it fun. The system prompt locks the five headings, forbids invented facts, and explicitly allows the word "unknown". A narrator that can admit it didn't see something is worth more than one that invents a motive. The prose is free. The nouns are not.
Gemma runs on localhost:11434. --model or HAUNT_MODEL takes any Ollama tag, so when something better ships I change one string and it's good to go.
Three things bit me, in case they bite you too:
- Sentry showed nothing at first. I initialized it after the transaction had already started, so the first traces were invisible. Order matters, and I found that out by staring at an empty dashboard for longer than I would like to admit.
-
A clean exit used to get narrated. The process would survive and I would still get a story about its death. Now exit 0 prints
The process survived. Nothing to bury.and writes nothing at all. - Segfaults have no traceback. On a native crash the report gets thinner and runs on the exit code, the signal, and whatever last reached stderr. That is a real limitation, not a feature.
Why Does Open Innovation Matter?
Crash data is the most personal thing a working programmer produces. A stack trace carries file paths, internal hostnames, user IDs, the names of services that aren't public, and every so often a string that looks suspiciously like a key. Uploading that to a hosted API means shipping a slice of your machine to someone else's datacenter every time you mistype a variable.
Running the narrator locally changes the deal. No account, no per-crash bill, no network. A crash should not come with a meter attached.
The model is swappable because the boundary is a serialized Evidence object and not an SDK. The prompt is a plain string in the repo. You can read exactly what the model is told, run it yourself, and compare the output. With a closed API, the post-mortem tool is a black box that has read your worst debugging data, which is the one part you would want to verify.
That is the argument, and it fits in a sentence: the tool that reads your worst debugging data should be one you can run yourself, and audit while it runs.
Prize Categories
- Best Use of Gemma: Gemma is the narrator, running entirely on your machine through Ollama, with the prompt and the evidence both open for inspection.
- Best Use of Sentry Agent Tracing: every stage of the pipeline is a traced Sentry span inside one transaction, verified in the Sentry UI, including the HTTP call to Ollama.
Built with Gemma, Ollama, Typer, Rich, and Sentry. Source: github.com/Ewan-Dkhar/Tales. Landing page: ewan-dkhar.github.io/Tales.
The ghost is local. It has seen enough of your stack traces already.
Top comments (0)