{"slug": "building-an-llm-wiki-for-your-project-a-step-by-step-guide-agent-maintained-base", "title": "Building an LLM Wiki for Your Project — a step-by-step guide (agent-maintained knowledge base: schema, ingest/query/lint workflows, qmd search, lint script)", "summary": "A developer has published a step-by-step guide for building an 'LLM Wiki'—a persistent, interlinked markdown knowledge base that an AI agent (such as Claude Code or Codex) maintains incrementally. The approach, based on Andrej Karpathy's pattern, compiles knowledge once and keeps it current, contrasting with typical RAG systems that re-derive answers on each query. The guide details a three-layer structure (raw sources, wiki pages, and a schema file) and includes optional search and linting workflows.", "body_md": "This guide shows you how to set up an **LLM Wiki** in your own project: a persistent, interlinked markdown knowledge base that an LLM agent (Claude Code, Codex, etc.) builds and maintains for you. It is based on [Andrej Karpathy's \"LLM Wiki\" pattern](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f).\n\nYou can go from nothing to a working wiki in ~15 minutes (Parts 1). Search and linting (Parts 2–3) are optional add-ons. Part 4 and the Use Cases show how to actually drive it day to day.\n\nMost people use LLMs with documents via **RAG**: upload files, the model retrieves chunks at query time, answers, and forgets. Nothing accumulates — every question rediscovers knowledge from scratch.\n\nAn **LLM Wiki** is different. Instead of re-deriving on every query, the LLM **incrementally builds and maintains a wiki** — a folder of cross-linked markdown pages that sits between you and your raw sources. Add a source, and the agent reads it, updates the relevant pages, flags contradictions, and files it. The knowledge is *compiled once and kept current*.\n\nThree layers:\n\n| Layer | What it is | Who owns it |\n|---|---|---|\nRaw sources (`raw/` ) |\nImmutable inputs — articles, transcripts, screenshots, page dumps, exported docs. Read-only. | You (curate) |\nThe wiki (`wiki/` ) |\nLLM-generated, cross-linked markdown pages. | The agent (writes & maintains) |\nThe schema (`WIKI.md` ) |\nConventions + the ingest/query/lint workflows the agent follows. | You + agent (co-evolve) |\n\nPlus two navigation files: `index.md`\n\n(a catalog) and `log.md`\n\n(an append-only history).\n\n**Division of labor:** you do sourcing, exploration, and asking good questions. The agent does the grunt work — summarizing, cross-referencing, filing, and bookkeeping. You (almost) never write the wiki by hand.\n\n**git**— the wiki is just a folder of markdown; version it.** A coding agent**— Claude Code, Codex CLI, OpenCode, etc. This is the \"engine.\"*(Optional)***Node.js ≥ 22**— only if you want the lint script (Part 3).*(Optional)*— a local, on-device search engine for markdown (Part 2). Install with[qmd](https://github.com/tobi/qmd)`npm install -g @tobilu/qmd`\n\n. Not needed until your wiki grows past a few hundred pages —`index.md`\n\nalone works well at small/medium scale.\n\nWhere to put the wiki.A dedicated top-level folder (e.g.`llm-wiki/`\n\n) with itsown git repois the cleanest choice, especially if your project root is not itself a repo, or if you don't want wiki churn mixed into your product's history.\n\n```\nmkdir -p llm-wiki/raw \\\n         llm-wiki/wiki/domains \\\n         llm-wiki/wiki/screens \\\n         llm-wiki/wiki/entities \\\n         llm-wiki/wiki/concepts \\\n         llm-wiki/scripts\ncd llm-wiki\n```\n\nAdjust the `wiki/`\n\nsubfolders to your domain. Common choices:\n\n`domains/`\n\n— top-level areas of your subject (the main axis).`entities/`\n\n— the \"nouns\" (people, objects, records, components).`concepts/`\n\n— cross-cutting ideas.`screens/`\n\n(for apps) /`modules/`\n\n(for codebases) /`sources/`\n\n(for research).\n\nThis is the most important file. It turns a generic chatbot into a disciplined wiki maintainer. Every agent session reads it first. Copy this template and adapt the bracketed parts:\n\n```\n# WIKI.md — Schema & Workflows\n\n> Read this file FIRST before ingesting, querying, or linting. It defines how\n> this wiki is structured and the workflows to follow.\n\n## 1. Three layers\n- `raw/` — immutable sources, read-only. Never edit after saving.\n- `wiki/` — LLM-generated pages. The agent owns these.\n- `WIKI.md` + `index.md` + `log.md` — schema, catalog, history.\n\n**Roles:** the human curates sources and asks questions; the LLM does all\nsummarizing, cross-referencing, filing, and bookkeeping.\n\n## 2. Navigation: index-first\nWhen answering, read `index.md` FIRST to find relevant pages, then read them.\n`index.md` is enough at moderate scale. (Optional: use `qmd` for semantic search\nwhen the wiki grows large — it augments, never replaces, `index.md`.)\n\n## 3. Page organization\n- `wiki/overview.md` — the map of the whole wiki.\n- `wiki/domains/<slug>.md` — [your main axis].\n- `wiki/entities/<slug>.md` — [your nouns].\n- `wiki/concepts/<slug>.md` — [cross-cutting ideas].\n\n## 4. Page conventions\n- Slugs are kebab-case ASCII. Filename = slug + `.md`.\n- Every page starts with YAML frontmatter:\n  ``` yaml\n  ---\n  title: <human title>\n  type: overview | domain | entity | concept\n  tags: [<tag>, ...]\n  sources: [raw/<path>/, ...]   # provenance\n  updated: YYYY-MM-DD\n  ---\n  ```\n- Link liberally with `[[slug]]` (or `[[slug|label]]`). Linking to a page that\n  doesn't exist yet is fine — it becomes a to-do (the linter flags it).\n- **Never hard-wrap prose. One paragraph, list item, or table cell = one line**,\n  however long. Never break mid-sentence to shorten a line; the editor\n  soft-wraps. Only exception: inside a code fence. Enforce with\n  `npx prettier --write <file>` and a `.prettierrc` holding\n  `{ \"proseWrap\": \"never\", \"embeddedLanguageFormatting\": \"off\" }`.\n\n## 5. The three workflows\n\n### Ingest (add one source)\n1. Read the source in `raw/`.\n2. Discuss the key takeaways with the human; ask what to emphasize.\n3. Create/update the primary page for this source.\n4. Propagate to related domain/entity/concept pages; add `[[cross-links]]`.\n5. Update `index.md` (add/adjust the line for each new page).\n6. Append to `log.md`: `## [YYYY-MM-DD] ingest | <title>`.\n7. Ingest one source at a time, supervised. One source may touch 10–15 pages.\n\n### Query (ask the wiki)\n1. Read `index.md` first (use `qmd query` for deep search if configured).\n2. Read the relevant pages; answer WITH citations.\n3. File valuable answers back as new wiki pages so explorations compound.\n\n### Lint (health check)\n1. Mechanical: run the lint script (broken links, orphans, missing-in-index).\n2. Semantic (the agent): contradictions, stale claims, concepts mentioned but\n   lacking a page, missing cross-references, data gaps.\n3. Suggest new questions to investigate and sources to add.\n```\n\n`index.md`\n\n:\n\n```\n# Index\n\n> Read this first. Each page: a link + a one-line summary. Updated on every ingest.\n\n## Overview\n- [overview](wiki/overview.md) — the map of the wiki.\n\n## Domains\n_(grows as you ingest)_\n\n## Entities\n_(grows as you ingest)_\n\n## Concepts\n_(grows as you ingest)_\n```\n\n`log.md`\n\n:\n\n```\n# Log\n\n> Append-only. Each entry starts `## [YYYY-MM-DD] <op> | <title>` so it greps:\n> `grep \"^## \\[\" log.md | tail -5`\n\n## [2025-01-01] init | Scaffolded the wiki\n```\n\n`wiki/overview.md`\n\n— a seed page describing your subject (even a rough one; the agent will enrich it):\n\n```\n---\ntitle: Overview\ntype: overview\ntags: [overview]\nsources: []\nupdated: 2025-01-01\n---\n\n# Overview\n\n[2–3 sentences on what this wiki covers.]\n\n## Map\n_(domains grow here as you ingest)_\ngit init\nprintf '.qmd-home/\\n.DS_Store\\n' > .gitignore   # ignore qmd's local index (Part 2)\ngit add -A\ngit commit -m \"chore: scaffold llm-wiki\"\n```\n\n**You now have a working LLM Wiki.** You can stop here and start ingesting (Part 4). Parts 2–3 add search and linting.\n\n`index.md`\n\nis enough to start. Add [qmd](https://github.com/tobi/qmd) when you want semantic/hybrid search over a larger wiki. qmd runs fully **on-device** (local GGUF models, no API key).\n\n```\nnpm install -g @tobilu/qmd      # or: bun install -g @tobilu/qmd\nqmd --help\n```\n\nqmd stores its index in a global cache by default (`~/.cache/qmd/index.sqlite`\n\n). To keep **this** wiki's index separate from any other qmd use, point qmd's `XDG`\n\ndirs at a local folder. Save this as `scripts/qmd-wiki.sh`\n\n:\n\n``` bash\n#!/usr/bin/env bash\n# Run qmd with an index ISOLATED to this wiki (won't touch your global qmd index).\nset -euo pipefail\nWIKI_ROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\nexport XDG_CACHE_HOME=\"$WIKI_ROOT/.qmd-home/cache\"\nexport XDG_CONFIG_HOME=\"$WIKI_ROOT/.qmd-home/config\"\n# Optional: pick an embedding model. For non-English / CJK content, Qwen3 is far better:\nexport QMD_EMBED_MODEL=\"hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf\"\nexec qmd \"$@\"\nchmod +x scripts/qmd-wiki.sh\nmkdir -p .qmd-home/cache/qmd\n# Share the (large) model cache with your global qmd, if present, to avoid re-downloading:\n[ -d \"$HOME/.cache/qmd/models\" ] && ln -s \"$HOME/.cache/qmd/models\" .qmd-home/cache/qmd/models\n```\n\nWhy a wrapper?It bundles the isolation env + model choice into one command you (and the MCP server) always use. Verify isolation with`./scripts/qmd-wiki.sh status`\n\n— the`Index:`\n\nline must point at`.qmd-home/`\n\n, not`~/.cache`\n\n.\n\n```\n./scripts/qmd-wiki.sh collection add ./wiki --name wiki\n./scripts/qmd-wiki.sh context add qmd://wiki \"What this wiki covers (one sentence).\"\n./scripts/qmd-wiki.sh embed        # first run downloads the model (~600MB); on-device\n./scripts/qmd-wiki.sh query \"your first question\"\n```\n\nRe-run `embed`\n\nafter each batch of ingests. Search commands:\n\n`qmd-wiki.sh search \"term\"`\n\n— fast BM25 keyword.`qmd-wiki.sh vsearch \"meaning\"`\n\n— semantic.`qmd-wiki.sh query \"question\"`\n\n— hybrid + rerank (best).\n\nSo the agent can search natively instead of shelling out. For Claude Code, add to `.mcp.json`\n\n:\n\n```\n{\n  \"mcpServers\": {\n    \"qmd-wiki\": {\n      \"command\": \"/absolute/path/to/llm-wiki/scripts/qmd-wiki.sh\",\n      \"args\": [\"mcp\"]\n    }\n  }\n}\n```\n\nPointing the MCP command at the wrapper ensures the server uses the isolated index. **Restart your agent** for MCP changes to take effect.\n\nMechanical checks are cheap and deterministic — let a script do them so the agent can focus on semantic linting. Save this as `scripts/lint.mjs`\n\n(Node ≥ 22, zero dependencies):\n\n``` js\nimport { readdirSync, readFileSync, existsSync } from 'node:fs';\nimport { join, relative, basename } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nconst WIKILINK = /\\[\\[([^\\]|]+)(?:\\|[^\\]]*)?\\]\\]/g;\n\nexport function lintWiki(root) {\n  const wikiDir = join(root, 'wiki');\n  const indexPath = join(root, 'index.md');\n  const files = existsSync(wikiDir)\n    ? readdirSync(wikiDir, { recursive: true }).map(String)\n        .filter((f) => f.endsWith('.md')).map((f) => join(wikiDir, f))\n    : [];\n\n  const slugOf = (p) => basename(p, '.md');\n  const pages = new Map();\n  for (const f of files) pages.set(slugOf(f), f);\n\n  const brokenLinks = [];\n  const inbound = new Map();\n  for (const slug of pages.keys()) inbound.set(slug, 0);\n\n  for (const f of files) {\n    readFileSync(f, 'utf8').split('\\n').forEach((line, i) => {\n      const re = new RegExp(WIKILINK.source, 'g');\n      let m;\n      while ((m = re.exec(line))) {\n        const target = m[1].trim();\n        if (pages.has(target)) inbound.set(target, inbound.get(target) + 1);\n        else brokenLinks.push({ file: relative(root, f), line: i + 1, target });\n      }\n    });\n  }\n\n  const orphans = [];\n  for (const [slug, count] of inbound)\n    if (count === 0 && slug !== 'overview')\n      orphans.push({ slug, file: relative(root, pages.get(slug)) });\n\n  const indexText = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : '';\n  const missingInIndex = [];\n  for (const [slug, f] of pages)\n    if (!indexText.includes(slug)) missingInIndex.push({ slug, file: relative(root, f) });\n\n  return { brokenLinks, orphans, missingInIndex };\n}\n\n// CLI — skipped when loaded by the test runner\nif (!process.env.NODE_TEST_CONTEXT && process.argv[1] &&\n    import.meta.url === pathToFileURL(process.argv[1]).href) {\n  const root = process.argv[2] || '.';\n  const r = lintWiki(root);\n  let problems = 0;\n  const section = (title, arr, fmt) => {\n    if (arr.length) { problems += arr.length; console.log(`\\n${title} (${arr.length}):`);\n      for (const x of arr) console.log('  - ' + fmt(x)); }\n  };\n  section('Broken [[links]]', r.brokenLinks, (x) => `${x.file}:${x.line} → [[${x.target}]]`);\n  section('Orphan pages', r.orphans, (x) => x.file);\n  section('Missing in index.md', r.missingInIndex, (x) => `${x.file} (slug: ${x.slug})`);\n  if (problems === 0) { console.log('✓ Lint clean.'); process.exit(0); }\n  console.log(`\\n✗ ${problems} problem(s).`); process.exit(1);\n}\n```\n\nRun it with `node scripts/lint.mjs .`\n\n. It exits non-zero when there are problems, so you can wire it into CI or a pre-commit hook.\n\nTip:keep a companion`scripts/lint.test.mjs`\n\nusing Node's built-in`node:test`\n\n. Run tests with`node --test scripts/*.test.mjs`\n\n(pass the glob —`node --test scripts/`\n\ntries to load the folder as a module and fails).\n\nThe wiki grows through three operations you trigger by talking to your agent.\n\nDrop a source into `raw/`\n\n(a saved article, a transcript, a screen dump, an exported doc), then tell the agent:\n\n\"Ingest\n\n`raw/<path>`\n\ninto the wiki following WIKI.md. Summarize the key takeaways first and ask me what to emphasize before writing pages.\"\n\nThe agent reads the source, discusses it with you, writes/updates pages, adds cross-links, updates `index.md`\n\n, and appends to `log.md`\n\n. Review its summary, then let it file. Commit.\n\nAsk questions against the accumulated knowledge:\n\n\"Using the wiki, how does the reservation approval flow work? Cite the pages.\"\n\nThe agent reads `index.md`\n\n, opens the relevant pages, and answers with citations. When an answer is valuable (a comparison, an analysis, a newly discovered connection), tell it to **file it back** as a new page — so your explorations compound just like ingested sources.\n\nPeriodically:\n\n\"Run\n\n`node scripts/lint.mjs .`\n\n, fix any broken links or missing index entries, then do a semantic lint per WIKI.md §5 and suggest what to ingest next.\"\n\nYou have an internal app or system with tribal knowledge and no docs. Point your agent's browser tools at each screen/module, dump the page text into `raw/<screen>/`\n\n, and ingest one screen at a time. The agent builds `screens/`\n\npages plus `domains/`\n\n(business areas) and `entities/`\n\n(the records the system manages), fully cross-linked. Later: *\"How does staff attendance get recorded?\"* → a cited answer synthesized across pages. This is exactly the pattern this repo was built for.\n\nTreat source directories as raw sources. Ingest module by module; the agent writes `modules/`\n\npages (responsibility, key files, dependencies) and `concepts/`\n\npages (auth, caching, the data model), linking them. Ask *\"how does auth work end to end?\"* and get a synthesized, cited walkthrough instead of grepping. Re-ingest a module after a big refactor; the linter flags stale cross-references.\n\nGoing deep on a topic over weeks. Clip articles/papers into `raw/`\n\n(the Obsidian Web Clipper is handy). Ingest each; the agent maintains an evolving thesis in `overview.md`\n\n, entity pages for key people/works, and concept pages, flagging where a new paper contradicts an earlier claim. The wiki becomes your compounding literature review.\n\nFeed meeting transcripts, decision records, and thread exports into `raw/`\n\n. The agent maintains pages per project, per decision, per person — the maintenance no one on the team wants to do. Query *\"why did we choose Postgres over DynamoDB?\"* months later and get the reasoning with sources.\n\n**Keep** It's your source of truth. The agent reads it, never edits it.`raw/`\n\nimmutable.**Don't hand-write the wiki.** Let the agent write it; you curate and ask. Hand edits drift from the conventions and the agent's model of the wiki.in your installed version — don't rely on it. Use the XDG wrapper (Step 6) for isolation.`qmd init`\n\nmay not existtreats the path as a module and fails. Pass a glob:`node --test scripts/`\n\n`node --test scripts/*.test.mjs`\n\n.**Non-English content?** The default embedding model is English-centric. Set`QMD_EMBED_MODEL`\n\nto a multilingual model (e.g. Qwen3-Embedding) for CJK/Vietnamese/etc.**MCP changes need an agent restart** to load.**The agent copies your corpus, not your rules.** If your existing pages hard-wrap prose at ~90 columns, the agent will keep hard-wrapping no matter what`WIKI.md`\n\nsays — in-context examples beat instructions. Worse,`WIKI.md`\n\nitself is usually the most hard-wrapped file you own, so it teaches the opposite of what it states. Reflow your rule files*first*, then write the rule. The same trap applies to any convention you state but don't demonstrate.**Don't reach for** It re-serializes what your markdown`prettier`\n\nbefore reading what it changes.*means*, which is not always what you see. A frontmatter template indented under a list item — with no code fence — parses as prose, and its`---`\n\nlines become setext underlines: prettier collapses the whole block into one line, because that block was already rendering as broken headings on GitHub and you never noticed. Fence it as```` yaml`\n\n, and set`embeddedLanguageFormatting: \"off\"`\n\nso prettier does not reformat the YAML inside. Same story for steps packed onto one line (`4. Do X. 5. Do Y.`\n\n): markdown sees one item, prettier renumbers to match. Both are bugs in the source, revealed rather than caused.**Browse with** Open the folder — its graph view is the fastest way to see hubs, orphans, and the shape of your knowledge. Obsidian is the \"IDE\"; the agent is the programmer; the wiki is the codebase.[Obsidian](https://obsidian.md).\n\n```\nllm-wiki/\n├── WIKI.md              # schema + ingest/query/lint workflows (agent reads first)\n├── index.md             # catalog — read first when answering\n├── log.md               # append-only history\n├── README.md            # human-facing readme\n├── .prettierrc          # { proseWrap: never, embeddedLanguageFormatting: off }\n├── raw/                 # immutable sources (read-only)\n├── wiki/                # LLM-generated pages\n│   ├── overview.md\n│   ├── domains/  entities/  concepts/   (+ screens/ or modules/ …)\n├── scripts/\n│   ├── lint.mjs         # mechanical lint (Part 3)\n│   └── qmd-wiki.sh      # isolated qmd wrapper (Part 2)\n└── .qmd-home/           # qmd's local index + model cache (git-ignored)\n```\n\nThat's it. Scaffold in 15 minutes, then let the agent do the bookkeeping while you curate and ask good questions. The wiki compounds with every source you add and every question you ask.", "url": "https://wpnews.pro/news/building-an-llm-wiki-for-your-project-a-step-by-step-guide-agent-maintained-base", "canonical_source": "https://gist.github.com/quangyendn/5683375a47d9e5cc1c90c43b97d3849a", "published_at": "2026-08-22 04:26:46+00:00", "updated_at": "2026-08-22 05:13:05.290519+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools"], "entities": ["Andrej Karpathy", "Claude Code", "Codex", "qmd", "LLM Wiki"], "alternates": {"html": "https://wpnews.pro/news/building-an-llm-wiki-for-your-project-a-step-by-step-guide-agent-maintained-base", "markdown": "https://wpnews.pro/news/building-an-llm-wiki-for-your-project-a-step-by-step-guide-agent-maintained-base.md", "text": "https://wpnews.pro/news/building-an-llm-wiki-for-your-project-a-step-by-step-guide-agent-maintained-base.txt", "jsonld": "https://wpnews.pro/news/building-an-llm-wiki-for-your-project-a-step-by-step-guide-agent-maintained-base.jsonld"}}