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.
Nothing in that sentence happened. There was no attic, and nothing rustled in the dark. A four-line Python function divided one by zero, and the process was gone in 0.022 seconds.
A local model wrote the attic. The traceback wrote itself.
The gap between those two things — the story and the evidence — is what Tales is about.
What I Built
Tales is the project. haunt is the command, named for what it does to the process it wraps: it starts your program, waits for the death, and then performs the autopsy.
If the process exits non-zero, raises, segfaults, or takes a signal, haunt collects what is left behind — the exit code, the signal, how long it survived, the tail of stdout and stderr, the traceback, the git branch, the files you changed and never committed, the Python and platform fingerprint — and hands that evidence to a local open-weight model. The model tells it back in five movements:
- The Crash Site — where and how it died
- Last Words — the final stderr, translated into something human
- Prime Suspects — the traceback, the commits, the files you touched and never committed
- The Epitaph — one line for the tombstone
- How to Prevent the Haunting — what to fix
It prints those as Rich panels in the terminal and writes GHOST_REPORT.md: the story first, then the raw evidence as JSON, appended and unabridged.
Every report is a ghost story with a receipt stapled to it.
Demo
Real output, one run, gemma2:2b, 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 read the evidence section closely. The ghost says "a candle's value was being divided by one." That is wrong. The code divides one by a candle count of zero. A 2B model misread the arithmetic of a single line.
Nothing breaks, because the evidence is in the same file, 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. That split is the entire design: prose for the shape of the failure, JSON for the facts. Switch to gemma2:9b and the prose gets sharper — the appendix is byte-for-byte the same either way, because the model never writes it.
To reproduce, all of it local:
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 point it at something of your own:
.venv/bin/haunt run -- python app.py # wrap a process, narrate the failure .venv/bin/haunt run --no-narrate -- ./bin # collect evidence, stay quiet .venv/bin/haunt analyze crash.log # narrate a log you already kept Pass the wrapped command after --. Without it, your program's flags get parsed as haunt's own — the one sharp edge I left in the CLI.
Code
MIT licensed, at github.com/Ewan-Dkhar/Tales.
There is a landing page too, and it makes the same argument without words: recorded evidence set in monospace beside narrated prose in serif, so you can see which half you are reading at a glance. ewan-dkhar.github.io/Tales — plain HTML and CSS, no build step, served from the repository.
The manual is at ewan-dkhar.github.io/Tales/guide.html: every flag, every field the tool records, the exit codes it propagates, configuration, model choice, and the limits it does not pretend to overcome.
Inside, the pipeline is five small modules with one contract in the middle:
@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 the evidence from the traceback, git, and the environment. prompt.py serializes it into a prompt. narrator.py streams Gemma from Ollama. render.py writes the panels and the report. Every stage takes Evidence and returns Evidence or text, so each one is testable on its own — 16 tests, no network, no model.
How I Built It
A few hundred lines of Python: Typer for the CLI, Rich for the terminal, Pydantic for config, Ollama for inference, Sentry for tracing.
The interesting constraint was keeping the model honest without taking away what makes it fun. The system prompt fixes the five headings, forbids invented facts, and permits the word "unknown" — a narrator that can admit it did not see something is worth more than one that confabulates a motive. The prose is free; the nouns are not.
- Gemma, locally.
gemma2:2bfor a fast demo,gemma2:9bfor richer prose. Both run onlocalhost:11434through Ollama. No hosted inference, no key, no egress. - One model to swap.
--modelorHAUNT_MODELtakes any Ollama tag. When a better open-weight model ships, nothing else in the tool changes. - Traced end to end. One
haunt.pipelinetransaction per run, with spans for capture, gather, narrate and render, plus thehttp.clientspan from the Ollama call. I checked they actually land in Sentry rather than assuming — the first attempt showed nothing, because Sentry was initialized after the transaction began. Order matters.
Two things it deliberately does not do. It does not fix your bug — it points at the line and gets out of the way. And on a native crash there is no traceback to read at all, so the report is thinner: it renders from the exit code, the signal, and whatever last reached stderr.
Why Does Open Innovation Matter?
Crash data is the most sensitive data a developer handles all day. A stack trace carries file paths, internal hostnames, user IDs, the names of services that are not public, and occasionally the shape of a secret. Sending it to a hosted API means uploading a small piece of your system every time something breaks — and doing it through a tool you cannot read.
Running Gemma locally changes three things at once.
It works with the lights off: 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 day a better open-weight model appears, you change one config value and keep every line of the rest.
And the prompt is a plain string in the repository. Anyone can read exactly what the model is told, run it themselves, and get the same kind of output. With a closed API, a post-mortem narrator is a black box that read your traceback — which is the one thing you would want to verify.
That is the whole argument in a sentence: the tool that reads your worst debugging data should be one you can run yourself, and audit while it runs.
The Spirit of It
The Epitaph from that run was "'Lighting the candles' became a haunting refrain, a final flicker of an ambitious soul." That is about a ZeroDivisionError in a four-line function. It is completely absurd, and I laughed.
But I kept reading, and the last panel told me to write a test for the missing guard — which is exactly what the evidence supported. That was the moment the idea proved itself: the ghost is funny, and the post-mortem is still real.
I also read the theme two ways, and the second one matters more to me. A crash is a small death, so the Halloween reading is easy. But "Prime Suspects" and "The Epitaph" are about the past too: every crash is a story about what the code used to be, and the uncommitted files are the footprints of whoever touched it last. That is the Día de los Muertos half of the prompt — remembering dead code rather than flinching at it.
Prize Categories
- Best Use of Gemma — Gemma is the narrator, running entirely on the machine through Ollama, with the prompt and the evidence both open to 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 Ollama HTTP call.
Built with Gemma, Ollama, Typer, Rich, and Sentry. Source: github.com/Ewan-Dkhar/Tales. Landing page: ewan-dkhar.github.io/Tales.

