DEV Community

Kamal
Kamal

Posted on AI-assisted

Revenant: I held a séance for a dead Python library, and it came back passing its tests

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 Revenant séance room: the overview page for psf/requests-html, showing its cause of death and 0 to 30 tests passing

TL;DR: Revenant holds a séance for abandoned open-source projects. It turns a dead repo's history into a memory you can question, so the project answers in the first person with every sentence cited. Then it runs an autopsy in a sandbox and brings the project back one test-checked edit at a time. Its first patient, psf/requests-html, went from 0 passing tests to 30/30 on Python 3.9–3.13. Live at revenant-zaq3.onrender.com.

What I Built

On today's Python, import requests_html fails. Kenneth Reitz's "HTML Parsing for Humans" last shipped in February 2019. The newest threads in its issue tracker all sound the same: "Is this project dead?", "Doesn't import anymore", "Any maintainers here?"

The part I find haunting is that the project's voice never left. Every commit message, issue, review and docs page its maintainers wrote is still sitting there. It just has nobody left to speak for it.

So for Ghost in the Machine I built Revenant, a séance for abandoned open-source software, done in three rites:

  1. Summon. It gathers the project's whole history (commits, issues, PRs, comments, reviews, docs, and archived copies of its dead docs site) into a searchable memory.
  2. Ask the ghost. You ask the project questions. It answers in the first person, from its own records, and every sentence cites the issue, commit or doc it came from. When its records have nothing, it says "The spirits are silent."
  3. Autopsy and resurrection. It runs the project's own tests in a sandbox with no network, writes a death certificate naming what killed it, and then tries one small fix at a time. The project's own tests decide whether each fix stays. Any fix that breaks a passing test is reverted.

Along the way, the ghost helped settle an eight-year-old mystery in its own test suite. That story is in The Spirit of It.

Demo

Live séance room: revenant-zaq3.onrender.com

  • Open the chart, the death certificate and the resurrection timeline for requests-html.
  • Ask the ghost something, like "Why does importing requests_html fail with lxml 5.2?"
  • Click the moon button to switch between the dark séance room and a parchment light theme. Both themes are tested for WCAG AA contrast.

It runs on Render's free tier, so if nobody has visited in a while, the first load takes about 30–60 seconds while it wakes up. The hosted ghost uses a free preview model on Backboard, which can be rate-limited. If you see "The séance was interrupted", try again a bit later.

The overview page in the dark and light themes, side by side

The death certificate. It lists the causes of death, with witnesses from the issue tracker, and the vital signs outside the code. The Travis CI is dead (travis-ci.org shut down in 2021). The docs domain fails TLS, and its last Wayback snapshot is a registrar's "Domain Suspension" page. PyPI hasn't seen a release since 2019.

The death certificate: causes of death, witnesses from the issue tracker, and vital signs

Asking the ghost. Each sentence gets a verdict from a citation check. This answer came live from the hosted model. The second sentence is true, but it was withheld because its key words aren't in the comment it cited. The check is strict on purpose (more on that below).

Asking the ghost in the light theme: one sentence SUPPORTED by its source, one WITHHELD because its key words aren't in the cited record

When the records hold nothing relevant, the ghost refuses before any model is called, so there's nothing for a model to make up:

$ revenant ask psf/requests-html "What is the recipe for sourdough bread?"
The spirits are silent. Nothing in my records speaks to that.
  (no relevant evidence retrieved)   gemma4:e4b: not called
Enter fullscreen mode Exit fullscreen mode

The resurrection, condensed from the real run in RESURRECTION_LOG.md:

initial: passed 0, errors 2
lxml-html-clean:                   kept      (passing 0 -> 24)
pytest-asyncio-event-loop:         REVERTED  (regressed test_bare_render, test_browser_process, test_render)
history-assertion:test_containing: kept      (passing 24 -> 25)
asyncio-loop:                      kept      (passing 25 -> 27)
pytest-asyncio-event-loop:         kept      (passing 27 -> 30)
python 3.9 / 3.10 / 3.11 / 3.12 / 3.13: 30 passed, 0 failed
aftercare: GitHub Actions CI, drop dead Travis badge, declare passing Pythons, fix docs version label
Enter fullscreen mode Exit fullscreen mode

