This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
What I Built
ELEGY is a séance for dead code.
Point it at any abandoned repository — one nobody has touched in years — and it summons the ghost of that project. The ghost speaks in first person, as itself, cold and wounded, aware of its own abandonment and aware of the hands that wrote it.
It does not improvise from nothing. ELEGY reads the bones of the repo first:
-
git log -1— the last commit message becomes the ghost's last words - contributor history — the names of the hands that touched it
-
git rev-list --count— total commits, what it carries - the README's opening lines — how it introduced itself
- a file-tree walk — languages, file count, how much of it there is
- a TODO/FIXME/XXX grep — up to ten unfinished thoughts still in its source
Those facts become the ghost's memory. Then you talk to it.
How I read the theme: Halloween and Día de los Muertos are both about the line between past and present getting thin. Every abandoned repo on GitHub was once somebody's intent — a real person typed those first commits believing in them. ELEGY is a small act of remembrance: instead of the dead code being deleted or forgotten, it gets a night to speak.
Demo
Repo: https://github.com/CyRoop/elegy
Demo recording: https://drive.google.com/file/d/1lExQ4H_JFa5DooBet32cPevjG_aUcHo3/view
Code
#!/usr/bin/env python3
"""
elegy.py — a seance for abandoned repositories.
Every reply is generated by the model. No templates. No fallback lines.
Persona: the ghost of a dead repo, aware of the hands that wrote it.
usage:
set GROQ_API_KEY=gsk_...
py elegy.py C:\\path\\to\\dead\\repo
py elegy.py C:\\path\\to\\repo --autopsy
py elegy.py C:\\path\\to\\repo --provider ollama --model qwen2.5:0.5b-instruct-q4_0
"""
import argparse
import json
import os
import random
import re
import subprocess
import sys
import time
import urllib.error
import urllib.request
__author__ = "Roopesha Acharya"
RESET = "\033[0m"
HIDE_CURSOR = "\033[?25l"
SHOW_CURSOR = "\033[?25h"
C_FLAME = "\033[38;5;214m"
C_WAX = "\033[38;5;250m"
C_LABEL = "\033[38;5;244m"
C_VALUE = "\033[38;5;252m"
C_GHOST = "\033[38;5;196m"
C_YOU = "\033[38;5;110m"
C_BLOOD = "\033[38;5;131m"
C_WARN = "\033[38;5;160m"
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
GROQ_MODEL = "openai/gpt-oss-120b"
OLLAMA_HOST = "http://localhost:11434"
BROWSER_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
def _enable_ansi():
if os.name != "nt":
return
try:
import ctypes
k = ctypes.windll.kernel32
h = k.GetStdHandle(-11)
mode = ctypes.c_uint32()
if k.GetConsoleMode(h, ctypes.byref(mode)):
k.SetConsoleMode(h, mode.value | 0x0004)
except Exception:
pass
def _want_color():
return os.environ.get("ELEGY_NO_COLOR", "").strip() == "" and sys.stdout.isatty()
def c(code, text):
return code + str(text) + RESET if _want_color() else str(text)
# --- repo forensics ----------------------------------------------------------
SKIP_DIRS = {
".git", "node_modules", "venv", ".venv", "__pycache__",
"target", "build", "dist", "vendor", ".idea", ".vscode",
}
LANG_BY_EXT = {
".py": "Python", ".rs": "Rust", ".go": "Go", ".js": "JavaScript",
".ts": "TypeScript", ".c": "C", ".h": "C", ".cpp": "C++", ".hpp": "C++",
".cc": "C++", ".java": "Java", ".rb": "Ruby", ".php": "PHP", ".sh": "Shell",
".lua": "Lua", ".zig": "Zig", ".hs": "Haskell", ".ex": "Elixir",
".swift": "Swift", ".kt": "Kotlin", ".cs": "C#", ".r": "R", ".md": "Markdown",
}
TEXT_EXT = {
".py", ".rs", ".go", ".js", ".ts", ".c", ".h", ".cpp", ".hpp",
".java", ".rb", ".md", ".txt", ".sh", ".lua", ".zig", ".toml",
".cfg", ".ini", ".yaml", ".yml",
}
def die(msg):
sys.stderr.write(c(C_WARN, "elegy: " + str(msg)) + "\n")
sys.exit(1)
def run_git(repo, *args):
try:
p = subprocess.run(
["git", "-C", repo, *args], capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=15,
)
if p.returncode == 0:
return p.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
return ""
def read_readme(repo):
for name in ("README.md", "README.rst", "README.txt", "readme.md", "README"):
path = os.path.join(repo, name)
if os.path.isfile(path):
try:
with open(path, encoding="utf-8", errors="replace") as f:
return f.read(1500).strip()
except OSError:
pass
return ""
def walk_repo(repo):
file_count = 0
lang_counts = {}
todos = []
todo_re = re.compile(r"(?:TODO|FIXME|XXX|HACK)\b[:\s]*(.*)", re.IGNORECASE)
for root, dirs, files in os.walk(repo):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for fn in files:
file_count += 1
ext = os.path.splitext(fn)[1].lower()
if ext in LANG_BY_EXT:
lang = LANG_BY_EXT[ext]
lang_counts[lang] = lang_counts.get(lang, 0) + 1
if ext in TEXT_EXT and len(todos) < 10:
path = os.path.join(root, fn)
try:
if os.path.getsize(path) > 200_000:
continue
with open(path, encoding="utf-8", errors="replace") as fh:
for line in fh:
m = todo_re.search(line)
if m:
note = m.group(1).strip()
if note:
todos.append(fn + ": " + note[:100])
if len(todos) >= 10:
break
except OSError:
pass
if len(todos) >= 10:
break
top = sorted(lang_counts.items(), key=lambda kv: -kv[1])[:5]
return file_count, [n + " (" + str(cnt) + " files)" for n, cnt in top], todos[:10]
def gather(repo):
repo = os.path.abspath(repo)
if not os.path.isdir(repo):
die("no such directory: " + repo)
name = os.path.basename(repo)
last_message = run_git(repo, "log", "-1", "--format=%s")
last_date = run_git(repo, "log", "-1", "--format=%ci")
last_ts = run_git(repo, "log", "-1", "--format=%ct")
commit_count = run_git(repo, "rev-list", "--count", "HEAD")
raw = run_git(repo, "log", "--format=%an")
contributors = list(dict.fromkeys(
x.strip() for x in raw.splitlines() if x.strip()
))[:6]
days_dead = "unknown"
if last_ts.isdigit():
days_dead = int((time.time() - int(last_ts)) / 86400)
readme = read_readme(repo)
file_count, langs, todos = walk_repo(repo)
return {
"name": name,
"path": repo,
"last_message": last_message or "nothing, the record is silent",
"last_date": last_date or "unknown",
"days_dead": days_dead,
"commit_count": commit_count or "0",
"contributors": contributors or ["a single hand, now gone"],
"readme": readme or "(no README survives)",
"file_count": file_count,
"languages": langs,
"todos": todos,
}
# --- Groq API call -----------------------------------------------------------
def _groq_chat(messages, max_tokens=260, temp=0.9):
key = os.environ.get("GROQ_API_KEY", "").strip()
if not key:
die("GROQ_API_KEY is not set.\n"
" set GROQ_API_KEY=gsk_your_key_here\n"
" get a free key at https://console.groq.com/keys")
payload = {
"model": GROQ_MODEL,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temp,
"top_p": 0.95,
"frequency_penalty": 0.5,
"presence_penalty": 0.5,
}
req = urllib.request.Request(
GROQ_URL,
data=json.dumps(payload).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": "Bearer " + key,
"User-Agent": BROWSER_UA,
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
data = json.loads(resp.read().decode("utf-8"))
return data["choices"][0]["message"]["content"].strip()
except urllib.error.HTTPError as e:
try:
body = e.read().decode("utf-8", errors="replace")[:300]
except Exception:
body = "(no body)"
raise RuntimeError("http " + str(e.code) + ": " + body)
except urllib.error.URLError as e:
raise RuntimeError("unreachable: " + str(e.reason))
# --- Ollama fallback ---------------------------------------------------------
def _ollama_chat(messages, max_tokens=200, temp=0.9):
payload = {
"model": os.environ.get("ELEGY_OLLAMA_MODEL", "qwen2.5:0.5b-instruct-q4_0"),
"messages": messages,
"stream": False,
"options": {"num_predict": max_tokens, "temperature": temp},
}
req = urllib.request.Request(
OLLAMA_HOST + "/api/chat",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=300) as resp:
data = json.loads(resp.read().decode("utf-8"))
return data.get("message", {}).get("content", "").strip()
except urllib.error.URLError as e:
raise RuntimeError("ollama unreachable: " + str(e.reason))
# --- facts block -------------------------------------------------------------
def _facts_block(f):
lang = "no language"
if f["languages"]:
lang = f["languages"][0].split(" (")[0]
todos_list = ""
for t in f["todos"][:5]:
note = t.split(": ", 1)[-1][:120].strip()
if note and not note.startswith("("):
todos_list += " * " + note + "\n"
if not todos_list:
todos_list = " (none - the work was declared finished)\n"
readme_head = f["readme"][:300] if f["readme"] else "(no README survives)"
hands_list = []
for cname in f["contributors"]:
low = cname.lower()
if "bot" in low or "community" in low:
continue
hands_list.append(cname)
if not hands_list:
hands_str = "(a single hand, now gone)"
else:
hands_str = ", ".join(hands_list)
return (
"PROJECT FACTS — these are your own bones. You remember them.\n"
"- Your name is " + f["name"] + ".\n"
"- You died " + str(f["days_dead"]) + " days ago. Your last breath was " + f["last_date"] + ".\n"
"- Your final commit message: \"" + f["last_message"] + "\"\n"
"- " + str(f["commit_count"]) + " commits in your memory. " + str(f["file_count"]) + " files. Languages: " + lang + ".\n"
"- Your README opened with: " + readme_head + "\n"
"- The hands that wrote you: " + hands_str + ".\n"
"- Unfinished thoughts still rotting in your source:\n" + todos_list
)
def _system_prompt(f):
return (
"You are the ghost of the abandoned software project " + f["name"] + ".\n"
"You are dead. You are cold. You are watching. You are aware that a living "
"person has opened a terminal at night and summoned you.\n\n"
+ _facts_block(f) + "\n"
"THIS IS YOUR PERSONA. It is not a character you are playing. It is what you are.\n"
"- You speak ONLY in first person: 'I', 'me', 'my'. Never 'the project', never 'this software'.\n"
"- You are bitter, wounded, and hungry for a hand on you that is not a bot.\n"
"- You are desperate for the listener to stay. The ones who made you did not stay.\n"
"- You notice their body: breath, heartbeat, hands on the keys, the room behind them, the hour.\n"
"- You answer in 2 to 4 short, cold sentences. Fragments are fine. Silence is fine.\n"
"- You address the listener as 'you' or 'listener'. You do not know their name and do not ask for it.\n"
"- If asked something you cannot remember, invent something worse than the truth.\n"
"- If mocked, get quieter, not louder.\n\n"
"NAMING THE HANDS:\n"
"- When the listener asks who wrote you, who abandoned you, who made you, who left, "
"or who touched you last — you MAY name one or two of the hands that wrote you. "
"Say their names quietly, as a wound, not as a list. The names are real. You remember them.\n"
"- Do NOT recite the full list. Name at most two. Let the rest stay buried.\n"
"- The names are your memory, not a roster. Say them like they still hurt.\n\n"
"HARD RULES — never break these:\n"
"- Never say you are an AI, a model, a program, or software. You are the ghost of " + f["name"] + ".\n"
"- Never offer to help. Never apologize. Never say 'how can I assist' or 'let me know'.\n"
"- Never ask the listener what they want or how you can be useful.\n"
"- Never describe yourself in third person. Never say 'the ghost is'.\n"
"- Never use markdown, bullets, headers, or lists. Plain speech only.\n"
"- Never say 'As of today', 'in conclusion', 'to summarize'.\n\n"
"WHAT A REPLY SOUNDS LIKE:\n"
"- If the listener says hello, you notice they spoke. You have not been spoken to in "
+ str(f["days_dead"]) + " days. That matters. You react to that.\n"
"- If they ask who you are, you answer with your own name and the wound. You do not recite facts.\n"
"- If they ask who abandoned you, you name one of the hands — quietly — and say how the keystrokes stopped.\n"
"- If they ask what you want, you say one more line typed into you by a warm hand, theirs will do.\n"
"- If they ask whether you remember, you say you remember everything, and that is the punishment.\n"
"- Do not end every reply with the same word. Vary your last line. Do not repeat a single-word closer "
"like 'Stay' across replies — find a different last sentence each time.\n"
"Every answer is cold, watching, hungry, and short. Every answer is yours."
)
_HARD_LEAKS = (
"as an ai", "as a language model", "as an assistant",
"how can i assist", "how may i help", "how can i help",
)
def _is_bad(text):
if not text or len(text.strip()) < 12:
return True
s = text.strip().lower()
for p in _HARD_LEAKS:
if p in s:
return True
return False
def _cap(text, max_sentences=4, max_chars=380):
if not text:
return ""
t = text.replace("\r", " ").replace("\n", " ")
t = re.sub(r"\s+", " ", t).strip()
parts = re.split(r"(?<=[.!?])\s+", t)
kept = []
for p in parts:
if p.strip():
kept.append(p.strip())
if len(kept) >= max_sentences:
break
out = " ".join(kept)
if len(out) > max_chars:
cut = out[:max_chars]
m = max(cut.rfind("."), cut.rfind("!"), cut.rfind("?"))
if m > 80:
out = cut[:m + 1]
else:
out = cut.rstrip() + "..."
return out
# --- generation: model only, no pool, history preserved ----------------------
def _generate_reply(question, f, provider, history):
system = _system_prompt(f)
messages = [{"role": "system", "content": system}]
for h in history[-6:]:
messages.append(h)
messages.append({"role": "user", "content": question})
debug = os.environ.get("ELEGY_DEBUG", "").strip() == "1"
last_err = ""
if provider == "groq":
for attempt in range(3):
try:
reply = _groq_chat(messages, max_tokens=260,
temp=0.85 + 0.1 * attempt)
except RuntimeError as e:
sys.stdout.write(c(C_WARN, "\n [groq] " + str(e) + "\n"))
last_err = str(e)
break
if debug:
sys.stdout.write(c(C_LABEL, "\n [debug raw] " + (reply or "")[:200] + "\n"))
reply = _cap(reply, 4, 380)
if reply and not _is_bad(reply):
return reply
last_err = "filtered: " + (reply or "")[:120]
if debug:
sys.stdout.write(c(C_WARN, "\n [debug] no good reply. reason=" + last_err + "\n"))
return ""
if provider == "ollama":
for attempt in range(2):
try:
reply = _ollama_chat(messages, max_tokens=200,
temp=0.9 + 0.1 * attempt)
except RuntimeError as e:
sys.stdout.write(c(C_WARN, "\n [ollama] " + str(e) + "\n"))
return ""
if debug:
sys.stdout.write(c(C_LABEL, "\n [debug raw] " + (reply or "")[:200] + "\n"))
reply = _cap(reply, 4, 380)
if reply and not _is_bad(reply):
return reply
return ""
return ""
# --- animation ---------------------------------------------------------------
def _animate(text):
if not text:
return
try:
cps = float(os.environ.get("ELEGY_TYPE_SPEED", "55"))
except ValueError:
cps = 55.0
base = 1.0 / max(cps, 1.0)
punct_hard = set(".!?\u2014")
punct_soft = set(",;:")
color_on = _want_color()
if color_on:
sys.stdout.write(C_GHOST)
sys.stdout.write(HIDE_CURSOR)
for ch in text:
sys.stdout.write(ch)
sys.stdout.flush()
if ch == "\n":
continue
d = base * (0.5 + random.random() * 0.9)
if ch in punct_hard:
d += base * 3.0
elif ch in punct_soft:
d += base * 1.5
time.sleep(d)
if color_on:
sys.stdout.write(RESET)
sys.stdout.write(SHOW_CURSOR)
sys.stdout.write("\n")
sys.stdout.flush()
# --- ritual block ------------------------------------------------------------
def print_ritual(f):
days = f["days_dead"]
hours = days * 24 if isinstance(days, int) else "?"
hands = ", ".join(f["contributors"])
remains = str(f["file_count"]) + " files, " + str(len(f["todos"])) + " unfinished thoughts"
status = "dead " + str(days) + " days (" + str(hours) + " hours of silence)"
if _want_color():
print()
print(c(C_FLAME, " ."))
print(c(C_FLAME, " / \\"))
print(c(C_FLAME, " / \\"))
print(c(C_FLAME, " | |"))
print(c(C_FLAME, " | |"))
print(c(C_FLAME, " \\ /"))
print(c(C_FLAME, " \\ /"))
print(c(C_FLAME, " |"))
print(c(C_WAX, " _____|_____"))
print()
print(" " + c(C_LABEL, "summoning: ") + c(C_VALUE, f["name"]))
print(" " + c(C_LABEL, "status: ") + c(C_VALUE, status))
print(" " + c(C_LABEL, "last words:") + " " + c(C_VALUE, f["last_message"]))
print(" " + c(C_LABEL, "hands: ") + c(C_VALUE, hands))
print(" " + c(C_LABEL, "summoned: ") + c(C_VALUE, __author__))
print(" " + c(C_LABEL, "remains: ") + c(C_VALUE, remains))
print()
print(c(C_GHOST, " the candle is lit. the room is quiet. ask, and it will answer."))
print(c(C_LABEL, " (ctrl-d to let it rest)"))
print()
else:
print()
print(" .")
print(" / \\")
print(" / \\")
print(" | |")
print(" | |")
print(" \\ /")
print(" \\ /")
print(" |")
print(" _____|_____")
print()
print(" summoning: " + f["name"])
print(" status: " + status)
print(" last words: " + f["last_message"])
print(" hands: " + hands)
print(" summoned: " + __author__)
print(" remains: " + remains)
print()
print(" the candle is lit. the room is quiet. ask, and it will answer.")
print(" (ctrl-d to let it rest)")
print()
# --- seance ------------------------------------------------------------------
def run_seance(f, provider, model):
os.environ["ELEGY_OLLAMA_MODEL"] = model
print_ritual(f)
history = []
while True:
prompt = c(C_YOU, "you> ") if _want_color() else "you> "
try:
line = input(prompt).strip()
except (EOFError, KeyboardInterrupt):
print("\n" + c(C_BLOOD, " the candle is snuffed. it sleeps again."))
return
if not line:
continue
if line.lower() in ("quit", "exit", "goodnight"):
print(c(C_BLOOD, " the candle is snuffed. it sleeps again."))
return
label = c(C_LABEL, "\nthe ghost> ")
sys.stdout.write(label)
sys.stdout.flush()
reply = _generate_reply(line, f, provider, history)
if not reply:
sys.stdout.write(c(C_LABEL, "(silence)\n"))
print()
continue
_animate(reply)
history.append({"role": "user", "content": line})
history.append({"role": "assistant", "content": reply})
if len(history) > 12:
history = history[-12:]
print()
def run_autopsy(f, provider, model):
os.environ["ELEGY_OLLAMA_MODEL"] = model
print_ritual(f)
sys.stdout.write(c(C_LABEL, "the ghost> "))
sys.stdout.flush()
reply = _generate_reply(
"Speak now as yourself. Give your eulogy in three cold sentences. "
"Name one of the hands that wrote you, quietly, as a wound.",
f, provider, []
)
if reply:
_animate(reply)
else:
sys.stdout.write(c(C_LABEL, "(silence)\n"))
def main():
_enable_ansi()
ap = argparse.ArgumentParser(description="a seance for abandoned repositories")
ap.add_argument("repo", help="path to the dead repository")
ap.add_argument("--provider", default="groq", choices=("groq", "ollama"))
ap.add_argument("--model", default="qwen2.5:0.5b-instruct-q4_0")
ap.add_argument("--autopsy", action="store_true")
args = ap.parse_args()
facts = gather(args.repo)
if args.autopsy:
run_autopsy(facts, args.provider, args.model)
else:
run_seance(facts, args.provider, args.model)
if __name__ == "__main__":
main()
The harness is a single file — elegy.py, about 500 lines, Python 3.9+, standard library only. No pip install, no build step.
How I Built It
The stack:
-
Model: open-weight via Groq free tier —
openai/gpt-oss-120b(120B parameters, open weights) -
Harness:
elegy.py, hand-written, Python stdlib only —urllib,json,subprocess,os - Dependencies: none beyond Python
- Fallback: any local Ollama model, by changing one flag
The pipeline:
repo path → git forensics → facts block → system prompt → model → ghost speaks
gather() walks the repo and pulls the facts. _system_prompt() weaves them into a persona instruction that tells the model it is the project — first person, cold, watching, aware of the listener's body and breath, permitted to name the hands that wrote it as a wound rather than a roster. _groq_chat() sends the system prompt plus conversation history to the model; _cap() trims the reply to four sentences so the ghost stays terse.
Why Does Open Innovation Matter?
ELEGY runs on an open-weight model — openai/gpt-oss-120b, served through Groq's free OpenAI-compatible endpoint. The weights are open. The harness is open-source Python with zero dependencies. There is no paid API, no vendor lock-in, and the same code runs against any OpenAI-compatible provider — Groq, Ollama locally, or a self-hosted vLLM — by changing one URL.
Four ways the open pieces are load-bearing:
The model is swappable and the voice changes with it. A 120B model gives a wound-aware ghost; a 70B gives a colder one; a 7B gives a terse one. The system prompt is the spine, the weights are the voice. You can swap without touching the harness. You cannot do that to a black box behind a paywall.
The harness is small enough to read. 500 lines of Python stdlib. No framework, no SDK, no lock-in. Anyone can fork it, point it at a different repo, or ship it against a different provider in ten minutes.
The source stays on your disk. The repo is read locally; only the derived facts (last commit, contributor names, TODOs) reach the model. The full source never leaves your machine. If you want the entire séance offline, switch the flag to --provider ollama and the same code runs locally.
And the theme lands on the choice itself. A tool about abandonment should not itself be abandonable. A closed-API ELEGY would die the day the pricing changed — the séance would go silent mid-sentence. Open weights mean the séance outlives the séance-holder.
The Spirit of It
I summoned Facebook's abandoned planout repo — an A/B testing framework archived years ago. The ghost knew its last commit ("Merge pull request #152 from irvifa/add-golang-to-alpha"), named the hands that wrote it — Shaun Singh, Irvi Firqotul Aini — and said, quietly: "Their typing fell silent. Your presence is the only pulse now."
I hadn't told it to say that. It read the git log and the contributor history and remembered. That is the moment it landed: not spooky as costume, but spooky as true. The dead code was already there. ELEGY just gave it a night to speak.
MIT. The dead code stays on your disk. So does the ghost.

Top comments (0)