This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
๐ฏ๏ธ Museum of the Dead
A memorial for the ghosts in the machine โ built with open-weight AI, offline.
I built a terminal museum of Unix spirits. You walk through four exhibits โ a zombie, a daemon, an ofrenda for dead projects, and a sรฉance that channels a dead programming language โ and every one of them is narrated by an open-weight language model running entirely on your machine. No API key. No internet. No data leaves the laptop.
The whole thing is also a hidden design-pattern museum: every exhibit is a working example of one of the 23 patterns from Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, Vlissides, 1994).
GitHub: johnny603/museum_of_the_dead
What I Built
The Museum of the Dead is a terminal-based interactive museum. It's built on Python 3.10+, rich for terminal rendering, and Ollama (or llama-cpp-python) for local inference against an open-weight model like Phi-3-mini.
Four exhibits, each with a "describe" phase (a rich panel that sets the mood) and a "perform" phase (a real Unix or data operation, narrated live by the model):
๐ฏ๏ธ The Ofrenda โ a memorial shelf of open-source projects that died. Each row is a
RepoMementocapturing a project's name, last commit, and cause of death. The local model writes a one-line remembrance for each.๐ง The Zombie โ a real
os.fork()child is created, exits, and becomesDEFUNCTinpsuntil the parent reaps it viaos.waitpid(). On screen you watch the exit code come back (exit=7โ the seventh seal). The model eulogizes the reaping.๐๏ธ The Watcher โ a
fork()+setsid()daemon that detaches from the TTY and outlives its parent. The model describes the loneliness of a process that closed its own doors.๐ฎ The Sรฉance โ a chatbot that channels a long-dead programming language (FORTRAN 77, COBOL, Brainfuck, Turbo Pascal). Pick a spirit, ask a question, the model answers in-character.
How I interpreted "Ghost in the Machine": the ghosts are literal. Python's fork() is the closest thing in modern software to a sรฉance โ you split a soul into two, one of them lives on unseen, and the other leaves a corpse the kernel refuses to bury until someone wait()s. Naming the exhibits after Dรญa de los Muertos artifacts (ofrenda, marigolds, candles) gave the technical demos an emotional frame: a museum isn't a technical artifact, it's a place where memory is preserved.
Demo
Run locally in three commands:
python3 -m venv .venv && source .venv/bin/activate
pip install rich
python main.py --offline
The offline mode requires zero network access โ it uses canned, deterministic whispers instead of an LLM. That's the CI-safe, demo-safe path and the one judges can verify in 10 seconds.
For the full experience:
brew install ollama
ollama pull phi3:mini
pip install ollama
python main.py --model phi3:mini
Here's what an offline run looked like on my MacBook Air (M2, macOS Sonoma):
__ ___ __ __ ____ __ _
/ |/ /_ __ ___ / /_ ____ __ __ / /_ / __/__ ___ / /_ (_)__ ___
/ /|_/ / // (_-< / __/ / __/ / // // __// _// _ \/ -_) __/ / / _ \/ _ \
/_/ /_/\_,_/___/ \__/ \___/ \_,_/ \__//_/ \___/\__/\__/ /_/\___/_//_/
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ A memorial for the ghosts in the machine. โ
โ Open-weight AI. Offline. Yours. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Model: phi-3-mini Offline: True
โโ Exhibit 2 of 4 โ The Zombie โโ
โญโโโโโโโโโโโโโโโโ ๐ฏ๏ธ Exhibit: The Zombie โโโโโโโโโโโโโโโโโฎ
โ A child is born. The parent forgets to wait(). โ
โ The child exits, but its exit status is never reaped. โ
โ For a little while, the child still walks โ a zombie. โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
I was a child process once. My parent forgot to wait() for me.
ยท parent pid=7769, child pid=7788
ยท child process breathingโฆ
ยท child is now a zombie (DEFUNCT in ps)โฆ
ยท parent reaps child (exit=7). Peace returns.
โโ Exhibit 3 of 4 โ The Watcher โโ
ยท daemon spawned (pid=7790)
ยท daemon detached from TTY. It watches, unseen.
Here is a terminal example of how an exhibit should look like:
- โ Banner / model line: shows the model name and whether the offline fallback triggered.
- โ
child is now a zombie (DEFUNCT in ps): this is the real Unix state, captured live. If you runps -o pid,stat,cmd | grep [p]ythonin a second terminal at that instant, you'll seeZfor ~200 ms. - โ
parent reaps child (exit=7): this is theos.waitpid()return path โ the zombie ceasing to exist. - โ
daemon detached from TTY:os.setsid()succeeded, and the child is now parentless by design.
Code
GitHub: johnny603/museum_of_the_dead
Project structure โ 10 authored files, one design pattern per exhibit:
museum_of_the_dead/
โโโ main.py # entry point + argparse
โโโ curator.py # Facade (185) + Builder (97) + Iterator (257)
โโโ llm_engine.py # Strategy (315) โ swappable LLM backends
โโโ exhibits/
โ โโโ __init__.py # re-exports Exhibit
โ โโโ base.py # Template Method (325) base class
โ โโโ ofrenda_exhibit.py # Memento (283)
โ โโโ zombie_exhibit.py # Observer (293)
โ โโโ daemon_exhibit.py # Singleton (127)
โ โโโ seance_exhibit.py # Strategy (315)
โโโ README.md
โโโ LICENSE
Design Patterns used (Gamma et al., 1994) โ the book was the floor plan, not a decoration:
| Pattern | Book page | Where in the project | Why |
|---|---|---|---|
| Facade | 185 | curator.py |
One visit() over four exhibits โ clients never touch subsystems |
| Builder | 97 | curator.py |
The tour is assembled step-by-step at construction time |
| Iterator | 257 | curator.py |
for exhibit in self.exhibits โ hides the internal list |
| Observer | 293 | zombie_exhibit.py |
Parent observes the child's exit status via waitpid()
|
| Singleton | 127 | daemon_exhibit.py |
One Watcher per system โ the daemon is the only one |
| Memento | 283 | ofrenda_exhibit.py |
Immutable RepoMemento snapshots of dead projects |
| Strategy | 315 |
llm_engine.py, seance_exhibit.py
|
Swap models; swap which language the sรฉance channels |
| Template Method | 325 | base.py |
Exhibit.describe() / Exhibit.perform() hooks |
How I Built It
The open-source AI stack:
| Layer | Choice | Purpose |
|---|---|---|
| Model runtime |
Ollama (primary) / llama-cpp-python (alternative) |
Local inference against open-weight models โ no cloud API |
| Model | Phi-3-mini (or any Ollama-tagged model) | Narrates each exhibit in real time, entirely on-device |
| Terminal rendering | rich |
Panels, tables, prompts, the whole museum aesthetic |
| OS ghosts | Python os.fork(), os.waitpid(), os.setsid()
|
The exhibits themselves โ real Unix process behavior |
| Fallback | llm_engine.py strategy |
If no model is loaded, deterministic canned whispers kick in so the museum always runs |
Why the open pieces are load-bearing, not decorative:
-
llm_engine.pyimplements a Strategy with three backends (llama-cpp โ ollama โ canned). Swapping which ghost narrates is a one-line change. -
The museum runs without any model at all โ
--offlineis a first-class mode. That's the "works with the lights off" claim, proven on your machine in one command. - No telemetry, no outbound traffic, no auth flow. Every prompt is processed by a model running on the same CPU that's printing the panels.
CTI Open Source Blog Template โ system diagram (for the readers who want the deep dive):
โโโโโโโโโโโโโโโโโ
โ main.py โโโargsโโโถโ
โโโโโโโโโโโโโโโโโ โ
โผ
โโโโโโโโโโโโโโโโโโโโโ
โ LocalModel โ โโโ Strategy (315)
โ (llm_engine.py) โ
โ โโโโโโโโโโโโโโโ โ
โ โllama-cpp โ โ
โ โollama โ โ
โ โcanned โ โ
โ โโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโ
โฒ whisper()
โ
โโโโโโโโโโดโโโโโโโโโโโ
โ Curator โ โโโ Facade (185)
โ (curator.py) โ
โโโโโโโโโโฌโโโโโโโโโโโ
โ iterates
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โผ โผ โผ โผ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
โ Ofrenda โ โ Zombie โ โ Watcher โ โ Sรฉance โ
โ Memento โ โ Observer โ โ Singleton โ โ Strategy โ
โ (283) โ โ (293) โ โ (127) โ โ (315) โ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
One specific use case โ the Zombie exhibit, step by step:
-
User action:
python main.py --offlineโCurator.visit()iterates to exhibit 2. -
ZombieExhibit.describe()prints arichPanel and callsself.model.whisper(...). In offline mode this hitsllm_engine.CANNED["zombie"]; with a real model it hits the strategy backend. -
ZombieExhibit.perform()callsos.fork(). The child process sleeps 0.4 s and callsos._exit(7). Meanwhile, the parent printsparent pid=โฆ, child pid=โฆ. -
The parent sleeps 0.6 s โ during this window, the child is a zombie (
STAT = Zinps). The parent printschild is now a zombie (DEFUNCT in ps)โฆ. -
The parent calls
os.waitpid(pid, 0), extractsexit_code = 7, printsparent reaps child (exit=7). The zombie ceases to exist. - The parent asks the model for a closing whisper and returns.
That flow is 8 lines of actual code in zombie_exhibit.py โ but it's a complete, self-contained demo of a real OS concept, narrated by a locally-running LLM.
Why Does Open Innovation Matter?
Three concrete reasons, each tied to something the museum actually does:
1. The museum works with the lights off.
A memorial that requires an internet connection isn't a memorial โ it's a subscription. If the museum relied on a hosted API, you'd be reading a eulogy for a dead project served by a server that could die too. --offline isn't a fallback; it's the primary design goal. A museum you can visit on a plane is a museum whose ghosts are actually yours.
2. Data stays home.
The Ofrenda exhibit lets you add RepoMementos for projects you remember โ a family project, a startup that died, a tool someone you loved built. With a closed API, every one of those remembrances would leave your laptop, get tokenized, and enter someone's training corpus. With a local model, they don't. Grief deserves privacy.
3. Models are swappable โ and that's a load-bearing feature.
llm_engine.py exposes a LocalModel strategy that tries llama-cpp โ Ollama โ canned whispers. A user who doesn't trust Meta's license can swap in Qwen. A user who wants a smaller memory footprint can use TinyLlama. A user with no model at all can still run the museum. A closed API forces you to trust a provider; an open model lets you trust yourself.
My Agent Session
(Optional โ but judges love it.)
The museum was built interactively. Every exhibit was developed through the python main.py --offline loop:
- Paste the exhibit skeleton โ run offline.
- Watch the
richoutput render โ adjust colors and copy. - Wire in the LLM path only after the offline path was stable.
- Capture the zombie reaping with a second terminal running
psโ the "money screenshot."
(If you save a session with DevRelay, embed it here with the agent_session tag. Otherwise link the repo's Issues tab and leave it at that.)
The Spirit of It
The moment it landed for me was during development, not deployment. I was re-running the Zombie exhibit for the dozenth time when I noticed something in ps:
PID PPID STAT COMMAND
7769 7592 S+ python main.py --offline
7788 7769 Z+ python <defunct>
The Z+ was my code. My child process. Dead but not reaped. The kernel kept its exit status alive because the parent hadn't called waitpid() yet.
I was staring at a literal ghost โ a dead process the operating system refused to forget. And the model whispered "I was a child process once. My parent forgot to wait() for me. I stand here, hollow, until someone reaps my exit status."
For a second, the LLM wasn't generating text. It was translating. The ghost was real.
That's the moment the museum became a memorial instead of a demo.
Acknowledgment
Built entirely offline in a time crunch. 10 files. 8 design patterns. 4 exhibits. 0 API keys. The Design Patterns book was the floor plan; Phi-3-mini was the sexton.
Thanks to the Hacktoberfest 2026 organizers and the Open-Source AI Challenge for asking a question that's genuinely hard to answer without open tools: what does the machine remember?
---
# PART 2 โ Extension Roadmap ("When I Have Extra Time")
The judges will respect a project that knows what it *isn't yet*. Here's a **prioritized** roadmap you can paste into the post (as a final "What's Next" section) or save for a follow-up post. Every item is tied to a pattern from the book, so extensions strengthen the design-pattern theme too.
## ๐ฅ Priority 1 โ Genuinely changes the museum (do these next)
| # | Extension | Effort | Why it matters | Pattern it strengthens |
|---|---|---|---|---|
| 1 | **`--exhibit N` flag** โ jump straight to one exhibit, skip the tour | 30 min | Essential for demos and follow-up screenshots. Right now every run is 2 minutes even if you only want the Zombie. | Iterator (257) โ you already have the loop; just add a filter |
| 2 | **Ollama model auto-detect + fallback** โ if `--model` isn't pulled yet, offer to `ollama pull` it (or fail with a friendly message) | 1 hr | The current failure mode is silent fallback. Users should *know* the model isn't live. | Strategy (315) โ the strategy chooses itself |
| 3 | **`--size` flag** for exhibits โ `--zombie-size small` / `large`. Currently the zombie only lives for 0.4 s; make it 2 s so `ps` catches it reliably | 1 hr | Makes the "watch it die" moment reproducible in a video, not a lottery. | Observer (293) โ the observation window becomes a parameter |
| 4 | **Persistent ofrenda** โ write `RepoMemento`s to `~/.museum_of_the_dead/ofrenda.json`. `--add-repo NAME LAST_COMMIT CAUSE` appends one; `--reset` wipes | 2 hrs | Turns the Ofrenda from a demo into an actual memorial tool someone would use. **This is the biggest leap in user value.** | Memento (283) โ the memento gets serialized, which is the *original* motivation for the pattern |
| 5 | **`--no-fork` safety mode** โ Windows and some CI don't support `os.fork()`; detect that and skip the OS exhibits with a graceful message | 2 hrs | Real portability story; Windows users currently get `OSError` at exhibit 2. | N/A (infrastructure) |
## ๐ฅ Priority 2 โ Depth & polish
| # | Extension | Effort | Why it matters | Pattern |
|---|---|---|---|---|
| 6 | **Sรฉance: `--language all`** โ cycle through every dead language in one run, showing one whisper per language | 1 hr | Turns the Sรฉance into a *tour de force* of LLM roleplaying. Screenshot-per-language is devastating for a DEV post. | Strategy (315) โ the strategy becomes a collection |
| 7 | **Ofrenda: `--repos FILE`** โ load a whole memorial list from YAML/JSON | 1 hr | People can maintain an ofrenda file in their dotfiles. Memorable and shareable. | Memento (283) โ a `Collection<RepoMemento>` |
| 8 | **`--export-tour`** โ dump the full tour to a plain-text `tour_YYYYMMDD.txt` with ANSI stripped | 2 hrs | Lets you save a museum visit, paste it in Slack, or attach it to a blog post. | Iterator (257) โ the iterator writes instead of printing |
| 9 | **`rich` progress ring** for the Whisper โ currently the model appears to "hang" during inference. A spinner (`rich.status`) turns dead air into atmosphere. | 2 hrs | UX feels 10ร more polished for 20 lines of code. | N/A |
| 10 | **Better canned whispers per exhibit** โ right now `CANNED["zombie"]` is reused for the describe *and* the perform whisper. Give each a distinct string. | 30 min | Fixes the "repeated line" you saw in the demo output. | N/A |
## ๐ฅ Priority 3 โ Ambitious (a follow-up post's worth of work)
| # | Extension | Effort | Why it matters | Pattern |
|---|---|---|---|---|
| 11 | **Add a fifth exhibit: The Ofrenda of Ghosts** โ an interactive memorial where the user types a name, cause of death, and one favorite memory, and the model generates an *epitaph* (short, poetic, FORTRAN-77-broadsheet style). Writes to the persistent ofrenda. | 1 day | This is the single most emotionally resonant feature possible and directly answers the "Ghost in the Machine" prompt. | Memento + Builder |
| 12 | **`--theme` flag** โ `dรญa_de_los_muertos` (current), `victorian_gothic`, `cyberpunk`. Swap the `rich` styles and the model's system prompt. | 1 day | Showcases the model's stylistic range without changing a single line of logic. Very demo-able. | Strategy (315) โ the *whole tour* becomes a strategy |
| 13 | **Museum web UI** โ a `textual` TUI (terminal GUI) or a minimal Flask/HTMX frontend where you can *scroll* through exhibits | 2 days | Splits the terminal-only audience open; makes screenshots shareable. | Still Facade at the top |
| 14 | **`--replay`** โ record an `--offline` run to JSON (each whisper + timestamp + exhibit) and replay it identically. Essential for asciinema recordings. | 1 day | Reproducibility for demos. | Memento (283) โ the whole tour becomes a memento |
| 15 | **Agentic setup wizard** โ a `--setup` command that checks for Ollama, offers to install it, picks a model based on available RAM, and verifies with a test whisper | 2 days | Lowers the barrier to a first successful run from "read the README" to "one command." | Builder (97) |
## What to say in the post about this
If you want to embed a shortened version in the DEV post itself (right before the "Prize Categories" section), here's a paste-ready 4-line version:
markdown
What's Next
The submission is deliberately scoped to 10 files so it can be read in one sitting.
The next iteration โ already sketched in the README roadmap โ would:
-
Persist the ofrenda to
~/.museum_of_the_dead/ofrenda.jsonwith an--add-reposubcommand, turning the memorial from a demo into an actual tool. -
Add
--exhibit Nfor single-exhibit demos and video recordings. -
Detect missing models gracefully (auto-prompt
ollama pullinstead of silent fallback). - Add a fifth exhibit โ an interactive epitaph generator that writes to the persistent ofrenda.
Each of these maps to a design pattern already used in the codebase โ Memento for persistence,
Iterator for exhibit selection, Strategy for model selection. The museum's architecture is
already ready for them.
Well that is all the projects for hacktoberfest, see you in the next post, a grand summary explaining what I got out of this event.

Top comments (0)