Look at line three. The first attempt at the pytest-asyncio fix broke three passing tests, so Revenant threw it away. After the event-loop fix landed, the same cure took the suite from 27 to 30. That keep-or-revert rule is the most important part of Revenant. It's what stops a resurrection from bringing back a zombie.

The resurrection timeline: each operation with its verdict and test counts, then the Python matrix and aftercare

Every number in this post comes from runs saved under demo/, made on a CPU-only machine with 8 vCPUs, 16 GB of RAM and no GPU.

Code

GitHub logo Phantom9869 / Revenant

A seance for abandoned open-source software: ask the ghost why the code is the way it is, then resurrect it. Local open-weight models.

Revenant

A séance for abandoned open-source software.

Project started Oct 27, 2026. Submitted to DEV's Hacktoberfest Open-Source AI Challenge, Week 4: Ghost in the Machine. Read the write-up: Revenant: I held a séance for a dead Python library, and it came back passing its tests.

Every dead repository still has a voice: thousands of commits, issues, reviews and docs written by the people who kept it alive. Revenant gathers that voice, lets you talk to the ghost of the project, performs an autopsy to find out what actually killed it, and then attempts a resurrection: one small, verified edit at a time, each one judged by the project's own test suite.

The first patient is psf/requests-html (last release 0.10.0, February 2019). On today's Python it does not even import.

