Files
JaySon 58aba1808c Make manifest source keys machine-portable (#217)
* 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>
2026-09-11 00:32:53 -07:00

16 KiB
Raw Permalink Blame History

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

  1. Agent resolves the vault path (@name → .env → global config)
  2. Agent reads .manifest.json to know what's already been done
  3. Agent reads the relevant skill for instructions
  4. Agent uses its built-in tools to do the work
  5. Agent updates .manifest.json, index.md, log.md, and hot.md
  6. 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:

  1. Resolve the vault and read .manifest.json; treat the project as new when no previous sync exists.
  2. Run obsidian-wiki code-understand --project "$(pwd)" --pretty across the tracked project.
  3. If CodeGraph is available, the first enhanced run initializes the local .codegraph/ index. Otherwise the built-in extractor + rg produces the focus map.
  4. Use the ranked symbols and file:line evidence to read the load-bearing code selectively instead of opening the whole repository.
  5. Distill architecture, decisions, dependencies, and reusable knowledge into project/global wiki pages.
  6. Update .manifest.json, index.md, log.md, and hot.md, recording the current HEAD as last_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:

  1. Read last_commit_synced from .manifest.json and verify that commit is still an ancestor of HEAD. If history was rewritten, fall back to the first-ingest/full-scan path.
  2. Compute the Git delta since the previous sync. If nothing meaningful changed, stop without rewriting the wiki.
  3. Run obsidian-wiki code-understand --project "$(pwd)" --since <last_commit_synced> --pretty.
  4. 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.
  5. Read only the ranked affected code, re-check previously recorded relationships, and prune relationships that are no longer valid.
  6. Merge only meaningful new knowledge into the wiki, then advance last_commit_synced to the current HEAD.
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.json after ingesting; index.md, log.md, and hot.md after 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 diff to 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-dedup finds 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). A provenance: block in frontmatter summarizes the mix per page, and wiki-lint flags pages drifting into mostly speculation. You can always tell what your wiki knows from what it guessed.

  • Trust ledger. obsidian-wiki trust-record / trust-check record 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-status can 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-status equilibrium 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-export turns the wikilink graph into graph.json, graph.graphml (Gephi/yEd), cypher.txt (Neo4j), postgres.sql (Postgres), a self-contained interactive graph.html, or an OKF bundle. wiki-import reads any of it back.

  • Tiered retrieval. wiki-query reads 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=true and 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.