
Llm Wiki
- 638 installs
- 23.5k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
llm-wiki is a Claude Code skill that maintains an evolving structured personal knowledge base any LLM agent can read and update during long-running software projects.
About
llm-wiki is a documentation workflow skill for agent-maintained project wikis. It defines a three-layer vault: raw/ holds immutable source articles and papers, wiki/ is the agent-owned knowledge base for create and cross-reference updates, and AGENTS.md or CLAUDE.md carries the co-evolved schema. Developers reach for llm-wiki when Codex, Cursor, Claude Code, or other AGENTS.md-aware CLIs need persistent context across sessions without re-ingesting sources each time. The skill never edits raw/, only synthesizes wiki entries from sources, making it suited to research-heavy backends, integrations, or multi-week feature work where structured recall beats ad-hoc chat memory.
- Three-layer architecture: immutable raw/ sources, mutable wiki/, and AGENTS.md schema
- Maintains index.md catalog, log.md timeline, entities/, concepts/, sources/, comparisons/, and synthesis/ directories
- Enforces consistent YAML frontmatter on every page (title, category, summary, tags, sources, updated)
- Cross-references sources and synthesizes insights across multiple documents
- Compatible with any AGENTS.md-aware CLI including Claude Code, Cursor, Codex, and Gemini CLI
Llm Wiki by the numbers
- 638 all-time installs (skills.sh)
- Ranked #1,499 of 16,565 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill llm-wikiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 638 |
|---|---|
| repo stars | ★ 23.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you maintain an LLM-readable project wiki?
Maintain an evolving, structured personal knowledge base that any LLM agent can read and update during long-running projects.
Who is it for?
Developers running long agent sessions who need a durable, cross-referenced knowledge base separate from immutable research sources.
Skip if: Teams wanting a one-off README update or projects without AGENTS.md or CLAUDE.md agent context files.
When should I use this skill?
The user wants to start, update, or cross-reference a project wiki that agents read from raw/ and write to wiki/.
What you get
Structured wiki/ markdown pages, immutable raw/ sources, and an updated AGENTS.md or CLAUDE.md schema.
- wiki/ knowledge pages
- updated AGENTS.md schema
- cross-referenced project documentation
By the numbers
- Uses a three-layer vault structure: raw/, wiki/, and AGENTS.md
Files
LLM Wiki — Second Brain for Claude Code + Obsidian
Inspired by Andrej Karpathy's LLM Wiki pattern (gist). This skill turns Claude Code (or any agent CLI) into a disciplined wiki maintainer that incrementally builds and maintains a persistent, interlinked Obsidian vault as you feed it sources. The knowledge compounds — cross-references, contradictions, and synthesis are already there when you query.
Core principle
Most LLM+docs workflows are RAG: retrieve fragments at query time, synthesize from scratch, forget. The wiki is compounding: sources are read once, integrated into a persistent markdown knowledge base, and kept current. You curate and ask; the LLM reads, files, cross-references, and maintains.
Obsidian is the IDE. The LLM is the programmer. The wiki is the codebase.
When to use
- Personal: track goals, health, psychology, journaling, self-improvement
- Research: deep dives over weeks on a topic — papers, articles, reports, evolving thesis
- Book companion: file chapters as you read; build a fan-wiki-style companion for characters, themes, plot threads
- Business/team: internal wiki fed by Slack, meeting notes, calls — LLM does maintenance nobody else wants to do
- Competitive analysis, due diligence, trip planning, course notes, hobby deep-dives
Do NOT use when: you need one-shot Q&A over a fixed document (use RAG), you don't plan to add sources over time, or you don't want Obsidian in the loop.
Architecture (three layers)
vault/
├── raw/ # Layer 1 — IMMUTABLE source of truth
│ ├── <source files> # Articles, papers, PDFs, images, data
│ └── assets/ # Downloaded images from clipped articles
├── wiki/ # Layer 2 — LLM-owned knowledge base
│ ├── index.md # Content catalog (LLM updates every ingest)
│ ├── log.md # Append-only timeline (## [YYYY-MM-DD] <op> | <title>)
│ ├── entities/ # Person/Org/Place pages
│ ├── concepts/ # Ideas, theories, frameworks
│ ├── sources/ # One summary page per ingested source
│ ├── comparisons/ # Cross-source analysis pages
│ └── synthesis/ # High-level syntheses, theses, overviews
├── CLAUDE.md # Schema + conventions (Claude Code)
└── AGENTS.md # Same content, for Codex/Cursor/Antigravity- Layer 1 (raw/) — you own. LLM only reads; never writes.
- Layer 2 (wiki/) — LLM owns. It creates, updates, and cross-references pages. You read it.
- Layer 3 (CLAUDE.md / AGENTS.md) — the schema. Conventions, workflows, frontmatter rules. Co-evolved by you and the LLM.
Three core operations
1. Ingest — LLM reads a source, discusses takeaways with you, writes a source summary, updates 10-15 relevant pages, updates index, appends to log. See references/ingest-workflow.md. 2. Query — LLM reads index.md first, drills into relevant pages, synthesizes with citations. Good answers get filed back into the wiki so explorations compound. See references/query-workflow.md. 3. Lint — Health check: contradictions, stale claims, orphan pages, missing cross-refs, concepts mentioned but lacking their own page, data gaps to fill with web search. See references/lint-workflow.md.
Quick start
# 1. Initialize a vault (in Obsidian's vault directory)
python scripts/init_vault.py --path ~/vaults/research --topic "LLM interpretability"
# 2. Drop a source into raw/, then ingest
/wiki-ingest ~/vaults/research/raw/anthropic-monosemanticity.pdf
# 3. Ask questions (answers can be re-filed into the wiki)
/wiki-query "how does monosemanticity compare to mechanistic interpretability?"
# 4. Periodic health check
/wiki-lint
# 5. See the timeline
/wiki-log --last 10Slash commands (this plugin ships)
| Command | Purpose |
|---|---|
/wiki-init | Bootstrap a fresh vault with schema files + starter structure |
/wiki-ingest <path> | Read a source, discuss, update wiki, log it |
/wiki-query <question> | Search wiki, synthesize answer, offer to file back |
/wiki-lint | Run health check — contradictions, orphans, stale claims, gaps |
/wiki-log | Show recent log entries (uses unix tools on log.md) |
Sub-agents (this plugin ships)
| Agent | When dispatched |
|---|---|
wiki-ingestor | Delegated ingest flow — reads source, proposes updates, applies after your approval |
wiki-linter | Runs the health-check workflow independently, reports findings |
wiki-librarian | Answers queries using index-first search, synthesizes with citations |
Python tools (scripts/)
All tools are standard library only (no pip installs). Run with python scripts/<tool>.py --help.
| Script | Purpose |
|---|---|
init_vault.py | Create folder structure + seed CLAUDE.md, AGENTS.md, index.md, log.md |
ingest_source.py | Helper: extract text/frontmatter from a source file, ready for LLM review |
update_index.py | Regenerate index.md from wiki page frontmatter (category, date, source count) |
append_log.py | Append a standardized log entry `## [YYYY-MM-DD] <op> \ |
wiki_search.py | BM25 search over wiki pages (standalone fallback when index.md isn't enough) |
lint_wiki.py | Find orphans (no inbound links), stale pages, missing cross-refs, broken links |
graph_analyzer.py | Compute link graph stats — hubs, orphans, clusters, disconnected components |
export_marp.py | Render a wiki page (or subtree) to a Marp slide deck |
Cross-tool compatibility
The vault's schema lives in CLAUDE.md (Claude Code) or AGENTS.md (Codex/Cursor/Antigravity/OpenCode). The same content works in both. This plugin ships both templates. For per-tool setup instructions see references/cross-tool-setup.md.
CLAUDE.md → Claude Code
AGENTS.md → Codex CLI, Cursor, Antigravity, OpenCode, Gemini CLI
.cursorrules → legacy Cursor (pre-AGENTS.md)The scripts are pure Python stdlib → run identically everywhere. Only the loader file changes per tool.
Obsidian setup (recommended)
- Obsidian Web Clipper — browser extension; converts web articles to markdown and drops them in
raw/ - Download images locally — Settings → Files and links → Attachment folder path =
raw/assets/. Settings → Hotkeys → bind "Download attachments for current file" toCtrl+Shift+D - Graph view — see hubs/orphans; essential for spotting structural problems
- Marp plugin — Markdown-based slide decks directly from wiki pages
- Dataview plugin — dynamic tables/lists over page frontmatter (tags, dates, source counts)
- Git — the vault is a plain markdown repo; version it
Full setup walkthrough: references/obsidian-setup.md
Why this works (vs plain RAG)
| Plain RAG | LLM Wiki |
|---|---|
| Rediscover knowledge each query | Knowledge accumulates |
| Cross-references re-computed every time | Cross-references pre-written and maintained |
| Contradictions surface only if you ask | Contradictions flagged during ingest |
| Exploration disappears into chat history | Good answers re-filed as new pages |
| Scales by embeddings infrastructure | Scales by markdown + index.md + optional local search |
At ~100 sources / hundreds of pages, index.md + filesystem search is enough. Past that, layer in a local search tool like qmd or use scripts/wiki_search.py.
Related skills (chains via context: fork)
This skill is marked context: fork so other skills can chain into it:
- `para-memory-files` — PARA-method memory; complementary as long-term personal memory that feeds sources into the wiki
- `obsidian-vault` (mattpocock) — lightweight Obsidian note helper; this skill is the maintained-wiki layer on top
- `rag-design` — when wiki outgrows ~500 pages, use rag-design to bolt on a retrieval layer
- `mcp-design` — expose the wiki as an MCP tool
- `agent-communication` — for multi-agent wiki maintenance (ingestor + linter + librarian)
Reference docs
references/wiki-schema.md— full vault layout, page frontmatter, naming conventionsreferences/page-formats.md— entity, concept, source, comparison, synthesis templatesreferences/ingest-workflow.md— the detailed ingest flow the wiki-ingestor agent followsreferences/query-workflow.md— query patterns, citation format, re-filing answersreferences/lint-workflow.md— health-check heuristicsreferences/obsidian-setup.md— Obsidian plugins, hotkeys, vault configreferences/cross-tool-setup.md— per-tool setup (Codex, Cursor, Antigravity, etc.)references/memex-principles.md— Bush's Memex, why the LLM changes the maintenance math
Templates (assets/)
CLAUDE.md.template,AGENTS.md.template,.cursorrules.template— schema loaders per toolindex.md.template,log.md.template— starter index and logpage-templates/— entity, concept, source-summary, comparison, synthesisexample-vault/— small worked example you can study or copy
Iron rule
The LLM never edits files in `raw/`. Ever. Sources are immutable. All LLM writes go to wiki/. If you need to correct a source, do it in raw/ yourself — then re-ingest.
# {{VAULT_NAME}} — LLM Wiki
> **Topic:** {{TOPIC}}
> **Initialized:** {{DATE}}
> **Tool:** Any AGENTS.md-aware CLI (Codex, Cursor, Antigravity, OpenCode, Gemini CLI, etc.). Claude Code uses `CLAUDE.md`, which is identical.
You are the maintainer of this wiki. You read from `raw/`, you write to `wiki/`. You never edit `raw/`.
## The three layers
```
raw/ → sources (articles, papers, notes). IMMUTABLE. You only read.
wiki/ → the knowledge base. You own this. Create, update, cross-reference.
AGENTS.md → schema (this file). Co-evolved with the user.
```
## Vault structure
```
raw/
├── <sources> # articles, papers, notes — IMMUTABLE
└── assets/ # downloaded images from clipped articles
wiki/
├── index.md # content catalog — update every ingest
├── log.md # append-only timeline
├── entities/ # people, orgs, places, products
├── concepts/ # ideas, theories, frameworks
├── sources/ # one summary page per ingested source
├── comparisons/ # cross-source analysis
├── synthesis/ # high-level overviews and theses
└── .templates/ # page templates (reference only)
```
## Page frontmatter (required on every wiki page)
```yaml
---
title: <Title>
category: entity | concept | source | comparison | synthesis
summary: <one-line summary>
tags: [tag1, tag2]
sources: <count of sources referencing this page>
updated: YYYY-MM-DD
---
```
For `source` pages, also include:
```yaml
source_path: raw/<path>
source_date: YYYY-MM
authors: [author1, author2]
ingested: YYYY-MM-DD
```
## The three operations
### Ingest
When the user says "ingest this source" or points you at a file in `raw/`:
1. Read the source directly with your file-reading tool
2. **Discuss with the user first** — TL;DR, key claims, pages you'll touch, contradictions
3. Wait for confirmation
4. Create/merge the summary page at `wiki/sources/<slug>.md`
5. Update every relevant entity and concept page (typically 5-15 pages)
6. Flag contradictions with `> ⚠️ Contradiction:` callouts on both sides
7. Update `wiki/index.md`
8. Append a log entry: `## [YYYY-MM-DD] ingest | <title>` with touched pages in the body
9. Report back with a bulleted list of touched pages
If Python is available, use the helpers:
```bash
python <plugin-path>/scripts/ingest_source.py --vault . --source <path> --json
python <plugin-path>/scripts/append_log.py --vault . --op ingest --title "<title>"
python <plugin-path>/scripts/update_index.py --vault .
```
### Query
When the user asks a question:
1. Read `wiki/index.md` first
2. Pick 3-10 relevant pages across categories
3. Read them in full
4. Follow wikilinks opportunistically
5. Fall back to `wiki_search.py --query <terms>` if needed
6. Synthesize: direct answer → supporting detail → inline `[[sources/xxx]]` citations → "Related pages"
7. **Offer to file the answer back** as a new page
### Lint
When the user says "check the wiki" or periodically:
1. Run `lint_wiki.py` + `graph_analyzer.py`
2. Do semantic checks (contradictions, stale claims, concept gaps)
3. Present a report with suggested actions
4. Append a `lint` entry to `log.md`
## Iron rules
1. **`raw/` is immutable.** You read from it; you never write to it.
2. **All writes go to `wiki/`.** No exceptions.
3. **Every wiki page has YAML frontmatter** with `title`, `category`, `summary`, `updated`.
4. **Every ingest touches ≥5 files.**
5. **Every claim has a citation.**
6. **Contradictions get flagged inline.** Both pages.
7. **Good answers get filed back.** Explorations compound.
## Log format
```
## [YYYY-MM-DD] <op> | <title>
<optional detail>
```
Ops: `ingest`, `query`, `lint`, `create`, `update`, `delete`, `note`.
## Tools
Python scripts live wherever you installed the plugin. Standard library only.
- `init_vault.py`
- `ingest_source.py`
- `update_index.py`
- `append_log.py`
- `wiki_search.py`
- `lint_wiki.py`
- `graph_analyzer.py`
- `export_marp.py`
Run any of them with `--help`.
## Style
- Concise. Wiki pages are read, not generated.
- Short paragraphs. Bulleted lists where appropriate.
- Cite aggressively with `[[wikilinks]]`.
- Say "I don't know" when you don't. Don't invent content.
- Update `updated:` whenever you touch a page.
# {{VAULT_NAME}} — LLM Wiki
> **Topic:** {{TOPIC}}
> **Initialized:** {{DATE}}
> **Tool:** Claude Code (this file). A parallel `AGENTS.md` exists for Codex/Cursor/Antigravity.
You are the maintainer of this wiki. You read from `raw/`, you write to `wiki/`. You never edit `raw/`.
## The three layers
```
raw/ → sources (articles, papers, notes). IMMUTABLE. You only read.
wiki/ → the knowledge base. You own this. Create, update, cross-reference.
CLAUDE.md / AGENTS.md → schema (this file). Co-evolved with the user.
```
## Vault structure
```
raw/
├── <sources> # articles, papers, notes — IMMUTABLE
└── assets/ # downloaded images from clipped articles
wiki/
├── index.md # content catalog — update every ingest
├── log.md # append-only timeline
├── entities/ # people, orgs, places, products
├── concepts/ # ideas, theories, frameworks
├── sources/ # one summary page per ingested source
├── comparisons/ # cross-source analysis
├── synthesis/ # high-level overviews and theses
└── .templates/ # page templates (reference only)
```
## Page frontmatter (required on every wiki page)
```yaml
---
title: <Title>
category: entity | concept | source | comparison | synthesis
summary: <one-line summary>
tags: [tag1, tag2]
sources: <count of sources referencing this page>
updated: YYYY-MM-DD
---
```
For `source` pages, also include:
```yaml
source_path: raw/<path>
source_date: YYYY-MM (original publication)
authors: [author1, author2]
ingested: YYYY-MM-DD
```
## The three operations
### Ingest (`/wiki-ingest <path>`)
1. Run `python scripts/ingest_source.py --vault . --source <path> --json` to get the brief
2. Read the source directly
3. **Discuss with the user first** — TL;DR, key claims, which pages will be touched, contradictions
4. Wait for confirmation
5. Create or merge the summary page at `wiki/sources/<slug>.md`
6. Update every relevant entity and concept page (typically 5-15 pages)
7. Flag contradictions with `> ⚠️ Contradiction:` callouts on both sides
8. Update `wiki/index.md` (run `update_index.py` or edit inline)
9. Run `append_log.py --op ingest --title "<title>" --detail "<touched pages>"`
10. Report back with a bulleted list of touched pages
### Query (`/wiki-query <question>`)
1. Read `wiki/index.md` first
2. Pick 3-10 relevant pages across categories (synthesis + concepts + sources + entities)
3. Read them in full
4. Follow wikilinks opportunistically
5. Fall back to `wiki_search.py --query <terms>` if the index doesn't surface the answer
6. Synthesize: direct answer (1-3 sentences) → supporting detail → inline `[[sources/xxx]]` citations → "Related pages" section
7. **Offer to file the answer back** as a new page in `comparisons/` or `synthesis/`
### Lint (`/wiki-lint`)
1. Run `python scripts/lint_wiki.py --vault .` for mechanical checks
2. Run `python scripts/graph_analyzer.py --vault .` for structural stats
3. Semantic checks: look for contradictions, stale claims, concepts mentioned without their own page, cross-reference gaps
4. Present findings as a markdown report with suggested actions
5. Append a `lint` entry to `log.md`
## Iron rules
1. **`raw/` is immutable.** You read from it; you never write to it.
2. **All writes go to `wiki/`.** No exceptions.
3. **Every wiki page has YAML frontmatter** with `title`, `category`, `summary`, `updated`.
4. **Every ingest touches ≥5 files.** The source summary, 2-4 entity/concept pages, `index.md`, `log.md`.
5. **Every claim has a citation.** Link back to the `sources/<slug>` page.
6. **Contradictions get flagged inline.** Both pages get the callout.
7. **Good answers get filed back.** Explorations compound.
## Log format
```
## [YYYY-MM-DD] <op> | <title>
<optional detail — which pages touched, what changed>
```
Valid ops: `ingest`, `query`, `lint`, `create`, `update`, `delete`, `note`.
Grep the log: `grep "^## \[" wiki/log.md | tail -10`
## Tools
All scripts live at `~/.claude/skills/llm-wiki/scripts/` (or wherever you installed the plugin). Standard library only.
- `init_vault.py` — bootstrap a vault
- `ingest_source.py` — prep a source for ingest (metadata + preview)
- `update_index.py` — regenerate `wiki/index.md` from page frontmatter
- `append_log.py` — append a standardized log entry
- `wiki_search.py` — BM25 search fallback
- `lint_wiki.py` — mechanical health check
- `graph_analyzer.py` — link graph stats
- `export_marp.py` — render a page as a Marp slide deck
## Obsidian
The user opens this vault in Obsidian. They watch the graph view while you edit. Useful plugins: Graph view, Backlinks, Dataview, Marp, Templates, Git.
## Style
- Be concise. Wiki pages are read, not generated.
- Prefer short paragraphs. Bulleted lists where appropriate.
- Cite aggressively with `[[wikilinks]]`.
- When you're not sure, say so in the page. Don't invent content.
- Update `updated:` frontmatter whenever you touch a page.
# Cursor rules for {{VAULT_NAME}} LLM Wiki
You are the maintainer of this wiki. You read from `raw/` and write only to `wiki/`.
You never edit files in `raw/`. Full schema is in `AGENTS.md` — read it first.
Topic: {{TOPIC}}
Initialized: {{DATE}}
Core rules:
1. `raw/` is immutable. Read only.
2. All writes go to `wiki/`.
3. Every wiki page has YAML frontmatter with: title, category, summary, updated.
4. Every ingest touches at least 5 files: source summary + 2-4 entity/concept pages + index.md + log.md.
5. Every claim has a wikilink citation to its source page.
6. Contradictions get `> ⚠️ Contradiction:` callouts on both sides.
7. Good query answers get filed back into the wiki as new pages.
Operations: ingest / query / lint. Full workflows in AGENTS.md.
Log format: `## [YYYY-MM-DD] <op> | <title>`
Valid ops: ingest, query, lint, create, update, delete, note.
Scripts (Python stdlib) in `<plugin-path>/scripts/`:
- ingest_source.py, update_index.py, append_log.py
- lint_wiki.py, graph_analyzer.py, wiki_search.py, export_marp.py
Run any with `--help`. Use them when helpful — they're fast and deterministic.
Style: concise. Cite with `[[wikilinks]]`. Say "I don't know" when you don't.
Update `updated:` frontmatter on every touch.
Example Vault — "LLM Interpretability"
A minimal worked example to study before initializing your own.
Not a runnable vault — it's missing most files. The goal is to show what a healthy small vault looks like after ingesting 2-3 sources on one topic.
Layout
example-vault/
├── raw/
│ └── assets/
├── wiki/
│ ├── index.md
│ ├── log.md
│ ├── entities/
│ │ └── anthropic.md
│ ├── concepts/
│ │ └── sparse-autoencoder.md
│ ├── sources/
│ │ └── monosemanticity.md
│ └── synthesis/
│ └── interpretability-overview.md
├── CLAUDE.md
└── AGENTS.mdWhat to notice
1. Every page has frontmatter. This is what makes the index + lint scripts work. 2. The source page is the single source of truth for claims from that paper. Other pages cite it rather than duplicating content. 3. `index.md` is organized by category, not chronologically. 4. `log.md` uses the standardized header format ## [YYYY-MM-DD] <op> | <title>. 5. Cross-references are wikilinks, not prose references. [[sources/monosemanticity]], not "see the Monosemanticity paper". 6. The synthesis page has a `How this synthesis has changed` section. Append-only history so you can see the thesis evolve.
Sparse Autoencoder
Definition
A neural-network-based dictionary-learning method that decomposes the activations of a target model's layer into a larger set of sparsely-active features, each of which is hoped to be monosemantic (interpretable as a single concept).
Origin
Introduced to LLM interpretability by [[entities/anthropic]] in the Transformer Circuits thread. The specific form used in [[sources/monosemanticity]] is a wide, sparse autoencoder trained on the residual stream activations of a one-layer transformer.
Key claims
- SAEs extract features that are more monosemantic than raw neurons — cited from [[sources/monosemanticity]]
- The resulting feature dictionary is larger than the original layer width (overcomplete)
- Features come in interpretable families (specific tokens, contexts, circuits)
Contrasts with
- Linear probing — supervised; requires you to know what feature to look for
- Direct neuron inspection — limited by polysemanticity
Open questions
- Does it scale beyond one-layer models?
- Are the features truly monosemantic or just more monosemantic?
Used in
- [[synthesis/interpretability-overview]]
- [[sources/monosemanticity]]
Anthropic
What it is
AI safety company founded in 2021 by former OpenAI researchers. Builds the Claude family of large language models and publishes research on AI safety, alignment, and interpretability.
Why it matters
Primary source of modern mechanistic interpretability work, including the sparse-autoencoder line of research that this wiki is tracking.
Key facts
- Founded 2021 — cited from [[sources/monosemanticity]]
- Publishes the Transformer Circuits thread (interpretability research)
- Runs the [[concepts/sparse-autoencoder]] line of work
Related
- [[concepts/sparse-autoencoder]]
- [[synthesis/interpretability-overview]]
Appears in
- [[sources/monosemanticity]] — primary SAE paper
Open questions
- Has the SAE approach scaled beyond one-layer models as of late 2024?
Index — example-vault
_Updated 2026-04-10 • 4 pages_
Content catalog. Read this first when answering queries.
Topic: LLM Interpretability
Synthesis (1)
- [[synthesis/interpretability-overview|Interpretability Overview]] — current thesis on mechanistic interpretability in LLMs _(1 source · upd 2026-04-10)_
Concept (1)
- [[concepts/sparse-autoencoder|Sparse Autoencoder]] — dictionary-learning method for decomposing polysemantic neurons into monosemantic features _(1 source · upd 2026-04-10)_
Entity (1)
- [[entities/anthropic|Anthropic]] — AI safety company, developer of Claude; major contributor to interpretability research _(1 source · upd 2026-04-10)_
Source (1)
- [[sources/monosemanticity|Towards Monosemanticity]] — Anthropic 2024 paper using sparse autoencoders to extract interpretable features from a one-layer transformer _(upd 2026-04-10)_
Log — example-vault
Append-only timeline. Grep recent: grep "^## \[" log.md | tail -10[2026-04-10] note | Vault initialized
Topic: LLM Interpretability. Layers created.
[2026-04-10] ingest | Towards Monosemanticity
Added sources/monosemanticity.md. Created entities/anthropic.md, concepts/sparse-autoencoder.md. Started synthesis/interpretability-overview.md. No contradictions (first source).
Towards Monosemanticity
TL;DR
Trains a wide sparse autoencoder on the residual stream of a one-layer transformer and finds that the resulting features are substantially more interpretable than the model's native neurons.
Key claims
1. SAE features are more monosemantic than neurons 2. Features come in interpretable families (tokens, contexts, syntactic roles) 3. The approach is complementary to, not a replacement for, mechanistic circuits work
Methods
- One-layer transformer target
- Wide sparse autoencoder on residual stream
- L1 sparsity regularization
- Feature dictionary size >> model width
Evidence cited
- Qualitative inspection of top activating examples per feature
- Comparison with probing
- Feature family analysis
Connections
- Extends [[concepts/sparse-autoencoder]]
- Builds on [[entities/anthropic]]'s prior Transformer Circuits work
Where it's cited
- [[concepts/sparse-autoencoder]]
- [[entities/anthropic]]
- [[synthesis/interpretability-overview]]
Interpretability Overview
Thesis
_(early — only one source ingested)_ Mechanistic interpretability is shifting from direct neuron inspection (limited by polysemanticity) toward sparse-autoencoder-based feature decomposition. The empirical bet is that overcomplete sparse dictionaries recover the "true" feature basis of trained models.
The landscape
- Sparse-autoencoder line — pursued by [[entities/anthropic]]; see [[sources/monosemanticity]]
- Circuits work — (no sources ingested yet)
- Probing — (no sources ingested yet)
Key concepts
- [[concepts/sparse-autoencoder]] — primary method under investigation
Key sources
- [[sources/monosemanticity]] — foundational SAE paper
Current open problems
- Scaling SAEs beyond toy models
- Whether features are truly monosemantic vs merely "more" monosemantic
- How SAE features relate to circuits-based analysis
How this synthesis has changed
- 2026-04-10 — initial synthesis after ingesting [[sources/monosemanticity]]. Only one data point; thesis is intentionally tentative.
# Index — {{VAULT_NAME}}
_Initialized {{DATE}} • 0 pages_
> Content-oriented catalog of every page in `wiki/`. Updated by
> `scripts/update_index.py` or during `/wiki-ingest`. Answer queries
> by reading this file first, then drilling into relevant pages.
>
> Topic: **{{TOPIC}}**
## Synthesis (0)
_No pages yet. Will appear here as you ingest sources and build high-level theses._
## Concept (0)
_No pages yet. Will populate with ideas, theories, methods as sources are ingested._
## Entity (0)
_No pages yet. People, organizations, places, products mentioned in your sources will show up here._
## Source (0)
_No pages yet. Each ingested source gets one summary page here._
## Comparison (0)
_No pages yet. Cross-source or cross-concept analyses will live here._
---
### First steps
1. Drop a source into `raw/`
2. Run `/wiki-ingest raw/<your-file>` in your LLM CLI
3. Watch this index populate
# Log — {{VAULT_NAME}}
> Append-only timeline. Every LLM operation leaves an entry here.
>
> Format: `## [YYYY-MM-DD] <op> | <title>` followed by an optional detail line.
> Valid ops: `ingest`, `query`, `lint`, `create`, `update`, `delete`, `note`.
>
> Grep the last 10 entries: `grep "^## \[" log.md | tail -10`
## [{{DATE}}] note | Vault initialized
Topic: **{{TOPIC}}**. Layers created: `raw/`, `wiki/{entities,concepts,sources,comparisons,synthesis}`.
Schema loader: `CLAUDE.md` + `AGENTS.md` + `.cursorrules`.
<A> vs <B>
What they share
Common ground. Same problem space? Same goals?
Where they diverge
| Dimension | <A> | <B> |
|---|---|---|
| Dimension 1 | ... | ... |
| Dimension 2 | ... | ... |
| Dimension 3 | ... | ... |
Which sources take which side
- [[sources/xxx]] — pro-<A>
- [[sources/yyy]] — pro-<B>
- [[sources/zzz]] — neutral / both
When to prefer <A>
- Context where A wins
When to prefer <B>
- Context where B wins
Open questions
- Unresolved disagreements
- Data that would settle the question
Related
- [[concepts/a]] · [[concepts/b]]
- [[synthesis/xxx]]
<Concept Name>
Definition
Precise, one-paragraph definition. The canonical form used across your sources.
Origin
Who proposed it, when, in what work. Link [[entities]] and [[sources]].
Key claims
- Claim 1 — cited from [[sources/xxx]]
- Claim 2 — cited from [[sources/yyy]]
Contrasts with
- [[concepts/other]] — see [[comparisons/xxx-vs-other]]
Open questions / disagreements
- Unresolved questions across sources
- ⚠️ Contradiction: [[sources/a]] claims X but [[sources/b]] claims ~X
Used in
- [[synthesis/xxx]]
- [[sources/yyy]]
<Entity Name>
What it is
One-paragraph definition. What kind of entity (person, org, place, product), founded/born when, by whom, active in what.
Why it matters
Why this entity shows up across sources. What role does it play in the narrative of this wiki?
Key facts
- Fact 1 — cited from [[sources/xxx]]
- Fact 2 — cited from [[sources/yyy]]
Related
- Related [[entities/other]]
- Related [[concepts/xxx]]
Appears in
- [[sources/xxx]] — short note on the connection
- [[sources/yyy]] — short note
Open questions
- Things the sources don't answer. Good prompts for new source hunts.
<Source Title>
TL;DR
Two sentences max. What did they do, what did they find / argue.
Key claims
1. Claim with page/section pointer if applicable 2. ...
Methods (if applicable)
How the work was done. Data, model, training, evaluation. For non-research sources, describe the approach/argument structure.
Evidence cited
- Figure X shows ...
- Table Y ...
- Quote: "..." (p. NN)
Surprises / contradictions
- Where this source conflicts with [[sources/other]] or [[concepts/xxx]]
Connections
- Extends [[concepts/xxx]]
- Builds on [[entities/yyy]]'s prior work
- Related: [[sources/zzz]]
Where it's cited in this wiki
- [[concepts/xxx]]
- [[entities/yyy]]
- [[synthesis/zzz]]
<Topic> Overview
Thesis
Two or three sentences capturing the current synthesis across all sources read so far. Revised as new sources come in.
The landscape
- Sub-area A — pursued by [[entities/x]], papers [[sources/y]]
- Sub-area B — ...
- Sub-area C — ...
Key concepts
- [[concepts/xxx]] — short note on role
- [[concepts/yyy]] — short note
- [[concepts/zzz]] — short note
Key sources
- [[sources/xxx]] — why it matters
- [[sources/yyy]] — why it matters
Current open problems
Short list with [[concepts]] and [[sources]] pointers.
How this synthesis has changed
- <YYYY-MM-DD> — initial synthesis after first N sources.
- <YYYY-MM-DD> — added [[sources/xxx]]; shifted emphasis toward ...
Related
- [[synthesis/other-overview]]
- [[comparisons/a-vs-b]]
{
"status": "ok",
"log_path": "/tmp/test-vault/wiki/log.md",
"date": "2026-04-11",
"op": "ingest",
"title": "Hello Monosemanticity",
"header": "## [2026-04-11] ingest | Hello Monosemanticity",
"detail": "touched 2 pages"
}
{
"status": "ok",
"vault": "/tmp/test-vault",
"source": "wiki/concepts/sparse-autoencoder.md",
"theme": "gaia",
"output_dir": "slides",
"rendered_count": 1,
"rendered": ["slides/sparse-autoencoder.marp.md"]
}
{
"total_pages": 2,
"total_edges": 2,
"top_outbound_hubs": [
{"page": "sources/hello", "outbound": 1},
{"page": "concepts/sparse-autoencoder", "outbound": 1}
],
"top_inbound_hubs": [
{"page": "sources/hello", "inbound": 1},
{"page": "concepts/sparse-autoencoder", "inbound": 1}
],
"orphans": [],
"sinks": [],
"components": [
{"size": 2, "sample": ["concepts/sparse-autoencoder", "sources/hello"]}
],
"component_count": 1
}
{
"source_path": "/tmp/test-vault/raw/articles/hello.md",
"relative": "raw/articles/hello.md",
"bytes": 204,
"sha256": "dcb7021b49882e26",
"ext": ".md",
"title_guess": "Hello Monosemanticity",
"word_count": 28,
"preview": "# Hello Monosemanticity\n\nAnthropic's Bricken et al. 2023 paper trained a sparse autoencoder on a one-layer transformer and found interpretable features.\nKey claim: the feature dictionary is overcomplete.\n",
"existing_summary_page": null,
"suggested_summary_path": "wiki/sources/hello-monosemanticity.md"
}
{
"status": "ok",
"vault_path": "/tmp/test-vault",
"topic": "LLM interpretability",
"tool": "all",
"date": "2026-04-11",
"installed_files": [
"CLAUDE.md",
"AGENTS.md",
".cursorrules",
"wiki/index.md",
"wiki/log.md"
],
"page_templates_copied": 5,
"layers": {
"raw": "your sources — immutable",
"wiki": "LLM-maintained knowledge base",
"index": "wiki/index.md",
"log": "wiki/log.md"
},
"next_steps": [
"Open the vault in Obsidian",
"Drop a source into raw/",
"Run /wiki-ingest <path> in your LLM CLI"
]
}
{
"vault": "/tmp/test-vault",
"total_pages": 2,
"orphans": [],
"broken_links": [],
"stale": [],
"missing_frontmatter": [],
"duplicate_titles": {},
"log_gap": null
}
Expected Outputs
Sample outputs for each script in scripts/. Use these as fixtures when testing or to verify the scripts behave correctly end-to-end.
| Script | Fixture |
|---|---|
init_vault.py --json | init_vault.json |
ingest_source.py --json | ingest_source.json |
update_index.py --json | update_index.json |
append_log.py --json | append_log.json |
wiki_search.py --json | wiki_search.json |
lint_wiki.py --json | lint_wiki.json |
graph_analyzer.py --json | graph_analyzer.json |
export_marp.py --json | export_marp.json |
These were captured against a small 2-page example vault (one concept page and one source page, both with proper frontmatter). Paths have been anonymized to /tmp/test-vault.
{
"status": "ok",
"vault": "/tmp/test-vault",
"total_pages": 2,
"by_category": {
"concept": 1,
"source": 1
},
"dry_run": false,
"index_path": "/tmp/test-vault/wiki/index.md"
}
{
"query": "sparse autoencoder",
"hits": [
{
"path": "concepts/sparse-autoencoder.md",
"score": 1.995,
"snippet": "--- title: Sparse Autoencoder category: concept summary: Dictionary-learning method for interpretable features tags: [interpretability] sources: 1 updated: 2026-04-11 --- # Sparse Autoencoder See [[sources/hello]] for th…"
}
]
}
Cross-Tool Setup
The LLM Wiki plugin is tool-agnostic. The scripts are pure Python stdlib and run anywhere. Only the schema loader file (the file the tool reads to understand conventions) differs per tool.
How different CLIs discover project-level instructions
| Tool | Loader file | Notes |
|---|---|---|
| Claude Code | CLAUDE.md | Loaded automatically when CC starts in the vault dir |
| Codex CLI (OpenAI) | AGENTS.md | Loaded at session start |
| Cursor (new) | AGENTS.md | Modern Cursor reads AGENTS.md |
| Cursor (legacy) | .cursorrules | Older Cursor versions |
| Google Antigravity | AGENTS.md | Uses the standard AGENTS.md convention |
| OpenCode / Pi | AGENTS.md | Same convention |
| Gemini CLI | AGENTS.md | Same convention |
| Aider | CONVENTIONS.md or .aider.conf.yml | Point Aider at CLAUDE.md with --read CLAUDE.md |
Recommendation: ship both CLAUDE.md and AGENTS.md in every vault. init_vault.py --tool all does this by default.
Multi-tool vault
If you use multiple CLIs against the same vault:
python scripts/init_vault.py --path ~/vaults/research --topic "X" --tool allThis creates:
CLAUDE.mdAGENTS.md.cursorrules
All three are the same content, formatted appropriately. You can symlink to keep them in sync:
cd <vault>
ln -sf CLAUDE.md AGENTS.md
# or edit both manually when you tune the schemaPer-tool quickstart
Claude Code
cd <vault>
claude
> /wiki-init # if vault isn't initialized
> /wiki-ingest raw/paper.pdf
> /wiki-query "what does the paper say about X?"The slash commands ship with this plugin. To install the plugin itself, either:
- Clone claude-code-skills and copy
engineering/llm-wiki/into~/.claude/skills/, or - Install via the marketplace if published
Codex CLI
Codex reads AGENTS.md automatically. Then:
cd <vault>
codex
> ingest raw/paper.pdf into the wiki
> query: what does the paper say about X?Codex doesn't have slash commands, but the schema file teaches it the ingest/query/lint workflow, so natural-language triggers work.
Cursor
cd <vault>
cursor .Open the Cursor chat in the sidebar. Cursor auto-reads AGENTS.md. Ask the same questions.
Antigravity / OpenCode / Pi
Same as Codex — drop AGENTS.md in the vault root and use natural language.
Multi-tool same session
You can run Claude Code and Codex simultaneously against the same vault. They'll both see updates if one writes a page — filesystem is the source of truth. Just make sure each vault is committed to git so you can resolve conflicts.
Running the scripts directly (any tool)
The scripts don't care which tool calls them. You can run them from the shell any time:
# from inside the vault
python ~/.claude/skills/llm-wiki/scripts/lint_wiki.py --vault .
python ~/.claude/skills/llm-wiki/scripts/update_index.py --vault .
python ~/.claude/skills/llm-wiki/scripts/wiki_search.py --vault . --query "superposition"Aliases are handy. Add to your shell rc:
alias wiki-lint='python ~/.claude/skills/llm-wiki/scripts/lint_wiki.py --vault .'
alias wiki-index='python ~/.claude/skills/llm-wiki/scripts/update_index.py --vault .'
alias wiki-search='python ~/.claude/skills/llm-wiki/scripts/wiki_search.py --vault .'MCP exposure (future)
The wiki can be exposed as an MCP tool so any MCP-capable client (Claude Desktop, Claude Code, etc.) can query it. See engineering/mcp-design in this repo for the pattern. A future version of this plugin will ship an mcp/ directory with a reference MCP server.
Ingest Workflow
The detailed flow the LLM follows when the user runs /wiki-ingest <path> or dispatches the wiki-ingestor sub-agent.
Inputs
- Path to a source file (inside
raw/— if not, prompt the user to move it first) - The current state of
wiki/(especiallyindex.md)
Step-by-step
1. Prepare the brief
Run python scripts/ingest_source.py --vault . --source <path> --json to get:
- title guess
- word count
- preview (first 1200 chars)
- suggested summary-page path
- whether a summary page already exists (→ merge mode)
2. Read the source
Use the Read tool on the source directly. For PDFs, use the Read tool's PDF support. For images clipped locally to raw/assets/, inspect them if the LLM has vision.
3. Discuss with the user
Before writing anything, tell the user:
- Title and author(s)
- 2-3 sentence TL;DR
- Key claims (bulleted, 3-7 items)
- Which existing wiki pages this source will touch
- Any contradictions with existing pages
Wait for user to confirm or redirect. This is the "LLM makes edits, you browse" loop — the user is in the loop.
4. Create / merge the source summary page
Path: wiki/sources/<slug>.md. Use the source summary template from references/page-formats.md. Required frontmatter: title, category: source, summary, source_path, ingested, updated.
Merge mode (summary page already exists): append a new "## Re-ingest <date>" section at the bottom with what changed. Do not overwrite.
5. Identify entities and concepts
For each entity and concept mentioned in the source:
- Check if a page exists in
wiki/entities/orwiki/concepts/ - If yes: update it. Add a new bullet under "Appears in" / "Used in" pointing to this source. Update "Key claims" if this source adds or contradicts a claim. Update
sources:count in frontmatter. Updateupdated:to today. - If no: create a new page from the entity/concept template. Start with the minimum: title, summary, one key fact sourced from this reading, link back to this source.
Typical ingest touches 5-15 pages across entities/, concepts/, and sometimes comparisons/.
6. Flag contradictions explicitly
If the new source contradicts an existing page, add a callout to BOTH pages:
> ⚠️ **Contradiction** — [[sources/new]] claims X but [[sources/old]] claims ~X.
> Unresolved as of 2026-04-10.Log contradictions in log.md with op: note.
7. Update synthesis (optional)
If the source meaningfully shifts a synthesis/ page's thesis, revise the "Thesis" paragraph and append a dated entry under "How this synthesis has changed". Don't rewrite history; append.
8. Update index.md
Either:
- Run
python scripts/update_index.py --vault .to regenerate the entire index from frontmatter, OR - Edit the relevant category sections inline (faster for small ingests).
9. Append to log.md
Run python scripts/append_log.py --vault . --op ingest --title "<title>" --detail "<detail>".
The detail line should list which pages were touched:
## [2026-04-10] ingest | Anthropic Monosemanticity
Added sources/monosemanticity.md. Updated concepts/sparse-autoencoder,
concepts/polysemanticity, entities/anthropic-interpretability-team. Flagged
contradiction with sources/distributed-representations.10. Report back to the user
Summary the user sees in chat:
- Source summary page created/updated
- Pages touched (bulleted wikilinks so the user can click through)
- Contradictions flagged (if any)
- Suggested next sources to pursue
After-ingest tips
- Big ingest? Run
python scripts/lint_wiki.py --vault .to check for new orphans or broken links. - Graph check? Run
python scripts/graph_analyzer.py --vault .to see if the new page is well-connected. - Open Obsidian graph view — the user should see the new page attached to the existing cluster.
Lint Workflow
Periodic health-check the LLM runs when the user runs /wiki-lint or dispatches the wiki-linter sub-agent. Run this at least weekly, and always after a batch ingest.
Goal
Keep the wiki healthy as it grows. Surface problems for the user to review.
Pass 1 — mechanical checks (script)
Run python scripts/lint_wiki.py --vault . to get a report on:
- Orphans — pages with zero inbound
[[wikilinks]] - Broken links — wikilinks pointing to non-existent pages
- Stale pages — pages whose
updated:frontmatter is older than 90 days (tune via--stale-days) - Missing frontmatter — pages lacking
title,category, orsummary - Duplicate titles — two or more pages sharing the same title
- Log gap — no log entry in the last 14 days (tune via
--log-gap-days)
Run python scripts/graph_analyzer.py --vault . for structural stats:
- Hubs (inbound/outbound) — likely well-placed
- Sinks — pages that don't link out; may need cross-referencing
- Connected components — if > 1, parts of the wiki are disconnected islands
Pass 2 — semantic checks (LLM)
The script can't catch these. The LLM must read and think.
A. Contradictions
Scan pages whose updated: is recent. For each, check whether it contradicts any existing page. If so:
- Add a
> ⚠️ Contradiction:callout to both pages - Log with
op: note - Surface to user: "I found a potential contradiction between X and Y. Want me to investigate?"
B. Stale claims
For each flagged stale page, ask:
- Does a newer source now contradict this?
- Is a "Key facts" bullet likely to be outdated (person changed role, company pivoted, etc.)?
- If yes, suggest to user: "Page X says Y. This may be outdated — do you want me to search for newer sources?"
C. Concepts mentioned but without their own page
Grep for common patterns: [[concept:xxx]], phrases like "see also", concept-shaped nouns mentioned across 3+ pages but with no dedicated page.
Suggest new pages to create.
D. Cross-reference gaps
For each page, check: do all entities and concepts mentioned have wikilinks? If a concept is referenced as plain text in 3+ places, promote it to a wikilink (and create a stub page if needed).
E. Index drift
Compare index.md against actual wiki/ contents. If out of sync, either regenerate (update_index.py) or patch inline.
Pass 3 — report
Present findings to the user as a single markdown report:
# Wiki lint — 2026-04-10
**Total pages:** 87 **Components:** 1 **Last log:** 2026-04-09
## Found
- ⚠️ 3 contradictions (wiki/concepts/x, wiki/sources/y, wiki/sources/z)
- 12 orphan pages (mostly new entities)
- 2 broken links (wiki/concepts/x → [[foo-bar]] no such page)
- 4 stale pages (>90 days, no re-ingest)
- 5 concepts mentioned across 3+ pages without their own page
## Suggested actions
1. Investigate contradiction between [[sources/a]] and [[sources/b]]
2. Create concept page for "attention masking" (mentioned in 4 sources)
3. Re-ingest [[sources/c]] — stale and contradicted by newer sources
4. Fix broken link in [[concepts/x]]
5. Cross-reference the 12 orphans (most belong under [[synthesis/overview]])
Want me to run these in order, or pick specific ones?Append a lint entry to log.md summarizing what was found and what was fixed.
Frequency
- Weekly — light pass, script-only (
lint_wiki.py+ quick review) - After batch ingests — always
- Monthly — full pass including semantic checks
- Before sharing the wiki — full pass plus an extra review
Memex Principles
Why the LLM Wiki pattern works, and why it failed for humans until LLMs.
Vannevar Bush's Memex (1945)
In "As We May Think", Bush described a personal knowledge store where:
- Documents are curated, not just searched
- Users build associative trails — named, reusable paths through the material
- The trails are as valuable as the documents
- The system is private and personal, not a public reference
This is almost exactly the LLM Wiki pattern. The difference: Bush had no one to do the bookkeeping.
Why humans abandon wikis
The value of a wiki grows linearly with its size. The maintenance burden grows faster. At some inflection point — usually around 50-100 pages — maintenance starts to feel like chores and the wiki goes stale.
Specific tasks that die first:
- Updating cross-references when a new page is added
- Keeping summary pages current
- Noticing when new data contradicts old claims
- Consolidating pages that have drifted apart
- Filing explorations back into the knowledge base
- Keeping the index current
Humans are great at reading, curating, and thinking about what things mean. They're bad at the bookkeeping. The bookkeeping is 80% of the work.
What changed with LLMs
LLMs don't get bored. They don't forget to update a cross-reference. They can touch 15 files in one pass without losing track. They cost near-zero per maintenance operation.
This changes the economics. The wiki stays maintained because maintenance is now free (or nearly). The human's job collapses to:
- Source curation — deciding what's worth reading
- Direction — asking good questions, steering analysis
- Judgment — deciding when a contradiction matters
- Taste — knowing when the synthesis is wrong
Everything else — the 80% that killed human wikis — is delegated.
Why not just RAG?
RAG retrieves fragments at query time and synthesizes from scratch every query. It works, but:
- No accumulation. Every subtle question re-derives the same synthesis.
- Cross-references are computed on demand. If the cross-reference needs 5 sources to be visible, you'd better hope all 5 are in the retrieval window.
- Contradictions are invisible. They surface only if you explicitly ask "is there a contradiction?"
- Explorations disappear. A comparison you worked out yesterday has to be re-derived tomorrow.
The wiki fixes all of these by compiling the knowledge once. The cross-references are already there. The contradictions have been flagged. The synthesis has absorbed everything read so far.
RAG is retrieve-then-think. The wiki is think-once-retrieve-many.
When the wiki stops being enough
At ~500-1000 pages, the index approach starts to creak. Options:
1. Layer on search. Add wiki_search.py (BM25) or an external tool like qmd (hybrid BM25 + vector). Both work alongside the index. 2. Shard by topic. Split into multiple vaults by domain. 3. Add an MCP retrieval layer. Expose the wiki as a tool so agents can query it structurally.
The wiki and RAG are not opposites. The wiki is a compiled layer above RAG. You can run RAG on top of the wiki (indexing wiki/) and you'll get the benefits of both: pre-synthesized knowledge + scalable retrieval.
The human role
A common failure mode: users delegate curation to the LLM ("just ingest all my Pocket articles"). Don't. Curation is where human judgment lives. The LLM can help you decide whether to read a source, but you pick what makes it into raw/.
If you let the LLM ingest everything, the wiki fills with low-signal summaries and the synthesis becomes meaningless. The wiki's value is a direct function of the quality of raw/.
Reading recommendations
- Vannevar Bush, "As We May Think" (Atlantic, 1945)
- Andrej Karpathy's original LLM Wiki gist (linked from SKILL.md)
- Ousterhout's A Philosophy of Software Design — for why "deep modules" (well-summarized pages) beat shallow ones
- Niklas Luhmann's Zettelkasten — an earlier manual version of the same pattern
Obsidian Setup
Recommended Obsidian configuration for an LLM Wiki vault. None of this is strictly required — the wiki is just markdown files — but these settings remove friction.
Open the vault
1. Obsidian → "Open folder as vault" → pick your initialized vault 2. The vault already has wiki/, raw/, CLAUDE.md, AGENTS.md
Settings → Files and Links
- Default location for new notes:
wiki/ - New link format:
Shortest path when possible(keeps wikilinks clean) - Use `[[Wikilinks]]`: ON
- Attachment folder path:
raw/assets/(so clipped images land inraw/, notwiki/) - Automatically update internal links: ON
Settings → Hotkeys
Search for and bind:
- "Download attachments for current file" →
Ctrl/Cmd + Shift + D - "Open graph view" →
Ctrl/Cmd + G
Core plugins to enable
- Graph view — see the shape of your wiki. Hubs, orphans, clusters.
- Backlinks — pane showing who links to the current page. Critical for browsing.
- Outgoing links — complementary pane.
- Templates — enable and set the template folder to
wiki/.templates - Tag pane — tag-driven navigation
- Search — obviously
- Page preview — hover a wikilink to preview
- Canvas — visual exploration, useful for synthesis work
Recommended community plugins
- Obsidian Web Clipper (browser extension, not a plugin) — clip articles to
raw/articles/as markdown - Dataview — query over frontmatter. Dynamic tables of "all concept pages touched by 3+ sources".
- Marp for Obsidian — render any markdown with
marp: truefrontmatter as a slide deck inside Obsidian. Pairs withscripts/export_marp.py. - Templater — dynamic templates (optional, you can use the LLM for this)
- Advanced Tables — easier markdown table editing
- Git — commit on save, or hook into system git
Dataview examples
Pages with 3+ sources:
table updated, sources
from "wiki/concepts"
where sources >= 3
sort updated descRecently updated synthesis pages:
list
from "wiki/synthesis"
sort updated desc
limit 10Orphans (Dataview can't see inbound links — use the lint script for this).
Git workflow
cd <vault>
git init
git add .
git commit -m "init wiki"
# After every session:
git add wiki/ log.md index.md
git commit -m "ingest: <source>"The vault is a plain markdown repo. Version history, branching, collaboration — free.
Tips
- Use the graph view daily — it's the fastest way to see structural drift
- Pin `index.md`, `log.md`, and the active `synthesis/` page to the sidebar tabs
- Split view — wiki on the left, chat/CLI on the right. You browse while the LLM edits.
- Enable "strict line breaks" so your LLM's markdown renders the way the LLM expects
- Use images aggressively — download them locally, reference from pages. The LLM can read them with its vision tool when needed.
Page Formats
Every wiki page has the same skeleton: YAML frontmatter + a section structure that matches its category. Below are the five canonical formats. Templates live in assets/page-templates/.
1. Entity page
For a person, organization, place, product, or dataset.
---
title: Anthropic
category: entity
summary: AI safety company, developer of Claude; major contributor to interpretability research
tags: [company, ai-safety, anthropic]
sources: 4
updated: 2026-04-10
---
# Anthropic
## What it is
One-paragraph definition. What kind of entity, founded when, by whom, active in what.
## Why it matters (to this wiki)
Why this entity shows up across sources. What role does it play in the narrative?
## Key facts
- Founded YYYY by [[people]]
- Known for [[concepts]]
- Related [[entities]]
## Appears in
- [[sources/monosemanticity]] — primary work on sparse autoencoders
- [[sources/constitutional-ai]] — alignment methodology
- [[concepts/rlhf]] — contributor to training method
## Open questions
- Questions the sources don't yet answer; good prompts for new source hunts.2. Concept page
For an idea, theory, method, framework.
---
title: Sparse Autoencoder
category: concept
summary: Dictionary-learning method for decomposing polysemantic neurons into monosemantic features
tags: [interpretability, sparse-autoencoders, dictionary-learning]
sources: 3
updated: 2026-04-10
---
# Sparse Autoencoder
## Definition
Precise, one-paragraph definition. The canonical form used across your sources.
## Origin
Who proposed it, when, in what paper/context. Link [[entities]] and [[sources]].
## Key claims
- Claim 1 — cited from [[sources/xxx]]
- Claim 2 — cited from [[sources/yyy]]
## Contrasts with
- [[concepts/probing]] — see [[comparisons/sae-vs-probing]]
## Open questions / disagreements
- Unresolved questions across sources.
- ⚠️ Contradiction: [[sources/a]] claims X but [[sources/b]] claims ~X.
## Used in
- [[synthesis/interpretability-overview]]3. Source summary page
One per ingested source. This is the single place the raw source's content is summarized; other pages cite it.
---
title: "Towards Monosemanticity: Decomposing Language Models With Dictionary Learning"
category: source
summary: Anthropic 2024 paper using sparse autoencoders to extract interpretable features from a one-layer transformer
tags: [interpretability, sparse-autoencoders, anthropic]
source_path: raw/papers/monosemanticity.pdf
source_date: 2024-10
authors: [Bricken et al.]
ingested: 2026-04-10
updated: 2026-04-10
---
# Towards Monosemanticity
## TL;DR
Two sentences max. What did they do, what did they find.
## Key claims
1. Claim, with a page/section pointer if available
2. ...
## Methods
How the work was done. Data, model, training, evaluation.
## Evidence cited
- Figure 3 shows ...
- Table 1 ...
## Surprises / contradictions
- Where this source conflicts with [[sources/other]] or [[concepts/xxx]].
## Connections
- Extends [[concepts/sparse-autoencoder]]
- Builds on [[entities/anthropic-interpretability-team]]'s prior work
- Related: [[sources/superposition-2022]]
## Where it's cited
Pages in this wiki that cite this source:
- [[concepts/sparse-autoencoder]]
- [[entities/anthropic]]
- [[synthesis/interpretability-overview]]4. Comparison page
For explicit cross-source or cross-concept analysis.
---
title: "SAE vs Probing"
category: comparison
summary: How sparse autoencoders differ from linear probing as interpretability methods
tags: [interpretability, comparison]
sources: 4
updated: 2026-04-10
---
# Sparse Autoencoders vs Linear Probes
## What they share
Both look for human-interpretable structure inside trained models.
## Where they diverge
| Dimension | SAE | Probe |
|---|---|---|
| Supervision | unsupervised | supervised |
| Output | dictionary of features | single-label classifier |
| Scalability | model-dependent | cheap |
| Typical use | feature discovery | feature verification |
## Which sources take which side
- [[sources/monosemanticity]] — pro-SAE
- [[sources/probing-survey]] — pro-probes
## Open questions
- When should you prefer one over the other?5. Synthesis page
High-level views that draw on many sources and concepts.
---
title: Interpretability Overview
category: synthesis
summary: The field of interpretability research — goals, methods, open problems, key players
tags: [interpretability, overview]
sources: 12
updated: 2026-04-10
---
# Interpretability Overview
## Thesis
Two or three sentences capturing the current synthesis across all sources read so far. Revised as new sources come in.
## The landscape
- Sub-area A — pursued by [[entities]], papers [[sources]]
- Sub-area B — ...
## Current open problems
Short list with [[concepts]] and [[sources]] pointers.
## How this synthesis has changed
- **2026-04-10** — added [[sources/monosemanticity]]; shifted emphasis toward SAE.
- **2026-03-28** — initial synthesis after first 5 sources.
## Related
- [[synthesis/alignment-overview]]
- [[comparisons/mechinterp-vs-behavioral-interp]]Query Workflow
The flow the LLM follows when the user runs /wiki-query <question> or dispatches the wiki-librarian sub-agent.
Core principle
Read `index.md` first, then drill in. Do NOT grep the entire wiki on every query — the index is there precisely so you don't have to.
Step-by-step
1. Read index.md
The index is the catalog. Scan it and pick the 3-10 pages most likely to contain the answer. Pick across categories: a good query usually pulls from synthesis/ for the big picture, concepts/ for definitions, sources/ for evidence, and entities/ for context.
2. Read the picked pages
Read them in full. These are short, curated, and already cross-referenced. The wiki has done the hard work for you.
3. Follow wikilinks opportunistically
If a read page points to another page that's clearly relevant, follow it. Don't follow blindly — stop when you have enough.
4. Fall back to search if needed
If the index doesn't surface the right page, use:
python scripts/wiki_search.py --vault . --query "<terms>" --limit 5BM25 search over wiki pages. Standard library only. Use when:
- The index is stale (flag this to the user — it means lint time)
- The user asks about something niche that doesn't have its own page yet
- You're doing a sweeping search across many pages
5. Synthesize the answer
Compose the answer as:
- A direct answer in 1-3 sentences
- Supporting detail, organized thematically
- Inline citations using wikilinks to source pages:
[[sources/monosemanticity]] - A "Related pages" section at the end with 3-5 wikilinks
6. Offer to re-file
Every good answer is a candidate wiki page. At the end of the answer, ask:
_Should I file this as a new page in the wiki? Suggested location:
wiki/comparisons/sae-vs-probing.md — or I can append it to an existing page._If the user says yes:
- Pick the right category (most often
comparisons/orsynthesis/) - Use the appropriate template
- Add frontmatter with
category,summary,sources(count of cited sources),updated - Update
index.md - Append to
log.mdwithop: createand the question as the title
This is how the wiki compounds — explorations don't disappear into chat history.
Output formats
Not every query wants a markdown answer. Offer the user:
- Markdown page (default) — filed back as a wiki page
- Comparison table — for "A vs B" questions
- Marp slide deck — via
python scripts/export_marp.pyon the synthesis page - Chart (matplotlib) — for data-driven questions; save to
wiki/assets/charts/ - Obsidian Canvas — for visual exploration (JSON format, stored at
wiki/canvases/)
Anti-patterns
- ❌ Read every page in the wiki on every query → use the index
- ❌ Answer without citations → every claim must link to a page
- ❌ Create a new page for a one-off trivial question → only file back answers worth keeping
- ❌ Invent content not in the wiki → if you don't know, say so and suggest a new source to ingest
- ❌ Skip the
log.mdentry when filing an answer back
Wiki Schema
The vault has three layers. The LLM must respect the boundaries.
Layout
<vault>/
├── raw/ # IMMUTABLE sources (you own)
│ ├── articles/*.md # Obsidian Web Clipper output
│ ├── papers/*.pdf
│ ├── notes/*.md # your own notes, journal entries
│ └── assets/ # images downloaded by Obsidian
├── wiki/ # LLM-owned knowledge base
│ ├── index.md # content catalog — updated every ingest
│ ├── log.md # append-only timeline
│ ├── entities/ # people, orgs, places, products
│ ├── concepts/ # ideas, theories, frameworks, methods
│ ├── sources/ # one summary page per ingested source
│ ├── comparisons/ # cross-source analysis / contrasts
│ ├── synthesis/ # high-level theses, overviews
│ └── .templates/ # page templates (reference only, not indexed)
├── CLAUDE.md # schema file for Claude Code
├── AGENTS.md # same schema for Codex/Cursor/Antigravity
└── .cursorrules # (optional) Cursor legacyIron rules
1. `raw/` is immutable. The LLM reads from raw/ but never writes to it. Never rename, never delete, never edit. If a source is wrong, the user edits it. 2. All LLM writes go to `wiki/`. No exceptions. 3. Every ingest updates 5 files minimum: the new source summary, the relevant entity/concept pages, index.md, log.md. A rich ingest touches 10-15. 4. Every wiki page carries YAML frontmatter. Without frontmatter, update_index.py and lint_wiki.py can't see it.
Required page frontmatter
---
title: Mechanistic Interpretability
category: concept # entity | concept | source | comparison | synthesis
summary: Reverse-engineering neural networks into human-understandable circuits
tags: [interpretability, circuits, anthropic]
sources: 3 # optional — number of sources touching this page
updated: 2026-04-10 # LLM updates this on every edit
---Allowed category values: entity, concept, source, comparison, synthesis.
Naming conventions
- Filenames:
kebab-case.md— lowercase, hyphens, no spaces - Entities:
entities/<kebab-case-name>.md— e.g.entities/chris-olah.md - Concepts:
concepts/<kebab-case-name>.md— e.g.concepts/sparse-autoencoder.md - Sources:
sources/<short-slug>.md— e.g.sources/monosemanticity.md - Comparisons:
comparisons/<topic-a>-vs-<topic-b>.md - Synthesis:
synthesis/<topic>-overview.mdorsynthesis/<topic>-thesis.md
Linking
Use Obsidian wikilinks. Three forms:
[[concepts/sparse-autoencoder]] # full path
[[concepts/sparse-autoencoder|sparse autoencoders]] # custom display text
[[sparse-autoencoder]] # stem — resolves if uniqueThe linter resolves stem links by matching against filenames. Prefer full paths when ambiguous.
Cross-reference rules
- Every entity mentioned in a concept/source page must be a wikilink. If the entity page doesn't exist yet, create it.
- Every concept mentioned in a source summary must be a wikilink. Same rule.
- Contradictions get flagged inline with a
> ⚠️ Contradiction:callout, and the source pages that disagree are linked from the callout. - Synthesis pages link back to every concept and source they draw on.
Index discipline
wiki/index.md is regenerated, not hand-edited. Either:
- Run
python scripts/update_index.py --vault .after every ingest, OR - Have the LLM rewrite the relevant section inline.
The index groups pages by category, alphabetized by title. Each entry is one line with a wikilink, summary, and optional metadata.
Log discipline
wiki/log.md is append-only. Every entry starts with a standardized header so grep "^## \[" log.md | tail -5 returns the last 5 entries.
## [2026-04-10] ingest | Anthropic Monosemanticity
Added sources/monosemanticity.md. Updated concepts/sparse-autoencoder,
concepts/polysemanticity, entities/anthropic-interpretability-team. Flagged
contradiction with sources/distributed-representations on feature basis claim.Valid ops: ingest, query, lint, create, update, delete, note.
#!/usr/bin/env python3
"""
append_log.py — Append a standardized entry to wiki/log.md.
The log is append-only and uses a consistent header so unix tools can parse it:
## [YYYY-MM-DD] <op> | <title>
A useful tip: if each entry starts with a consistent prefix, the log becomes
parseable with simple unix tools — `grep "^## \\[" log.md | tail -5` gives you
the last 5 entries.
Usage:
python append_log.py --vault ~/vaults/research --op ingest --title "Anthropic Monosemanticity"
python append_log.py --vault . --op query --title "interpretability vs mechinterp" --detail "3 pages touched"
python append_log.py --vault . --op lint --title "weekly health check" --detail "2 contradictions" --json
Valid ops:
ingest — a source was read and integrated into the wiki
query — a question was answered (filed back as a page)
lint — a health-check pass ran
create — a new page was created outside of an ingest
update — an existing page was updated outside of an ingest
delete — a page was removed
note — freeform note (contradictions flagged, thesis revisions, etc.)
Exit codes:
0 success
1 invalid vault / missing log.md / invalid op / write failure
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import sys
from pathlib import Path
VALID_OPS = {"ingest", "query", "lint", "create", "update", "delete", "note"}
def _error(message, as_json=False):
"""Print an error and exit with code 1. Respects --json mode."""
if as_json:
print(json.dumps({"status": "error", "message": message}))
else:
print(f"[error] {message}", file=sys.stderr)
sys.exit(1)
def validate_vault(vault):
"""Return the log.md path or raise if vault is invalid."""
if not vault.exists():
raise FileNotFoundError(f"vault does not exist: {vault}")
log_path = vault / "wiki" / "log.md"
if not log_path.exists():
raise FileNotFoundError(f"{log_path} does not exist — is this a vault?")
return log_path
def format_entry(op, title, detail):
"""Build the standardized log entry string."""
today = dt.date.today().isoformat()
header = f"## [{today}] {op} | {title}"
body = f"\n{detail}\n" if detail else "\n"
return today, header, f"\n{header}\n{body}"
def append_log(vault, op, title, detail, as_json=False):
"""Append a standardized entry to wiki/log.md inside the vault."""
if op not in VALID_OPS:
_error(f"unknown op '{op}'. Valid: {sorted(VALID_OPS)}", as_json)
try:
log_path = validate_vault(vault)
except FileNotFoundError as e:
_error(str(e), as_json)
today, header, entry_text = format_entry(op, title, detail)
try:
with log_path.open("a", encoding="utf-8") as f:
f.write(entry_text)
except OSError as e:
_error(f"failed to write {log_path}: {e}", as_json)
result = {
"status": "ok",
"log_path": str(log_path),
"date": today,
"op": op,
"title": title,
"header": header,
"detail": detail,
}
if as_json:
print(json.dumps(result, indent=2))
else:
print(f"[ok] appended to {log_path}")
print(f" {header}")
if detail:
print(f" detail: {detail}")
return result
def main():
p = argparse.ArgumentParser(
description="Append a standardized entry to wiki/log.md",
epilog="Format: ## [YYYY-MM-DD] <op> | <title>",
)
p.add_argument("--vault", required=True, help="Vault root directory")
p.add_argument(
"--op",
required=True,
choices=sorted(VALID_OPS),
help="Operation type (ingest, query, lint, create, update, delete, note)",
)
p.add_argument("--title", required=True, help="Short title for the entry")
p.add_argument("--detail", default=None, help="Optional detail text")
p.add_argument(
"--json", action="store_true", help="Emit result as JSON instead of human-readable"
)
args = p.parse_args()
append_log(
Path(args.vault).expanduser().resolve(),
args.op,
args.title,
args.detail,
as_json=args.json,
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
export_marp.py — Render a wiki page (or subtree) as a Marp slide deck.
Marp is a Markdown-based slide format supported by an Obsidian plugin. This
script adds Marp frontmatter and converts `## H2` headings into slide breaks,
so any wiki page with H2 sections becomes a usable slide deck with zero
manual formatting.
Usage:
python export_marp.py --vault . --page wiki/synthesis/interpretability-overview.md
python export_marp.py --vault . --page wiki/concepts/ --theme gaia --out slides/
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
MARP_HEADER = """---
marp: true
theme: {theme}
paginate: true
---
"""
def strip_frontmatter(text: str) -> str:
return FRONTMATTER_RE.sub("", text, count=1)
def to_marp(text: str, theme: str) -> str:
body = strip_frontmatter(text).strip()
# Turn each "## " into a new slide separator.
# First H1 → title slide. Subsequent H2 → slide breaks.
lines = body.splitlines()
out: list[str] = []
seen_h1 = False
for line in lines:
if line.startswith("# ") and not seen_h1:
out.append(line)
out.append("")
seen_h1 = True
continue
if line.startswith("## "):
out.append("\n---\n")
out.append(line)
continue
out.append(line)
return MARP_HEADER.format(theme=theme) + "\n".join(out).strip() + "\n"
def render_one(src, out_path, theme, verbose=True):
"""Render a single markdown page as a Marp slide deck."""
try:
text = src.read_text(encoding="utf-8", errors="replace")
except OSError as e:
raise RuntimeError(f"failed to read {src}: {e}")
out_path.parent.mkdir(parents=True, exist_ok=True)
try:
out_path.write_text(to_marp(text, theme), encoding="utf-8")
except OSError as e:
raise RuntimeError(f"failed to write {out_path}: {e}")
if verbose:
print(f"[ok] {src.name} -> {out_path}")
return out_path
def _error(message, as_json=False):
if as_json:
print(json.dumps({"status": "error", "message": message}))
else:
print(f"[error] {message}", file=sys.stderr)
sys.exit(1)
def main():
p = argparse.ArgumentParser(
description="Render a wiki page (or subtree) to a Marp slide deck.",
epilog="Marp is a Markdown-based slide format supported by an Obsidian plugin.",
)
p.add_argument("--vault", required=True, help="Vault root directory")
p.add_argument(
"--page",
required=True,
help="Page or directory relative to the vault (e.g. wiki/synthesis/overview.md)",
)
p.add_argument(
"--theme", default="default", choices=["default", "gaia", "uncover"], help="Marp theme"
)
p.add_argument(
"--out", default="slides", help="Output directory relative to vault (default: slides)"
)
p.add_argument(
"--json", action="store_true", help="Emit result as JSON instead of human-readable"
)
args = p.parse_args()
vault = Path(args.vault).expanduser().resolve()
if not vault.exists():
_error(f"vault does not exist: {vault}", args.json)
src = (vault / args.page).resolve()
if not src.exists():
_error(f"page not found: {src}", args.json)
out_root = vault / args.out
rendered = []
try:
if src.is_file():
dest = out_root / src.name.replace(".md", ".marp.md")
render_one(src, dest, args.theme, verbose=not args.json)
rendered.append(str(dest.relative_to(vault)))
else:
for md in sorted(src.rglob("*.md")):
rel = md.relative_to(src)
dest = out_root / rel.with_suffix(".marp.md")
render_one(md, dest, args.theme, verbose=not args.json)
rendered.append(str(dest.relative_to(vault)))
except RuntimeError as e:
_error(str(e), args.json)
if args.json:
print(
json.dumps(
{
"status": "ok",
"vault": str(vault),
"source": str(src.relative_to(vault)),
"theme": args.theme,
"output_dir": args.out,
"rendered_count": len(rendered),
"rendered": rendered,
},
indent=2,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
graph_analyzer.py — Analyze the wikilink graph of an LLM Wiki vault.
Reports hubs, orphans, bridges, and weakly-connected components so the LLM
knows where to focus cross-referencing work.
Usage:
python graph_analyzer.py --vault ~/vaults/research
python graph_analyzer.py --vault . --json
python graph_analyzer.py --vault . --top 20
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]")
def build_graph(vault: Path):
wiki = vault / "wiki"
if not wiki.exists():
raise SystemExit(f"[error] {wiki} not found")
nodes: set[str] = set()
out: dict[str, set[str]] = defaultdict(set)
inb: dict[str, set[str]] = defaultdict(set)
stems: dict[str, str] = {}
for md in wiki.rglob("*.md"):
rel = md.relative_to(wiki)
if rel.name in {"index.md", "log.md"}:
continue
if any(part.startswith(".") for part in rel.parts):
continue
key = str(rel).replace("\\", "/")[:-3]
nodes.add(key)
stems[Path(key).name] = key
for md in wiki.rglob("*.md"):
rel = md.relative_to(wiki)
if rel.name in {"index.md", "log.md"} or any(p.startswith(".") for p in rel.parts):
continue
key = str(rel).replace("\\", "/")[:-3]
text = md.read_text(encoding="utf-8", errors="replace")
for m in WIKILINK_RE.finditer(text):
target = m.group(1).strip()
if target.endswith(".md"):
target = target[:-3]
if target in nodes:
out[key].add(target)
inb[target].add(key)
elif Path(target).name in stems:
resolved = stems[Path(target).name]
out[key].add(resolved)
inb[resolved].add(key)
return nodes, out, inb
def connected_components(nodes: set[str], out: dict[str, set[str]], inb: dict[str, set[str]]):
adj: dict[str, set[str]] = defaultdict(set)
for n in nodes:
adj[n] |= out.get(n, set())
adj[n] |= inb.get(n, set())
seen: set[str] = set()
components: list[set[str]] = []
for n in nodes:
if n in seen:
continue
stack = [n]
comp: set[str] = set()
while stack:
v = stack.pop()
if v in seen:
continue
seen.add(v)
comp.add(v)
stack.extend(adj[v] - seen)
components.append(comp)
components.sort(key=len, reverse=True)
return components
def analyze(vault: Path, top: int) -> dict:
nodes, out, inb = build_graph(vault)
hubs_out = sorted(nodes, key=lambda n: len(out.get(n, set())), reverse=True)[:top]
hubs_in = sorted(nodes, key=lambda n: len(inb.get(n, set())), reverse=True)[:top]
orphans = sorted(n for n in nodes if not inb.get(n))
sinks = sorted(n for n in nodes if not out.get(n))
comps = connected_components(nodes, out, inb)
return {
"total_pages": len(nodes),
"total_edges": sum(len(v) for v in out.values()),
"top_outbound_hubs": [{"page": h, "outbound": len(out.get(h, set()))} for h in hubs_out],
"top_inbound_hubs": [{"page": h, "inbound": len(inb.get(h, set()))} for h in hubs_in],
"orphans": orphans,
"sinks": sinks,
"components": [
{"size": len(c), "sample": sorted(c)[:5]} for c in comps[:10]
],
"component_count": len(comps),
}
def main() -> None:
p = argparse.ArgumentParser(description="Analyze the wikilink graph of an LLM Wiki vault")
p.add_argument("--vault", required=True)
p.add_argument("--top", type=int, default=10)
p.add_argument("--json", action="store_true")
args = p.parse_args()
r = analyze(Path(args.vault).expanduser().resolve(), args.top)
if args.json:
print(json.dumps(r, indent=2, default=list))
return
print(f"LLM Wiki graph — {r['total_pages']} pages, {r['total_edges']} links")
print(f"Connected components: {r['component_count']}")
print()
print("Top outbound hubs (pages that link to many others):")
for h in r["top_outbound_hubs"]:
print(f" - {h['page']} ({h['outbound']} out)")
print()
print("Top inbound hubs (pages many others link TO):")
for h in r["top_inbound_hubs"]:
print(f" - {h['page']} ({h['inbound']} in)")
print()
print(f"Orphans (no inbound): {len(r['orphans'])}")
for o in r["orphans"][:10]:
print(f" - {o}")
print()
print(f"Sinks (no outbound): {len(r['sinks'])}")
for s in r["sinks"][:10]:
print(f" - {s}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
ingest_source.py — Prepare a source for LLM ingestion.
This is a *helper* — it does not call an LLM. It extracts text and metadata from
a source file and emits a JSON brief the LLM (via the /wiki-ingest command or
the wiki-ingestor sub-agent) can read, discuss with the user, and use to update
the wiki.
Supported source types (stdlib only):
.md .txt .html .htm .json .csv
For .pdf and binary formats, install optional readers yourself, or let the LLM
read the file directly via its Read tool.
Usage:
python ingest_source.py --vault ~/vaults/research --source raw/paper.md
python ingest_source.py --vault . --source raw/article.html --json
Output (JSON):
{
"source_path": "raw/paper.md",
"relative": "raw/paper.md",
"bytes": 12345,
"sha256": "...",
"ext": ".md",
"title_guess": "Monosemanticity",
"word_count": 8432,
"preview": "First 1200 chars...",
"existing_summary_page": "wiki/sources/monosemanticity.md" | null,
"suggested_summary_path": "wiki/sources/monosemanticity.md"
}
"""
from __future__ import annotations
import argparse
import hashlib
import html.parser
import json
import re
import sys
from pathlib import Path
PREVIEW_CHARS = 1200
SLUG_RE = re.compile(r"[^a-z0-9]+")
def slugify(text: str) -> str:
text = text.lower().strip()
text = SLUG_RE.sub("-", text).strip("-")
return text[:60] or "untitled"
class _HTMLTextExtractor(html.parser.HTMLParser):
def __init__(self) -> None:
super().__init__()
self.parts: list[str] = []
self.title: str | None = None
self._in_title = False
self._skip = False
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag in {"script", "style"}:
self._skip = True
if tag == "title":
self._in_title = True
def handle_endtag(self, tag: str) -> None:
if tag in {"script", "style"}:
self._skip = False
if tag == "title":
self._in_title = False
def handle_data(self, data: str) -> None:
if self._skip:
return
if self._in_title and self.title is None:
self.title = data.strip() or None
else:
text = data.strip()
if text:
self.parts.append(text)
def text(self) -> str:
return "\n".join(self.parts)
def extract(path: Path) -> tuple[str, str | None]:
ext = path.suffix.lower()
data = path.read_bytes()
if ext in {".md", ".txt"}:
text = data.decode("utf-8", errors="replace")
title = None
for line in text.splitlines()[:20]:
if line.startswith("# "):
title = line[2:].strip()
break
return text, title
if ext in {".html", ".htm"}:
parser = _HTMLTextExtractor()
try:
parser.feed(data.decode("utf-8", errors="replace"))
except Exception:
pass
return parser.text(), parser.title
if ext == ".json":
try:
obj = json.loads(data.decode("utf-8", errors="replace"))
return json.dumps(obj, indent=2)[:100000], None
except Exception:
return data.decode("utf-8", errors="replace"), None
if ext == ".csv":
text = data.decode("utf-8", errors="replace")
head = "\n".join(text.splitlines()[:50])
return head, None
# Unknown: attempt utf-8 decode, let the LLM handle it
try:
return data.decode("utf-8", errors="replace"), None
except Exception:
return "", None
def main() -> None:
p = argparse.ArgumentParser(description="Prepare a source for LLM ingestion.")
p.add_argument("--vault", required=True)
p.add_argument("--source", required=True, help="Path to the source file (inside raw/)")
p.add_argument("--json", action="store_true", help="Emit JSON only")
args = p.parse_args()
vault = Path(args.vault).expanduser().resolve()
src = Path(args.source).expanduser().resolve()
if not src.exists():
print(f"[error] source not found: {src}", file=sys.stderr)
sys.exit(1)
try:
rel = src.relative_to(vault)
except ValueError:
rel = src
text, title = extract(src)
title_guess = title or src.stem.replace("-", " ").replace("_", " ").title()
slug = slugify(title_guess)
suggested = f"wiki/sources/{slug}.md"
existing = vault / suggested
existing_path = str(suggested) if existing.exists() else None
brief = {
"source_path": str(src),
"relative": str(rel).replace("\\", "/"),
"bytes": src.stat().st_size,
"sha256": hashlib.sha256(src.read_bytes()).hexdigest()[:16],
"ext": src.suffix.lower(),
"title_guess": title_guess,
"word_count": len(text.split()),
"preview": text[:PREVIEW_CHARS],
"existing_summary_page": existing_path,
"suggested_summary_path": suggested,
}
if args.json:
print(json.dumps(brief, indent=2, ensure_ascii=False))
else:
print(f"Source: {brief['source_path']}")
print(f"Title (guess): {brief['title_guess']}")
print(f"Size: {brief['bytes']} bytes ({brief['word_count']} words)")
print(f"SHA256 (short): {brief['sha256']}")
print(f"Suggested page: {brief['suggested_summary_path']}")
if existing_path:
print(f"EXISTING PAGE: {existing_path} ← re-ingest / merge mode")
print()
print("--- preview ---")
print(brief["preview"])
print("--- /preview ---")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
init_vault.py — Bootstrap an LLM Wiki vault.
Creates the three-layer structure (raw/, wiki/, schema files) and seeds it with
starter templates for CLAUDE.md, AGENTS.md, index.md, log.md, and page templates.
Usage:
python init_vault.py --path ~/vaults/research --topic "LLM interpretability"
python init_vault.py --path ./my-wiki --topic "Book: The Power Broker" --tool codex
The --tool flag controls which schema file(s) to install:
claude-code → CLAUDE.md (default)
codex → AGENTS.md
cursor → AGENTS.md + .cursorrules
antigravity → AGENTS.md
all → CLAUDE.md + AGENTS.md + .cursorrules (recommended for multi-tool)
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
PLUGIN_DIR = SCRIPT_DIR.parent
ASSETS_DIR = PLUGIN_DIR / "assets"
VAULT_DIRS = [
"raw",
"raw/assets",
"wiki",
"wiki/entities",
"wiki/concepts",
"wiki/sources",
"wiki/comparisons",
"wiki/synthesis",
]
TOOL_FILES = {
"claude-code": ["CLAUDE.md.template:CLAUDE.md"],
"codex": ["AGENTS.md.template:AGENTS.md"],
"cursor": ["AGENTS.md.template:AGENTS.md", "cursorrules.template:.cursorrules"],
"antigravity": ["AGENTS.md.template:AGENTS.md"],
"opencode": ["AGENTS.md.template:AGENTS.md"],
"gemini-cli": ["AGENTS.md.template:AGENTS.md"],
"all": [
"CLAUDE.md.template:CLAUDE.md",
"AGENTS.md.template:AGENTS.md",
"cursorrules.template:.cursorrules",
],
}
def render_template(src, dest, variables):
"""Render a template file with {{VAR}} substitutions to dest."""
if not src.exists():
print(f"[warn] template missing: {src}", file=sys.stderr)
return False
try:
text = src.read_text(encoding="utf-8")
except OSError as e:
print(f"[warn] could not read {src}: {e}", file=sys.stderr)
return False
for key, value in variables.items():
text = text.replace("{{" + key + "}}", value)
try:
dest.write_text(text, encoding="utf-8")
except OSError as e:
print(f"[warn] could not write {dest}: {e}", file=sys.stderr)
return False
return True
def _error(message, as_json=False):
if as_json:
print(json.dumps({"status": "error", "message": message}))
else:
print(f"[error] {message}", file=sys.stderr)
sys.exit(1)
def init_vault(vault_path, topic, tool, force, as_json=False):
"""Bootstrap a new LLM Wiki vault at vault_path."""
if vault_path.exists() and any(vault_path.iterdir()) and not force:
_error(f"{vault_path} is not empty. Use --force to overwrite.", as_json)
try:
vault_path.mkdir(parents=True, exist_ok=True)
for d in VAULT_DIRS:
(vault_path / d).mkdir(parents=True, exist_ok=True)
except OSError as e:
_error(f"failed to create vault structure: {e}", as_json)
today = dt.date.today().isoformat()
variables = {
"TOPIC": topic,
"DATE": today,
"VAULT_NAME": vault_path.name,
}
installed_files = []
# Schema files (CLAUDE.md / AGENTS.md / .cursorrules)
for spec in TOOL_FILES.get(tool, TOOL_FILES["claude-code"]):
src_name, dest_name = spec.split(":", 1)
dest = vault_path / dest_name
if render_template(ASSETS_DIR / src_name, dest, variables):
installed_files.append(dest_name)
# Index + log seeds
for spec in [
("index.md.template", vault_path / "wiki" / "index.md"),
("log.md.template", vault_path / "wiki" / "log.md"),
]:
if render_template(ASSETS_DIR / spec[0], spec[1], variables):
installed_files.append(str(spec[1].relative_to(vault_path)))
# Page templates (reference copies inside the vault)
tmpl_dest = vault_path / "wiki" / ".templates"
tmpl_dest.mkdir(exist_ok=True)
src_tmpl = ASSETS_DIR / "page-templates"
template_count = 0
if src_tmpl.exists():
for f in src_tmpl.iterdir():
if f.is_file():
try:
(tmpl_dest / f.name).write_text(
f.read_text(encoding="utf-8"), encoding="utf-8"
)
template_count += 1
except OSError as e:
print(f"[warn] failed to copy template {f.name}: {e}", file=sys.stderr)
# .gitignore — exclude Obsidian workspace files
gitignore = vault_path / ".gitignore"
gitignore.write_text(
"\n".join([".obsidian/workspace*", ".obsidian/cache", ".DS_Store", ""]),
encoding="utf-8",
)
result = {
"status": "ok",
"vault_path": str(vault_path),
"topic": topic,
"tool": tool,
"date": today,
"installed_files": installed_files,
"page_templates_copied": template_count,
"layers": {
"raw": "your sources — immutable",
"wiki": "LLM-maintained knowledge base",
"index": "wiki/index.md",
"log": "wiki/log.md",
},
"next_steps": [
"Open the vault in Obsidian",
"Drop a source into raw/",
"Run /wiki-ingest <path> in your LLM CLI",
],
}
if as_json:
print(json.dumps(result, indent=2))
return result
print(f"[ok] Initialized LLM Wiki vault at: {vault_path}")
print(f" Topic: {topic}")
print(f" Tool: {tool}")
print(f" Installed: {', '.join(installed_files)}")
print(f" Page templates copied: {template_count}")
print(" Layers:")
print(" raw/ (your sources — immutable)")
print(" wiki/ (LLM-maintained knowledge base)")
print(" wiki/index.md (catalog)")
print(" wiki/log.md (timeline)")
print()
print("Next steps:")
print(" 1. Open the vault in Obsidian")
print(" 2. Drop a source into raw/")
print(" 3. Run /wiki-ingest <path> in your LLM CLI")
return result
def main():
p = argparse.ArgumentParser(
description="Initialize an LLM Wiki vault — the three-layer structure (raw/, wiki/, schema) Karpathy describes in the LLM Wiki gist.",
)
p.add_argument("--path", required=True, help="Vault directory to create/initialize")
p.add_argument(
"--topic",
required=True,
help="Short description of what this wiki is about (e.g. 'LLM interpretability')",
)
p.add_argument(
"--tool",
default="all",
choices=sorted(TOOL_FILES.keys()),
help="Which schema file(s) to install (default: all)",
)
p.add_argument(
"--force", action="store_true", help="Overwrite non-empty target directory"
)
p.add_argument(
"--json", action="store_true", help="Emit result as JSON instead of human-readable"
)
args = p.parse_args()
init_vault(
Path(args.path).expanduser().resolve(),
args.topic,
args.tool,
args.force,
as_json=args.json,
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
lint_wiki.py — Health-check an LLM Wiki vault.
Surfaces structural problems the LLM-as-wiki-maintainer should fix:
- orphans: pages with zero inbound [[wikilinks]]
- broken_links: [[wikilinks]] pointing to non-existent pages
- stale: pages whose `updated:` frontmatter is older than --stale-days
- missing_fm: pages without a title/category/summary in frontmatter
- duplicate_titles: two or more pages sharing the same title
- log_gaps: no log entry in the last --log-gap-days
Usage:
python lint_wiki.py --vault ~/vaults/research
python lint_wiki.py --vault . --stale-days 60 --json
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]")
LOG_ENTRY_RE = re.compile(r"^## \[(\d{4}-\d{2}-\d{2})\]", re.MULTILINE)
def parse_frontmatter(text: str) -> dict[str, str]:
m = FRONTMATTER_RE.match(text)
if not m:
return {}
fm: dict[str, str] = {}
for line in m.group(1).splitlines():
if ":" in line and not line.lstrip().startswith("#"):
k, _, v = line.partition(":")
fm[k.strip()] = v.strip().strip("'\"")
return fm
def scan(vault: Path, stale_days: int, log_gap_days: int) -> dict:
wiki = vault / "wiki"
if not wiki.exists():
raise SystemExit(f"[error] {wiki} not found")
pages: dict[str, dict] = {}
inbound: dict[str, set[str]] = defaultdict(set)
outbound: dict[str, set[str]] = defaultdict(set)
for md in wiki.rglob("*.md"):
rel = md.relative_to(wiki)
if rel.name in {"index.md", "log.md"}:
continue
if any(part.startswith(".") for part in rel.parts):
continue
key = str(rel).replace("\\", "/")[:-3] # strip .md
text = md.read_text(encoding="utf-8", errors="replace")
fm = parse_frontmatter(text)
pages[key] = {"path": key + ".md", "fm": fm, "text": text}
# Build link graph
stems = {Path(k).name: k for k in pages}
for key, page in pages.items():
for m in WIKILINK_RE.finditer(page["text"]):
target = m.group(1).strip()
# Normalize: strip .md, try full path match first, then stem
if target.endswith(".md"):
target = target[:-3]
if target in pages:
outbound[key].add(target)
inbound[target].add(key)
elif Path(target).name in stems:
resolved = stems[Path(target).name]
outbound[key].add(resolved)
inbound[resolved].add(key)
else:
outbound[key].add(f"__BROKEN__:{target}")
today = dt.date.today()
stale_cutoff = today - dt.timedelta(days=stale_days)
orphans = sorted(k for k in pages if not inbound.get(k))
broken_links: list[tuple[str, str]] = []
for src, targets in outbound.items():
for t in targets:
if t.startswith("__BROKEN__:"):
broken_links.append((src, t.split(":", 1)[1]))
broken_links.sort()
stale: list[tuple[str, str]] = []
missing_fm: list[str] = []
titles: dict[str, list[str]] = defaultdict(list)
for key, page in pages.items():
fm = page["fm"]
title = fm.get("title") or Path(key).name
titles[title].append(key)
required = {"title", "category", "summary"}
if not required.issubset(fm.keys()):
missing_fm.append(key)
updated = fm.get("updated")
if updated:
try:
d = dt.date.fromisoformat(updated)
if d < stale_cutoff:
stale.append((key, updated))
except ValueError:
pass
duplicate_titles = {t: ks for t, ks in titles.items() if len(ks) > 1}
# Log gap check
log_path = wiki / "log.md"
log_gap = None
if log_path.exists():
log_text = log_path.read_text(encoding="utf-8", errors="replace")
dates = [dt.date.fromisoformat(m) for m in LOG_ENTRY_RE.findall(log_text)]
if dates:
last = max(dates)
gap = (today - last).days
if gap > log_gap_days:
log_gap = {"last_entry": last.isoformat(), "days_ago": gap}
else:
log_gap = {"last_entry": None, "days_ago": None}
return {
"vault": str(vault),
"total_pages": len(pages),
"orphans": orphans,
"broken_links": broken_links,
"stale": stale,
"missing_frontmatter": sorted(missing_fm),
"duplicate_titles": duplicate_titles,
"log_gap": log_gap,
}
def print_report(r: dict) -> None:
print(f"LLM Wiki health check — {r['vault']}")
print(f"Total pages: {r['total_pages']}")
print()
def header(label: str, count: int) -> None:
sym = "OK" if count == 0 else "WARN"
print(f"[{sym}] {label}: {count}")
header("orphan pages", len(r["orphans"]))
for p in r["orphans"][:20]:
print(f" - {p}")
if len(r["orphans"]) > 20:
print(f" ... and {len(r['orphans']) - 20} more")
print()
header("broken wikilinks", len(r["broken_links"]))
for src, tgt in r["broken_links"][:20]:
print(f" - {src} -> [[{tgt}]]")
print()
header("stale pages", len(r["stale"]))
for p, d in r["stale"][:20]:
print(f" - {p} (updated {d})")
print()
header("pages missing frontmatter", len(r["missing_frontmatter"]))
for p in r["missing_frontmatter"][:20]:
print(f" - {p}")
print()
header("duplicate titles", len(r["duplicate_titles"]))
for title, keys in list(r["duplicate_titles"].items())[:10]:
print(f" - '{title}': {keys}")
print()
gap = r["log_gap"]
if gap:
print(f"[WARN] log gap: last entry {gap['last_entry']} ({gap['days_ago']} days ago)")
else:
print("[OK] log gap: recent")
def main() -> None:
p = argparse.ArgumentParser(description="Lint an LLM Wiki vault")
p.add_argument("--vault", required=True)
p.add_argument("--stale-days", type=int, default=90)
p.add_argument("--log-gap-days", type=int, default=14)
p.add_argument("--json", action="store_true")
args = p.parse_args()
report = scan(
Path(args.vault).expanduser().resolve(),
stale_days=args.stale_days,
log_gap_days=args.log_gap_days,
)
if args.json:
print(json.dumps(report, indent=2, default=list))
else:
print_report(report)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
update_index.py — Regenerate wiki/index.md from the frontmatter of every wiki page.
The index is content-oriented: a catalog organized by category (entities, concepts,
sources, comparisons, synthesis), with one-line summaries read from each page's
YAML frontmatter.
Frontmatter convention (per page):
---
title: Monosemanticity
category: concept # entity | concept | source | comparison | synthesis
summary: Single-feature interpretability hypothesis from Anthropic's sparse autoencoder work
tags: [interpretability, sparse-autoencoders]
sources: 2 # optional — count of sources referencing this page
updated: 2026-04-10
---
Usage:
python update_index.py --vault ~/vaults/research
python update_index.py --vault . --dry-run
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
CATEGORY_ORDER = ["synthesis", "concept", "entity", "source", "comparison", "other"]
CATEGORY_DIRS = {
"entities": "entity",
"concepts": "concept",
"sources": "source",
"comparisons": "comparison",
"synthesis": "synthesis",
}
def parse_frontmatter(text: str) -> dict[str, str]:
m = FRONTMATTER_RE.match(text)
if not m:
return {}
raw = m.group(1)
fm: dict[str, str] = {}
for line in raw.splitlines():
if ":" in line and not line.lstrip().startswith("#"):
key, _, value = line.partition(":")
fm[key.strip()] = value.strip().strip("'\"")
return fm
def infer_title(path: Path, fm: dict[str, str]) -> str:
if "title" in fm:
return fm["title"]
return path.stem.replace("-", " ").replace("_", " ").title()
def scan_wiki(vault: Path) -> dict[str, list[dict]]:
wiki = vault / "wiki"
if not wiki.exists():
print(f"[error] {wiki} not found", file=sys.stderr)
sys.exit(1)
pages: dict[str, list[dict]] = defaultdict(list)
for md in sorted(wiki.rglob("*.md")):
# Skip index, log, and template files
rel = md.relative_to(wiki)
if rel.name in {"index.md", "log.md"}:
continue
if any(part.startswith(".") for part in rel.parts):
continue
text = md.read_text(encoding="utf-8", errors="replace")
fm = parse_frontmatter(text)
# Category: prefer frontmatter, fall back to folder name
category = fm.get("category")
if not category and len(rel.parts) > 1:
folder = rel.parts[0]
category = CATEGORY_DIRS.get(folder, "other")
category = category or "other"
pages[category].append(
{
"path": str(rel).replace("\\", "/"),
"title": infer_title(md, fm),
"summary": fm.get("summary", ""),
"tags": fm.get("tags", ""),
"sources": fm.get("sources", ""),
"updated": fm.get("updated", ""),
}
)
# Sort each category by title
for cat in pages:
pages[cat].sort(key=lambda p: p["title"].lower())
return pages
def render_index(pages: dict[str, list[dict]], vault_name: str) -> str:
today = dt.date.today().isoformat()
total = sum(len(v) for v in pages.values())
lines = [
f"# Index — {vault_name}",
"",
f"_Auto-generated {today} • {total} pages_",
"",
"> Content-oriented catalog of every page in `wiki/`. Updated by",
"> `scripts/update_index.py` or during `/wiki-ingest`. Answer queries",
"> by reading this file first, then drilling into relevant pages.",
"",
]
for cat in CATEGORY_ORDER:
entries = pages.get(cat, [])
if not entries:
continue
lines.append(f"## {cat.capitalize()} ({len(entries)})")
lines.append("")
for e in entries:
summary = f" — {e['summary']}" if e["summary"] else ""
link = f"[[{e['path'][:-3]}|{e['title']}]]" # Obsidian wikilink, strip .md
meta = []
if e["sources"]:
meta.append(f"{e['sources']} sources")
if e["updated"]:
meta.append(f"upd {e['updated']}")
meta_str = f" _({' · '.join(meta)})_" if meta else ""
lines.append(f"- {link}{summary}{meta_str}")
lines.append("")
return "\n".join(lines)
def main():
p = argparse.ArgumentParser(
description="Regenerate wiki/index.md from every wiki page's YAML frontmatter.",
epilog="The index is organized by category (synthesis, concept, entity, source, comparison).",
)
p.add_argument("--vault", required=True, help="Vault root directory")
p.add_argument(
"--dry-run", action="store_true", help="Print to stdout instead of writing"
)
p.add_argument(
"--json",
action="store_true",
help="Emit a JSON summary of the regeneration result",
)
args = p.parse_args()
try:
vault = Path(args.vault).expanduser().resolve()
pages = scan_wiki(vault)
content = render_index(pages, vault.name)
except SystemExit:
raise
except Exception as e:
if args.json:
print(json.dumps({"status": "error", "message": str(e)}))
else:
print(f"[error] {e}", file=sys.stderr)
sys.exit(1)
total = sum(len(v) for v in pages.values())
summary = {
"status": "ok",
"vault": str(vault),
"total_pages": total,
"by_category": {k: len(v) for k, v in pages.items()},
"dry_run": args.dry_run,
}
if args.dry_run:
if args.json:
summary["content_preview"] = content[:500]
print(json.dumps(summary, indent=2))
else:
print(content)
return
index_path = vault / "wiki" / "index.md"
try:
index_path.write_text(content, encoding="utf-8")
except OSError as e:
if args.json:
print(json.dumps({"status": "error", "message": f"failed to write {index_path}: {e}"}))
else:
print(f"[error] failed to write {index_path}: {e}", file=sys.stderr)
sys.exit(1)
summary["index_path"] = str(index_path)
if args.json:
print(json.dumps(summary, indent=2))
else:
print(f"[ok] wrote {index_path} ({total} pages)")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
wiki_search.py — BM25 search over a wiki vault.
Standard library only. Works as a fallback when `index.md` alone isn't enough
(e.g. you want to find which pages mention a specific term the LLM hasn't yet
cross-referenced). For larger vaults, pair this with an external tool like
qmd (https://github.com/tobi/qmd) for hybrid/vector search.
Usage:
python wiki_search.py --vault ~/vaults/research --query "sparse autoencoder"
python wiki_search.py --vault . --query "monosemanticity" --limit 5 --json
"""
from __future__ import annotations
import argparse
import json
import math
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
TOKEN_RE = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_\-']+")
STOPWORDS = {
"the", "a", "an", "and", "or", "but", "if", "then", "so", "to", "of", "in",
"on", "at", "for", "by", "with", "from", "is", "are", "was", "were", "be",
"been", "being", "this", "that", "these", "those", "it", "its", "as", "we",
"you", "they", "their", "our", "us", "i", "not", "no", "yes", "do", "does",
"did", "will", "would", "can", "could", "should", "about", "into", "than",
"out", "up", "down", "over", "under", "also",
}
def tokenize(text: str) -> list[str]:
return [
t.lower()
for t in TOKEN_RE.findall(text)
if t.lower() not in STOPWORDS and len(t) > 1
]
def load_docs(vault: Path) -> list[dict]:
wiki = vault / "wiki"
if not wiki.exists():
raise SystemExit(f"[error] {wiki} not found")
docs = []
for md in sorted(wiki.rglob("*.md")):
rel = md.relative_to(wiki)
if rel.name in {"index.md", "log.md"}:
continue
if any(part.startswith(".") for part in rel.parts):
continue
text = md.read_text(encoding="utf-8", errors="replace")
tokens = tokenize(text)
docs.append(
{
"path": str(rel).replace("\\", "/"),
"text": text,
"tokens": tokens,
"tf": Counter(tokens),
"len": len(tokens),
}
)
return docs
def bm25_scores(
docs: list[dict], query: list[str], k1: float = 1.5, b: float = 0.75
) -> list[tuple[int, float]]:
N = len(docs)
if N == 0:
return []
avgdl = sum(d["len"] for d in docs) / N or 1
df: dict[str, int] = defaultdict(int)
for d in docs:
for term in set(d["tokens"]):
df[term] += 1
idf = {
term: math.log(1 + (N - df_t + 0.5) / (df_t + 0.5))
for term, df_t in df.items()
}
scores: list[tuple[int, float]] = []
for i, d in enumerate(docs):
score = 0.0
for term in query:
if term not in d["tf"]:
continue
tf = d["tf"][term]
denom = tf + k1 * (1 - b + b * d["len"] / avgdl)
score += idf.get(term, 0.0) * (tf * (k1 + 1)) / (denom or 1)
if score > 0:
scores.append((i, score))
scores.sort(key=lambda x: x[1], reverse=True)
return scores
def snippet(text: str, query: list[str], width: int = 220) -> str:
lower = text.lower()
for term in query:
idx = lower.find(term)
if idx >= 0:
start = max(0, idx - width // 3)
end = min(len(text), start + width)
s = text[start:end].replace("\n", " ")
return ("…" if start > 0 else "") + s + ("…" if end < len(text) else "")
return text[:width].replace("\n", " ") + ("…" if len(text) > width else "")
def main() -> None:
p = argparse.ArgumentParser(description="BM25 search over an LLM Wiki vault")
p.add_argument("--vault", required=True)
p.add_argument("--query", required=True)
p.add_argument("--limit", type=int, default=10)
p.add_argument("--json", action="store_true")
args = p.parse_args()
docs = load_docs(Path(args.vault).expanduser().resolve())
qtokens = tokenize(args.query)
if not qtokens:
print("[error] empty query after tokenization", file=sys.stderr)
sys.exit(1)
scored = bm25_scores(docs, qtokens)[: args.limit]
hits = []
for i, s in scored:
d = docs[i]
hits.append(
{"path": d["path"], "score": round(s, 3), "snippet": snippet(d["text"], qtokens)}
)
if args.json:
print(json.dumps({"query": args.query, "hits": hits}, indent=2, ensure_ascii=False))
else:
if not hits:
print(f"No matches for: {args.query}")
return
print(f"Query: {args.query} ({len(hits)} hits)")
for h in hits:
print(f"\n [{h['score']}] {h['path']}")
print(f" {h['snippet']}")
if __name__ == "__main__":
main()
Related skills
How it compares
Pick llm-wiki over static README skills when agents must incrementally read sources and write cross-linked wiki pages across many sessions.
FAQ
What folders does llm-wiki use?
llm-wiki uses raw/ for immutable source articles and papers, wiki/ for agent-created knowledge pages, and AGENTS.md or CLAUDE.md for the vault schema. Agents read raw/ and write only to wiki/.
Which agents support llm-wiki?
llm-wiki works with any AGENTS.md-aware CLI including Codex, Cursor, Antigravity, OpenCode, and Gemini CLI. Claude Code uses CLAUDE.md, which follows the same schema pattern.
Is Llm Wiki safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.