
Llm Wiki
- 58 installs
- 643 repo stars
- Updated April 16, 2026
- lewislulu/llm-wiki-skill
Helps with ai & agent building tasks.
About
llm-wiki is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- llm-wiki
- AI & Agent Building
- AI-coding skill
Llm Wiki by the numbers
- 58 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #6,589 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lewislulu/llm-wiki-skill --skill llm-wikiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 643 |
| Last updated | April 16, 2026 |
| Repository | lewislulu/llm-wiki-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
LLM Wiki — Karpathy Knowledge Base Pattern
Experimental skill — iterating.
Authored by Lewis Liu (lylewis@outlook.com) · Inspired by Karpathy's llm-wiki Gist
Core idea
Instead of RAG (re-retrieving raw docs on every query), the LLM compiles raw sources into a persistent, cross-linked wiki. Every ingest, query, lint, and audit pass makes the wiki richer. Knowledge compounds — and the human stays in the loop via a structured feedback channel instead of ad-hoc corrections that get lost.
- You own: sourcing raw material, asking good questions, steering direction, filing feedback on anything the AI got wrong.
- LLM owns: all writing, cross-referencing, filing, bookkeeping, and acting on your feedback.
The wiki is a living artifact with five operations — compile, ingest, query, lint, audit. Every session starts by reading CLAUDE.md and wiki/index.md.
Directory layout
<wiki-root>/
├── CLAUDE.md ← Schema: scope, conventions, current articles, gaps
├── log/ ← Per-day operation log (one file per day)
│ ├── 20260409.md
│ └── 20260410.md
├── audit/ ← Human feedback inbox (one file per comment)
│ ├── 20260409-143022-claude-code-size.md
│ └── resolved/ ← Processed feedback, archived with resolution notes
├── raw/ ← Immutable source documents (LLM reads, never writes)
│ ├── articles/
│ ├── papers/
│ ├── notes/
│ └── refs/ ← Pointer files for large binaries kept outside raw/
├── wiki/ ← LLM-generated knowledge (LLM writes, you read)
│ ├── index.md ← Master catalog — every page, structured by category
│ ├── concepts/ ← Concept/topic pages (split into subfolders when >1200 words)
│ ├── entities/ ← People, tools, papers, organizations
│ └── summaries/ ← Per-source summary pages
└── outputs/
└── queries/ ← Query answers (promote durable ones to wiki/)CLAUDE.md is the schema file — the single most important configuration. It tells the LLM the wiki's scope, naming conventions, current article list, open questions, and research gaps. Read references/schema-guide.md for what to put in it. Read it at the start of every session.
Core principles
Four rules govern everything below. If a future instruction contradicts one, flag it to the user before acting.
1. Divide and conquer
A single concept page should never try to cover a complex topic end-to-end. Target: 400–1200 words per page. When a topic would blow past that:
- Create a subfolder:
wiki/concepts/<topic>/ - Put a short index page at
wiki/concepts/<topic>/index.md— definition, list of sub-pages, one-line summaries - Put each aspect in its own file:
wiki/concepts/<topic>/<aspect>.md - In
wiki/index.md, show the hierarchy via indented bullets
Example layout (from a real wiki):
wiki/tech/claude-code/
├── index.md (overview + links to sub-pages)
├── Claude_Code_Architecture.md
├── Claude_Code_Agent_Framework.md
├── Claude_Code_Bridge_System.md
├── Claude_Code_Query_Engine.md
├── Claude_Code_Skills_Plugins.md
├── Claude_Code_State_Management.md
└── Claude_Code_Tool_System.mdOne fat file covering all seven aspects would be unreadable and unlinkable. Seven focused files + an index page give you navigation, selective reading, clean backlinks, and small audit targets.
2. Mermaid for diagrams, KaTeX for formulas
- Any flow, sequence, hierarchy, or state diagram must be written in mermaid — never ASCII art. ASCII boxes rot fast and are impossible to annotate.
````
flowchart LR
A[raw/article.md] --> B[summary]
B --> C[concept page]
C --> D[index.md]````
- Any formula must be written in KaTeX: inline
$f(x) = \sum_i w_i x_i$or block$$...$$.
Both render in the web viewer (server-side KaTeX, client-side mermaid) and in Obsidian with default settings.
3. Raw file policy
Small text-based sources (md, txt, small pdfs, small images) → copy into raw/<subfolder>/.
Large binaries (videos, model weights, installers, datasets, large PDFs >10 MB) → do not copy. Instead:
- Create a pointer file at
raw/refs/<slug>.mdwith:
---
kind: ref
external_path: /Volumes/external/models/llama-3-70b/
size: ~140 GB
---followed by a short description of what it is and why it matters to this wiki.
- Wiki pages cite
[[raw/refs/<slug>]]exactly like any other source.
This keeps the wiki repo git-friendly and portable.
4. Audit is the human feedback surface
The wiki is AI-written; it will be wrong sometimes. The raw sources are human-written; they will contradict each other. The audit/ directory is how humans correct both without losing the corrections in chat history.
- Humans file feedback via the Obsidian plugin or the web viewer. Each feedback is one file in
audit/with YAML frontmatter (anchor, target, severity) and a markdown body. - The AI must periodically run the
auditop — never silently ignoreaudit/*.mdfiles. - When feedback is applied, the file moves to
audit/resolved/with a# Resolutionsection appended and a log entry recorded inlog/YYYYMMDD.md.
See references/audit-guide.md for the full file format and processing workflow.
---
The five operations
Every action on the wiki is one of these five. Each appends an entry to the current day's log file (log/YYYYMMDD.md).
1. compile
(Re)structure wiki content from existing raw/ material — including splitting oversized pages, merging near-duplicates, and rebuilding index.md.
When to run: after a big ingest batch, when an existing page has outgrown 1200 words, when index.md no longer reflects reality, or when the user says "clean up the wiki".
Steps: 1. Read CLAUDE.md, wiki/index.md, and every file in the target subtree. 2. For each page over ~1200 words: plan a split into concepts/<topic>/ with an index + sub-pages. Confirm the plan with the user before writing. 3. For each pair of near-duplicate pages: propose a merge. Confirm, then rewrite. 4. Regenerate wiki/index.md so every page is listed exactly once. 5. Log: ## [HH:MM] compile | <what you did — files touched, splits, merges>
2. ingest
Add a new source. One source typically touches 5–15 wiki pages.
Steps: 1. Save source to the right subfolder:
- web article →
raw/articles/<slug>.md - paper →
raw/papers/<slug>.md(extracted text for big PDFs) - note →
raw/notes/<slug>.md - large binary →
raw/refs/<slug>.mdpointer file (see raw file policy)
2. Read the source in full. 3. Create wiki/summaries/<slug>.md (200–400 words — key takeaways, not a rewrite; see references/article-guide.md). 4. Create or update relevant concept pages in wiki/concepts/. Respect divide-and-conquer: if a concept page would exceed 1200 words, split instead of cramming. 5. Create or update entity pages in wiki/entities/ for any new people / tools / papers / organizations referenced. 6. Update wiki/index.md so the new pages appear under the right category. 7. Log: ## [HH:MM] ingest | <slug> — <one-line description> (touched N pages)
3. query
Answer a question grounded in the wiki, not general knowledge.
Steps: 1. Read wiki/index.md. Scan for relevant pages by category. 2. Read the identified pages in full; follow one level of wikilinks. 3. If the wiki doesn't have enough material, say so and suggest what to ingest next instead of making something up. 4. Synthesize the answer, citing pages inline with [[Page Name]]. 5. Save to outputs/queries/<YYYY-MM-DD>-<question-slug>.md. 6. If the answer is durable (a comparison, analysis, or new synthesis) → promote a cleaned-up version to wiki/concepts/, add to index.md. 7. Log: ## [HH:MM] query | <question-slug> (and a separate ## [HH:MM] promote | ... line if promoted).
4. lint
Health check. Run:
python3 scripts/lint_wiki.py <wiki-root>The script reports:
- Dead wikilinks —
[[Target]]whereTarget.mddoesn't exist - Orphan pages — pages with no inbound wikilinks
- Missing index entries — pages not listed in
wiki/index.md - Frequently-linked missing pages —
[[X]]referenced 3+ times but no page - log/ shape — stray files or wrong filenames in
log/ - audit/ shape — malformed YAML frontmatter in
audit/*.md - Audit target resolution — every open audit's
targetfile must exist
For each issue, propose a fix, confirm with the user, then apply. Log: ## [HH:MM] lint | <N> issues found, <M> fixed.
5. audit
Process human feedback from audit/.
Steps: 1. Run python3 scripts/audit_review.py <wiki-root> --open to get a grouped list. 2. For each open audit, read the file. Use the anchor_before / anchor_text / anchor_after window to locate the exact range in the target file (line numbers may have drifted). 3. Decide the action:
- Accept: apply the correction to the target file.
- Partially accept: apply what makes sense, note the rest in the resolution.
- Reject: explain why in the resolution — the feedback may be based on a misreading of scope or a contradictory source.
- Defer: add to
CLAUDE.md"Open research questions" and leave the audit in place with a comment.
4. For applied audits, append a # Resolution section to the audit file:
# Resolution
2026-04-10 · accepted.
Fixed the file count (was "~1,900", corrected to "~1,800" per commit abc123).
Updated: tech/Claude_Code.md lines 47–48.5. Move the file from audit/ to audit/resolved/. Filename unchanged. 6. Log per resolved audit:
## [HH:MM] audit | resolved 20260409-143022-a1b2 — <one-line what>7. Never delete audit files. Rejected ones still go to resolved/ with the rejection rationale in their resolution section — that's valuable history.
See references/audit-guide.md for the full audit file format.
---
Tooling
| Tool | Purpose |
|---|---|
| Obsidian | IDE for browsing the wiki; graph view shows connections |
| `plugins/obsidian-audit/` | Obsidian plugin — select text → add feedback → writes to audit/ |
| `web/` | Local Node.js server — preview the wiki with mermaid/math rendered; select → feedback → audit/ |
scripts/scaffold.py | Bootstrap a new wiki directory tree |
scripts/lint_wiki.py | Seven-pass health check |
scripts/audit_review.py | Group open/resolved audits by target file |
| qmd | Optional local semantic search (useful at >100 pages) |
The Obsidian plugin and the web viewer both write audit files in the same format with the same anchor algorithm, so feedback filed from either place can be resolved by either place.
Starting a new wiki
python3 scripts/scaffold.py <wiki-root> "<Topic Title>"Creates the full tree (including log/<today>.md, audit/, audit/resolved/), a blank CLAUDE.md based on the new template, and a blank wiki/index.md with the recommended category layout.
After scaffolding: 1. Fill in CLAUDE.md — define scope, naming conventions, initial research questions. 2. Start ingesting sources. 3. Ask questions to build up outputs/queries/; promote durable answers. 4. Run lint periodically. 5. Run audit whenever new feedback accumulates.
wiki/index.md format
The LLM rebuilds index.md on every compile and touches it on every ingest. Format:
# Index — <Topic>
> One-sentence scope of the wiki.
## 🔖 Navigation
- [[#Concepts]] · [[#Entities]] · [[#Summaries]] · [[#Open Questions]]
## Concepts
### <Category A>
- [[concepts/Foo]] — one-line summary
- [[concepts/Bar/index|Bar]] — (folder-split) one-line summary
- [[concepts/Bar/aspect-1]] — ...
- [[concepts/Bar/aspect-2]] — ...
### <Category B>
- ...
## Entities
- [[entities/Andrej Karpathy]] — AI researcher, author of the llm-wiki pattern
## Summaries (chronological)
- 2026-04-09 — [[summaries/llm-wiki-gist]] — Karpathy's original Gist
## Open Questions
- Q1: ...Rules:
- Every wiki page must appear exactly once in
index.md.lintenforces this. - Folder-split concepts show hierarchy via indented bullets.
index.md+CLAUDE.mdtogether are what the AI reads at session start.
log/ format
See references/log-guide.md for full details. Minimum:
- One file per day:
log/YYYYMMDD.md - H1 = the date; H2 per entry with
## [HH:MM] <op> | <one-line description> - Ops:
compile,ingest,query,lint,audit,promote,split,scaffold
Quick grep across history: grep -rh "^## \[" log/ | tail -20.
Use cases
- Research deep-dive — reading papers/articles on a topic over weeks; the wiki evolves with your understanding, and the audit trail keeps AI mistakes from silently accumulating
- Personal wiki — journal entries, notes, ideas compiled into a personal encyclopedia; comment on anything you disagree with later, the AI corrects it
- Team knowledge base — fed by Slack threads, meeting notes, docs; team members file corrections through the web viewer
- Reading companion — filing each book chapter as you go; builds a rich companion wiki by the end
References
references/schema-guide.md— What to put inCLAUDE.mdreferences/article-guide.md— How to write good wiki articles (length, wikilinks, mermaid, math, divide-and-conquer)references/log-guide.md— Thelog/folder conventionreferences/audit-guide.md— Audit file format, anchor strategy, processing workflowreferences/tooling-tips.md— Obsidian setup, Web Clipper, qmd, plugin + web installation
Wiki Article Writing Guide
Guidelines for writing high-quality wiki articles. Read before compiling a new concept or entity page.
Length targets
| Page type | Target length | Notes |
|---|---|---|
| Concept page | 400–1200 words | Dense, no padding. Hard ceiling: 1200. |
Folder-split index.md | 150–400 words | Definition + map of sub-pages |
| Sub-page under a folder-split | 400–1200 words | Covers one aspect |
| Entity page | 200–500 words | Factual, link-heavy |
| Summary page | 150–400 words | Takeaways, not a rewrite |
Avoid padding. A 400-word article that's dense beats an 800-word article with filler.
Divide and conquer — when to split
If a concept page would exceed ~1200 words, do not write it as a single file. Split it:
1. Create wiki/concepts/<topic>/. 2. Write wiki/concepts/<topic>/index.md:
---
title: <Topic>
type: concept
...
---
# <Topic>
<One-sentence definition.>
## What it is
<150–300 words of overview.>
## Sub-pages
- [[<Topic>/<aspect-1>]] — <one-line summary>
- [[<Topic>/<aspect-2>]] — <one-line summary>
- ...
## Sources
- [[summaries/...]]3. Write each <aspect-N>.md as a focused 400–1200 word page. 4. Update wiki/index.md to show the hierarchy with indented bullets under the folder-split entry.
Signs a page needs to be split:
- Word count creeping past 1000.
- Three or more
##top-level sections, each with its own###subsections. - Multiple distinct concepts mentioned but not explored because there's no room.
- You find yourself wanting to link to a specific section with
[[Page#Section]]— that section probably deserves its own page.
Concept page structure
---
title: <Title>
type: concept
created: YYYY-MM-DD
updated: YYYY-MM-DD
sources: [slug1, slug2]
tags: [tag1, tag2]
---
# <Title>
<One-sentence definition or core idea.>
## What it is
<Explain the concept clearly. Assume the reader is technically literate but unfamiliar with this specific topic.>
## How it works
<Mechanism, process, or structure. Use a mermaid diagram if it's a flow, sequence, hierarchy, or state.>
flowchart LR A --> B --> C
## Key properties / tradeoffs
<Bullet list or short paragraphs. Use KaTeX for any formula — inline `$...$` or block `$$...$$`.>
## Relationship to other concepts
- [[Related Concept A]] — how they relate
- [[Related Concept B]] — contrast or connection
## Open questions
<What this wiki doesn't yet know about this concept. Drives future ingest.>
## Sources
- [[summaries/source-slug-1]] — (date) one-line description
- [[summaries/source-slug-2]] — (date) one-line descriptionEntity page structure
---
title: <Name>
type: entity
entity_type: person | tool | paper | organization
created: YYYY-MM-DD
updated: YYYY-MM-DD
sources: [slug1]
tags: [tag1]
---
# <Name>
<One-sentence description.>
## Key contributions / features
<What this entity is known for in the context of this wiki's topic.>
## Related concepts
- [[Concept A]] — connection
## Sources
- [[summaries/source-slug]]Summary page structure
Summaries are concise representations of a single source. They are not rewrites.
---
title: summaries/<slug>
type: summary
source_url: https://...
source_type: article | paper | gist | video | podcast | ref
date: YYYY-MM-DD
ingested: YYYY-MM-DD
tags: [tag1]
---
# <Source Title>
**Source**: [<Author/Org>](<URL>) · <date>
## Key takeaways
- <Most important insight 1>
- <Most important insight 2>
- <Most important insight 3>
## Core claims
<2–4 sentences on the main argument or findings.>
## Notable quotes
> "<exact quote>" — <attribution>
## Concepts introduced / referenced
- [[Concept A]]
- [[Entity B]]Diagrams — always mermaid
ASCII art is banned. Any flow, sequence, hierarchy, or state diagram is mermaid. Examples:
Flow: ````markdown
flowchart TB
source[raw/article.md] --> ingest
ingest --> summary[wiki/summaries/...]
ingest --> concept[wiki/concepts/...]
concept --> index[wiki/index.md]````
Sequence: ````markdown
sequenceDiagram
User->>Web: select text + comment
Web->>Server: POST /api/audit
Server->>FS: write audit/*.md
Server-->>User: audit id````
State: ````markdown
stateDiagram-v2
[*] --> open
open --> resolved: audit op
open --> deferred: add to Open Questions````
Formulas — always KaTeX
Inline: The loss is $\mathcal{L}(\theta) = \sum_i \ell(f_\theta(x_i), y_i)$.
Block:
$$
\mathcal{L}(\theta) = \frac{1}{N}\sum_{i=1}^{N} \ell\bigl(f_\theta(x_i), y_i\bigr) + \lambda \|\theta\|_2^2
$$The web viewer renders math server-side with KaTeX. Obsidian renders it natively.
Wikilink rules
1. Link first mention of every entity or concept — don't wait for "a natural place". 2. Link maximum twice per article — don't over-link the same page. 3. Link concepts that exist — check wiki/index.md before creating a new link target. 4. For folder-split pages, link the index with an alias: [[concepts/Foo/index|Foo]]. 5. Backlink audit — after writing a new article, grep existing articles for the new page's title and add incoming links.
Handling contradictions between sources
When two sources contradict each other:
1. State both claims explicitly. 2. Note which source supports each claim. 3. Add to the article's "Open questions" section and the wiki's CLAUDE.md research questions. 4. Do NOT silently pick one — contradictions are valuable signal.
Example:
Source A (2024) claims X. Source B (2026) claims Y, which contradicts A. It's unclear whether this reflects a methodological difference or an error in one source. See [[summaries/source-a]] and [[summaries/source-b]].
If a human later files an audit comment resolving the contradiction, update the article and move the audit to audit/resolved/ with a resolution note.
Incorporating audit feedback
When processing an open audit that targets an article you're editing:
1. Locate the anchor using anchor_before / anchor_text / anchor_after. 2. Apply the correction in the smallest edit that fixes the issue. 3. Bump the updated: field in the frontmatter. 4. Add a line to the # Resolution section of the audit file explaining what changed. 5. Move the audit file to audit/resolved/. 6. Log the resolution under the current day's log/YYYYMMDD.md.
Audit Guide — human feedback on wiki content
The audit/ directory is the human feedback surface. One file per feedback, YAML frontmatter + markdown body. Feedback is produced by the Obsidian plugin or the web viewer and consumed by the AI during the `audit` operation.
Why it exists
AI-written content is wrong sometimes. Raw sources contradict each other. Feedback in chat is lost the moment the conversation ends. The audit directory gives corrections a permanent, location-anchored home that every tool (Obsidian plugin, web viewer, AI, lint script) understands.
Directory layout
<wiki-root>/audit/
├── 20260409-143022-claude-code-size.md ← open feedback
├── 20260409-150110-rag-definition.md ← open feedback
└── resolved/
├── 20260408-110505-typo-gemma.md ← processed, with resolution
└── 20260407-180012-rejected-scope.md ← rejected, with rationaleaudit/*.md— open feedback, not yet processed.audit/resolved/*.md— processed feedback. Nothing ever gets deleted; rejections stay with their rationale.
File format
Filename: YYYYMMDD-HHMMSS-<short-slug>.md. The prefix is the creation timestamp (local time); the slug is a human-readable hint derived from the selected text or the comment.
---
id: 20260409-143022-a1b2
target: tech/Claude_Code.md
target_lines: [45, 52]
anchor_before: "## 技术概览\n\n| 维度 | 详情 |\n|------|------|\n"
anchor_text: "| **规模** | ~1,900 个文件,512,000+ 行代码 |"
anchor_after: "\n| **语言** | TypeScript(strict 模式) |"
severity: warn
author: lewis
source: obsidian-plugin
created: 2026-04-09T14:30:22+08:00
status: open
---
# Comment
实际应该是 ~1,800 个文件,参考 2026-03-31 commit abc123 的 tree。
`find . -type f | wc -l` 当时是 1817。这个数字直接影响下面几个估算。
# Resolution
<!-- Filled in when the audit is processed and moved to resolved/ -->Frontmatter fields
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique id: YYYYMMDD-HHMMSS-<4hex>. Must match filename prefix. |
target | string | yes | Path relative to wiki root. Must be a file that exists (lint check). |
target_lines | [int, int] | yes | Best-effort 1-indexed inclusive line range at the time of writing. May drift. |
anchor_before | string | yes | Up to ~80 chars of text immediately before the selection. Verbatim, preserves newlines. |
anchor_text | string | yes | The exact selected text. Verbatim. |
anchor_after | string | yes | Up to ~80 chars of text immediately after the selection. Verbatim. |
severity | enum | yes | One of info, suggest, warn, error. |
author | string | yes | Free text. The Obsidian plugin defaults to the OS username; the web viewer has a config. |
source | enum | yes | One of obsidian-plugin, web-viewer, manual. |
created | ISO 8601 | yes | Timestamp with timezone. |
status | enum | yes | open for files in audit/, resolved for files in audit/resolved/. |
Severity semantics
- info — "worth noting but not wrong". Example: additional context, alternate phrasing.
- suggest — "consider this". Example: reword, reorganize.
- warn — "something looks off". Example: stale number, ambiguous sentence.
- error — "this is wrong". Example: factual mistake, broken link, wrong attribution.
The AI should process error and warn first, then suggest, then info.
Anchor strategy
Line numbers alone are fragile — any edit earlier in the file invalidates them. So every audit file carries a text-based anchor window alongside the line numbers.
On write (Obsidian plugin / web viewer): 1. Capture target_lines from the selection range. 2. Extract anchor_text = the exact selected characters. 3. Extract anchor_before = up to 80 characters immediately before the selection start (clamped to start of file). 4. Extract anchor_after = up to 80 characters immediately after the selection end (clamped to end of file).
On read (AI during audit, audit_review.py, both tools): 1. Try target_lines — check whether the text in that line range contains anchor_text. 2. If not, search the whole file for anchor_text. If exactly one match, use it. 3. If multiple matches, use anchor_before + anchor_text + anchor_after as a combined search key. 4. If still no match, the anchor is stale — flag to the user during the audit op. Do not silently drop; ask whether to re-anchor, reject, or archive.
This algorithm lives in audit-shared/src/anchor.ts and is the single source of truth for all tools.
Processing workflow (the audit op)
See SKILL.md → "The five operations" → audit for the canonical version. In short:
1. python3 scripts/audit_review.py <wiki-root> --open → get a grouped list. 2. For each open audit:
- Read the file, use the anchor to locate the range in the target.
- Decide: accept / partial / reject / defer.
- Apply edits in the target file (in the smallest edit that fixes the issue).
- Append a
# Resolutionsection to the audit file. - Flip
status: open→status: resolvedin the frontmatter. - Move the file to
audit/resolved/. - Append a
## [HH:MM] audit | resolved <id> — <one-liner>entry tolog/YYYYMMDD.md.
3. If an audit is deferred (e.g., unresolvable contradiction), leave the file in audit/ and add the question to CLAUDE.md "Open research questions" with a reference to the audit id.
Resolution section format
# Resolution
2026-04-10 · accepted.
Fixed the file count (was "~1,900", corrected to "~1,800" per commit abc123).
Updated: tech/Claude_Code.md lines 47–48.
Log: [[log/20260410#1430 audit]]Fields:
- Date · decision (
accepted,partial,rejected,deferred). - 1–3 sentences on what you did and why.
- Which files were touched (for non-trivial edits).
- Pointer to the log entry.
For rejected audits: explain why — most often "out of scope per CLAUDE.md" or "contradicts more authoritative source X". Rejected audits still move to resolved/ so they're not processed again, but they remain visible in case the scope changes.
Tooling
- `scripts/lint_wiki.py` validates audit file shape and that every
targetfile exists. - `scripts/audit_review.py` lists and groups audits.
- `plugins/obsidian-audit/` writes audit files from inside Obsidian on selection.
- `web/` writes audit files from the local web viewer on selection.
- `audit-shared/` — TypeScript library implementing the schema, anchor algorithm, id generator, and YAML (de)serialization used by the plugin and the web server.
Log Guide — the log/ folder
The wiki's operation log is a folder, not a single file. One file per day, named log/YYYYMMDD.md. This keeps individual files small, makes daily activity easy to scan, and plays well with git diffs.
File naming
- Filename:
log/YYYYMMDD.md(e.g.,log/20260409.md) - Regex:
^\d{8}\.md$ - No other files are allowed at the top of
log/.scripts/lint_wiki.pywill flag stray files.
File format
# 2026-04-09
## [09:15] ingest | google-gemma-4-article
- Source: raw/articles/google-gemma-4.md
- Touched: 5 wiki pages
- summaries/google-gemma-4 (new)
- concepts/Gemma.md (updated)
- entities/Google.md (updated)
- entities/Gemma 4.md (new)
- index.md (updated)
## [14:30] audit | resolved 20260409-143022-a1b2
- Target: tech/Claude_Code.md
- Change: corrected file count from ~1,900 to ~1,800 per commit abc123
- Moved to: audit/resolved/20260409-143022-a1b2.md
## [15:05] lint | 2 dead links found, 2 fixed
- [[Claude Code Architecture]] → [[tech/claude-code/Claude_Code_Architecture]] in 2 filesRules:
- One H1 per file, matching the filename date in ISO format (
YYYY-MM-DD). - One H2 per operation, starting with
## [HH:MM] <op> | <one-line description>. - Time is local time, 24h.
- Body is a short bullet list summarising what changed. Link to the files touched with wikilinks.
Ops allowed in the log
| Op | When it appears | Example |
|---|---|---|
compile | Structural edits, splits, merges, index rebuild | `## [10:00] compile \ |
ingest | New source added to raw/, wiki updated | `## [09:15] ingest \ |
query | Question answered, output file written | `## [11:20] query \ |
promote | Output promoted to wiki/concepts/ | `## [11:35] promote \ |
lint | Lint run with issues fixed | `## [15:05] lint \ |
audit | Feedback applied and moved to audit/resolved/ | `## [14:30] audit \ |
split | A single page split into a folder | `## [10:00] split \ |
scaffold | Initial wiki setup | `## [08:00] scaffold \ |
Quick grep
# All operations on a day
cat log/20260409.md
# Recent activity across all days
grep -rh "^## \[" log/ | sort | tail -20
# All audit resolutions
grep -rh "^## \[.*\] audit" log/
# Activity on a specific file
grep -rl "Claude_Code" log/Migration from single-file log.md
If you have an existing log.md (from the v1 skill), convert it:
1. Parse each ## [YYYY-MM-DD] op | description header. 2. Group entries by date. 3. For each date D, create log/D.md with an H1 of the date and H2s for each op — convert [YYYY-MM-DD] to [HH:MM] (use 00:00 if no time recorded). 4. Delete the old log.md.
This is a one-time manual operation; the skill doesn't automate it.
What not to put in the log
- Content: don't copy-paste chunks of the article you wrote into the log. The log is a pointer, not a diary.
- Long rationale: put design decisions and rationale in
CLAUDE.md"Notes for the LLM", not in the log. - Secrets / credentials: never.
- Audit file bodies: only the audit ID and a one-liner. The audit file itself already has the full content.
CLAUDE.md Schema Guide
CLAUDE.md (also read as AGENTS.md by some tools) is the schema document for a wiki topic. It tells the LLM agent the scope, conventions, current state, and open questions — every session should start by reading it together with wiki/index.md.
Why it matters
Without a schema, the LLM creates inconsistent page names, overlapping articles, and drifts from the wiki's intended scope. With a well-maintained schema, the LLM becomes a disciplined, consistent wiki maintainer.
Co-evolve it with the wiki — update after every major compile, ingest batch, or structural change.
Full template
# <Topic Title> Knowledge Base
> Schema document — read at the start of every session together with wiki/index.md.
## Scope
What this wiki covers:
- <bullet list of included areas>
What this wiki deliberately excludes:
- <bullet list of out-of-scope areas>
## Operations
This wiki follows the llm-wiki skill's five operations: `compile`, `ingest`, `query`, `lint`, `audit`.
Every operation appends an entry to `log/YYYYMMDD.md`.
## Naming conventions
### Pages
- **Concept pages** (`wiki/concepts/`): Title Case noun phrases. E.g., "Market Making Strategy", not "market making" or "MarketMakingStrategy".
- **Folder-split concepts** (`wiki/concepts/<topic>/`): used when a topic would exceed ~1200 words as a single page. Contains `index.md` + one file per aspect.
- **Entity pages** (`wiki/entities/`): Proper names. E.g., "Andrej Karpathy", "Obsidian", "Avellaneda-Stoikov Model".
- **Summary pages** (`wiki/summaries/`): kebab-case source slug. E.g., "karpathy-llm-wiki-gist".
### Wikilinks
- Always use `[[Page Title]]` — exact page title, case-sensitive.
- For folder-split pages, link to the index: `[[concepts/Foo/index|Foo]]`.
- Link the first mention of every entity or concept. Do not link the same page more than twice per article.
### Frontmatter
Every wiki page has YAML frontmatter:--- title: <Page Title> type: concept | entity | summary created: YYYY-MM-DD updated: YYYY-MM-DD sources: [list of raw/ slugs this page draws from] tags: [relevant tags] ---
### Diagrams and formulas
- All diagrams are **mermaid**. No ASCII art.
- All formulas are **KaTeX** (inline `$...$` or block `$$...$$`).
### Raw file policy
- Small text sources → copy into `raw/<subfolder>/`.
- Large binaries → create a pointer file at `raw/refs/<slug>.md` with `kind: ref` frontmatter and an `external_path` field. Do not copy the binary.
## Current articles
### Concepts
- [[<Concept Title>]] — one-line summary
- [[concepts/<Topic>/index|<Topic>]] — (folder-split) one-line summary
- [[<Topic>/<aspect-1>]] — ...
### Entities
- [[<Entity Name>]] — one-line summary
### Summaries
- [[summaries/<slug>]] — source title (date)
## Open research questions
- <Questions that should drive future ingest/query work>
- <Things the wiki currently doesn't cover well>
- <Contradictions or gaps noticed between articles>
## Research gaps
Sources to ingest:
- [ ] <URL or paper title> — why it's relevant
## Audit backlog
Count of open audits per target (filled in after running `audit_review.py --open`):
- <file> — N open
- ...
## Notes for the LLM
<Any special instructions: tone, depth level, language (zh/en), how to handle contradictions, etc.>What makes a good schema
Good scope definition prevents sprawl. A wiki about "LLM memory techniques" should exclude "LLM training" even though they're related.
Explicit naming conventions keep wikilinks from breaking. If you decide concept pages use Title Case, enforce it — a broken wikilink is an orphan.
Maintained article list lets the LLM know what already exists before creating a new page. The most common error is creating duplicate articles with slightly different names.
Open research questions give the LLM direction. Without them, the LLM defaults to ingesting the most obvious sources and missing your actual questions.
Audit backlog surfaces what the human has flagged as wrong. The AI should glance at it at the start of every session to decide whether to run an audit op before ingesting new material.
Update cadence
- After every new concept page: add to "Current articles".
- After every ingest batch: update "Sources to ingest" checklist.
- After every lint pass: update "Research gaps".
- After every audit pass: refresh the "Audit backlog" counts.
- Monthly: review scope, prune stale research questions.
Tooling Tips
Practical setup and usage notes for the LLM Wiki stack.
Obsidian setup
Essential settings
1. Attachment folder: Settings → Files and links → "Attachment folder path" → raw/assets/. 2. New file location: Settings → Files and links → "Default location for new notes" → wiki/concepts/. 3. Download attachments hotkey: Settings → Hotkeys → search "Download attachments" → bind to Ctrl+Shift+D. After clipping an article, hit the hotkey to download all images locally.
Plugins to install
- `plugins/obsidian-audit/` (this repo) — select text → add feedback → writes to
audit/. See "Audit plugin" section below. - Obsidian Web Clipper (browser extension) — converts any webpage to Markdown and saves to your vault. Configure to save to
raw/articles/. - Dataview (optional) — query frontmatter fields; build dynamic tables of articles by tag, date, source count.
- Marp (optional) — render wiki content as slide decks directly from Obsidian.
Graph view
Graph view (Ctrl+G) is the best way to see your wiki's shape:
- Dense hub = a well-connected concept page.
- Isolated node = orphan page (needs inbound links or removal).
lint_wiki.pyflags these. - Cluster = a sub-topic worth a dedicated folder-split under
wiki/concepts/.
Audit plugin — plugins/obsidian-audit/
Installs into a local Obsidian vault. Workflow:
1. Build the plugin once:
cd plugins/obsidian-audit
npm install
npm run build2. Symlink (or copy) the plugin folder into your vault:
npm run link -- "/path/to/your/vault"3. In Obsidian, enable Community Plugins, then enable "LLM Wiki Audit". 4. In the plugin settings, set:
- Wiki root — path relative to the vault root (usually
.). - Audit directory — path relative to the wiki root (default
audit). - Author — your name.
Commands (bind to hotkeys if you like):
- `Audit: Add feedback on selection` — opens a modal with severity + comment → writes an audit file.
- `Audit: List open feedback for current file` — shows a notice summarising open audits targeting the current file.
- `Audit: Open audit folder` — reveals
audit/in the file explorer.
The plugin uses the shared audit-shared library, so files it writes are byte-identical in shape to files the web viewer writes.
Web viewer — web/
Local Node.js server that renders the wiki with mermaid, KaTeX, and wikilinks, and lets you file feedback from your browser.
cd web
npm install
npm run build
npm start -- --wiki "/path/to/wiki-root" --port 4175Then open http://127.0.0.1:4175. Features:
- Left sidebar: navigation tree built from
wiki/index.md. - Main pane: rendered markdown, mermaid diagrams rendered client-side, formulas rendered server-side.
- Right sidebar: list of open audits for the current page.
- Select any text → "💬 Add feedback" popover appears → submit → writes an audit file to
<wiki-root>/audit/.
The server binds to 127.0.0.1 only. No auth; intended for personal use on your own machine.
Obsidian Web Clipper usage
1. Install from obsidian.md/clipper. 2. Configure template to save to raw/articles/. 3. Clip an article → hit the download-images hotkey → file is ready for ingest.
For complex pages (paywalled, dynamic): copy-paste the main text manually, save as raw/articles/<slug>.md.
qmd (optional, for large wikis)
qmd is a local semantic search engine for Markdown files with BM25 + vector hybrid search. Useful when the wiki grows beyond ~100 pages and wiki/index.md scanning becomes slow.
pip install qmd
qmd collection add wiki/ --name my-wiki
qmd embed
qmd query "what are the tradeoffs of RAG vs wiki" --collection my-wikiqmd also has an MCP server so LLMs can use it as a native tool.
Marp — generating slide decks from wiki content
---
marp: true
theme: default
---
# Slide title
Content here
---
# Next slideInstall the Marp plugin in Obsidian to preview/export directly.
Generating charts
For quantitative analyses, ask the LLM to generate a matplotlib script and save to outputs/charts/:
# outputs/charts/my-analysis.py
import matplotlib.pyplot as plt
# ... chart code ...
plt.savefig('outputs/charts/my-analysis.png')Embed in a wiki article: ![[my-analysis.png]].
Git workflow
The wiki is a git repo. Benefits:
- Version history for every article.
- Branching for experimental research directions.
- Audit files are tracked, so "who suggested this and when" is first-class.
git add .
git commit -m "ingest: 3 papers on attention mechanisms"
git pushKeep large files (PDFs >10 MB, raw images at full resolution, video, model weights) in .gitignore. Use the raw file policy: pointer files in raw/refs/, not copies.
Interactive HTML outputs
For complex analyses, the LLM can generate interactive HTML with JavaScript and save to outputs/. These can be opened in a browser or embedded in Obsidian with the HTML plugin.
#!/usr/bin/env python3
"""
audit_review.py — List and group audit feedback by target file.
Usage:
python3 audit_review.py <wiki-root> [--open|--resolved|--all]
Examples:
python3 audit_review.py ~/wikis/ai-research --open
python3 audit_review.py ~/wikis/ai-research --resolved
python3 audit_review.py ~/wikis/ai-research --all
Reads every file under `<wiki-root>/audit/` (open) and `<wiki-root>/audit/resolved/`
(resolved), parses the YAML frontmatter, and prints a report grouped by target
file. Use this at the start of an `audit` operation to decide processing order.
Exit codes:
0 — done (always, regardless of audit count)
"""
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
def parse_frontmatter(text: str) -> dict | None:
m = FRONTMATTER_RE.match(text)
if not m:
return None
body = m.group(1)
result: dict = {}
for line in body.split("\n"):
if not line.strip() or line.lstrip().startswith("#"):
continue
if ":" not in line:
continue
key, _, rest = line.partition(":")
key = key.strip()
val = rest.strip()
if val.startswith("[") and val.endswith("]"):
inner = val[1:-1].strip()
result[key] = [p.strip().strip('"').strip("'") for p in inner.split(",") if p.strip()]
elif val.startswith('"') and val.endswith('"'):
result[key] = val[1:-1].replace("\\n", "\n").replace('\\"', '"')
elif val.startswith("'") and val.endswith("'"):
result[key] = val[1:-1]
else:
result[key] = val
return result
def extract_comment_one_line(text: str) -> str:
"""Pull the first non-empty line of the # Comment section."""
in_comment = False
for line in text.splitlines():
stripped = line.strip()
if stripped.lower().startswith("# comment"):
in_comment = True
continue
if not in_comment:
continue
if not stripped:
continue
if stripped.startswith("#"):
break
return stripped[:100]
return "(no comment body)"
SEVERITY_ORDER = {"error": 0, "warn": 1, "suggest": 2, "info": 3}
def main(root: str, mode: str) -> int:
root_path = Path(root)
audit_dir = root_path / "audit"
if not audit_dir.exists():
print(f"ERROR: audit/ not found at {audit_dir}", file=sys.stderr)
return 1
files: list[Path] = []
if mode in ("open", "all"):
files.extend(sorted(p for p in audit_dir.glob("*.md") if p.name != ".gitkeep"))
if mode in ("resolved", "all"):
resolved = audit_dir / "resolved"
if resolved.exists():
files.extend(sorted(p for p in resolved.glob("*.md") if p.name != ".gitkeep"))
if not files:
print(f"No {mode} audit files found.")
return 0
grouped: dict[str, list[dict]] = defaultdict(list)
for p in files:
text = p.read_text(encoding="utf-8")
fm = parse_frontmatter(text)
if fm is None:
print(f"⚠️ {p.relative_to(root_path)} — missing frontmatter", file=sys.stderr)
continue
fm["_path"] = str(p.relative_to(root_path))
fm["_one_liner"] = extract_comment_one_line(text)
grouped[fm.get("target", "(no-target)")].append(fm)
total = sum(len(v) for v in grouped.values())
print(f"{mode.upper()} audits: {total} across {len(grouped)} target files\n")
for target in sorted(grouped.keys()):
entries = grouped[target]
entries.sort(key=lambda e: (
SEVERITY_ORDER.get(e.get("severity", "info"), 99),
e.get("created", ""),
))
print(f"{target} ({len(entries)} {mode})")
for e in entries:
sev = e.get("severity", "?")
aid = e.get("id", "?")
author = e.get("author", "?")
created = e.get("created", "?")[:10] # date only
line = e.get("_one_liner", "")
print(f" [{aid}] {sev}: {line} — {author}, {created}")
print()
return 0
if __name__ == "__main__":
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
root = sys.argv[1]
mode = "open"
for arg in sys.argv[2:]:
if arg == "--open":
mode = "open"
elif arg == "--resolved":
mode = "resolved"
elif arg == "--all":
mode = "all"
else:
print(f"Unknown flag: {arg}", file=sys.stderr)
sys.exit(1)
sys.exit(main(root, mode))
#!/usr/bin/env python3
"""
lint_wiki.py — Health check for an LLM Wiki.
Usage:
python3 lint_wiki.py <wiki-root>
Example:
python3 lint_wiki.py ~/wikis/ai-research
Checks:
1. Dead wikilinks — [[Target]] where Target.md doesn't exist
2. Orphan pages — wiki pages with no inbound links
3. Missing index entries — wiki pages not listed in wiki/index.md
4. Unlinked concepts — terms mentioned 3+ times but lacking their own page
5. log/ shape — every file matches YYYYMMDD.md and has the right H1
6. audit/ shape — every audit/*.md parses as a valid AuditEntry
7. Audit targets — every open audit's `target` file must exist
Exit codes:
0 — no issues found
1 — issues found (printed to stdout)
"""
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]")
LOG_FILENAME_RE = re.compile(r"^(\d{4})(\d{2})(\d{2})\.md$")
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
# Required audit frontmatter fields
AUDIT_REQUIRED_FIELDS = {
"id", "target", "target_lines", "anchor_before", "anchor_text",
"anchor_after", "severity", "author", "source", "created", "status",
}
VALID_SEVERITIES = {"info", "suggest", "warn", "error"}
VALID_STATUSES = {"open", "resolved"}
VALID_SOURCES = {"obsidian-plugin", "web-viewer", "manual"}
def load_pages(wiki_dir: Path) -> dict[str, Path]:
pages: dict[str, Path] = {}
for p in wiki_dir.rglob("*.md"):
pages[p.stem] = p
rel = p.relative_to(wiki_dir)
pages[str(rel.with_suffix(""))] = p
return pages
def extract_wikilinks(text: str) -> list[str]:
return WIKILINK_RE.findall(text)
def parse_frontmatter(text: str) -> dict | None:
"""Minimal YAML-ish frontmatter parser. Handles the flat key:value fields
and one-level lists/arrays actually used by audit files. Does not handle
arbitrary YAML — intentional, to avoid a pyyaml dependency."""
m = FRONTMATTER_RE.match(text)
if not m:
return None
body = m.group(1)
result: dict = {}
# Track multi-line folded strings via simple heuristic: quoted scalars
# can contain \n; unquoted values are single-line.
i = 0
lines = body.split("\n")
while i < len(lines):
line = lines[i]
if not line.strip() or line.lstrip().startswith("#"):
i += 1
continue
if ":" not in line:
i += 1
continue
key, _, rest = line.partition(":")
key = key.strip()
val = rest.strip()
if val.startswith("[") and val.endswith("]"):
inner = val[1:-1].strip()
if not inner:
result[key] = []
else:
parts = [p.strip() for p in inner.split(",")]
parsed: list = []
for p in parts:
if p.isdigit() or (p.startswith("-") and p[1:].isdigit()):
parsed.append(int(p))
else:
parsed.append(p.strip('"').strip("'"))
result[key] = parsed
elif val.startswith('"') and val.endswith('"'):
result[key] = val[1:-1].replace("\\n", "\n").replace('\\"', '"')
elif val.startswith("'") and val.endswith("'"):
result[key] = val[1:-1]
else:
result[key] = val
i += 1
return result
def lint(root: str) -> int:
root_path = Path(root)
wiki_path = root_path / "wiki"
log_path = root_path / "log"
audit_path = root_path / "audit"
if not wiki_path.exists():
print(f"ERROR: wiki/ directory not found at {wiki_path}", file=sys.stderr)
return 1
pages = load_pages(wiki_path)
all_wiki_files = list(wiki_path.rglob("*.md"))
index_path = wiki_path / "index.md"
issues = 0
inbound: dict[str, list[str]] = defaultdict(list)
# ── Pass 1: dead wikilinks ──────────────────────────────────────────────
dead_links: list[tuple[str, str]] = []
for md_file in all_wiki_files:
text = md_file.read_text(encoding="utf-8")
for link in extract_wikilinks(text):
link = link.strip()
if link not in pages and Path(link).stem not in pages:
dead_links.append((str(md_file.relative_to(root_path)), link))
else:
target = pages.get(link) or pages.get(Path(link).stem)
if target:
inbound[target.stem].append(md_file.stem)
if dead_links:
print(f"\n🔴 Dead wikilinks ({len(dead_links)}):")
for source, link in dead_links:
print(f" {source} → [[{link}]]")
issues += len(dead_links)
else:
print("✅ No dead wikilinks")
# ── Pass 2: orphan pages ────────────────────────────────────────────────
skip_orphan = {"index"}
orphans = [
p for p in all_wiki_files
if p.stem not in inbound and p.stem not in skip_orphan
and p.parent != wiki_path # skip index.md at root
]
if orphans:
print(f"\n🟡 Orphan pages ({len(orphans)}) — no inbound wikilinks:")
for p in orphans:
print(f" {p.relative_to(root_path)}")
issues += len(orphans)
else:
print("✅ No orphan pages")
# ── Pass 3: missing index entries ───────────────────────────────────────
if index_path.exists():
index_text = index_path.read_text(encoding="utf-8")
not_in_index = [
p for p in all_wiki_files
if p != index_path
and f"[[{p.stem}]]" not in index_text
and str(p.relative_to(wiki_path).with_suffix("")) not in index_text
]
if not_in_index:
print(f"\n🟡 Pages missing from index.md ({len(not_in_index)}):")
for p in not_in_index:
print(f" {p.relative_to(root_path)}")
issues += len(not_in_index)
else:
print("✅ All pages in index.md")
else:
print("⚠️ wiki/index.md not found — skipping index check")
# ── Pass 4: unlinked concepts ───────────────────────────────────────────
all_text = " ".join(p.read_text(encoding="utf-8") for p in all_wiki_files)
all_links = WIKILINK_RE.findall(all_text)
link_counts: dict[str, int] = defaultdict(int)
for link in all_links:
link_counts[link.strip()] += 1
missing_pages = [
(link, count) for link, count in link_counts.items()
if count >= 3 and link not in pages and Path(link).stem not in pages
]
if missing_pages:
print(f"\n🟡 Frequently linked but no page ({len(missing_pages)}):")
for link, count in sorted(missing_pages, key=lambda x: -x[1]):
print(f" [[{link}]] — mentioned {count}x")
issues += len(missing_pages)
else:
print("✅ No frequently-linked missing pages")
# ── Pass 5: log/ shape ───────────────────────────────────────────────────
if log_path.exists() and log_path.is_dir():
log_issues: list[str] = []
for p in sorted(log_path.iterdir()):
if p.is_dir():
continue
if p.name == ".gitkeep":
continue
m = LOG_FILENAME_RE.match(p.name)
if not m:
log_issues.append(f" {p.relative_to(root_path)} — filename doesn't match YYYYMMDD.md")
continue
y, mo, d = m.groups()
iso = f"{y}-{mo}-{d}"
first_line = p.read_text(encoding="utf-8").splitlines()[:1]
if not first_line or first_line[0].strip() != f"# {iso}":
log_issues.append(f" {p.relative_to(root_path)} — expected H1 '# {iso}'")
if log_issues:
print(f"\n🟡 log/ shape issues ({len(log_issues)}):")
for s in log_issues:
print(s)
issues += len(log_issues)
else:
print("✅ log/ shape OK")
else:
print("⚠️ log/ directory not found — skipping log shape check")
# ── Pass 6: audit/ shape ─────────────────────────────────────────────────
audit_targets_to_check: list[tuple[str, str]] = [] # (audit_id, target)
if audit_path.exists() and audit_path.is_dir():
audit_files = [
p for p in audit_path.rglob("*.md") if p.name != ".gitkeep"
]
audit_issues: list[str] = []
for p in audit_files:
text = p.read_text(encoding="utf-8")
fm = parse_frontmatter(text)
rel = p.relative_to(root_path)
if fm is None:
audit_issues.append(f" {rel} — missing YAML frontmatter")
continue
missing = AUDIT_REQUIRED_FIELDS - set(fm.keys())
if missing:
audit_issues.append(
f" {rel} — missing fields: {', '.join(sorted(missing))}"
)
continue
if fm["severity"] not in VALID_SEVERITIES:
audit_issues.append(
f" {rel} — invalid severity '{fm['severity']}' (expected {sorted(VALID_SEVERITIES)})"
)
if fm["source"] not in VALID_SOURCES:
audit_issues.append(
f" {rel} — invalid source '{fm['source']}'"
)
expected_status = "resolved" if "resolved" in p.parts else "open"
if fm["status"] != expected_status:
audit_issues.append(
f" {rel} — status '{fm['status']}' doesn't match directory (expected '{expected_status}')"
)
if fm["status"] == "open":
audit_targets_to_check.append((fm["id"], fm["target"]))
if audit_issues:
print(f"\n🔴 audit/ shape issues ({len(audit_issues)}):")
for s in audit_issues:
print(s)
issues += len(audit_issues)
else:
print(f"✅ audit/ shape OK ({len(audit_files)} files)")
else:
print("⚠️ audit/ directory not found — skipping audit shape check")
# ── Pass 7: audit targets exist ──────────────────────────────────────────
missing_targets: list[tuple[str, str]] = []
for audit_id, target in audit_targets_to_check:
target_path = root_path / target
# Audit target paths are relative to wiki-root but typically point
# at files under wiki/. Check both locations.
if not target_path.exists():
alt = wiki_path / target
if not alt.exists():
missing_targets.append((audit_id, target))
if missing_targets:
print(f"\n🔴 Open audits with missing target files ({len(missing_targets)}):")
for audit_id, target in missing_targets:
print(f" {audit_id} → {target}")
issues += len(missing_targets)
elif audit_targets_to_check:
print("✅ All open-audit targets exist")
# ── Summary ─────────────────────────────────────────────────────────────
print(f"\n{'─'*40}")
if issues == 0:
print("✅ Wiki is healthy — no issues found")
else:
print(f"⚠️ {issues} issue(s) found — review above and fix before next ingest")
return 0 if issues == 0 else 1
if __name__ == "__main__":
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
sys.exit(lint(sys.argv[1]))
#!/usr/bin/env python3
"""
scaffold.py — Bootstrap a new LLM Wiki directory structure.
Usage:
python3 scaffold.py <wiki-root> "<Topic Title>"
Example:
python3 scaffold.py ~/wikis/ai-research "AI Research"
Creates:
<wiki-root>/
├── CLAUDE.md (schema template)
├── log/
│ └── YYYYMMDD.md (first day's log with scaffold entry)
├── audit/
│ ├── .gitkeep
│ └── resolved/
│ └── .gitkeep
├── raw/
│ ├── articles/
│ ├── papers/
│ ├── notes/
│ └── refs/
├── wiki/
│ ├── index.md (category-structured catalog)
│ ├── concepts/
│ ├── entities/
│ └── summaries/
└── outputs/
└── queries/
"""
import os
import sys
from datetime import date, datetime
def scaffold(root: str, title: str) -> None:
today = date.today()
today_iso = today.isoformat()
today_compact = today.strftime("%Y%m%d")
now_hm = datetime.now().strftime("%H:%M")
dirs = [
"raw/articles",
"raw/papers",
"raw/notes",
"raw/refs",
"wiki/concepts",
"wiki/entities",
"wiki/summaries",
"outputs/queries",
"log",
"audit",
"audit/resolved",
]
for d in dirs:
os.makedirs(os.path.join(root, d), exist_ok=True)
print(f"✓ Created directory tree under {root}/")
# .gitkeep for empty audit dirs
_write(root, "audit/.gitkeep", "")
_write(root, "audit/resolved/.gitkeep", "")
# CLAUDE.md
claude_md = f"""# {title} Knowledge Base
> Schema document — read at the start of every session together with `wiki/index.md`.
> Update after every major compile, ingest batch, or structural change.
## Scope
What this wiki covers:
- <describe the topic area>
What this wiki deliberately excludes:
- <describe out-of-scope areas>
## Operations
This wiki follows the llm-wiki skill's five operations: `compile`, `ingest`, `query`, `lint`, `audit`.
Every operation appends an entry to `log/YYYYMMDD.md`.
## Naming conventions
- **Concept pages** (`wiki/concepts/`): Title Case noun phrases.
- **Folder-split concepts** (`wiki/concepts/<topic>/`): used when a topic exceeds ~1200 words. Contains `index.md` + one file per aspect.
- **Entity pages** (`wiki/entities/`): Proper names.
- **Summary pages** (`wiki/summaries/`): kebab-case source slug.
All pages require YAML frontmatter: `title`, `type`, `created`, `updated`, `sources`, `tags`.
### Diagrams and formulas
- All diagrams are **mermaid**. No ASCII art.
- All formulas are **KaTeX** (inline `$...$` or block `$$...$$`).
### Raw file policy
- Small text sources → copy into `raw/<subfolder>/`.
- Large binaries → create a pointer file at `raw/refs/<slug>.md` with `kind: ref` and `external_path` fields. Do not copy the binary.
## Current articles
*None yet — update this list after every compile.*
### Concepts
*(none)*
### Entities
*(none)*
### Summaries
*(none)*
## Open research questions
- <What do you want to understand better?>
- <What are the key open questions in this domain?>
## Research gaps
Sources to ingest:
- [ ] <URL or paper title> — why it's relevant
## Audit backlog
*(none — run `python3 scripts/audit_review.py <wiki-root> --open` to refresh)*
## Notes for the LLM
- Language: <en | zh | bilingual>
- Tone: <neutral, academic, conversational, ...>
- Depth: <survey-level | deep technical>
- Handling contradictions: state both, cite each, add to Open Research Questions.
"""
_write(root, "CLAUDE.md", claude_md)
print("✓ Created CLAUDE.md")
# log/<today>.md
log_md = f"""# {today_iso}
## [{now_hm}] scaffold | Initialized {title} knowledge base
- Created directory tree (raw/, wiki/, log/, audit/, outputs/)
- Created CLAUDE.md schema template
- Created wiki/index.md category skeleton
"""
_write(root, f"log/{today_compact}.md", log_md)
print(f"✓ Created log/{today_compact}.md")
# wiki/index.md
index_md = f"""# Index — {title}
> One-sentence scope of the wiki.
## 🔖 Navigation
- [[#Concepts]] · [[#Entities]] · [[#Summaries]] · [[#Open Questions]]
## Concepts
*(none yet)*
## Entities
*(none yet)*
## Summaries (chronological)
*(none yet)*
## Open Questions
- <First research question>
"""
_write(root, "wiki/index.md", index_md)
print("✓ Created wiki/index.md")
print(f"""
✅ Wiki scaffolded at: {root}/
Next steps:
1. Fill in CLAUDE.md — define scope and naming conventions
2. Add sources to raw/ (use Obsidian Web Clipper for web articles)
3. Run ingest: tell your LLM agent "ingest raw/<file>.md"
4. Ask questions: "what does the wiki say about X?"
5. Run lint periodically: python3 scripts/lint_wiki.py {root}
6. Process feedback: python3 scripts/audit_review.py {root} --open
""")
def _write(root: str, path: str, content: str) -> None:
full = os.path.join(root, path)
os.makedirs(os.path.dirname(full) or ".", exist_ok=True)
with open(full, "w", encoding="utf-8") as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(__doc__)
sys.exit(1)
scaffold(sys.argv[1], sys.argv[2])