references/details.md
# Grounded vault: details
Deep material for the `grounded-vault` skill. `SKILL.md` carries the convention; this file
carries the scripts, templates, and edge cases.
## Vault check script
`scripts/check_vault.py` walks every page under `wiki/`, verifies each linked claim against
its raw source, and compares each fingerprint with the current tree. It uses only the standard
library and `git`.
```python
#!/usr/bin/env python3
"""Grounding and drift checks for a grounded vault. Run from the vault root."""
import re
import subprocess
import sys
from pathlib import Path
RAW = (Path.cwd() / "raw").resolve()
WIKI = Path("wiki")
HEADER = re.compile(r"^> (Raw|Fingerprint|Monitored|Status): (.*)$", re.M)
LINK = re.compile(r"\[([^\]]+)\]\(([^)]+\.md)\)")
NUMBER = re.compile(r"(?<![\w.,])\d[\d.,]*%?(?![\w.,])")
QUOTE = re.compile(r"[\"\u201c]([^\"\u201d]{8,})[\"\u201d]") # shorter quoted words are not claims
def header(text: str) -> dict[str, str]:
return {k: v.strip() for k, v in HEADER.findall(text)}
def prose(text: str) -> str:
"""Body text without the header block, headings, and fenced code."""
kept, fenced = [], False
for line in text.splitlines():
if line.startswith("```"):
fenced = not fenced
continue
if fenced or line.startswith((">", "#")):
continue
kept.append(line)
return "\n".join(kept)
def claims(text: str):
"""Yield (item, is_number, raw_links) for every figure or quotation in the prose."""
for sentence in re.split(r"(?<=[.!?])\s+", prose(text)):
links = [path for _, path in LINK.findall(sentence) if "raw/" in path]
bare = LINK.sub(" ", sentence) # link labels and paths are not claims
for item in NUMBER.findall(bare):
yield item, True, links
for item in QUOTE.findall(bare):
yield item, False, links
def raw_source(page: Path, rel: str) -> Path | None:
"""The raw file a link points at, or None when it escapes raw/ or is missing."""
src = (page.parent / rel).resolve()
return src if src.is_relative_to(RAW) and src.is_file() else None
def grounded(item: str, is_number: bool, sources: list[str], page: Path) -> bool:
token = re.compile(r"(?<![\w.,])" + re.escape(item) + r"(?![\w.,])")
for rel in sources:
src = raw_source(page, rel)
if src is None:
continue
text = src.read_text(errors="replace")
if token.search(text) if is_number else item in text:
return True
return False
def drift(fingerprint: str, monitored: str) -> str:
"""Non-empty when monitored paths changed since the fingerprint, or git cannot tell."""
sha = fingerprint.removeprefix("git:")
paths = [p.strip() for p in monitored.split(",") if p.strip()]
if not paths:
return ""
out = subprocess.run(
["git", "diff", "--stat", f"{sha}..HEAD", "--", *paths],
capture_output=True, text=True, check=False,
)
if out.returncode != 0:
return f"git cannot compare {sha}: {out.stderr.strip() or 'unknown fingerprint'}"
return out.stdout.strip()
def main(strict: bool) -> int:
errors = 0
for page in sorted(WIKI.rglob("*.md")):
text = page.read_text()
meta = header(text)
if meta.get("Status", "Current") != "Current":
continue
if not meta.get("Fingerprint"):
print(f"{page}: no Fingerprint header")
errors += 1
for item, is_number, sources in claims(text):
if not sources:
if strict:
print(f"{page}: {item!r} has no raw/ source link")
errors += 1
continue
if not grounded(item, is_number, sources, page):
print(f"{page}: {item!r} not found in {', '.join(sources)}")
errors += 1
stat = drift(meta["Fingerprint"], meta.get("Monitored", "")) if meta.get("Fingerprint") else ""
if stat:
print(f"{page}: drifted since {meta['Fingerprint']}\n{stat}")
errors += 1
print(f"{errors} problem(s)")
return 1 if (errors and strict) else 0
if __name__ == "__main__":
sys.exit(main(strict="--strict" in sys.argv))
```
What it checks, and what it deliberately does not:
- Only the prose is scanned. The header block, headings, fenced code, and the labels and
paths of Markdown links are excluded, so a source path such as `raw/adr/0007-jwt.md`
never reads as a claim of `0007`.
- A number is grounded when it appears in a linked source as a whole token, so `15` does
not match `150` or `2015`. A quotation must appear verbatim. Quoted phrases shorter than
eight characters are not treated as claims; a two-word quote is not evidence of anything.
Reformatted figures (`1,000` versus `1000`) fail on purpose; copy the source's form.
- A link must resolve inside `raw/`. A path that escapes it, or points at a file that is
missing, is a miss, so a page cannot ground a claim on something outside the vault.
- Under `--strict`, a number or quotation with no `raw/` link in its sentence is an error.
Without `--strict` it is skipped, which is the mode for a first pass over an old vault.
- Every current page needs a `Fingerprint:`. An empty `Monitored:` is allowed: a page
compiled only from `raw/` has no code to drift against.
- Drift is a non-empty `git diff --stat` between the fingerprint and `HEAD` restricted to
the monitored paths. When git cannot compare, for example after a history rewrite removed
the fingerprint, that counts as drift too; recompile and stamp a fresh fingerprint.
- Archived and disputed pages are skipped; they are records, not claims.
## Batching the drift check
For a vault with many pages, collect fingerprints first and run one `git diff` per distinct
fingerprint rather than one per page:
```bash
grep -rh '^> Fingerprint: git:' wiki | sort -u | sed 's/^> Fingerprint: git://' \
| while read -r sha; do
echo "== $sha"; git diff --stat "$sha..HEAD" -- $(grep -rl "git:$sha" wiki \
| xargs grep -h '^> Monitored:' | sed 's/^> Monitored: //' | tr ',' '\n' | sort -u)
done
```
## Templates
`index.md`:
```markdown
# Vault index
| Page | Status | Fingerprint | Sources |
|---|---|---|---|
| [Authentication architecture](wiki/auth-architecture.md) | Current | git:5b237fa | raw/notes/auth-v1.md, raw/adr/0007-jwt.md |
```
`log.md`, one line per change, newest last:
```markdown
2026-09-01 compile wiki/auth-architecture.md from raw/adr/0007-jwt.md at git:5b237fa
2026-09-14 archive wiki/session-store.md: Outdated, src/auth/session.ts changed after git:5b237fa
```
Pre-commit hook (`.git/hooks/pre-commit`):
```bash
#!/bin/sh
python3 scripts/check_vault.py --strict || {
echo "vault check failed; fix the claim or the fingerprint before committing" >&2
exit 1
}
```
The same command runs as a CI step on pull requests so the gate holds for every contributor.
## Edge cases
- **Renamed or deleted monitored files.** `git diff` reports the deletion as a change, which
is correct: the page describes something that moved. Recompile with the new paths in
`Monitored:`.
- **Binary sources** (PDFs, images). Store the binary in `raw/` and add a sibling text
extraction (`report.pdf` and `report.pdf.txt`) produced once by a deterministic tool. Link
claims to the text file so the grounding check can read it.
- **External URLs.** A URL is not immutable. Save a dated snapshot into `raw/` and link the
snapshot; keep the URL in the snapshot's first line for attribution.
- **Multiple repositories.** Fingerprint with `git:<repo-name>@<sha>` and keep one vault per
repository, or one vault whose `Monitored:` paths are prefixed by repository. The check
script above assumes a single repository.
- **Large raw files.** Verbatim search is linear in file size; it stays fast up to tens of
megabytes. Split larger exports by date when ingesting.
## Reference implementation
`llm-wiki-loop` (MIT, <https://github.com/PALAN-K/llm-wiki-loop>) scaffolds this layout with
`npx llm-wiki-loop init`, ships a stricter `check_evidence.py`, and adds an event-driven
garbage collector and a step that promotes repeated fixes in `log.md` into agent skills. Read
it for the full loop; nothing in this skill requires it. The pattern was proposed for this
catalog by its author in wshobson/agents issue #673.
SKILL.md
---
name: grounded-vault
description: Use when maintaining a durable Markdown knowledge store that agents compile from sources, when every number or quote in a wiki page must trace back to an immutable source, or when compiled pages need cheap drift detection against the code they describe. Teaches the raw/wiki/archive layout, per-claim provenance links, and git fingerprints for zero-token staleness checks.
---
# Grounded Vault
A grounded vault is a three-layer Markdown store in which every compiled claim can be traced
back to an immutable source and every page can be checked for staleness with one `git diff`.
It needs a git repository and nothing else. The convention comes from the `llm-wiki-loop`
project, which is a reference implementation rather than a dependency; this skill teaches the
pattern so it works with plain files and whatever agent is in the session.
## When to Use
- An agent compiles notes, papers, transcripts, logs, or code into wiki pages that later sessions rely on.
- A page cites numbers, dates, or quotes, and a reader must be able to verify each one against its source.
- Pages describe code, and rereading the codebase every session to check whether they still hold is too expensive.
- Knowledge must be corrected without losing history: superseded pages are archived, never deleted.
Session state, task queues, and conversation continuity are a different problem; use the
context-management or conductor plugins for those. This skill is about provenance and drift on a
durable knowledge store.
## The three layers
| Layer | Contents | Who writes it | Rule |
|---|---|---|---|
| `raw/` | source material: notes, papers, transcripts, logs, exported data | people and ingestion only | immutable once added; agents never edit a raw file |
| `wiki/` | compiled pages built from `raw/` and from code | agents and people | every number, date, and quote links to its source |
| `archive/` | pages that drifted or were superseded | agents, during garbage collection | moved, never deleted; the header says why |
Two files sit at the vault root. `index.md` is the map of every current page. `log.md` is an
append-only record of what changed and why. Both change in the same commit as the page they
describe.
## Page header contract
Every `wiki/` page opens with a header block:
```markdown
# Authentication architecture
> Raw: [raw/notes/auth-v1.md](../raw/notes/auth-v1.md), [raw/adr/0007-jwt.md](../raw/adr/0007-jwt.md)
> Fingerprint: git:5b237fa
> Monitored: src/auth/jwt.ts, src/auth/session.ts, package.json
> Status: Current
```
- `Raw:` lists every source the page was compiled from. Inline claims link to their specific source as well: `Tokens expire after 15 minutes ([raw/adr/0007-jwt.md](../raw/adr/0007-jwt.md)).`
- `Fingerprint:` is the short commit hash the page was compiled against.
- `Monitored:` lists the code paths the page describes. A change to any of them after the fingerprint means the page may be stale.
- `Status:` is `Current`, `Outdated` (monitored code moved on), or `Disputed` (a newer source contradicts the page).
## Grounding rule
A compiled page states only what a source supports, and every number, date, or quotation
appears verbatim in the linked source. A synthesis says it is one and links its inputs. A gap
in the sources is written into the page as a gap rather than filled by guessing.
Check it mechanically: for each linked claim, search the linked raw file for the exact figure
or quoted phrase. A miss is a grounding error and blocks the commit. The script in
`references/details.md` does this for a whole vault.
## Drift detection
Compare the fingerprint with the current tree instead of rereading monitored code:
```bash
git diff --stat 5b237fa..HEAD -- src/auth/jwt.ts src/auth/session.ts package.json
```
Empty output means the page still describes the code it was compiled against. Any output means
recompile: reread only the changed files, update the page, and stamp the new fingerprint. The
check runs in milliseconds and spends no model tokens.
## Workflow
1. **Ingest.** Put new material in `raw/` under a dated or sourced filename. Never rewrite an existing raw file; add a new one beside it.
2. **Compile.** Write or update the `wiki/` page with the header block, a source link on every claim, and the fingerprint of the commit the code was read at.
3. **Check.** Run the grounding check and the drift check before committing. Fix misses at the source; do not weaken a claim to make the check pass.
4. **Garbage collect.** When drift or a contradicting source appears and the page is not recompiled now, change its status, move it to `archive/`, and record the reason:
```markdown
> Status: Outdated
> Reason: src/auth/session.ts changed after git:5b237fa; see log.md 2026-09-01
```
5. **Update the map.** Every add, move, or archive updates `index.md` and appends one line to `log.md` in the same commit.
## Commit gate
Run both checks from a pre-commit hook or a CI step so a page cannot land with an unverifiable
number or a stale fingerprint:
```bash
python3 scripts/check_vault.py --strict # exits 1 on any grounding miss or drifted page
```
## Going deeper
`references/details.md` covers: the vault check script, batching the drift check across pages,
templates for `index.md` and `log.md`, renamed or deleted monitored files, sources that are
binary or live at external URLs, and the reference implementation.