Every repository is haunted
There is a file in vLLM that imports a module which does not exist. # tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/dummy_attention_backend.py from vllm.attention.backends.placeholder_attn import Place
There is a file in vLLM that imports a module which does not exist.
# tests/plugins/vllm_add_dummy_platform/vllm_add_dummy_platform/dummy_attention_backend.py
from vllm.attention.backends.placeholder_attn import PlaceholderAttentionBackend
On upstream main at 900b8b08e2, checked today:
vllm/attention/backends/placeholder_attn.py was deleted on 24 September 2025,
in commit 969b4da3a6, titled "[V0 Deprecation] Remove placeholder attn (#25510)".
PlaceholderAttentionBackend is defined nowhere in the repository today. That
line has been importing a module that does not exist for thirteen months.
Being careful about what that does and does not mean, because I checked and my
first guess was wrong. The file is not orphaned: dummy_platform.py:32 names
vllm_add_dummy_platform.dummy_attention_backend.DummyAttentionBackend as a
string that vLLM resolves at runtime, and the plugin is pip-installed in a two
GPU CI job. My first instinct was to say this must break that job. Then I looked:
forty one commits have touched the paths that trigger it since the deletion, the
most recent in September, and the line is still there. So either that code path
is never reached or the job does not gate anything, and I cannot tell which
without two L4s.
What I can tell you, from reading alone, is that the reference is dead. That is
the claim this tool makes, and it is the only claim it makes.
I did not find it by reading vLLM. I found it by asking a repository who it
still talks to, and listening for the answers that nobody is there to give.
Ghost in the Machine
The theme this week is spooky, or honouring the past. I went for both, because
the thing I wanted to build only works if you take the haunting seriously.
Every repository is built on top of its own dead. Files get deleted, and the
deletion is clean in the only place anyone checks, which is the directory
listing. What does not get cleaned is everything that was pointing at the file:
the import that resolves to nothing, the CI script that still invokes a test
that was removed, the docstring that says "see this other module" about a module
that was buried two years ago. Those references survive. They say a name, and
nothing answers.
So: ouija finds them, and ofrenda builds them an altar.
Live: https://codersacademy006.github.io/ouija/
What counts as a ghost
The whole tool is one claim, and I wanted the claim to be the kind that can be
proven wrong:
This exact file is gone. Nothing replaced it. Something still says its name.
Getting from the idea to something that survives contact with a real repository
was almost entirely a process of deleting my own false positives. Six of them,
in the order they embarrassed me:
It was renamed. moved_module.py disappears from the directory, so git calls
it a deletion. A file with that basename exists one folder over. Not dead.
It became a package. tracing.py is deleted and tracing/__init__.py appears.
Every from vllm.tracing import ... still works perfectly. My first run reported
this as a ghost with 29 live references, and the top five results in vLLM were
all the same mistake wearing different names.
The name is a substring. My first version matched symbol names, so a deleted
ir/value.h matched the text value.handle in a test file, and size_t was
confidently declared dead across the whole of Triton. A reference only counts if
the characters either side of it are not more path.
It is a bulk list. TensorRT-LLM reported 234 proven ghosts, more than
every other repository in the corpus put together. Four files produced 232 of
them: legacy-files.txt, ruff-legacy.toml, pyproject.toml and
.pre-commit-config.yaml, each enumerating the same long inventory of paths for
tooling to skip. That inventory being full of files that no longer exist is one
piece of stale config. Calling it 232 separate hauntings would be inflating a
headline on purpose. A living file that names ten or more distinct dead files is
a list of them, not a reference to any one of them, and its mentions stopped
counting. That repository has 31.
It is a changelog. This one was subtler. My first full corpus run reported
77 ghosts in a single repository, more than every other repository combined.
All 77 came from docs/daily-updates/2026-04-*.mdx, a dated log that lists the
pull requests merged each day. A changelog naming a file it deleted is doing
exactly its job. That is a memory, not a haunting. Once dated and archival
documents stopped counting, that repository had four.
It is a redirect. vLLM's mkdocs.yaml has a line reading
training/routed_experts_replay.md: training/routed_experts.md. The file on the
left is gone, which is the whole point: the line is the redirect that sends
anyone following an old link to the page that replaced it. That is a deletion
handled properly, and my tool was reading it as a deletion forgotten. A line
that names both a dead file and a living one is pointing at the replacement.
It is a module path that means something else. The worst one, and the last
to go. A python file's import path is a strong signal, so I was also matching
the last two components of it. TensorRT-LLM deleted
.../modeling_v2/catalog/torch/empty.py, whose last two components are
torch.empty. That repository was reported as having twenty three dangling
references to it, every one of them a line that allocates a tensor. A module path
now has to be the file's whole path from the repository root, and has to appear
in an import or quoted in prose rather than called.
I kept the evidence tiering rather than hiding the weak cases. If the surviving
reference carries two or more path segments, or the file's full import path used
as an import path, it can only mean this file, and it is reported as proven.
If only the bare filename survives it is suspected, listed separately, and
never folded into a headline number, because a bare image_processor.py in a
docstring is as likely to be talking about somebody else's file as about this
repository's.
What it found
| repository | proven | suspected | files buried | deletions read |
|---|---|---|---|---|
TensorRT-LLM |
31 | 6 | 319 | 696 |
vllm |
9 | 4 | 1093 | 1799 |
llama.cpp |
7 | 0 | 586 | 773 |
pytorch |
6 | 1 | 163 | 578 |
cutlass |
5 | 6 | 1630 | 1875 |
opensre |
3 | 5 | 655 | 1687 |
TensorRT |
3 | 2 | 622 | 924 |
triton |
2 | 3 | 584 | 916 |
numba-cuda |
2 | 0 | 31 | 49 |
automodel |
1 | 3 | 99 | 458 |
graphify |
0 | 2 | 23 | 43 |
TransformerEngine |
0 | 1 | 87 | 208 |
flash-attention |
0 | 1 | 365 | 422 |
29 distinct projects, 19 of them with any deletion history at all. 69 proven
ghosts and 34 suspected, out of 6391 files buried for good across 10671
deletions. 6 projects came back clean.
The sweep ran against local clones. For any repository where my checkout carries
my own branches, the numbers above come from a throwaway worktree at that
project's upstream default branch instead, so every line quoted below is one
anybody can go and read.
Three worth reading:
vLLM. The dead import at the top of this post.
TensorRT-LLM, and a correction I had to make to myself. My clone of it sits
on an older branch of my own, and there the tool found three example scripts
opening with from tensorrt_llm.builder import Builder against a deleted
module. A good finding, except that upstream had already deleted those example
scripts too. They were ghosts in my checkout, not in the project.
What is still there on upstream main at 6adeccc026 is quieter and I like it
more. Line 42 of .github/CODEOWNERS:
/tensorrt_llm/builder.py @NVIDIA/trt-llm-devs
A rule routing review of a file that does not exist. Two live files also carry
the comment # Modified from tensorrt_llm/builder.py _init_max_seq_len, which
is now a pointer to nothing.
Triton, on upstream main at 0861b90f69. .pre-commit-config.yaml still
defines a hook that regenerates .github/workflows/integration-tests.yml from
an .in file. Both the workflow and the .in file were deleted in April 2025,
in "[REPO] modularize worklfows (#6582)". The hook is still in the config, and
it survives precisely because it is broken: its files: pattern can never
match, so it never runs, so it never fails.
The model writes prose, and nothing else
Each ghost gets a two-sentence epitaph from gemma3:4b, running locally through
Ollama. Every path, date, commit hash, line number and count on the page is read
from git by the detector. The model never sees a number.
That was not the original design. It is the result of catching it lying.
The first prompt included the burial date. gemma3:4b took a file buried on the
eighteenth of June and eulogised it as having vanished "on the sixth of June". It
had not been asked to mention a date at all. The fix was not a better
instruction. The fix was taking the date out of the prompt, so there was nothing
left for it to get wrong. It now receives the commit subject with the digits
stripped out, the kinds of files that still mention the dead one, and nothing
else.
Two guards survive on every epitaph: no digits, and no month or weekday names,
since it is never given a date. Across the whole published site, 46 epitaphs
were requested and one tripped a guard. It is printed on the page, flagged
in red, next to the epitaph that broke the rule. Regenerating it quietly until
it behaved would have made the page prettier and the claim worthless.
A small model given one prompt repeats itself
The first batch of epitaphs was unusable for a different reason. Eight ghosts,
eight calls, and five of them opened with the words "It rests here". Measured
properly, eight epitaphs produced 3 distinct openings.
The fix is one line of prompt. There are eight opening instructions, and each
ghost gets one chosen by hashing its file path, so a given file always receives
the same instruction and the output stays reproducible. Same model, same
temperature, same everything else:
| distinct openings | |
|---|---|
| one prompt for every ghost | 3 of 8 |
| rotated opening instruction | 7 of 8 |
bench_angles.py runs both arms back to back and writes every raw epitaph to
bench_angles.json, so the table above can be checked rather than believed.
Sweeping a whole laptop without losing the sweep
Fifty clones on one laptop, twenty nine distinct projects, is a long job, and
ray alone (thirty one thousand commits, seven and a half thousand deleted
files) takes a larger share of it than everything else put together. The first
version was a shell loop, and I killed it twice to fix detection bugs. Both
times I threw away everything it had already done, including a very expensive
ray.
So the corpus run is a Temporal workflow. One repository
is one activity. Temporal records each completed activity in the workflow
history, so a worker that dies is replaced by one that replays the history and
continues from where the last one stopped.
That is a claim worth testing rather than asserting, so durability_test.sh
does it: it waits for the sweep to get going, sends the worker SIGKILL with no
chance to save anything, starts a fresh one, and checks the finished count only
ever goes up.
before kill: 8 repositories finished
SIGKILL sent to worker pid 11807 at 07:00:12
with no worker: query unanswerable (), workflow state is on the server
new worker: pid 12098
after restart: 10 repositories finished
RESULT: resumed at repository 9, the first 8 were not scanned again
While the worker is dead, the progress query comes back
no poller seen for task queue recently, worker may be down. The work is not in
the worker. The worker is just the thing holding the shovel.
Building that found a real bug of my own. The activity declared a five minute
heartbeat timeout and never sent a heartbeat, so any repository slower than five
minutes would be abandoned and retried until the workflow failed. ray is very
much slower than five minutes.
Why open matters here
This tool reads your git history. Not your public repositories, your real ones:
the private monorepo, the client work, the thing you have not shipped yet. It
walks the whole deletion record, pulls the last living version of every buried
file, and greps the entire working tree.
Sending all of that to a hosted API so a model can write a nice sentence about
ghosts would be an absurd trade. The valuable half of this tool, the detection,
has no model in it at all. It is git and the Python standard library, nothing
installed, nothing to import, no key. --no-model gives you the complete
findings with nothing loaded.
The prose half is 3.3 GB on disk and takes about seven seconds per epitaph on an
M-series laptop with no GPU. It runs on a plane. That is the whole point: the
part that touches your source never leaves the machine, and the part that writes
is small enough to come with you.
That cuts both ways, and it caught me out. I built the site, went to push it,
and my own tooling refused on the grounds that I was about to publish private
data. It was right. The pages quote real paths and real lines, and the sweep had
read repositories that are nobody's business but mine. Worse, a file listing
every project folder on this laptop was already committed.
So publishing has three rules now, each of which fails closed. Only repositories
confirmed public. Never my own clones, because a clone of my own public
repository can still hold a branch I have not pushed. And for a third-party
repository where my checkout carries my branches, scan a throwaway worktree at
upstream's default branch instead of my working copy. Fourteen repositories made
it onto the site. Fifteen did not.
A tool that reads everything has to be careful about what it says out loud, and
that is far easier to get right when the model is on the same machine as the
secrets rather than on the other end of a request.
There is a quieter reason too. A tool about things that outlive their
infrastructure should not itself depend on an endpoint that might be gone in
three years. An ofrenda built on an API that gets deprecated is a joke with a
punchline I did not want to be in.
Run it
git clone https://github.com/CodersAcademy006/ouija
cd ouija
python3 ouija.py /path/to/your/repo # the findings, no model
python3 ouija.py /path/to/your/repo --json > ghosts.json
python3 ofrenda.py ghosts.json -o ofrenda.html # epitaphs, needs ollama
Python 3.9+, git, and ollama pull gemma3:4b if you want the altar. No pip
install. There is nothing to install.
python3 test_ouija.py
builds a throwaway repository containing one real ghost and three things that
only look like ghosts, and asserts it finds exactly the one.
What it does not do
It finds dangling references to deleted files. It does not find dangling
references to deleted symbols inside files that still exist, which is the
larger and harder problem, and the one my first version failed at so badly that
it declared size_t dead.
It will not tell you a ghost is harmful. The vLLM import is a real broken import.
The Triton hook is a leftover that cannot fire. A docstring pointing at a
deleted module is just stale. Those are three different severities and the tool
reports all three the same way, because deciding which of them matters in your
repository is not something a filename can tell you.
And it only sees what git saw. A file that was never committed was never buried,
and leaves no ghost.
Every repository that has lived long enough is standing on files it does not
remember deleting. Most of them went quietly. Go and find the ones that did not.
Repo: https://github.com/CodersAcademy006/ouija
Licence: MIT
Built for the Hacktoberfest Open-Source AI Challenge: Week 4, Ghost in the Machine.
Entering Best Use of Gemma, because gemma3:4b writes every epitaph locally
and nothing else writes them, and Best Use of Temporal, because the corpus
sweep is a durable workflow and the kill test above is the receipt.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.
