17 Dead References in My Handover Docs Turned Out to Be 2 Files, and the Real Risk Was a Different Site's Playbook Mixed In
The full Python script I use to check the root .md files an agent reads. My naive version counted 17 dead references; fixed, it was 7 across 2 files. Then it missed the real problem. Here is how I found it.
I had put the repository in a fresh working directory and was about to hand it to an agent. First I listed the root. Six .md files, 68,943 bytes in total.
I opened HANDOVER.md and stopped at the first line. It was the heading of a playbook for building a different site, not this one.
This repository was branched from the template of another site I run. The documents I carried over at the time were still sitting in the root. It is the kind of state that builds up quietly when you are an indie developer moving fast.
Here is the conclusion up front. A count of broken references says nothing about how much needs fixing, and a reference check alone misses the most dangerous document. I rewrote the checker three times to learn that, and I'm leaving the full script here.
The six documents I checked
The scope is the .md files in the root of this site's repository. That is the first place an agent looks, so I wanted to know whether it would be safe for an agent to take them at their word.
File
Size
What it is
CONTENT_LOG.md
30,371 B
Work history (mostly external links)
HANDOVER.md
14,317 B
A playbook for standing up another site
MIGRATION_GUIDE.md
11,406 B
Notes for moving the template over
SKILL.md
9,903 B
Working procedure
REMAINING_TASKS.md
2,846 B
Open-items memo
README.md
100 B
A one-line description
What caught my eye: of the 68,943 bytes, those two heavy documents, the playbook and the migration notes, take 25,723 bytes, or 37.3 %. It was not the tiny README. The heaviest documents were the ones talking about another project.
The naive version counted 17 "broken references"
My first version was very short. It pulled out path-looking strings wrapped in backticks and checked whether each one existed in the repository.
Document
References found
Missing
HANDOVER.md
27
7
MIGRATION_GUIDE.md
26
9
REMAINING_TASKS.md
6
1
SKILL.md
2
0
Total
61
17
Seventeen out of 61 is 28 %. Read at face value, more than a quarter of the documents were broken.
Opening them one by one told a different story. Many were bare file names with no directory, like Header.tsx or layout.tsx, and my script was looking for them at the repository root. They existed under src/components/layout/.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll be able to check, in under a tenth of a second, whether the root .md files in a repository carry dead references or another project's instructions before an agent reads them
✦You'll be able to stop trusting raw dead-reference counts and separate false positives, intentional future-tense mentions, and real fixes, so you only touch what needs touching
✦You'll be able to catch, before handing the repository over, the case where an agent reads a playbook inherited from a template and starts making changes meant for a different site
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
First, a bare file name is now looked up against the set of all file names in the repository. That took 17 to 8.
Second, references with brackets such as [locale] or [slug]. In Next.js those are real directory names, so a literal match works. My first version skipped them as undecidable, so they were not in the count. Once I evaluated them, the number went up from 8 to 10. The checker got smarter and the count rose, because it could now see what it had been ignoring.
Third, an explicit ignore list for references that are meant not to exist. Three of the ten qualified.
Reference
Why it is correct that it doesn't exist
/home/user/project/
A string quoted as an example of a task that assumed a fixed path
antigravitylab.net/
The top label of a directory-tree diagram
src/proxy.ts
A planned future rename, to be done once a feature is supported
src/proxy.ts is a reference I must not "fix" even though the check calls it broken. Not existing yet is its correct state at the time it was written.
The remaining seven converged on two files: BookRecommendation.tsx and opengraph-image.tsx. The first is a book-recommendation component that has already been removed. The second is an OGP image generator that I can no longer find in the repository. What looked like 17 problems was, as a place to fix, mentions of two files.
The real danger wasn't a reference. It was whose document it was
Something bothered me at that point. Even after those two files stopped being referenced, HANDOVER.md would still be a playbook for a different site. A reference check doesn't look at who the document is about.
So I added another angle: give the checker the other domains that came from the same template, and suspect any document that mentions them at least as often as it mentions this site.
Document
Mentions of this site
Mentions of the other domains
HANDOVER.md
7
9
MIGRATION_GUIDE.md
1
4
SKILL.md
2
0
In HANDOVER.md the other site's name (including non-domain spellings) appears 34 times and this site's name 16 times. It has 40 unchecked items (- [ ]), and near the top are "create a new repository", "change the name in package.json", and "change the name and account in wrangler.toml".
A person reads that and knows it's for the other site. But picture an agent that takes root files as instructions and works down the checkboxes. A proposal to rewrite this site's package.json name would not be surprising. That strikes me as much closer to real harm than any dead reference.
What surprised me is that a script I wrote to find document drift was built in a way that could not find the biggest problem. With zero broken references, a document about another project still passes.
The full checker
It runs on the standard library alone. The final version is 53 lines.
#!/usr/bin/env python3"""doc_drift.py - リポジトリ直下の *.md を、エージェントに読ませる前に点検する。検出するもの: (1) 実在しないパス参照 (2) テンプレート元の別ドメインが自ドメイン以上に出てくる文書 使い方: python3 -I doc_drift.py <repo> <own-domain> [--siblings DOMAIN ...] [--ignore PATH ...]終了コード: 0=問題なし / 1=要確認あり(CI や起動前フックで止められる)"""import argparse, collections, pathlib, re, sysap = argparse.ArgumentParser()ap.add_argument("repo"); ap.add_argument("own")ap.add_argument("--siblings", nargs="*", default=[], help="同じテンプレート由来の別ドメイン(混入検出用)")ap.add_argument("--ignore", nargs="*", default=[], help="意図して実在しない参照(失敗例・将来の予定など)")a = ap.parse_args()repo = pathlib.Path(a.repo)# 追跡対象の全パス。.git と node_modules は除くfiles = {p.relative_to(repo).as_posix() for p in repo.rglob("*") if p.is_file() and not {".git", "node_modules"} & set(p.parts)}basenames = {f.rsplit("/", 1)[-1] for f in files}# バッククォートで囲まれた「パスらしい」文字列だけを拾うREF = re.compile(r"`([\w@.\[\]{}/-]+/[\w@.\[\]{}-]*|[\w-]+\.(?:mjs|ts|tsx|js|json|toml|md|mdx|txt))`")DOM = re.compile(r"\b([a-z0-9-]+\.(?:net|jp|org|com))\b")def resolve(ref: str): ref = ref.strip("./") if "{" in ref: # テンプレート記法は判定しない return None if "/" not in ref: # 裸のファイル名は basename で探す return ref in basenames if ref in files or any(f.startswith(ref.rstrip("/") + "/") for f in files): return True # [locale] などのブラケットも実在ディレクトリ名として一致する return any(f.endswith("/" + ref) for f in files) # 先頭を省略した相対参照problems = 0for doc in sorted(repo.glob("*.md")): text = doc.read_text(encoding="utf-8") dead, skipped = collections.Counter(), 0 for ref in set(REF.findall(text)): if ref in a.ignore: continue r = resolve(ref) if r is None: skipped += 1 elif not r: dead[ref] = text.count(f"`{ref}`") doms = collections.Counter(DOM.findall(text)) foreign = {d: n for d, n in doms.items() if d in a.siblings} own_n = doms.get(a.own, 0) flag = bool(dead) or sum(foreign.values()) > own_n # 兄弟ドメインの言及が自ドメイン以上なら疑う problems += flag print(f"{'NG' if flag else 'ok'} {doc.name:<20} {len(text.encode()):>6}B " f"dead={sum(dead.values())} skipped={skipped} own={own_n} foreign={sum(foreign.values())}") for ref, n in dead.items(): print(f" dead {ref} ×{n}") for d, n in foreign.items(): print(f" foreign {d} ×{n}")sys.exit(1 if problems else 0)
Three notes on why it's written this way.
resolve returns None for template syntax like {name}. Counting something whose existence can't be asked as "broken" would teach people to stop reading the warnings. Brackets, in contrast, are treated as real directory names.
The exit codes are 0 and 1 so the script can gate a launch, not only be read by eye.
The sibling-domain rule is "at least as many as this site" so a document with only a faint trace of the template isn't blocked. That threshold is my rule of thumb, not a number with evidence behind it.
What it printed on this repository
First, with no ignore list. I did not touch a line of any document. The other domains are replaced with placeholders like sibling-a.example.
ok CONTENT_LOG.md 30371B dead=0 skipped=0 own=0 foreign=0NG HANDOVER.md 14317B dead=5 skipped=0 own=7 foreign=9 dead antigravitylab.net/ ×1 dead /home/user/project/ ×1 dead opengraph-image.tsx ×1 dead src/app/[locale]/opengraph-image.tsx ×1 dead BookRecommendation.tsx ×1 foreign sibling-a.example ×8 foreign sibling-b.example ×1NG MIGRATION_GUIDE.md 11406B dead=5 skipped=1 own=1 foreign=4 dead src/components/ui/BookRecommendation.tsx ×1 dead opengraph-image.tsx ×1 dead src/app/[locale]/opengraph-image.tsx ×2 dead BookRecommendation.tsx ×1 foreign sibling-b.example ×2 foreign sibling-a.example ×2ok README.md 100B dead=0 skipped=0 own=1 foreign=0NG REMAINING_TASKS.md 2846B dead=1 skipped=0 own=1 foreign=0 dead src/proxy.ts ×1ok SKILL.md 9903B dead=0 skipped=1 own=2 foreign=0
It ran in 0.084 seconds. CONTENT_LOG.md is ok because it only has many external links and no sibling domains.
Next, with the three intentional entries ignored.
ok CONTENT_LOG.md 30371B dead=0 skipped=0 own=0 foreign=0NG HANDOVER.md 14317B dead=3 skipped=0 own=7 foreign=9 dead opengraph-image.tsx ×1 dead BookRecommendation.tsx ×1 dead src/app/[locale]/opengraph-image.tsx ×1 foreign sibling-a.example ×8 foreign sibling-b.example ×1NG MIGRATION_GUIDE.md 11406B dead=5 skipped=1 own=1 foreign=4 dead opengraph-image.tsx ×1 dead src/components/ui/BookRecommendation.tsx ×1 dead BookRecommendation.tsx ×1 dead src/app/[locale]/opengraph-image.tsx ×2 foreign sibling-b.example ×2 foreign sibling-a.example ×2ok README.md 100B dead=0 skipped=0 own=1 foreign=0ok REMAINING_TASKS.md 2846B dead=0 skipped=0 own=1 foreign=0ok SKILL.md 9903B dead=0 skipped=1 own=2 foreign=0
REMAINING_TASKS.md became ok. HANDOVER.md and MIGRATION_GUIDE.md are still NG. I think staying NG is exactly this script's job. Fixing the references isn't enough to pass; it stops until a person decides whose document each one is.
A wrapper at the entrance
The ignore list lives in .doc-drift-ignore at the repository root, one reference per line. A thin wrapper reads it and calls the checker, and I put it at the entrance before launching an agent.
I wrote the three entries above into .doc-drift-ignore and ran it. It printed the "review the documents first" message and exited with 1. I deleted the ignore file afterwards.
Four pitfalls
First, looking bare file names up by basename is a lenient check. If a file with the same name exists anywhere, it passes even when the reference points somewhere else. In a repository with several Header.tsx files, that lets mistakes through.
Second, it only picks up strings wrapped in backticks. References written inside code blocks, such as a directory-tree diagram, are outside its reach. A stale file name left in a tree diagram will not be found.
Third, the ignore list grows. In my pre-flight check for .zip attachments I recorded acknowledged warnings per line. Here the exclusions are per path, which is coarse. If the same path breaks for a different reason, I won't notice.
Fourth, "count of other domains" is shaped around my repository. If the template's leftovers survive as product names or color names instead of domains, they slip past.
What I haven't verified
I haven't verified in what order or how heavily Antigravity reads root .md files. Every number above comes from running the script on my own repository, not from measuring agent behavior. I also didn't edit the body of any document in this pass.
Next steps
Check the number and byte size of the .md files at the root of the repository you're about to hand over
Run doc_drift.py once with no ignore list and open each entry under dead
Write only the references that are meant not to exist into .doc-drift-ignore, then run it again
For any document that shows foreign, decide whose document it is before fixing references. If it belongs to another project, moving it out of the repository is the safest option I can think of
Put agent_preflight.sh at the entrance and let the exit code stop you
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.