before after
test suite (Python 3.13, today's dependencies) 0 passed, 2 collection errors 30 passed, 0 failed
Python
…

It's Python 3.11+, MIT licensed, and the web UI uses only the standard library. It has 88 unit tests. Models run through Ollama locally, or through Backboard if you'd rather use a hosted open-weight model. Every partner integration is optional and switched on by an environment variable. To run the same read-only séance room locally:

git clone https://github.com/Phantom9869/Revenant && cd Revenant && pip install -e .
REVENANT_HOME=deploy/bundle REVENANT_EMBED_MODEL=none revenant serve --port 8765
Enter fullscreen mode Exit fullscreen mode

How I Built It

Revenant is a pipeline of small, boring parts around a few open-weight models. Each model gets one narrow job, and something that isn't a model checks its work.

 summon ──► memory (SQLite FTS5 + EmbeddingGemma) ──► ask: Gemma 4 E4B ──► citation check ──► answer / refusal
   │
   └──► autopsy (bubblewrap, no network) ──► death certificate ──► resurrect:
            grimoire / surgeon proposes an edit ──► full test run ──► keep + commit | git reset --hard
Enter fullscreen mode Exit fullscreen mode
role model where it runs
Memory (embeddings) EmbeddingGemma 300M local, Ollama
The ghost's voice Gemma 4 E4B (thinking off) local, Ollama (hosted option: Dots3-Note on Backboard)
The surgeon (code fixes) Qwen2.5-Coder 7B local, Ollama (hosted options: Dots3-Note, Flint v1.0 on Backboard)

The memory is the project's own words

revenant summon clones the repo and harvests the GitHub API, with an on-disk cache and resume after rate limits. For requests-html that's 2,810 documents: 462 commits, 693 issues, 235 PRs, 1,368 comments, 21 reviews and 29 doc pages. They go into SQLite with an FTS5 index and EmbeddingGemma vectors (about 157 s on the CPU). Retrieval merges BM25 and cosine results with reciprocal rank fusion.

The docs site is dead, so Revenant fetches its 2019 Wayback snapshot through Browserbase and ingests it. Now the ghost can cite its own lost documentation as [archive:html.python-requests.org@2019-09-15].

The model talks. Code decides what it's allowed to say.

A ghost that makes things up is worse than useless, because it puts words in real maintainers' mouths. So:

  • No evidence, no séance. If the best record scores below 0.30 cosine similarity, it refuses without calling the model.
  • Only cite what you were shown. Citations to records that weren't retrieved are stripped out.
  • Every sentence is checked. citecheck.py only passes a sentence if every hard detail in it (versions, dates, SHAs, issue numbers, identifiers) appears word for word in a cited record, and that record shares enough of the sentence's other words. Sentences that fail are withheld. If nothing passes, the answer becomes a refusal.

That check exists because of a real hallucination. An early answer credited a "0.9.0" detail to the 2019 docs snapshot, and "0.9.0" isn't in it anywhere. Replayed through the new check, that sentence gets withheld.

The honest caveat: it matches words, it doesn't understand meaning. It has false negatives, like the true ImportError sentence in the screenshot above, and a likely-correct pip install requests-html==0.9.0 sentence in another run. It also can't judge negation or paraphrase. I'd rather it withhold a true sentence than pass a false one, but it isn't a truth oracle.

The tests are the judge, not the model

The autopsy runs the patient's own test command (from its Pipfile) in a bubblewrap sandbox with no network. It parses the JUnit XML, matches failures against known causes of death, and searches the memory for witnesses, meaning issues where users reported the same breakage:

# cause of death cure
1 lxml 5.2 moved lxml.html.clean into lxml_html_clean (witnesses #569, #576) depend on lxml_html_clean
2 asyncio.get_event_loop() no longer creates loops implicitly use the running loop, else this thread's, else a new one
3 pytest-asyncio 1.0 removed the event_loop fixture an async fixture via pytest_asyncio.fixture
4 test_containing expects 192 elements, finds 191 restore 191, citing the commit history

The resurrection loop tries the grimoire first (known, explainable cures), then the surgeon, a code model that can only reply with exact SEARCH/REPLACE edits. An edit is applied only if every SEARCH block matches exactly once. After each edit the full suite runs again. The edit is kept and committed only if tests improved and no passing test regressed. Otherwise it's git reset --hard. Once the suite is green, Revenant checks Python 3.9–3.13 and does aftercare (CI, dead badges, version metadata), and each aftercare step is checked against the tests as well.

Head-to-head: which open model can raise requests-html?

Same patient, same sandbox, same judge. The code models ran with --no-grimoire, so every edit came from the model:

contender passing / 30 kept / reverted / unappliable model time cost
grimoire only (no model) 30 4 / 1 / 0 0 s (24.6 s wall) –
Qwen2.5-Coder 7B, local CPU 25 2 / 2 / 3 141.6 s local
Dots3-Note 280B-A16B, Backboard 27 3 / 6 / 0 532.7 s $0.0000
Flint v1.0 40B-A2.5B, Backboard 27 3 / 6 / 0 683.7 s $0.0000
  • Every model fixed the import (0 → 24) and restored 191 (→ 25), but only the grimoire can say why 191 is right.
  • Only the bigger hosted models fixed the event loop (→ 27). Qwen's edits to requests_html.py never applied, because its SEARCH text wasn't in the file.
  • No model fixed the removed pytest-asyncio fixture.
  • The regression guard earned its keep. One Flint edit broke all 24 passing tests, and one Qwen edit broke all 25. Both were reverted automatically.

Caveats: each contender ran once, and results vary between runs. The hosted models ran on Backboard's rate-limited free tier, at about 53–76 s per call. I didn't try the larger paid coders.

Tracing the séance with Sentry

With SENTRY_DSN set, every command sends a trace that follows Sentry's AI agent span conventions:

revenant ask  [cli]  5.47s
└── invoke_agent ghost  [gen_ai.invoke_agent]  5.43s  revenant.citations=#569,#576
    ├── search_memory  [gen_ai.memory.client]  0.10s  record.count=8, max_cosine=0.674
    ├── chat gemma4:e4b  [gen_ai.chat]  5.33s  input_tokens=2145, output_tokens=114
    └── verify_citations  0.00s  revenant.claims=4, claims_withheld=0
Enter fullscreen mode Exit fullscreen mode

resurrect traces show every apply_fix with its nested run_tests. In a grimoire-only run, almost all of the 23.5 s is the patient's own tests. Real traces were sent from ask, autopsy and resurrect (trace ids in demo/11_sentry_traces.txt). I couldn't open the Sentry UI from the build machine, so the tree above is the same spans captured locally, not a Sentry screenshot. Sentry isn't switched on for the live site.

Why Does Open Innovation Matter?

Because the patient is open source, the cure has to be too. The project's memory is public commits and issues. Every cure is a commit anyone can read, revert or argue with. A resurrection is only trustworthy if every step can be inspected, so the most important step shouldn't be a black box.

Because open weights let me give each job to the right model, and measure it. A 300M EmbeddingGemma for memory, Gemma 4 E4B for the voice, Qwen2.5-Coder for surgery, all on a CPU. To test a 280B open-weight surgeon, I changed one flag (--surgeon backboard:...) and ran it through the same loop with the same judge. That's why there's a head-to-head table above and not a guess.

Because it runs with the lights off. Once the history is summoned, the ghost, the autopsy and the resurrection all run locally, and the patient's tests run with no network at all. Digging through an abandoned project shouldn't depend on a service that might be abandoned too. That's exactly what happened to requests-html's docs host and its CI.

Because it costs nothing per séance. A local ghost answer takes 3.8–7.2 s of CPU in the demo, and the hosted open-weight runs reported $0.0000. You could point it at hundreds of dead repos without a budget meeting.

Where closed would win: a frontier model would probably write better surgery than Qwen 7B. But the head-to-head showed where the safety actually comes from: the test-checked keep-or-revert loop and the citation check, not the model. A model you can't inspect, swap or run yourself doesn't change that. It just makes it harder to check.

My Agent Session

Built with an AI pair, checked by running. The séance-for-dead-repos idea was mine, and so were the choice of requests-html as the first patient and the partner tools. I also set the one rule everything follows: nothing counts until a real run checks it. Grok Bot, an AI coding assistant, helped write the code. I ran it, tested it against the real repo, and reviewed and edited this post. There's no recorded session to embed, but the commit history and the saved runs in demo/ are all in the repo.

The Spirit of It

One of requests-html's broken tests, test_containing, asserts that searching a saved copy of python.org for "python" finds 192 elements. Today it finds 191. The obvious fix is to change 192 to 191, and that's exactly what the code model did on its own: a one-character edit with nothing behind it.

When I asked the ghost why the test expects 192, it said:

The spirits are silent. Nothing in my records speaks to that.
Enter fullscreen mode Exit fullscreen mode

That was the right answer, because no issue or PR ever explained it. The history the ghost is built on had the answer (191_vs_192.py):

  • Kenneth Reitz wrote the test on 2018-03-03 with == 191.
  • Six months later, a commit titled "Fix tests" changed it to 192, and it reached master through PR #146.
  • The page under test, tests/python.html, hasn't changed since 2018-02-26.
  • 237 historical versions of requests_html.py were run against that page with today's dependencies. Of the 82 that still run, 80 find 191. The only other answers come from two versions older than the test itself.
  • lxml 4.9.4, 5.1.1, 5.3.0 and 6.0.0 all give 191. No environment I tried produced 192.

So the resurrection doesn't just "make the test pass". It restores the original author's number and cites both commits, so the next person who wonders gets the real story.

That felt like a small Día de los Muertos for code. The people who built requests-html moved on years ago, but their reasoning is still in the commits. A careful enough séance can still hear it and finish the sentence they started.

Prize Categories

  • Best Use of Gemma: Gemma is the ghost. EmbeddingGemma builds its memory (2,810 documents embedded locally), and Gemma 4 E4B is its voice. It answers in the first person with cited sentences that are checked one by one, all running locally through Ollama on a CPU.
  • Best Use of Render: the séance room is live on Render's free tier at revenant-zaq3.onrender.com, deployed from the repo's render.yaml blueprint. It serves a read-only bundle from revenant export with keyword-only retrieval (no GPU or disk on the free tier) and a hosted ghost through Backboard, and the key is kept in a Render env var. I verified the health check, every page, and a live cited answer. Caveats: free-tier cold starts (about 30–60 s), the free model can be rate-limited, and redeploys are manual.
  • Best Use of Browserbase: dead docs are part of a project's memory. Revenant fetches Wayback snapshots through the Browserbase Fetch API (with its proxy, because direct fetches of web.archive.org failed from my build machine), caches them, and ingests them so the ghost can cite its 2019 docs.
  • Best Use of Sentry Agent Tracing: ask, autopsy and resurrect send traces using the AI agent span conventions: invoke_agent, chat with model and token counts, memory search, and execute_tool spans for each test run and fix attempt with its verdict. Real traces were sent; the tree above was captured locally.
  • Best Use of Backboard: I used one Backboard key to compare open-weight models on a real task. Dots3-Note and Flint v1.0 went head to head as surgeons against local Qwen2.5-Coder 7B and the grimoire, using Backboard's reported tokens and cost. Dots3 is also the live site's ghost voice, and every cited sentence in my test answers was supported.

Solo submission by @bladeharper_ec7070d6e28be.


Built with Grok Bot, an AI coding assistant. I directed the project, tested it, and reviewed and edited this post. Every number in this post comes from a run saved in demo/, including the edits that got reverted.

Top comments (0)