* docs(contract): define the portable source key contract A vault can be checked out on more than one machine and synced over git, so a stored source key must be machine-independent. Replace the "keys MUST be absolute paths with ~ expanded" rule with three canonical forms: - vault-relative for sources inside the vault (`Raw/x.pdf`), - home-relative for sources under `$HOME` (`~/.claude/...`), - a pseudo-key (any `scheme:` / `://` identifier) for anything that is not a file. Never store a bare absolute path; normalize before comparing; identity must survive a path change. The pseudo-key namespaces are recommended names, not a closed list — what matters is the shape, so a custom scheme still works. Project identity follows the same rule: `source_repo` (`host/owner/name`), with an optional `~`-relative `source_cwd_hint` for a machine-local checkout. Legacy manifests full of absolute keys stay readable, and `scripts/manifest.py migrate` converts them (merging collisions, keeping the newest `ingested_at`); after a cross-machine move the old vault root is supplied with `--from-root`. This is the single canonical definition — the other skills reference it. * feat(cache): resolve portable keys and classify cross-machine sources Read and write manifest source keys in the portable forms instead of treating the raw string as the identity. Write side: `stored_key` maps a source to vault-relative (`Raw/x.pdf`) or home-relative (`~/.claude/...`), and `update_source` records that form. A source with no portable representation keeps its path only as a fallback and now warns on stderr; an explicit `key=` from the caller is authoritative and re-keys an entry the manifest previously tracked by path. Read side: `resolve_key` normalizes every stored form to an absolute path (or None for a pseudo-key), and `_same_source` / `_missing_on_disk` compare through it, so an absolute query matches a vault-relative or `~`-relative key and legacy absolute keys keep working unchanged. Classification: `check_sources` gains an `unavailable` bucket. A key that is machine-specific and points outside this vault may simply not exist here, so it is reported as `unavailable` rather than `missing` — `missing` narrows to vault-local sources genuinely gone, which is what `wiki-ingest` reads as "optionally clean up". A relative key counts as vault-local only when its first segment names a real top-level vault entry; a bare vault-root filename is vault-local by construction, while a legacy ingest-root key, an out-of-vault namespace, and a key written for another OS (drive-letter or backslash form) are not. When the vault cannot be listed the lexical answer stands, so an I/O hiccup never hides a real loss. CLI: `cache-update --key` records an explicit pseudo-key (`repo:` / `url:` / `agent:`) for a source outside the vault and `$HOME`, and the JSON receipt reports the normalized key. * feat(manifest): mirror the key helper, resolve delta keys, add migrate `scripts/manifest.py` is a standalone stdlib helper that cannot import the package, so it carries its own copies of `resolve_key` / `stored_key` (kept honest by the parity test in `tests/test_manifest_portable_keys.py`). `delta` now resolves every stored key to an absolute path before matching, so a vault-relative or home-relative key is directly hit by a scanned absolute path instead of showing up as new. `migrate` (with `normalize` kept as an alias for existing instructions) rewrites legacy absolute keys to the portable form: in-vault keys become vault-relative, `$HOME` keys become `~`-relative, and pseudo-keys and legacy ingest-root-relative keys are preserved untouched. A path with no portable form is kept as-is with a warning rather than dropped, so provenance is never lost, and collisions merge by keeping the newest `ingested_at` while unioning `pages_created` / `pages_updated` / `pages_produced`. After a vault moves between machines its old keys are rooted at the *old* vault path, which matches neither the new root nor `$HOME`; `--from-root <old-vault-root>` (repeatable) strips it explicitly. Without it the summary says `nothing portable to write — N key(s) kept non-portable` rather than claiming success. The old root is never guessed — a wrong guess would write plausible-looking but incorrect relative keys — so the hint names the flag to pass instead. * feat(lint): report machine paths in page sources frontmatter A page `sources:` entry holding a machine absolute path cannot resolve on another machine, so a synced vault silently loses the trail. Add a read-only finding, `machine_path_sources`, that names the page and the offending entries. It is reported at warn level and rewrites nothing — cleaning existing pages is a one-time, owner-reviewed pass, not something a lint run should do. All three frontmatter shapes are handled (flow list, scalar value, block list), and both POSIX and Windows-drive absolute paths are recognized. `~`-relative and vault-relative entries are fine and are not reported. * docs: converge skills on the key contract and document the new surface Point the dependent skills at the contract instead of restating (and diverging from) it: - `wiki-status`: describe portable keys, document the `Unavailable` classification, and identify a project by `source_repo` + `source_cwd_hint` in the manifest example. - `wiki-ingest`: document the `unavailable` bucket and state that the manifest key and page `sources:` must be portable; note `cache-update --key` for a source with no portable path form. - `wiki-update`: record project identity as `source_repo` and an optional `~`-relative `source_cwd_hint`. - `wiki-query`: resolve a project's source from `source_repo`, falling back through `source_cwd_hint` -> legacy `source_cwd` -> page `source_path`, and report the repository rather than a machine path. - `claude-history-ingest`: replace the "keys are absolute paths" instruction. - `wiki-rebuild`: use an `$OBSIDIAN_VAULT_PATH` placeholder instead of a user path. User docs get the same treatment: `docs/cli.md` documents `cache-update --key`, `manifest.py migrate` / `--from-root`, the `unavailable` bucket and the lint finding, and `docs/architecture.md` states the key rule. Structural doc tests pin the contract section, the documented CLI surface, the portable manifest example, and that all four skills agree on the project identity fields. * fix(manifest): read the ingest timestamp under either field name cache.py writes `last_ingested`; `_newest` and `cmd_delta` read only `ingested_at`. On a real manifest both comparisons saw "", so a migrate collision kept the first-seen entry rather than the newest, and every already-ingested file came back MOD. --------- Co-authored-by: ar9av <ar9avg@gmail.com>
16 KiB
Architecture
The wiki is the artifact. The agent is the maintainer. Obsidian is the viewer.
No scripts run your knowledge pipeline — the skills are markdown files that tell an AI agent how to operate on your vault. The agent uses the same read/write/search tools it already has.
The four stages
Every time you feed the brain, it runs through these:
1. Ingest
The agent reads your source material directly — markdown, PDFs (with page ranges), JSONL conversation exports, plain text logs, chat exports, meeting transcripts, and images (screenshots, whiteboard photos, diagrams; vision-capable model required). No preprocessing step, no pipeline to run. The agent reads the file the same way it reads code.
2. Pull information
From the raw source, the agent pulls out concepts, entities, claims, relationships, and open questions. A conversation about debugging a React hook yields a "stale closure" pattern. A research paper yields the key idea and its caveats. A work log yields decisions and their rationale. Noise gets dropped, signal gets kept.
Each page also gets a 1–2 sentence summary: in its frontmatter at write time — later queries use this to preview pages without opening them.
3. Merge
New knowledge merges against what's already there. If a concept page exists, the agent updates it: merging new information, noting contradictions, strengthening cross-references. If it's genuinely new, a page gets created. Nothing is duplicated. Sources are tracked in frontmatter so every claim stays attributable.
4. Schema
The schema isn't fixed upfront. It emerges from your sources and evolves as you add more. The agent maintains coherence: categories stay consistent, wikilinks point to real pages, the index reflects what's actually there. When you add a new domain, the schema expands without breaking what exists.
A .manifest.json tracks every source that's been ingested — path, timestamps, which pages it produced. On the next run, the agent computes the delta and only processes what's new or changed.
The loop
- Agent resolves the vault path (
@name→.env→ global config) - Agent reads
.manifest.jsonto know what's already been done - Agent reads the relevant skill for instructions
- Agent uses its built-in tools to do the work
- Agent updates
.manifest.json,index.md,log.md, andhot.md - Output is standard Obsidian-compatible markdown with frontmatter and
[[wikilinks]]
Code-aware project ingest
/wiki-update adds a code-understanding step before distillation when the current project contains source code. Git answers what changed; code-understand answers what that change touches; the agent decides what is worth remembering; the vault stores only the distilled result.
With the default CODE_UNDERSTANDING_BACKEND=auto, CodeGraph is used when available and the built-in extractor + rg is used otherwise. CodeGraph is an optional local accelerator, not part of the vault: its .codegraph/ index stays beside the project and is never copied into wiki pages.
Use obsidian-wiki doctor --project . from the project root to verify CodeGraph availability and index freshness. With auto, a healthy CodeGraph backend is preferred; otherwise the built-in fallback is used.
First project ingest
On the first /wiki-update, there is no last_commit_synced entry for the project, so the agent needs an initial architecture map:
- Resolve the vault and read
.manifest.json; treat the project as new when no previous sync exists. - Run
obsidian-wiki code-understand --project "$(pwd)" --prettyacross the tracked project. - If CodeGraph is available, the first enhanced run initializes the local
.codegraph/index. Otherwise the built-in extractor +rgproduces the focus map. - Use the ranked symbols and
file:lineevidence to read the load-bearing code selectively instead of opening the whole repository. - Distill architecture, decisions, dependencies, and reusable knowledge into project/global wiki pages.
- Update
.manifest.json,index.md,log.md, andhot.md, recording the currentHEADaslast_commit_synced.
project
↓
full code-understand pass
↓
CodeGraph index (when available)
↓
ranked focus map
↓
selective source reads
↓
LLM distillation
↓
Obsidian wiki + last_commit_synced
Ongoing project maintenance
Later /wiki-update runs are delta-based. The previous last_commit_synced narrows the work to what changed and the impact area around it:
- Read
last_commit_syncedfrom.manifest.jsonand verify that commit is still an ancestor ofHEAD. If history was rewritten, fall back to the first-ingest/full-scan path. - Compute the Git delta since the previous sync. If nothing meaningful changed, stop without rewriting the wiki.
- Run
obsidian-wiki code-understand --project "$(pwd)" --since <last_commit_synced> --pretty. - When CodeGraph is active, reuse the existing local index, sync the changed code, and expand changed files into relevant callers, callees, tests, and impact areas.
- Read only the ranked affected code, re-check previously recorded relationships, and prune relationships that are no longer valid.
- Merge only meaningful new knowledge into the wiki, then advance
last_commit_syncedto the currentHEAD.
last_commit_synced
↓
Git delta
↓
code-understand --since
↓
changed symbols + impact area
↓
selective re-read / stale-link pruning
↓
LLM distillation
↓
updated wiki + new last_commit_synced
This keeps the responsibilities separate: Git tracks change history, CodeGraph (or the built-in fallback) maps code structure and impact, the LLM performs judgement and distillation, and the Obsidian vault remains the persistent knowledge artifact.
Vault structure
$OBSIDIAN_VAULT_PATH/
├── index.md # Master index — every page, always current
├── log.md # Chronological activity log
├── hot.md # ~500-word semantic snapshot of recent activity
├── .manifest.json # Ingest ledger: path, timestamps, pages produced
├── .manifest.lock # Transient advisory lock held during manifest writes
├── _meta/
│ ├── taxonomy.md # Controlled tag vocabulary
│ └── *.base # Obsidian Bases dashboard definitions
├── _insights.md # Graph analysis: hubs, bridges, dead ends
├── _raw/ # Staging — drop rough notes, next ingest promotes them
├── _staging/ # Review queue when WIKI_STAGED_WRITES=true
├── _archives/ # Timestamped snapshots for rebuild/restore
├── _readouts/ # Narrative readouts from wiki-narrate
├── concepts/ # Abstract ideas, patterns, mental models
├── entities/ # Concrete things — people, tools, libraries, companies
├── skills/ # How-to knowledge, techniques, procedures
├── references/ # Factual lookups — specs, APIs, configs
├── synthesis/ # Cross-cutting analysis connecting multiple concepts
├── journal/ # Time-bound entries — daily logs, session notes
└── projects/
└── <project-name>.md # One page per project, synced via wiki-update
Knowledge that's project-specific goes under projects/. Knowledge that's general goes in the global category directories. Both are cross-referenced with [[wikilinks]].
Every page carries required frontmatter: title, category, tags, sources, created, updated.
hot.md deserves a mention — it's a running semantic snapshot every write skill updates, so the next session picks up where the last one left off without crawling the whole vault.
.manifest.json is the one file several writers touch at once — parallel ingest subagents from batch-plan, and the Dockerized server writing a vault a local skill is also using. Updates to it go through obsidian-wiki cache-update, which takes an advisory lock (.manifest.lock) and replaces the file atomically. Hand-editing the manifest in a parallel run bypasses that and drops whichever write lands second. The prose files take no lock: log.md is append-only, and index.md/hot.md are rewritten wholesale by whichever skill last ran.
Manifest sources keys (and page sources: frontmatter) are portable: vault-relative for in-vault sources, ~-relative under $HOME, or a repo:/url:/agent: pseudo-key — never a machine absolute path, because the vault is synced across machines. Legacy absolute keys keep working, and scripts/manifest.py migrate <vault> rewrites them. See CLI Reference → Source keys and legacy migration.
Core principles
- Compile, don't retrieve. The wiki is pre-compiled knowledge. Update existing pages — don't append or duplicate.
- Track everything.
.manifest.jsonafter ingesting;index.md,log.md, andhot.mdafter any write. - Connect with
[[wikilinks]]. This is what makes it a knowledge graph rather than a folder of files. - Frontmatter is required. Every page, every time.
- Single source of truth. Visibility tags shape how content surfaces — they never duplicate or separate it.
What we added on top of Karpathy's pattern
The original gist is the seed: compile knowledge once into interconnected markdown and keep it current, instead of asking an LLM the same questions repeatedly or running RAG every time. Here's what got built around it.
-
Delta tracking. A manifest tracks every source file ingested. Come back later and it computes the delta, processing only what's new or changed. You're not re-ingesting your document library every time.
-
Project-based organization. Knowledge is filed under projects when project-specific, globally when not. Both cross-referenced. Ten codebases, ten spaces in the vault.
-
Archive and rebuild. When the wiki drifts too far from its sources, archive the whole thing (timestamped snapshot, nothing lost) and rebuild. Or restore any previous archive.
-
Multi-agent ingest. Documents, PDFs, Claude Code history, Codex sessions, Hermes memories, OpenClaw
MEMORY.md, Pi sessions, Copilot CLI history, Windsurf data, ChatGPT exports, Slack logs, meeting transcripts, raw text. Dedicated skills for each agent, plus a catch-all for arbitrary exports. -
Cross-agent targeted search.
/wiki-codex "rust ownership"from inside Claude Code finds your Codex sessions on that topic, extracts the relevant blobs, distills them into pages, and returns a synthesized answer. Topic-first, not session-first. Each agent has its own extraction strategy. Pair with/memory-bridge diffto see what each tool uniquely contributed. -
Audit and lint. Orphaned pages, broken wikilinks, stale content, contradictions, missing frontmatter — plus a dashboard of what's ingested vs. pending.
-
Identity resolution.
wiki-dedupfinds pages covering the same concept under different names ("RSC" vs. "React Server Components") and merges them. -
Automated cross-linking. After ingest, the cross-linker scans for unlinked mentions and weaves them into the graph.
-
Tag taxonomy. A controlled vocabulary in
_meta/taxonomy.md, with a skill that audits and normalizes tags vault-wide. -
Provenance tracking. Every claim is tagged: extracted (default),
^[inferred](LLM synthesis), or^[ambiguous](sources disagree). Aprovenance:block in frontmatter summarizes the mix per page, andwiki-lintflags pages drifting into mostly speculation. You can always tell what your wiki knows from what it guessed. -
Trust ledger.
obsidian-wiki trust-record/trust-checkrecord and validate human-approved confidence reviews against material fingerprints, so CI can gate on "a person actually checked this." -
Multimodal sources. Screenshots, whiteboard photos, slide captures, and diagrams ingest like text — visible text transcribed verbatim, interpreted content tagged as inferred.
-
Wiki insights.
wiki-statuscan analyze the shape of the vault itself: top hubs, bridge pages (nodes whose removal would partition the graph), tag cluster cohesion, scored surprising connections, a graph delta since last run, and questions the structure is uniquely positioned to answer. Output goes to_insights.md. -
Vault equilibrium. The maintenance skills (
wiki-lint,wiki-dedup,cross-linker,tag-taxonomy) each optimize one shared vault for a different objective, so any of them can undo another's work.wiki-statusequilibrium mode runs every audit in report-only form and reports whether any skill still has a pending change — the vault is converged only when all of them propose nothing. It also detects oscillation, where two skills keep reversing each other and the vault can never settle. -
Graph export and import.
wiki-exportturns the wikilink graph intograph.json,graph.graphml(Gephi/yEd),cypher.txt(Neo4j),postgres.sql(Postgres), a self-contained interactivegraph.html, or an OKF bundle.wiki-importreads any of it back. -
Tiered retrieval.
wiki-queryreads titles, tags, and summaries first, opening page bodies only when the cheap pass can't answer. Say "quick answer" to force index-only mode. Query cost stays roughly flat from 20 pages to 2000. -
Session brain. A topic graph over your raw agent session history, so you can find the session where something happened. See Session Brain.
-
Staged writes. Set
WIKI_STAGED_WRITES=trueand LLM-written pages queue in_staging/for review before landing in the live vault.
Open Knowledge Format
The vault format is structurally conformant with OKF v0.1 — markdown with YAML frontmatter, category subfolders, reserved index.md/log.md.
wiki-export (OKF mode) and wiki-import are the bridge: they translate between native frontmatter (title/category/tags/sources/created/updated + summary) and OKF (type/title/description/resource/tags/timestamp), making vaults exchangeable with any OKF tool.
The OKF round-trip is lossless. The graph.json round-trip is not — it carries structure, not page bodies.
Repo layout
obsidian-wiki/
├── .skills/ # ← Canonical skill definitions (source of truth)
│ └── <skill-name>/SKILL.md # 39 skills — see docs/skills.md
│
├── obsidian_wiki/ # Python package — CLI, setup, sync, session brain
├── extensions/brain/ # Zero-build Chrome extension: capture + form fill
├── tools/check_readme_sync.py # Translation drift reporter
│
├── CLAUDE.md # Bootstrap → Claude Code / Kilocode (→ AGENTS.md)
├── GEMINI.md # Bootstrap → Gemini CLI (→ AGENTS.md)
├── AGENTS.md # Bootstrap → Codex, OpenCode, Aider, Droid, Trae, Hermes, OpenClaw
├── .hermes.md # Bootstrap → Hermes (symlink → AGENTS.md)
├── .cursor/rules/obsidian-wiki.mdc # Always-on → Cursor (alwaysApply: true)
├── .windsurf/rules/obsidian-wiki.md # Always-on → Windsurf
├── .kiro/steering/obsidian-wiki.md # Always-on → Kiro (inclusion: always)
├── .agent/rules/obsidian-wiki.md # Always-on → Google Antigravity
├── .agent/workflows/obsidian-wiki.md # Slash-command registry → Antigravity
├── .github/copilot-instructions.md # Always-on → GitHub Copilot (VS Code Chat)
│
├── .claude/skills/ → symlinks to .skills/* (created by setup)
├── .cursor/skills/ → symlinks to .skills/*
├── .windsurf/skills/ → symlinks to .skills/*
├── .agents/skills/ → symlinks to .skills/*
├── .pi/skills/ → symlinks to .skills/*
├── .kiro/skills/ → symlinks to .skills/*
│
├── setup.sh # One-command agent setup
├── .env.example # Configuration template
└── docs/ # You are here
Global symlink targets created by setup are listed in Installation.
For the full pattern — three-layer architecture, page templates, project organization — read .skills/llm-wiki/SKILL.md.