substrate
A folder of markdown, read as a graph
There is no database behind the wiki. There is a directory tree of .md files, a
convention for linking them, and about two hundred lines of TypeScript that turn that into
something a model can navigate.
The source calls this the Karpathy LLM Wiki pattern — cited twice, at
src/tools/files.ts:89 and :301. In practice the pattern means: keep
knowledge as plain files a human can edit, let the model read them directly instead of through
a retrieval abstraction, and accept the token cost when the corpus is small enough. Everything
in this section exists to make that cost negotiable.
What counts as a page
A single recursive walk defines the corpus. It is deliberately blunt: files ending in
.md, absolute paths, sorted, with six directory names excluded.
// Walk wiki dir, return absolute .md paths (skips raw/, _proposed/, graphify-out/, .git) async function walkWiki(): Promise<string[]> { const fs = await import('node:fs/promises'); const out: string[] = []; async function walk(dir: string) { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const e of entries) { const full = `${dir}/${e.name}`; if (e.isDirectory()) { if (['.git', 'graphify-out', '_proposed', 'proposed', 'imports', 'raw'].includes(e.name)) continue; await walk(full); } else if (e.isFile() && e.name.endsWith('.md')) { out.push(full); } } } await walk(WIKI_DIR); out.sort(); return out; }
Every read tool in files.ts starts by calling this and re-reading files from disk.
There is no cache and no watcher: the corpus is whatever is on disk at the moment of the call.
For a wiki of a few dozen pages that is the right trade — correctness for free, cost paid in
stat calls.
Two walkers, two definitions of the corpus
The write path keeps its own walker. src/tools/wiki_append.ts:17 excludes eight
directories — the six above plus _review and auto — and it starts from
<wiki>/wiki/ rather than the wiki root. So the set of pages
files_neighbors can read is a strict superset of the set wiki_append
will route to. A page living outside wiki/ is readable but is not an append target;
asking to append to it produces a candidate list, or a new page under wiki/.
Page anatomy and link resolution
Three pure functions do all the parsing. extractTitle looks at only the first 800
characters and prefers YAML frontmatter over the first # heading, falling back to the
relative path. extractSnippet strips frontmatter, skips headings, and returns the
first real paragraph collapsed to a single line. extractWikilinks normalises link
targets into slugs.
Links are text, and text is lossy
Outgoing edges are cheap: one regular expression over the target file. Inbound edges are expensive and approximate — there is no reverse index, so the tool opens every other page and tests two patterns against it.
const re = /\[\[([^\]|#]+)(?:[#|][^\]]*)?\]\]/g; // capture group 1 stops at ] | or # → anchors and pipe aliases are discarded // then: target.trim().toLowerCase().replace(/\s+/g, '-')
[[Memory System#Design|the memory system]] and [[memory system]] both
normalise to the slug memory-system. A link whose slug matches no file is still
returned, flagged resolved: false and rendered as
“(not yet written)” — so an unwritten page is a visible hole in the graph rather than
a silent omission.
const incomingPatterns = [ new RegExp(`\\[\\[${targetSlug}(?:[#|][^\\]]*)?\\]\\]`, 'i'), new RegExp(`\\b${targetSlug.replace(/-/g, '[- ]')}\\.md\\b`, 'i'), ];
The second pattern lets a page be found by prose that mentions its filename, with hyphens
matching either a hyphen or a space. Note that the slug is interpolated into a regular
expression without escaping — a page slug containing regex metacharacters would change the
meaning of the pattern. Slugs produced by the write path are constrained to
[a-z0-9-] (wiki_append.ts:22-24), so this only matters for files
created by hand outside that path.
The cheap index
wiki_index exists so an agent never has to guess. It returns one line per page —
relative path, byte size, extracted title — and the tool description states the intended cost:
roughly 500 tokens for a 35-page wiki. Its input schema is empty; there is nothing to get wrong.
| Tool | Returns | Files opened | Size control |
|---|---|---|---|
wiki_index |
Path, size and title of every page | All, first 800 B each | none needed |
files_neighbors |
Target page in full, plus snippets of every outgoing and inbound neighbour | All, in full | max_bytes 1,000–500,000, default 30,000; snippet_chars 80–2,000, default 280 |
wiki_read_all |
Every page concatenated under ## <relative path> headers |
All, in full | max_bytes 1,000–2,000,000, default 1,000,000 |
files_read |
Raw bytes of one file, local or over SSH | One | max_bytes 1–5,000,000, default 1,000,000 |
Truncation is always explicit. files_neighbors appends
[... TRUNCATED at N bytes — pass larger max_bytes to see more ...];
wiki_read_all reports how many pages it did not reach. The model is told what it is
missing and how to ask for it.
The corpus is small enough that the honest answer to “what is in the wiki?” is a directory listing.
the design premise of wiki_index
How material gets in
Ingestion is deliberately two-phase and the tool refuses to blur them.
files_wiki_ingest writes a raw source into <wiki>/raw/ — the one
directory the page walker ignores — and returns instructions rather than a page. Turning a
source into curated prose is an agent's job, performed later, with the wiki's own schema
document in scope.
The derived graph
The three graph_* tools do not traverse markdown. They shell out to a Python module
against a pre-built graph.json, with a guard that runs first and returns a
remediation message if the file is absent. That guard is the whole error-handling strategy: fail
with instructions, never with an empty array that a model might read as “nothing connects these”.
| Tool | Arguments | Subcommand | Timeout |
|---|---|---|---|
graph_query |
question, dfs? (default breadth-first), budget? 200–20,000 output tokens, default 2,000 |
query <question> --graph … [--dfs] [--budget N] |
60 s |
graph_path |
from, to — node labels as they appear in the wiki |
path <from> <to> --graph … |
30 s |
graph_explain |
node |
explain <node> --graph … |
30 s |
The distinction that matters: files_neighbors answers “what is one hop from this
page?” from the live text, while graph_query answers “what connects these
two ideas?” from a snapshot that may be up to half an hour stale. Freshness and reach pull in
opposite directions, and the tool descriptions say so.
No wiki page names, page contents, directory listings or file paths from the live corpus appear
on this site. <slug>, <wiki> and <host> are
placeholders.