DEV Community

Ewansalan Dkhar
Ewansalan Dkhar

Posted on

Ghost in the Machine: I Built Tales, a CLI That Narrates Crashes as Ghost Stories

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission ๐Ÿ‘ป๐ŸŒผ

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
Enter fullscreen mode Exit fullscreen mode

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.               โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)