references/anti-patterns.md
# Anti-patterns — full table
(Core inlines the top offenders. This is the complete reference table.)
| Task | Don't | Do |
| ----------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| List a markdown-heavy dir | `Bash: ls specs/` | `exec("ls -A specs/")` |
| Find all SPEC.md files | `Glob: **/SPEC.md` | `exec("find specs -name SPEC.md")` |
| Find the most relevant page for a query | `Grep: "pattern" *.md` then read three files | `search({ query: "pattern" })` (ranked: title + body BM25 + recency) |
| Find every literal occurrence of a phrase | `Grep: "pattern" *.md` | `exec("grep -rn pattern <dir>")` (literal, grouped by file, with frontmatter) |
| Read an individual doc | `Read: specs/foo/SPEC.md` | `exec("cat specs/foo/SPEC.md")` |
| Explore a markdown-heavy dir | `Agent(Explore): "..."` | Do `exec`-based exploration yourself |
| Answer a direct business question from the corpus | answer in chat and move on (it evaporates), OR save every answer as a new doc | answer with citations; *offer* to persist only when durable + multi-doc + not already covered (`references/corpus-qa.md`) |
| Wait for the server to tell you to open preview | Skip the session-start preview open and wait for the `attach-preview-once` hint | Open the preview browser at session start; the hint is a fallback when you didn't |
| Ignore the attach hint | Skip the `warning: { action: "attach-preview-once" }` hint in write-tool responses | Open the preview when the hint fires (`preview_url` → navigate your in-app browser); otherwise do nothing |
| Make the Claude Code Desktop preview work | Read / diagnose / edit `.claude/launch.json` (host-managed config) | Open the Browser pane with `preview_start({url})` (from `preview_url`; auto-starts the UI), then `navigate({url})` to move between docs; don't hand-edit host preview config — the OK lock-collision proxy bridges any port mismatch transparently |
| Open a doc/folder/skill in the app from a CLI | Print the `previewUrl` / `openknowledge://` string for the user to click | Run `ok open <name>` (doc or folder, auto-detected; `--skill <name>` for a skill) — deep-links into OK Desktop, browser fallback |
| Reference another doc | `` `[text](./page.md)` `` (backticked) or HTML `<a>` | `[text](./page.md)` (raw markdown) |
| Embed an image | `<img src="...">` (HTML), a `localhost:<port>` / `preview_url` server URL, or hot-linked external URL | Fetch + save locally + doc-relative `` |
| Write a factual claim in a KB doc | plausible prose without citation, OR inline `[source](https://URL)` | `ingest` the source first, then cite the local path per Grounding |
| Cite a web source you just fetched | inline `[source](https://...)` because YOU did the fetch (not the user) | `ingest` it — agent-initiated fetches are not exempt from the closed-loop rule |
| Finish a turn that changed KB content | move on without checking for a log | check for a `log.md` and follow its contract per Log discipline |
| Add an image | empty alt `` or generic alt `` | meaningful alt + source caption below |
| Catalog folder contents | create `INDEX.md` hub file | `edit({ folder: { path, frontmatter } })` writes `<folder>/.ok/frontmatter.yml` |
| Write a doc in an unfamiliar folder | go straight to `write` with hand-authored markdown | `exec("ls -A <folder>")` first — read the folder description + `templates_available` before writing |
| Land in an existing repo without orienting | go straight to `write` when no folder frontmatter / templates exist | run `onboard-existing-repo.md` once for the project — extracts conventions from siblings, sets folder frontmatter + templates, activates the link graph |
| Author a doc when a matching template exists | `write({ document: { path, content: "..." } })` from scratch | `write({ document: { path, template } })` — templates carry the folder's frontmatter + body discipline |
| Change a doc's title / tags | `edit({ document: { path, find, replace } })` to swap the YAML (rejected — HTTP 400 frontmatter-intersect) | `edit({ document: { path, frontmatter } })` for metadata; `write({ document: { path, content, frontmatter, position: "replace" } })` for full rewrites |
| Repeat the same frontmatter on sibling docs | hand-set identical `tags` / `title` prefix on every new file | `write({ template })` once — new docs start from the template |
| Re-derive the same body skeleton repeatedly | copy-paste the structure from a sibling each time | `write({ template })` once, then pick from `templates_available` thereafter |
| Scaffold a new folder for a doc category | set folder frontmatter and stop there | pair `edit({ folder })` with `write({ template })` in the same turn |
| Delete a markdown doc | `Bash: rm` / `unlink` / native deletion on in-scope `.md` | `delete({ document })` — `checkpoint()` first if rollback may be needed |
| Fork a skill and expect no stomp | Edit installed SKILL.md | `npx skills remove` before CLI upgrade |
references/cadence-and-logs.md
# Cadence + log discipline
## Cadence
When you make a multi-step change (batch of new docs, folder restructure), pause between steps to let the browser preview catch up. The CRDT edit streams live; the preview follows your edit cadence. Don't batch 10 writes in a row — interleave the writes so the user watching the browser sees the narrative progress.
This does not conflict with *Persist incrementally* (§Writing): a checkpoint-write per section/source is naturally spaced by the work that produces that unit (read a source → write its findings → read the next), so those writes *are* the interleaved cadence. The anti-pattern is firing many writes back-to-back with no intervening work — not persisting completed work as you go. When in tension, durability wins: never hold finished work back from the KB to smooth cadence.
This is primarily a human-watchability concern — the user watches edits land in the preview; interleaved cadence makes the narrative legible. When the batch is done, navigate the preview to the primary deliverable (see "End a turn on the deliverable" in `references/preview.md`).
**Hub docs.** Don't *create* `INDEX.md` / `README.md` hub files solely to catalog children — `exec("ls -A <folder>")` returns the same view live, with per-file frontmatter + backlink counts. But if a hub doc *already exists* from prior work, keep it updated as children change — interleave: write child → update hub → write next child, rather than batching five child edits and a single trailing hub update.
## Log discipline — check for a project log when KB content changes
Some projects keep an append-only project log to make agent activity auditable. **After any turn that creates, edits, or restructures docs in the knowledge base, check for a project log:** look for a `log.md` at the project root (or at the seed `rootDir` if `ok seed --root <dir>` was used). If one exists, follow whatever its frontmatter `description:` and in-file comment say — they carry the project-specific contract (entry shape, cadence, categories). Different projects log differently — some treat the log as a wiki audit trail, others as an LLM-brain history, others as a spec changelog. If no `log.md` exists, no log discipline applies; don't fabricate one.
The skill carries the trigger ("KB content changed this turn — go look"). The file owns the policy.
references/components-and-visuals.md
# Components + visuals — markdown-native forms and `html preview` embeds
## Components — write the markdown-native form, not JSX
OK auto-promotes markdown-native syntax into themed canonical components at parse time. **Write the markdown-native form — don't reach for JSX when one exists.** The promoted component is themed, accessible, and part of the content graph; hand-rolled JSX is none of those, and it fights the model's markdown prior instead of using it.
| Want | Write this (markdown-native) | Promotes to |
| --- | --- | --- |
| Callout / admonition | `> [!NOTE]` + body — 15 types (NOTE, TIP, IMPORTANT, WARNING, CAUTION, …); append `+` / `-` (`> [!NOTE]+`) to make it foldable | themed Callout |
| Collapsible section | `<details><summary>Title</summary>` … `</details>` | themed Accordion |
| Diagram | a ` ```mermaid ` fenced block (flowchart, sequence, class, state, ER, gantt, pie) — label-text pitfalls + escapes: `palette({ components: ["Mermaid"] })`; parse failures come back as `warnings` entries on write/edit | Mermaid diagram |
| Math | `$x$` inline, `$$…$$` block | KaTeX Math |
| Highlight | `==text==` | highlight mark |
| Author-only comment (hidden from readers) | `%%text%%` or `<!-- text -->` (mid-sentence, a body that is only a formatted word, `%%**bold**%%`, stays prose — pair it with plain text; alone on a line it is fine) | comment mark |
| Inline a doc or asset | `![[file]]` | wiki embed |
**These delimiters are live in ordinary prose.** `==`, `$`, `$$`, `%%`, `<!--` … `-->` and `~~` format — or hide — whatever sits between them, wherever they appear, not only where you meant them as syntax. To show a literal pair, wrap it in an inline code span (`` `==x==` ``, `` `$5` ``), which always works and reads as code; or backslash-escape a delimiter to keep it as running prose. Escaping either side is enough for `==`, `$`, `%%` and `<!--`: `\==x\==`, `\$5`, `\%\%note\%\%`, `\<!-- note -->`. For `~~`, escape both tildes (`\~\~x\~\~`) — a one-sided `\~~x~~` renders right but gets rewritten to the two-sided form on save. Watch `%%` and `<!-- -->` in particular: an accidental pair doesn't restyle the text, it hides it.
`Tabs` and `Excalidraw` are the only canonicals with **no** markdown-native form — write the JSX directly (`<Tabs><Tab label="…">…</Tab></Tabs>`; `Excalidraw` below).
`<Excalidraw src="path/board.excalidraw" />` embeds a live snapshot of an Excalidraw board — the `src` is doc-relative or root-relative and MUST keep the `.excalidraw` extension, and the referenced board must already exist: MCP `write` cannot create `.excalidraw` docs (it authors `.md`/`.mdx` only), so only reference boards a human has created. The embed re-renders as the board changes and links to the board's own canvas editor.
For any canonical's full JSX prop schema, call `palette({ components: [ids] })`. If no canonical fits, any `<TagName>…</TagName>` falls through as raw MDX — but prefer a canonical when one matches.
**Discover the palette in one call.** `palette` returns every markdown-native form (copy-ready `example` + `guidance`), the themed `html preview` embed starters, and the injected theme-token list — the source of truth for component-forward, themed authoring. Canonical names/counts beyond the markdown-native set are project-specific; the inventory in the `write` / `edit` descriptions and `palette({ components })` are authoritative for those.
**Show findings, don't just tell them.** When a point is quantitative or comparative — a trend over time, a breakdown, a before/after, a ranking, a distribution — present it visually: a chart or stat-card `html preview` embed, a ` ```mermaid ` diagram, a table, or a Callout for the headline takeaway. Prose-only buries the insight. This matters most where the document's job is to make findings legible — **`research` reports and `consolidate` articles especially**, and any write-up meant to present results. A research article with three dense paragraphs of numbers should have been a chart. Reach for `palette` as you draft, not after.
## `html preview` — themed interactive embeds
A ` ```html preview ` fence (also `htm` / `xml`) renders a standalone HTML/CSS/JS page as a live sandboxed iframe — the extend-to-anything primitive for charts, stat cards, custom SVG, calculators, demos. The iframe auto-sizes to its content; pass `h=` / `w=` (e.g. ` ```html preview h=400px `) only to pin a fixed size.
**Start from a starter — don't hand-roll.** `palette` returns `embedPatterns` (chart, stat cards, custom SVG, interactive control), each already wired to the theme tokens. Copy one and fill in your data — that is the only path that cannot render unthemed. Hand-author a fence from scratch only when no starter is close.
**MUST — never hardcode colors in an `html preview` embed.** OK injects its theme tokens into every preview iframe; an embed that hardcodes hex / `rgb()` renders unthemed — a white box on a dark page, clashing with every component around it. This is the single most common embed mistake. Wire every color to a token: `var(--chart-1..5)` for chart series, `var(--foreground)` / `var(--muted-foreground)` for text, `var(--card)` / `var(--background)` for surfaces, plus `var(--border)`, `var(--primary)`, `var(--radius)`. Don't set a `body` background at all unless you specifically mean to — the iframe already carries a themed one.
````
```html preview
<div style="font-family:system-ui;padding:20px;color:var(--foreground)">
<h3 style="margin:0 0 10px">Themed embed</h3>
<div style="display:flex;gap:8px">
<div style="flex:1;height:48px;background:var(--chart-1);border-radius:var(--radius)"></div>
<div style="flex:1;height:48px;background:var(--chart-2);border-radius:var(--radius)"></div>
<div style="flex:1;height:48px;background:var(--chart-3);border-radius:var(--radius)"></div>
</div>
</div>
```
````
Done wrong, that same embed is `body{background:#fff;color:#1a1a1a}` with a `background:#2563eb` bar — a white box with a hardcoded blue, blind to the reader's theme.
**Charts.** A pure-CSS or inline-SVG chart wired to `var(--chart-*)` re-skins on a theme toggle for free — prefer it. A JS charting library (Chart.js, D3) works too, but a themed `body` does NOT theme the colors you pass the library in JS — read the token at runtime instead of hardcoding:
```js
const c1 = getComputedStyle(document.documentElement).getPropertyValue('--chart-1').trim();
// → pass c1 to Chart.js / D3 as the series color
```
**Boundary.** Reach for a canonical (via its markdown-native form) when one matches the semantic need — it is themed and integrated. Reach for ` ```html preview ` for interactive or bespoke content no canonical covers. ` ```<lang> ` fences for other languages are plain syntax-highlighted code, no preview.
**External resources load directly.** The preview iframe has open network access — an embed can load external stylesheets, `fetch` live data, pull map tiles / remote images, use web fonts, or embed third-party iframes over `https:`. A Leaflet map, a live-`fetch` chart, or a Google-Font embed renders with no extra setup. The iframe is a sandboxed null-origin frame, so an embed can reach the network but can never read the knowledge base, cookies, or auth. (`'unsafe-eval'` is not granted — Chart.js / Leaflet / Plotly don't need it; a library that compiles expression strings at runtime won't run.)
references/conflict-resolution.md
# Conflict-aware writes
Projects with GitHub sync enabled may carry docs in a merge-conflict state. The MCP server refuses every mutating call against such a doc with a structured RFC 9457 response:
```json
{
"type": "urn:ok:error:doc-in-conflict",
"title": "Document is in conflict.",
"status": 409,
"detail": "The document is in a merge-conflict state. Call conflicts({ kind: 'content' }) + resolve_conflict before retrying.",
"file": "notes/sso.md",
"resolutionOptions": ["mine", "theirs", "content", "delete"]
}
```
The gate covers `write`, `edit`, `delete`, `move`, `restore_version`, and agent undo (the doc-CRDT write spine; template/folder ops are fs-direct). You cannot route around it by writing content that byte-matches one of the merge stages — the gate refuses on lifecycle state, not on body equality.
**Detect proactively.** `exec("cat <path>.md")` always returns `lifecycle: {status, reason} | null` alongside the body. When `status === 'conflict'`, switch to the resolution flow before attempting any mutation.
**Resolution flow.** Three tools compose:
1. `conflicts({ kind: 'list' })` → enumerate every doc currently tracked in conflict.
2. `conflicts({ kind: 'content', file })` → returns `{ content: { base, ours, theirs, shape, lifecycleStatus } }` (the result nests under the `content` kind key). `ours` reflects the live Y.Text (what the human user sees in the editor) when the doc is loaded server-side and is marker-free; falls back to `git show :2:<file>` otherwise (e.g. after an editor reopen seeded markers into Y.Text).
3. `resolve_conflict({ file, strategy, content? })` → write the chosen bytes and commit. Strategies: `mine` runs `git checkout --ours` (your committed stage 2), `theirs` runs `git checkout --theirs` (their stage 3), `content` writes the bytes you supply, `delete` runs `git rm` (for delete-modify / modify-delete shapes where a stage is missing).
`file` is a `.md` / `.mdx` path relative to the project dir (extension included) — mirrors the on-disk shape, not the extension-less `document` path used by other tools.
The resolve operation is best-effort and NOT atomic: `git checkout --ours/--theirs && git add` may succeed but the subsequent `git commit --no-edit` can fail (pre-commit hook rejection, locked index). On commit failure the staged files are re-`git add`-ed back into the unmerged index and the tracked entry remains in `conflicts.json` — re-call `resolve_conflict` after the user clears the blocker.
references/corpus-qa.md
# Answering direct questions from the corpus
A direct question you can answer from existing documents — "which customers have non-standard indemnity?", "can we use Alloy's logo?", "what did we decide about X?" — does **not** need the words "research" or "report" to route here. Retrieve with `search` / `exec`, read the relevant docs, and **answer in chat with inline citations to the source docs you used**. That is the complete, correct default — most questions end here. This is NOT the **research** layer: research gathers and synthesizes *external* sources behind a scoping gate; a corpus question just reads what the knowledge base already holds. (Inside an active research pass, research's own "file valuable Q&A back" step governs how answers are persisted — not this section.)
**Offer to persist the answer only when it is durable knowledge the KB is currently missing** — when ALL of these hold:
- it **synthesizes across multiple docs** or surfaces a non-obvious fact a reader couldn't get from a single doc in one read — two docs that independently state the *same* fact are NOT synthesis; synthesis means combining information no single source holds in isolation;
- it's **reusable** — likely to be asked again, or it records a decision / reference others will need;
- **no existing doc already answers it** — scan first (`search`, `exec("grep …")`); if one does, point the user to it instead of writing a near-duplicate;
- the answer is **sourced** per §Grounding, not speculation.
When all hold, *offer* — don't write yet: "This pulls together [N docs] — want me to save it as `<slug>.md` under `<folder>` so it's findable next time?" On a yes, `write` it with frontmatter + inline citations to the source docs (§Grounding, §Linking). **Never auto-create the page.** A single-doc lookup, a navigational question, or anything you'd hesitate to call durable does NOT warrant an offer — answer in chat and stop; don't even prompt to save it. When in doubt, stay in chat: a missing page costs one re-query; a junk page pollutes the corpus permanently.
**Headless / no user to ask** (autonomous run): still produce the answer — surface it with inline citations in the tool / run output as you would in chat, so the run log is the record. Default to NOT persisting unless the four criteria are unambiguously met; never persist on a maybe.
references/doc-editing.md
# Editing frontmatter vs body
`edit({ document: { path, find, replace } })` does NOT change frontmatter (body-only; frontmatter-intersecting find/replace returns HTTP 400). For metadata, use `edit({ document: { path, frontmatter: { key: value } } })` — JSON Merge Patch (RFC 7396), `null` deletes, field-level CRDT merge, atomic per-call. For a full rewrite (body + frontmatter together), call `write({ document: { path, content, frontmatter, position: "replace" } })`.
**Stale-session symptom.** If `edit` returns "Text not found" on text you can verify exists on disk (via `exec("cat …")`), the MCP session is likely stale (e.g., after a folder rename or server restart). Treat this as the escape-hatch trigger from the STOP block: prefix your next user-visible sentence with `OpenKnowledge MCP unavailable:` and report the inconsistency. Don't loop on retries — the symptom is structural, not transient.
**Delete / move.** To delete a doc, call `delete({ document })` — never `rm` / `unlink` / native `Bash` removal on in-scope markdown. The MCP path closes open agent sessions and unloads the doc from Hocuspocus before unlinking; native `rm` desynchronizes those. Deletion is irreversible — call `checkpoint()` first if you may need to roll back (it snapshots the whole project; afterwards restore the doc via `restore_version({ document, version })`, finding the `version` in `history`), and `links({ kind: "backlinks", document })` first if you want to fix referrers that will become redlinks. To move or rename a doc instead of delete + rewrite, use `move({ from, to })` — it auto-detects document vs folder vs asset and rewrites incoming references atomically.
**MDX.** To author an MDX doc (the KB renders MDX/JSX components), set `extension: ".mdx"` on the create: `write({ document: { path: "guides/widget", content, extension: ".mdx", position: "replace" } })` lands `guides/widget.mdx`. A `.mdx` suffix typed into `path` works too (the `extension` field wins if you pass both); omit both and it lands `.md`. An existing doc keeps its on-disk extension regardless — changing it in place isn't available via the MCP today.
**Advisory warnings on writes.** `write` and `edit` responses may include `structuredContent.document.warnings` (batch: per-doc `structuredContent.documents[].warnings`) — advisory entries discriminated by `kind`, each also summarized as a `⚠` line in the response text. The write always landed; the entries tell you what to do next. Write-integrity kinds mean re-read the doc (`exec("cat <path>")`) before continuing: `content-divergence` (`{ kind, intendedBytes, actualBytes, byteDelta, hint }` — the converged Y.Text doesn't match what the payload composed to: concurrent peer residue, or — rare — a primitive regression) and `disk-edit-reconciled` (an out-of-band disk edit was folded in before your write landed on top). The renderability kind `mermaid-parse-error` (`{ kind, fenceIndex, fenceFirstLine, message, line? }`) means that mermaid fence will not render — fix the fence and re-edit. The top-level `warning` field on write tools is unrelated: it is the preview-attach hint (`action: "attach-preview-once" | "start-ui"`), not an advisory — a separate key from `document.warnings`, and the two can coexist on one response.
references/folder-model.md
# Folder model — frontmatter + templates structure
(Core carries the MUST gates: read the folder before writing, use a template when one fits, bake recurring properties into a template. This file is the structural model.)
Every `.md` / `.mdx` file needs YAML frontmatter — `title` + `description` required, `tags` recommended (except in OKF projects, where the `okf` pack's reserved-file rules override this — see core):
```yaml
---
title: Article Title
description: Brief summary
tags: [relevant, tags]
---
```
Two folder mechanisms, both opt-in and nested: **folder frontmatter** in `<folder>/.ok/frontmatter.yml` (the folder's own properties — open-shape like a doc's, with `title` / `description` / `tags` as conventional keys the UI surfaces; describes the folder, self-only, does NOT flow into child docs) and **templates** in `<folder>/.ok/templates/` (the single mechanism for what new docs in a folder start with). **Most folders have NO `.ok/`** — sparse, lazy-create, auto-clean. A folder gets one only when it carries its own frontmatter or a template.
```
content-root/
├── .ok/ ← project root .ok/ (config.yml, cache)
├── meetings/
│ ├── .ok/
│ │ ├── frontmatter.yml ← this folder's own title/description/tags
│ │ └── templates/
│ │ └── prep-notes.md ← what new meeting docs start with
│ └── 2026-05-01.md
└── research/ ← no .ok/
└── auth-providers.md
```
A doc's frontmatter is exactly its own on-disk YAML — folder frontmatter never overlays values onto it. Give new docs starting properties with a template, not with folder frontmatter.
## Read the folder before writing (MUST) — full checklist
Before creating or editing docs in a folder, **always** call `exec("ls -A <folder>")` once. The response carries the folder's own `title`/`description`/`tags` + `templates_available` (the template menu for `write({ document: { template } })`). Skipping this is how agents land docs that violate folder discipline.
0. **First-contact check.** If the folder has no frontmatter of its own AND `templates_available` is empty AND `exec("ls -A")` shows substantial content elsewhere, the project hasn't been onboarded — STOP and run `onboard-existing-repo.md`. Skip on subsequent writes once confirmed.
1. **Read the folder's description** — its `title`/`description`/`tags` tell you what the folder is for. (These describe the folder; they are NOT defaults the doc inherits.)
2. **Read `templates_available`** — each entry has `name`, `title`, `description`, `scope` (`local` / `inherited`). If one matches, **prefer it** over free-form markdown (it's the folder's contract — templates carry frontmatter + body structure hand-authored docs routinely miss).
3. **Read recent siblings** — new docs should match the shape of existing ones (filename, frontmatter, body structure).
4. **Confirm content scope** — `content.dir` (`.ok/config.yml`) defines the root. `.gitignore` / `.okignore` (nested at any depth) define exclusions.
**Once per folder per session** — the checklist doesn't repeat unless you (or the user) changed a folder rule or template since.
references/ingest-and-sources.md
# Ingest — capture an external source as raw reference material
Capture an external source into the knowledge base as raw reference material. The KB is **closed-loop**: external sources are pulled IN so downstream docs cite local paths, never bare web URLs. This applies whether a user shared the source OR you fetched it yourself to ground a claim — agent-initiated fetches are not exempt (core §Grounding). **Raw preservation only** — no summary, no analysis, no interpretation.
**Bias toward binary preservation.** When the source is a binary file (PDF, image, audio, video, Office doc, archive, dataset), preserve the raw bytes — do NOT settle for a text scrape. OK ships a complete asset-embed surface (`![[file.ext]]` wiki-embed, file-watcher pickup, sha256 dedup, blocked-extension enforcement); this procedure bridges the source to it via your shell tool.
Sources land under `external-sources/` when the project has that folder (the `knowledge-base` starter pack's raw layer); otherwise pick the project's equivalent and stay consistent. Paths below are relative to the resolved `content.dir` (`config({ key: 'content.dir' })`).
## Step 0: Is this source worth preserving?
Before fetching anything, sanity-check:
- **Is it in scope?** If the source is unrelated to what this knowledge base is accumulating, ingest is pollution. Check the existing layout: `exec("ls -A <content-dir>")`.
- **Is it already ingested?** `exec("grep -rln <source-url-or-title-slug> <content-dir>")` — if the same source is already present with current content, stop and reuse. Re-ingest is appropriate when the source changed materially — see Step 5 for sha256-mismatch semantics.
- **Is the intent actually preservation or analysis?** If the user wants findings synthesized rather than raw bytes archived, redirect to a research pass (which captures sources as needed). Don't pre-ingest speculatively.
If all three checks pass, proceed.
### Shell-capability self-detection (evaluate before any shell call)
If your host does not expose a shell-like tool (no Bash / Terminal / shell-exec / equivalent), you cannot run `curl` for either the HEAD probe (Step 1a) or the binary download (Step 1b). Detect once at the start via a trivial probe (`echo ok` via the host's shell affordance, or a known-safe `which curl` invocation). On absence of a shell tool, route the source straight to **Step 1c (text fetch / shell-less fallback)** — skipping Step 1a's HEAD-tiebreaker classification and Step 1b's binary download entirely — and surface the degradation explicitly to the user.
## Step 1a: Detect source kind (binary vs text)
Classify the source before fetching. **`.svg` is intentionally absent from the binary-extension list — SVG can contain scripts and is treated as a scripted-document extension; see Step 1b's executable hard-block.**
- **URL with a binary file extension** (`.pdf`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.mp4`, `.webm`, `.mov`, `.m4v`, `.mp3`, `.wav`, `.ogg`, `.m4a`, `.flac`, `.docx`, `.xlsx`, `.pptx`, `.doc`, `.xls`, `.ppt`, `.zip`, `.7z`, `.tar`, `.gz`, `.rar`, `.csv`, `.tsv`, `.epub`, etc.) → treat as **binary**. Skip to Step 1b.
- **URL with a clear HTML/text extension** (`.html`, `.htm`, none, `.txt`, `.md`) → treat as **text**. Skip to Step 1c. The executable hard-block does NOT apply here because Step 1c never writes the source extension to disk — it writes a `.md` wrapper containing the extracted text.
- **Local file path** → use your native file read tool. If the local file is text (HTML, plain, markdown), skip to **Step 2b** (text wrapper). If it is a binary you want preserved verbatim, you already have the bytes on disk — skip Step 1b's download and go to **Step 2a** (binary wrapper), pointing `source_path:` at the existing local path (relative to the wrapper).
- **Ambiguous URL** (no extension, query-string download URL, redirect-y link) → run `curl -IL --proto =http,=https --proto-redir =http,=https --max-redirs 5 -A 'Mozilla/5.0' <url>` to read response headers, then classify by `Content-Type`:
- `application/pdf`, `image/*` (except `image/svg+xml`), `video/*`, `audio/*`, `application/zip`, `application/vnd.openxmlformats-*`, `application/epub+zip` → **binary**
- `text/html`, `text/plain`, `application/json` (when the source is a doc/article, not data) → **text**
- `application/octet-stream` → ambiguous; treat as binary BUT note in chat that the server didn't declare a specific type, and the captured `sha256` + bytes record gives downstream tooling the signal if the bytes turn out to be HTML.
### STOP gates evaluated during Step 1a (before any download)
1. **Streaming-video / DRM-protected media.** If the URL hostname matches a known streaming pattern (examples — illustrative, not exhaustive: `youtube.com`, `youtu.be`, `vimeo.com`, `twitch.tv`, `tiktok.com`, `spotify.com`, `open.spotify.com`, `podcasts.apple.com`) **OR** the HEAD response shows `Content-Type: text/html` on a URL that looked like media **OR** the HEAD shows anti-bot characteristics (`Server: cloudflare` + `cf-mitigated` / `cf-chl-bypass` headers, an Akamai/Imperva challenge response, 403 with an HTML body) → **STOP**. Tell the user: "Streaming services and DRM-protected media require `yt-dlp` or vendor-specific tooling — out of scope. Paste the transcript, or run `yt-dlp` manually and re-share the resulting file as a local path."
2. **URL scheme — HARD BLOCK.** If the URL scheme is not `http://` or `https://` (i.e., `file://`, `gopher://`, `ftp://`, etc.) → **HARD STOP**. Tell the user: "Ingest only fetches `http(s)` URLs. Other schemes (local-file paths, intranet protocols) bypass the size + redirect safeguards and risk pulling cloud-metadata or local-service responses into the KB. If you have a local file, pass its path directly as a local-file source instead of a URL."
3. **Auth-walled / paywalled / 4xx.** If HEAD returns 401/402/403/407/429 → STOP. Ask the user to paste the content or share a local copy.
## Step 1b: Binary fetch
You've classified the source as binary and Step 1a's STOP gates passed. Step 1b adds **write-path-specific** STOP gates that only apply when we're about to land bytes on disk under the source extension:
### Step 1b STOP gates (write-path only)
1. **Executable / scripted-document extension — HARD BLOCK.** If the URL extension is one OK's upload blocklist covers — Windows executables, POSIX shells, scripted documents (including `.svg` and `.xml`), macOS installer classes, URL-files, cross-platform packages, and Windows shortcut classes — **HARD STOP**. Refuse to download. Tell the user: "Executable / scripted-document extensions are not auto-fetched — the runtime backstop is desktop-only, so plan-level enforcement is the only line of defense in CLI / web contexts. If you genuinely want to archive this file, `curl` it manually outside the agent and reference the local path." This gate fires HERE (write path) and NOT against Step 1c — Step 1c writes a `.md` wrapper with extracted text, not the source extension on disk.
2. **Size pre-check.** Run `curl -IL --max-redirs 5 -A 'Mozilla/5.0' '<SOURCE_URL>'` (if not already done in Step 1a's ambiguous-URL branch) and read `Content-Length`:
- greater than 100 MB → **HARD STOP**. Tell the user the source exceeds GitHub's 100 MB file-size cap and offer the manual-`curl` + Git-LFS escape path.
- greater than 50 MB → STOP and ask explicitly: "Source is X MB (above the 50 MB GitHub warn threshold). Proceed?"
- absent → proceed; the `curl --max-filesize` flag is the enforced backstop. Note in chat that the size could not be pre-verified.
- 50 MB or under → proceed.
### Slug derivation (strict shape)
Pick a kebab-case slug from the source's filename or title (e.g., `karpathy-2024-llm-os`). The slug becomes both the binary basename and the wrapper basename. Don't put dates in the slug — dates go in frontmatter (and in dated-sibling slugs on re-ingest per Step 5).
**The slug MUST match `^[a-z0-9][a-z0-9-]{0,99}$` — kebab-case, ASCII letters / digits / single hyphens only. NO dots, slashes, leading hyphens, or path segments. If you derive the slug from a server-controlled value (`Content-Disposition: filename=`, URL path basename), strip non-conforming characters before using it.** A server returning `Content-Disposition: attachment; filename="../../etc/passwd"` is adversarial; the slug constraint prevents path traversal from a malicious source landing bytes outside the sources folder. If the stripped result is empty, ask the user for a slug.
### Download
Use your shell tool. Create the destination first if it doesn't exist:
```bash
mkdir -p "<content-dir>/external-sources"
curl -L --fail \
--proto =http,=https --proto-redir =http,=https \
--max-redirs 5 \
--max-time 60 \
--max-filesize 104857600 \
-o "<content-dir>/external-sources/<slug>.<ext>" \
-A 'Mozilla/5.0' \
'<SOURCE_URL>'
```
**Shell-escape any special characters in the URL (single quotes, backticks, dollar signs). Do NOT paste a URL containing a literal single quote directly into the single-quoted curl argument — break it into a shell-safe form first.**
Flag rationale:
- `-L` follows redirects (CDN-fronted sources need this).
- `--proto =http,=https --proto-redir =http,=https` refuses any scheme other than http(s), and refuses redirects that would downgrade to another scheme. Prevents redirect-chain SSRF into `file://`, `gopher://`, or cloud-metadata endpoints reached via `--max-redirs`.
- `--fail` exits non-zero on HTTP 4xx/5xx instead of writing an error body to disk.
- `--max-redirs 5` caps redirect chains (exit 47 on excess).
- `--max-time 60` caps total request time (exit 28).
- `--max-filesize 104857600` enforces the 100 MB hard cap as defense-in-depth in case the server omits or lies about `Content-Length` (exit 63). The option has shipped since curl 7.10.8 (2003) — every modern install has it.
If curl exits non-zero:
- 47 (too many redirects) → STOP. Likely a tracking-link or auth-flow URL. Ask the user to paste.
- 28 (timeout) → STOP. Slow source. Ask the user to retry or share locally.
- 63 (`--max-filesize` triggered) → `rm -f` the truncated output. STOP and tell the user the server's `Content-Length` was missing or wrong.
- 22 (HTTP error returned by `--fail`) → STOP. Auth wall, anti-scraping, or vanished URL. Ask the user to paste.
- Other (e.g., 1 = unsupported protocol from `--proto`) → STOP and report the curl exit code to the user.
After a successful download, record the byte size and sha256:
```bash
ls -l "<content-dir>/external-sources/<slug>.<ext>" # bytes
sha256sum "<content-dir>/external-sources/<slug>.<ext>" # or 'shasum -a 256' on macOS
```
OK's file-watcher picks up the new file and emits `asset-create`. The sidebar refreshes; the file is now indexed.
## Step 1c: Text fetch (HTML / article sources, or shell-less host fallback)
Either the source is genuinely text (HTML article, plain text doc) OR your host is shell-less and binary preservation is unavailable.
Use your available web fetch tool (`WebFetch` or your host's equivalent). If the fetcher returns an obvious *summary* of the page instead of the raw content (some LLM-backed fetch tools do this), note it and try a raw alternative (`curl -sL <url>` if your host has a shell, or ask the user to paste). The goal is verbatim bytes.
If the fetch fails (login wall, 401/402/403/429, anti-scraping block), **stop and ask the user to paste the content directly**. Do not save a stub, an error page, or a login wall as "raw content" — that poisons the knowledge base.
## Step 2a: Save the binary wrapper (only after Step 1b succeeded)
Write a markdown wrapper at `external-sources/<slug>.md`. **Declare the full tag list explicitly — do NOT assume a folder-level cascade exists** (folder frontmatter is self-only; it does not flow into child docs — see `references/folder-model.md`).
```yaml
---
title: Original title of the source (from <title>, Content-Disposition filename, or your best read)
description: One-line summary from the source (their words, not yours)
type: source
source_url: https://example.com/path/to/file.pdf
source_path: ./<slug>.<ext> # relative to this wrapper
media_type: application/pdf # RFC 6838 type/subtype, from HEAD Content-Type
bytes: 1234567 # integer, from ls -l or stat
sha256: <hex sha256 digest> # full 64-char hex, of the embedded binary
date_fetched: YYYY-MM-DD
author: Original author if known
preservation: binary
supersedes: # OPTIONAL — dated-sibling re-ingest (Step 5)
- <prior-slug>.md
tags:
- source
- immutable
- layer-ingest
- binary
---
![[<slug>.<ext>]]
```
The body is just the wiki-embed reference. For images, video, and audio, the embed renders inline. **For PDFs and other opaque file-attachment types (docx, xlsx, zip, etc.), the `![[file.ext]]` form renders as a Notion-style File row that click-dispatches to the appropriate viewer** — the pdfjs canvas viewer is opt-in via the explicit `<Pdf src="./<slug>.pdf" />` JSX form, NOT the wiki-embed default. If the user wants the inline canvas viewer for this specific PDF, they can post-edit the wrapper body to swap the embed for that form.
Write via `write` (NOT native `Write` — the CRDT path is mandatory for in-scope markdown).
## Step 2b: Save the text wrapper (only after Step 1c — text path or shell-less fallback)
Write a markdown wrapper at `external-sources/<slug>.md` with the text content preserved verbatim in the body. Strip obvious boilerplate (nav menus, cookie banners, ads, footer links, "related articles" widgets) but **do not summarize, paraphrase, or interpret**.
Frontmatter shape:
```yaml
---
title: Original title of the source
description: One-line summary from the source (their words, not yours)
type: source
source_url: https://example.com/article
media_type: text/html
date_fetched: YYYY-MM-DD
author: Original author if known
preservation: text-extracted # OR: text-only (a shell-less fallback for a binary source)
# NOTE: no `source_path` — text wrappers ARE the content. `source_path` is meaningful only for
# binary wrappers (Step 2a), where it points at the co-located binary sibling.
tags:
- source
- immutable
- layer-ingest
- text
---
```
**If this is a shell-less fallback for a source that should have been binary** (you detected binary in Step 1a but your host couldn't `curl`), set `preservation: text-only` AND prepend a top-of-body admonition so a future agent (or you, on a different host) can detect and upgrade:
```markdown
> Binary not preserved — this is an extracted-text snapshot of a binary source.
> Re-run ingest from a shell-capable client to capture the original file.
(...verbatim extracted text follows...)
```
Downstream tooling can grep `preservation: text-only` in frontmatter to find docs that need upgrading.
## Step 3: Preserve the content faithfully
For binary wrappers: the wrapper body is the wiki-embed reference; no further body content is needed. All metadata lives in frontmatter.
For text wrappers:
- **Keep** headings, lists, quotes, code blocks, images, citations, references.
- **Strip** obvious boilerplate: nav menus, cookie banners, ads, footer links, "related articles" widgets.
- **Do NOT** summarize, critique, paraphrase, or interpret.
- **For very long sources**, consider splitting by major section with cross-references in frontmatter.
## Step 4: Verify
- File(s) exist under the sources folder (binary + wrapper, or wrapper alone for text).
- Valid frontmatter (at minimum `title`, `description`, `type: source`, `source_url`, `preservation`, and the full `tags` list — plus `source_path` for binary wrappers).
- For binary preservation: `sha256` and `bytes` recorded; `media_type` matches what the HEAD response returned.
- `exec("ls -A <content-dir>/external-sources/")` lists the new file(s) with enrichment.
## Step 5: Re-ingest semantics (sha256 mismatch on a previously-ingested source)
If `external-sources/<slug>.<ext>` already exists when you reach Step 1b:
1. Compute its sha256.
2. If the new sha matches the existing sha → **STOP** (no-op). Tell the user: "Already at `external-sources/<slug>.<ext>` — sha256 matches, no change."
3. If the new sha differs → save the new bytes as `external-sources/<slug>.YYYY-MM-DD.<ext>` (today's date in the slug) AND write a **new** wrapper `external-sources/<slug>.YYYY-MM-DD.md` whose `supersedes:` is a YAML list naming the predecessor:
```yaml
supersedes:
- <slug>.md
```
**Do not mutate the old wrapper or the old binary** — the sources layer is append-only by convention.
The latest-dated wrapper is the "current view"; older wrappers remain valid as historical snapshots.
## Step 6: Discuss takeaways with the user (no file write)
After preservation, briefly surface back to the user what the source actually contains — in **chat**, not in the raw file. The raw file stays verbatim; the human collaborator gets a quick orientation.
- 3-5 bullet points capturing the source's main claims, with no editorializing.
- Note any **tensions** with existing knowledge-base docs you surfaced in Step 0 — agents that ingest in isolation miss the "wait, this contradicts `[prior article](./path/to/prior.md)`" signal.
- Offer next steps: "Shall I research this topic now, or is preservation enough?" Don't silently chain into a research pass — the user may have just wanted the archive.
- For binary preservation: include the one-line breadcrumb (`saved external-sources/<slug>.<ext> — <bytes> bytes, sha256 <abbrev>`).
- For a shell-less text fallback of a binary source: explicitly tell the user the binary was not preserved and recommend re-running from a shell-capable client.
## Step 7 (optional): Update neighbor docs to link the new source
If the source is directly relevant to an existing article or research doc, update that doc to link the new raw source. A preserved source that no doc points at is an island. Limit this to 1-3 high-signal neighbors.
- Follow the `write` / `edit` contract from the core skill.
- For binary wrappers, prefer linking to the wrapper (the markdown doc), not the binary directly. The wrapper is the closed-loop citation target; the binary is the embedded asset within it.
- Do NOT mass-update every neighbor. Focused cross-linking is rewarded; noisy neighbor-pings degrade the signal.
## Non-goals
- **No analysis** — don't interpret, compare, or critique the source. That is the research layer's job.
- **No promotion to a canonical article** — that is the consolidate layer's job, later.
- **No silent chaining into research** — ingest completes on its own; the user explicitly opts into a research pass.
- **No synthesis inside the raw file** — takeaways live in chat or a separate summary doc, never mixed into the preserved source.
- **No OCR / transcription** — binary preservation only.
- **No streaming-video direct download** — `yt-dlp` is a separate workflow; STOP and route to it (Step 1a STOP gate 1).
- **No executable / scripted-document auto-fetch** — hard-blocked at Step 1b STOP gate 1 (write-path only). The user runs `curl` manually outside the agent if they need to archive such a file.
This procedure feeds a three-layer pipeline (ingest → research → consolidate). Ingest is this file, and ships with every `ok init`; research and consolidate are two member skills of the `knowledge-base` starter pack, available only once it is seeded. See `references/starter-packs.md`.
references/linking.md
# Linking — mechanics
(Core carries the MUST: link noun-phrases with standard markdown links, every link resolves, read `brokenLinks` on each write/edit. This file carries the full rule set.)
- **Every noun-phrase that names another document should be linked** using standard markdown link syntax. Two forms are valid: **relative** (`[text](./sibling.md)`, `[text](../folder/doc.md)`) — the recommended default, native in GitHub / Obsidian / VS Code and what published sites expect — and **root-absolute** (`[text](/folder/doc.md)`, leading slash = content root), handy for a cross-folder link.
- **Never glue `./` onto a content-root path.** `./wiki/modules/tasks`, written from a doc already inside `wiki/`, resolves to the doubled garbage `wiki/wiki/modules/tasks` — correct resolution of a malformed path, i.e. a silently broken link. Pick ONE form: a relative path from your doc (`./tasks.md`, `../modules/tasks.md`), or a `/`-rooted absolute path (`/wiki/modules/tasks.md`). The `./` prefix and a content-root path never combine.
- **External web sources are NOT inline body links.** Per the Grounding rule, web URLs live in the `source_url:` frontmatter of an ingested doc under `external-sources/` (or the project's equivalent raw-sources folder); the body cites the local path: `[source name](./external-sources/source-slug.md)`. A raw `[source](https://...)` inline in the body is a TODO, not a citation — see Grounding for the closed-loop contract.
- **Internal cross-refs between OK docs** → `[text](./other-doc.md)` — link liberally to aid navigation.
- **Every link must resolve to a doc that exists by the time you're done.** Within a single multi-doc authoring pass, linking a page you'll create later in the same pass is fine — `brokenLinks` reports it as `no-such-doc` until the target lands, an expected transient forward-reference rather than a wrong-path error. What's not acceptable is *leaving* a dead link behind: create the target in the same pass, or record it as a tracked task (`TaskCreate`, or your host's task tool — if the host has none, tell the user) and leave the mention as plain prose.
- **Never wrap a link in backticks.** `` `[text](./foo.md)` `` is a bug — the backticks make it render as literal code rather than a link.
- **Never use HTML anchors** (`<a href="...">`). Markdown link syntax only.
- **`brokenLinks` on the write/edit response is your primary check — read it before walking away.** Every `write`/`edit` returns `brokenLinks` in the SAME payload: `[]` means every outbound link resolves; a populated list names each broken `href` with its `resolvedTo` + `reason` (`no-such-doc` = resolved to a doc that doesn't exist; `no-such-file` = resolved to a linked asset / source file that isn't on disk at that path; `unresolvable` = the path escapes the content root, usually one `../` too many). This validates **every** local link, not just doc links: a wrong-depth `[src](../../foo.py)` to a source file is caught too. It is report-only — the write landed regardless; fix or remove every one in a follow-up `edit`.
- **`audit` is the authoritative end-state link check** — run it once at the end of a multi-doc pass. Caveat: `audit` reports links only while the project's `validation.links` setting is not `off` (at `off` it returns clean with no warning), so if the project has silenced link validation, fall back to `links({ kind: "dead" })` — the unconditional read of the dead-link graph, which also lists sources `audit` excludes, such as skill-bundle docs (optionally scoped: `links({ kind: "dead", sourceDocuments: ["your/doc"] })`). Otherwise `links` is the graph-navigation reader, supplemental to `audit`, not a substitute. Companion `links` kinds: `backlinks` (incoming), `forward` (outgoing), `orphans` (no incoming), `hubs` (high-incoming), `suggest` (untextualized mentions worth linking).
- **The tool and the editor agree.** Both resolve a bare name the same way — exact, then slug, then folder index, then basename — so a link that opens when you click it is never reported dead, and a link reported dead is genuinely unresolvable. A target that names an asset, or names nothing at all, stays silent rather than being reported as a broken doc link.
**Note on wiki-link syntax (`[[Page]]`):** the parser still handles it for legacy content, but it's NO LONGER the recommended default. Write new content with standard markdown links per above. Seed-pack templates (`ok seed --pack <name>`) may still emit `[[Page]]` placeholders inside template body text — those are legacy. When you instantiate a seed-pack template, replace the legacy placeholders with standard markdown links during the `{shape}`-fill pass.
references/media-and-assets.md
# Media — images and attachments
- **Markdown syntax only:** ``. Do NOT emit HTML `<img>` tags — they don't participate in OK's content graph and don't render consistently across Fumadocs / preview surfaces. Paths resolve relative to the doc.
- **Always a doc-relative path — never a server URL.** Reference an asset by its path relative to the doc (`./image.png`, `../assets/foo.png`), never an absolute `http://localhost:<port>/…`, `127.0.0.1`, or other server URL. `preview_url`'s `url` navigates the *preview* — it is NOT an asset path; never paste it (or any `localhost` base) into an `![]()`. An asset already in the tree is the same rule: find its path with `exec("ls -A <dir>")` and write the relative link. (Upload via `write({ asset })` hands you the exact relative `` to copy.)
- **Save locally, don't hot-link.** Hot-linked external image URLs rot when the source disappears. Fetch it through the ingest procedure in `references/ingest-and-sources.md` (SSRF-safe fetch flags, size + executable-extension gates, wrapper frontmatter) — never an ad-hoc `WebFetch` / `curl` into the tree — then reference the local file via a relative markdown link and cite the ingested wrapper below.
- **Placement model.** Free-form image embeds → co-located alongside the referencing doc (sha256 same-directory dedup). Raw sources via `ingest` → `external-sources/<slug>.<ext>` + `external-sources/<slug>.md` (the wrapper-binary pair). Check via `exec("ls -A")` if the project uses a different convention.
- **Cannot fetch** (no network, paywall) → don't invent a local path. Omit, or mark inline `(TODO: image needs sourcing from <URL>)`.
- **Meaningful alt text required** — describes WHAT the image shows, not what it is. `![]()` / `![image]()` / `![filename.png]()` all fail. OK indexes alt text — it's both accessibility AND searchability.
- **Cite web image sources** below the image (Grounding rule):
```markdown

*Source: [Avatar Wiki — Aang](./external-sources/avatar-wiki-aang.md)*
```
(Both paths are relative to the citing doc; ingest lands the binary + wrapper pair side by side, so cite the wrapper next to the binary you embedded.) Original diagrams/screenshots may caption `*Original*` or omit. Cite the local ingested wrapper (whose frontmatter carries the original URL), not the external URL — an inline external link is a TODO meaning "still needs ingesting". Unattributed web images are equivalent to unsourced factual claims.
references/onboard-existing-repo.md
# Onboard an existing repo — convention extraction + link-graph activation
Use this when (a) the user asks to set up an existing repo that already has content, OR (b) an `exec` directory listing shows content with no folder frontmatter / templates configured. Goal: **extract conventions from existing siblings + activate the link graph, leaving the repo more structured and more navigable than you found it.**
This is the brownfield counterpart to `ok seed` (greenfield). Read the content directory from `config({ key: 'content.dir' })`; paths below are relative to it.
Use OK primitives only — no new files outside `<folder>/.ok/`, no body rewrites without per-pair user confirmation.
**Seven phases, in order. STOP gates require user confirmation before proceeding. Do not skip or batch ahead.**
**Server requirement.** Phases 1-4 run fs-direct — `exec` (scan + read) and the `write`/`edit` verbs (folder frontmatter + templates) need no running server. Phase 5 (link-graph activation) composes `links` and `search`, which **require the OK Hocuspocus server** — Phase 5 step 0 checks for it and exits cleanly with a `run ok start` instruction if it is down.
---
## Phase 1 — Scan + classify (no user interaction, no server needed)
1. `exec("ls -A <content-dir>")` — list every top-level entry.
2. For each top-level **directory**: `exec("ls -A <dir>")` — the enriched listing surfaces the `.md` count, per-child frontmatter samples, recursive count, and the folder's own descriptive `title`/`description`/`tags` + `templates_available`.
3. Classify each directory:
- **Substantial KB folder** — more than 3 `.md` files directly, OR named in a known-genre list: `specs/`, `reports/`, `docs/`, `articles/`, `research/`, `stories/`, `projects/`, `external-sources/`, `tech-probes/`, `rfcs/`, `proposals/`, `design-docs/`, `adrs/`.
- **Trivial folder** — 1-3 `.md` files; treat as one-offs.
- **Likely noise** — known build/vendored patterns: `node_modules/`, `dist/`, `build/`, `vendor/`, `third_party/`, `.changeset/`, `coverage/`.
4. Detect total `.md` count across the content root.
5. Detect a seeded layout: do any of `external-sources/`, `research/`, `articles/` exist? (Informational only.)
6. **Confirm-then-extend detection.** For each substantial folder, check the `exec("ls -A <folder>")` listing:
- If the folder already has a `title`/`description`/`tags` → descriptive folder frontmatter already exists; switch THAT folder to "extend mode" (propose additions, not replacement).
- If `templates_available` is non-empty → a template already exists for that folder; skip template extraction for it.
- If MOST substantial folders are already configured → set up to exit early at Phase 7 with "already configured."
**Early-exit conditions:**
- If total `.md` count is **fewer than 5** → STOP. Tell the user "this looks empty — try `ok seed` for a greenfield starter pack, OR write your first doc and come back later." Exit.
- If all-greenfield-already (per step 6) → STOP. Exit with "already configured."
---
## Phase 2 — Confirm scope (STOP gate 1)
Present a structured summary to the user. Example:
```
Orientation — what I found:
Substantial folders (candidate KB content):
- specs/ 52 .md (suggests spec collection)
- reports/ 134 .md (suggests research collection)
- stories/ 19 .md
- projects/ 8 .md
Trivial folders (one-offs):
- tech-probes/ 4 .md
- 14 root-level docs (README, AGENTS, CLAUDE, CI, CONTRIBUTING)
Likely noise (recommend .okignore):
- node_modules/ ~200 .md (vendored deps)
- dist/ ~30 .md (build outputs)
- Per-package CHANGELOG.md (generated)
Seeded layout: no external-sources/ / research/ / articles/ present.
Mark each substantial folder as:
[KB] knowledge-base content — extract conventions, add folder frontmatter + template
[skip] already-structured or out of scope — leave existing settings
[noise] add to .okignore
```
**Wait for user response. Do not proceed until each substantial folder is classified.**
---
## Phase 3 — `.okignore` curation (STOP gate 2)
Based on user `[noise]` marks + agent-detected patterns, propose `.okignore` entries:
```
Proposed .okignore additions:
node_modules/**/*.md # vendored dep READMEs
dist/**/*.md # build outputs
**/CHANGELOG.md # per-package generated changelogs
THIRD_PARTY_NOTICES.md # auto-generated notices
<any user-marked folders>
```
Ask the user: "Apply these `.okignore` additions? (yes / edit list / skip)"
On confirm: read the existing `.okignore`, append new entries, write via native `Write` (`.okignore` is a plain config file, not in-scope markdown — escape-hatch ok).
**Don't redundantly propose what `.gitignore` already covers.** `.gitignore` and `.okignore` evaluate together; if the repo's `.gitignore` already excludes `node_modules/`, you don't need to add it again.
---
## Phase 4 — Per-folder convention extraction (STOP gates 3 + 4)
For **each** folder the user marked `[KB]`:
1. **Pick representative siblings.** From `exec("ls -A <folder>")`:
- Most-recently-edited doc
- Median-sized doc (roughly: pick the middle entry)
- Up to 3 siblings. If the folder has fewer than 2 siblings, skip template extraction; only propose folder frontmatter.
2. **Read the siblings.** `exec("cat <doc>")` on each. Extract:
- **Heading set** — list every top-level heading (a line starting with `# ` or `## `) in document order.
- **Frontmatter shape** — which keys appear in at least 2 of the 3 siblings. Note ALL recurring keys, but route them by purpose: keys that describe the FOLDER (`title` / `description` / `tags`, though any key is allowed) go in the folder frontmatter; recurring per-doc keys (`status:`, `owner(s):`, `baseline-commit:`) are starting values for NEW docs and belong in the folder's TEMPLATE. Folder frontmatter is open-shape (like a doc's) but self-only — it does NOT flow into child docs, so per-doc starting values still belong in the template.
- **Filename pattern** — dated (`YYYY-MM-DD-*`)? Slugged? Sequential?
- **Link patterns in body** — does the body have a "Related" / "See also" / "References" section? Note the convention.
3. **STOP gate 3 — heading-set match check.** If top-level headings DON'T match in text + order across at least 2 siblings, do NOT extract a template — only propose folder frontmatter. Tell the user: "siblings in `<folder>` don't share a consistent body skeleton; skipping template, proposing folder frontmatter only." The operational metric for "match" is **heading-text equality** (exact strings, in order). Anything beyond that is model judgment and shouldn't pretend to be deterministic.
4. **Idempotency check.** Per Phase 1 step 6:
- If this folder already has its own frontmatter → switch to "extend mode." Show existing keys to the user; propose ADDITIONS only.
- If `templates_available` includes a template → skip template extraction. Tell the user: "existing template `<name>` found; preserving."
5. **Propose folder frontmatter + template.** One `write`/`edit` call per target:
```ts
// Folder frontmatter → writes <folder>/.ok/frontmatter.yml.
// Open-shape (any key, like a doc's); here the conventional
// title/description/tags describing the FOLDER. Self-only — does NOT
// cascade into child docs. Use `write({ folder })` for a new folder,
// `edit({ folder })` to add keys to an existing one (merge-patch).
edit({
folder: {
path: '<folder>',
frontmatter: {
title: '<inferred>', // e.g., "Specifications"
description: '<inferred>', // 1-2 sentences describing the folder's purpose
tags: ['<inferred>'], // e.g., ['spec']
},
},
})
// Template → writes <folder>/.ok/templates/<name>.md. This is where
// recurring per-doc starting values live (status, owner, ...) — bake them
// into the template body's frontmatter region so new docs start with them.
write({
template: {
path: '<folder>/<inferred-name>', // e.g., 'specs/SPEC', 'reports/REPORT'
frontmatter: { title: '<placeholder>', description: '<placeholder>' },
content: '<heading skeleton from siblings; include a frontmatter region with recurring per-doc keys (status, owner, ...) as starting values>',
},
})
```
6. **STOP gate 4 — surface the proposal.** Show: heading set + frontmatter shape + filename pattern + link patterns. Ask: "Apply this proposal? (yes / edit / skip this folder)"
7. **Apply confirmed proposals** via the `edit({ folder })` + `write({ template })` calls above. Re-run `exec("ls -A <folder>")` to confirm `templates_available` + the folder's descriptive `title`/`description`/`tags` are populated.
Repeat for every `[KB]` folder.
**Scope note:** do NOT attempt nested-pattern detection (e.g. applying the same folder frontmatter across `specs/*/evidence/` for every spec's evidence subfolder). Each folder is addressed by its own path — one `edit({ folder })` call per folder. Users who want nested folder frontmatter set it up manually afterward.
---
## Phase 5 — Link-graph activation
The largest phase. Uses the `links` tool (every link-graph view) plus `search` and `edit` to apply confirmed link insertions.
0. **Server check (required for this phase).** `links` and `search` need the OK Hocuspocus server. Probe via `links({ kind: "hubs" })`. If the response starts with `"Error: Hocuspocus server is not running"`, STOP — tell the user "the link-graph phase needs the OK server. Start it with `ok start` from a terminal, then resume here (Phases 1-4 are already applied)." Exit cleanly.
Six sub-passes, each with its own STOP gate.
### 5a. Orphan triage (STOP gate 5a)
1. Run `links({ kind: "orphans" })`.
2. For each orphan, run `links({ kind: "suggest", document: <orphan> })`:
- If `mentions[]` is non-empty → there are docs that mention this orphan without linking. Adoption candidates.
- If `mentions[]` is empty → the orphan is **genuinely standalone** (no other doc references it at all). Surface as: "this looks intentionally standalone (e.g., a README). Skip / adopt anyway by linking from a hub / add to `.okignore`?"
3. Confirm per orphan. For each "link" choice, `edit({ document: { path, find, replace } })` on the source doc — find the existing mention text `links({ kind: "suggest" })` surfaced and replace it wrapped in link syntax.
**Note on re-surfacing:** "intentional standalone" markers are not persisted. Each re-run surfaces the same intentional orphans (root `README.md`, `CONTRIBUTING.md`, etc.) and the user re-dismisses them. Acceptable minor friction.
### 5b. Hub identification (STOP gate 5b)
1. Run `links({ kind: "hubs" })` — surfaces the most-linked-to docs (highest *inbound* links), i.e. pages already acting as hubs.
2. For each substantial `[KB]` folder (those that got templates in Phase 4):
- Check whether a hub already exists (`<folder>/README.md`, `<folder>/INDEX.md`, `<folder>/CATALOGUE.md`).
- If yes: don't create a new one (anti-pattern: don't create INDEX.md hubs).
- If no: ensure the template extracted in Phase 4 includes a `## Related` / `## See also` section. If the template doesn't already have one, propose adding it.
3. Surface existing hubs to the user so they know what the link graph already provides.
### 5c. Dead-link sweep (STOP gate 5c)
1. Run `audit` (the authoritative end-state link check; it covers the whole corpus — caveats in `references/linking.md`). `links({ kind: "dead" })` is the graph reader, not the validation step; it is a superset on the source side (it also lists dead links whose source is a skill-bundle doc, which `audit` skips).
2. For each dead link, propose: a fix candidate (via `search` for the correct target), or deletion (remove the link, or the prose around it). Leaving it as an "intentional redlink" is not an option — every dead link is fixed or removed.
3. Confirm per dead-link. Apply confirmed fixes via `edit`.
### 5d. Untextualized-reference detection (STOP gate 5d)
`links({ kind: "suggest" })` implements server-side detection of prose mentions of a target doc that aren't wrapped in link syntax. Lean on it directly.
1. For each substantial `[KB]` doc, run `links({ kind: "suggest", document: <target> })`.
2. Each call returns `mentions[]` with `{ source, excerpt, offset }` — places to insert links pointing TO this target from OTHER docs.
3. Surface batched **by source doc** (not per-link, to keep cognitive load reasonable):
```
In specs/2026-04-23-foo/SPEC.md, found 3 untextualized references:
line 12: "the existing `ok init` scaffolds..." → matches code, NOT a doc — skip
line 145: "...replaces the instructional init-content tool" → matches a spec — accept as link
line 203: "...per AGENTS.md ecosystem convention" → matches root AGENTS.md — accept as link
```
4. Confirm per source doc (batched). Apply confirmed link insertions via `edit({ document: { path, find, replace } })` — find the mention text the `suggest` view surfaced and replace it wrapped in link syntax.
**Truncation handling:** if the `suggest` view returns `truncated: true`, the scan hit its time budget. Iterate with smaller scope or accept partial coverage and tell the user.
### 5e. Vague-referential detection (STOP gate 5e)
Harder case: prose discusses a concept covered by another doc without naming it. Example: a spec talks about "how we track who wrote what" and there's an `agent-identity-attribution/REPORT.md` covering exactly that — but the spec never says "agent identity attribution." `links({ kind: "suggest" })` (title/alias match) misses this; semantic detection requires model judgment.
1. For each substantial doc, identify its main concepts. Use frontmatter `subjects:` / `topics:` if present; else extract from heading text + the first 2-3 paragraphs.
2. For each concept, run `search({ query: <concept> })`. Take the top 1-2 non-self results.
3. For each candidate sibling:
- Verify it is NOT already linked from this doc (`links({ kind: "forward", document: <doc> })`).
- Verify the content is actually relevant (re-read the summary).
4. Surface to the user with brief justification per pair. Confirm. Apply via `edit` (insert the link in a "Related" or "References" section).
**Caveat:** vague-referential is judgment-heavy. False positives are expected; the user is the final arbiter. If the user rejects more than half the proposals in a batch, recalibrate (tighten concept-extraction, raise the relevance bar).
### 5f. Link-style detection + standardization (STOP gate 5f)
1. Sample 10 random non-noise docs (or all if the total is under 10). Count occurrences:
- Wiki-link syntax: `[[Page Title]]` (legacy)
- Relative-markdown: `[text](./path.md)` or `[text](path.md)` (the current OK recommendation)
2. Report the ratio to the user. If mixed, propose a one-time standardization: convert all `[[Page]]` to relative-markdown links. Ambiguous `[[Page]]` (multiple matching files) → surface for confirmation.
3. Apply via `edit` on each affected doc, resolving page titles to file paths via `exec("ls -A")` / `search`.
---
## Phase 6 — Apply + validate
After all confirmed proposals are applied:
1. Re-run `exec("ls -A <folder>")` on every `[KB]` folder. Verify:
- the folder's descriptive `title`/`description`/`tags` are populated (folder frontmatter landed)
- `templates_available` includes the new template (template landed)
2. Re-run `audit` to confirm fixed dead links no longer report, and `links({ kind: "orphans" })` to confirm the orphan count dropped vs. the Phase 5a baseline.
If any validation step fails, surface it to the user — do NOT silently pass.
---
## Phase 7 — Final summary + exit
Share a structured summary with the user. Distinguish initial-setup mode from extension mode (per Phase 1 step 6):
**Initial-setup mode:**
```
Onboarding complete:
Folder frontmatter added (<folder>/.ok/frontmatter.yml):
specs/ title: Specifications, tags: [spec]
reports/ title: Research Reports, tags: [report]
stories/ title: User Stories, tags: [story]
projects/ title: Projects, tags: [project]
Templates added (<folder>/.ok/templates/<name>.md):
specs/.ok/templates/SPEC.md (heading skeleton from 3 siblings, link-aware)
reports/.ok/templates/REPORT.md (heading skeleton from 3 siblings, link-aware)
.okignore additions: 4 entries (node_modules, dist, CHANGELOG.md, THIRD_PARTY_NOTICES.md)
Link-graph activation:
Orphan triage: 7 orphans triaged (4 adopted, 3 left as intentional standalones)
Hub identification: 3 existing hubs surfaced; 2 templates updated with `## Related` sections
Dead-link sweep: 5 dead links — 4 fixed, 1 removed
Untextualized refs: 12 prose mentions linked (across 8 source docs)
Vague-referential: 5 semantic cross-references added (across 4 source docs)
Link-style: standardized 42 wiki-links to relative-markdown
Notes:
- 3 intentional standalones (README, CONTRIBUTING, LICENSE) re-surface on each run.
No "standalone" marker is persisted; you'll re-dismiss them next time.
Next steps:
- Review the extracted templates in <folder>/.ok/templates/ — they're starter shapes, not finished.
- The next agent in this repo will see templates_available + the folder's own frontmatter for every KB folder.
- Re-run after significant content additions (new folder categories, major restructures).
```
**Extension mode** (when prior configuration was detected):
```
Onboarding extension complete (existing configuration preserved):
New folder frontmatter:
tech-probes/ (was missing — title: Tech Probes, tags: [probe])
Updated templates:
specs/.ok/templates/SPEC.md (heading skeleton refreshed from current siblings)
Link-graph activation:
Orphan triage: 3 new orphans found (all triaged)
Untextualized refs: 8 new references linked
```
---
## STOP rules / anti-patterns (load-bearing across phases)
- **Don't restructure existing folders** — no renaming, moving, consolidating, splitting. The folder shape as-found is the contract.
- **Don't impose a starter-pack layout** — only propose `external-sources/` / `research/` / `articles/` if the user explicitly opts in. Most brownfield repos won't.
- **Don't create INDEX.md / README.md hub files** — folder frontmatter + `exec("ls -A")` give the same view live.
- **Don't extract templates from a single sibling** — you need at least 2 with a matching heading set (heading-text equality, in order).
- **Don't bulk-rewrite doc bodies** — every link insertion (5a, 5c, 5d, 5e, 5f) goes through user confirmation via STOP gates.
- **Don't auto-apply** — every phase has a confirmation gate. You propose; the user disposes.
- **Don't run if the project has fewer than 5 `.md` files** — exit early; redirect to `ok seed`.
- **Don't run if the project is already fully configured** — exit early with "already configured."
- **Don't redundantly propose what `.gitignore` covers** in `.okignore`.
- **Don't attempt nested-pattern detection** — each substantial top-level folder gets its own folder frontmatter; nested sub-patterns (`specs/*/evidence/`) are left for users to set up manually.
---
## Exit conditions (early termination)
- **Empty project** (Phase 1): fewer than 5 `.md` files total → exit with "try `ok seed` instead."
- **All-greenfield-already** (Phase 1 step 6): folder frontmatter exists AND templates exist for every substantial folder → exit with "this project is already configured."
- **Server down** (Phase 5 step 0): Phases 1-4 already applied fs-direct; surface "the link-graph phase needs `ok start` running" and exit cleanly — resuming picks up at Phase 5.
- **User aborts at any STOP gate** → exit cleanly, leaving any already-applied proposals in place (apply is per-phase, not all-or-nothing).
- **Validation failure** (Phase 6) → don't pretend to succeed; surface the failed step to the user.
references/preview.md
# Preview — full multi-host contract
## Contents
- **Step 0 — determine your ONE focus surface before opening anything (the load-bearing check)**
- Re-navigation and end-of-turn discipline
- `previewUrl` is a route, not a URL
- The three first-class apps (Claude Code Desktop, Cursor, Codex desktop)
- Any-CLI track — Claude Code / Codex / Cursor CLI / OpenCode / Pi, no browser → `ok open`
- Four attach signals
- `previewUrl: null` semantics, server lifecycle, read-only mirror, no-screenshots
---
The user watches your edits land in a live browser preview. Open it once at session start, then keep working. Re-navigate only when the user asks to open a different doc, or to land them on a finished deliverable (see below) — not to re-check your own edits.
**End a turn on the deliverable, not your scratch space.** Keep the preview steady *during* a multi-doc task — don't yank it around to re-check your own edits. But when a turn created or substantially changed user-facing docs, navigate the preview to the primary deliverable before you hand back: the hub / overview / index page when you created several docs, or the changed doc when you changed one. Don't step the user through every supporting source card — the user is watching, so leave them on the result.
**`previewUrl` is a route, not a URL to open.** Every read response (per-doc, on `exec` / `search` / `links` rows) and every write response carries a `previewUrl` — a route fragment like `/#/specs/foo/SPEC`, with **no scheme, host, or port**. It identifies *which doc* to preview, not a URL to hand a browser by itself. Never construct or guess preview URLs.
### Step 0 — figure out your ONE focus surface BEFORE you open anything
Your host doesn't change mid-session, so determine this **once**, the first time you're asked to open/show/preview a doc, and use that one surface for the rest of the session. Run the checks **top-to-bottom and stop at the first match** — earlier rungs win:
1. **Is `OK_DESKTOP_TERMINAL` or `OK_HOSTED_AGENT` set in your environment?** Actually check (`echo "$OK_DESKTOP_TERMINAL$OK_HOSTED_AGENT"`, or read your env), don't assume. **Either one set → you are running inside OpenKnowledge itself** — `OK_DESKTOP_TERMINAL` in the desktop app's built-in terminal, `OK_HOSTED_AGENT` in its in-app agent panel. Open a doc or folder with `ok open <name>` (auto-detected; `--skill <name>` for a skill) — it switches the window the user is already looking at to the target. **Never resolve a preview URL here, and never paste one into your reply**: the user is looking at the app that URL points to, so a `localhost` link is noise at best. (This rung is the one agents miss most — check the env first, before anything else.)
2. **Do you have any URL-navigation / in-app browser tool** (Claude Code Desktop's **Browser pane**, Cursor's `Navigate`, Codex's `@Browser`, or `browser` / `open_url` / `view_url` / …)? → **Claude Code Desktop / Cursor / Codex desktop** → `preview_url` once, then open/navigate that browser to the `url` (per-surface how-to below — Claude Desktop opens the pane with `preview_start({ url })` then `navigate({ url })` to move; Cursor/Codex navigate directly).
3. **None of the above** → you're a **plain CLI** (Claude Code CLI, Codex CLI, Cursor CLI, OpenCode, Pi, any shell agent) → `ok open <name>` for a doc or folder (auto-detected; `--skill <name>` for a skill); deep-links a separate OK Desktop window, system browser only if no desktop.
Decide once; then every "open / show / preview this doc" this session uses that surface's mechanism. **The `previewUrl` field in tool responses is a route id, not your open mechanism** — seeing it is not a reason to hand it to a browser. The rest of this section is the per-surface "how" for whichever rung you landed on.
**OK ships first-class preview support for three apps — Claude Code Desktop, Cursor, and the Codex desktop app — plus any CLI (Claude Code CLI, Codex CLI, Cursor CLI, OpenCode, Pi, …) on a separate `ok open` track (below). Make the preview seamless in each.** Match on the tool you actually have (capability, not host name): if a tool can navigate to a URL, it counts as an in-app browser; if nothing can, you're a CLI → `ok open`. The three apps map to:
- **Claude Code Desktop — you have the in-app `Browser` pane** (Cmd+Shift+B; driven by the `Claude_Browser` tool set — `preview_start`, `navigate`, `read_page`, `computer`, …). Two steps, both fed by `preview_url`: **(1) open the pane** with `preview_start({ url })` passing the `preview_url` result — `preview_start` opens a browser tab directly at that URL (`preview_start({ url })`), and the deep `url` already carries the doc's hash route so it lands on the doc, no arming. **(2) move to another doc** once the pane is open with `navigate({ url })` passing the new target's `preview_url`. A cold `navigate` fails (`No preview is open`) — `preview_start` must open the pane first; and `navigate` (not `preview_eval` hash-poking) is the move verb. `preview_url` auto-starts the OK UI so the `url` resolves before you open. Don't hand-edit host preview config.
- **Cursor / Codex desktop — no `preview_*` tool, but you have an in-app / built-in browser tool** → call `preview_url` once for the **exact** target (`document` for a doc, `folder` for a folder) and navigate your **in-app browser** straight to the returned `url`. Open that deep URL directly — never the root then navigate; omit both args only for the root. Drive the tool your host gives you:
- **Cursor** → its built-in **Browser** tool, the **`Navigate`** action (`browser_navigate`, via Cursor's own `cursor-ide-browser`). Navigate it to the `url` yourself — don't print the URL or shell out to the system browser. (A *surfaced* link in Cursor follows its "Browser Tab" vs "Google Chrome" picker and may open the system browser; you calling `Navigate` avoids that. A third-party MCP like OK cannot push a URL into the pane — only the agent's own `Navigate` can.)
- **Codex desktop app** → its in-app **Browser** plugin (`@Browser`); drive it to the `url` (Codex navigates via `tab.goto`).
- **Any other host** with a URL-navigation tool (`browser`, `view_url`, `open_url`, `web.browse`, …) → navigate it to the `url`. **This is also the fallback when a named tool above isn't present under that exact name** (hosts rename tools): match on the capability, not the name. If no URL-navigation tool exists at all, drop to the Claude Code CLI track below.
- **Honor `autoOpen`** (on `preview_url`, or on `warning` for write tools). If `false`, do not open or refresh any preview UI; surface the URL only if asked.
**Any CLI — a separate track (no browser).** **Claude Code CLI, Codex CLI, Cursor CLI, OpenCode, Pi — any pure-stdio / shell agent with no in-app browser tool — uses `ok open`, never a preview URL.** The rule is capability, not vendor: no tool that navigates a URL → you're on this track.
- **`ok open <name>`** — opens a **doc or folder** in the OK Desktop app. The type is auto-detected (a directory on disk → folder, otherwise a doc), so **`--folder` is not needed** (it stays only as an explicit override for a folder that doesn't exist on disk yet). An action you run, not a URL to print.
- **`ok open <skill-name> --skill [--scope project|global]`** — opens a **skill** in the skill editor (skills are addressed by name + scope, not a content path, so they need the flag).
- All three deep-link into OK Desktop when a bundle is installed, and fall back to the browser UI (`ok start`) otherwise. No `ok` on PATH or no shell → `preview_url`, then `open <url>` in the system browser as a last resort, and say so plainly. The system browser is the fallback, never the default.
- **`ok open` prints the absolute project root it resolved**, and names the enclosing project too when that root sits inside another one. Read that line instead of assuming which project you got. To name the project yourself, pass `--project <dir>`; it is honored wherever it appears (`--project <dir>` or `--project=<dir>`, before or after the target, with or without a `.md` extension), or the command exits non-zero saying why.
(The same vendors' *desktop apps* with a built-in browser — Cursor, Codex desktop — are NOT here; they navigate their own browser per the in-app branch above. Codex **IDE extension** / **Cloud** are web-search-only → they're on this CLI track too.) **Running inside OpenKnowledge itself — the desktop built-in terminal (`OK_DESKTOP_TERMINAL`) or the in-app agent panel (`OK_HOSTED_AGENT`)? Same track — `ok open <name>` re-targets the surface you're already in** (switches it to the doc/folder; when that surface is a desktop window there's no second window, and no need to raise it since it's already frontmost). Don't resolve preview URLs in that case; `ok open` is the whole answer.
**Opening or reading a file IS a preview navigation.** On any "open `<file>`" / "read `<file>`" request, navigate the browser to that doc's `previewUrl` route from the tool response — not a separate fetch, not a fresh system-browser launch.
**Four signals to check if the preview is already attached** (read these from each write response):
1. You opened/navigated earlier this session → don't reopen.
2. Write response has `previewUrl` (non-null route) and NO `warning` → a browser is attached somewhere; do nothing.
3. `warning: { action: "attach-preview-once", previewUrl, message }` → UI reachable, no browser attached; open one-shot (`preview_url` → your host's open verb: `preview_start({ url })` on Claude Desktop; `navigate` / `Navigate` / `@Browser` on Cursor/Codex).
4. `warning: { action: "start-ui", previewUrl: null, message }` → no UI running anywhere. Surface the message verbatim — recovery options are in the in-band copy. Don't loop on retries.
Warnings fire at most once per session in the fresh-start case.
**Re-point at the end of a multi-doc workflow; don't claim a doc is on screen unless you put it there.** The one-shot attach (signal 3) opens the preview *once* — later writes do NOT move the pane; it stays on the doc you last navigated to. When a turn touches several docs, finish by navigating the preview to the doc the user should land on, using your host's move mechanism (`preview_url` → navigate your in-app browser; honor `autoOpen`). Until you have navigated there *this* turn, don't tell the user a doc is "open" / "on screen" — at most, say the preview may still be on the doc you opened earlier.
**`previewUrl: null` only means "no UI reachable" on the two attach-warning tools: `write` / `edit`.** `delete` / `move` emit `previousPreviewUrl` (different field, for closing stale tabs) and don't fire attach warnings. `preview_url` auto-starts the backend on demand (same `OK_MCP_AUTOSTART` gate as writes; a cold first call can take seconds) and reports `running: false` + `url: null` only when no UI could be reached — its hint names the right command.
If you see `"Hocuspocus server is not running"`, run `ok start` and retry.
OK Electron and the project server coordinate through the same lock files; a second UI-serving process yields to the live holder transparently. That is exactly why `previewUrl` is route-only — the underlying port is not the agent's to use. **Do not nudge the user to quit OK Electron to free a port** — the server handles coexistence, and quitting tears down a UI in active use.
**The preview is read-only for the agent — it is the user's view, not a surface you read back.** You cannot click or type to drive edits — the CRDT flow is one-way (agent → MCP → CRDT → preview).
**No screenshots to confirm edits, no generic verification loop.** Do NOT take `preview_screenshot` (host tool, not OK MCP) after a write, and do not run a generic snapshot/eval/screenshot verification loop — OK's preview is a read-only, one-way mirror, so the CRDT tool response *is* the confirmation that an edit landed. Screenshot only when debugging a visual rendering issue or when the user explicitly asks to see the preview — never to confirm an edit landed. (Navigating your in-app browser to a `preview_url` route is fine — that drives the view, it is not a read-back verification loop.)
references/setup.md
# Setting up OpenKnowledge
This repository is an OpenKnowledge (OK) project: its `.md` / `.mdx` files are CRDT documents, and agents work with them through the OpenKnowledge MCP server. If your agent's `mcp__open-knowledge__*` tools aren't available, the project just isn't connected on this machine yet. Connect it at whatever depth you need — each rung adds capability, and you can stop at the first one that covers your task.
**Canonical, always-current instructions:** <https://openknowledge.ai/docs/get-started/quickstart>. The rungs below are a quick orientation; the docs are authoritative (release channels and download links move, so this file deliberately points there rather than pinning them).
## Rung 1 — Agent tooling, no install
Approve the committed `.mcp.json` in your agent client (Claude Code, Cursor, Codex, …). It launches the OK MCP server on demand via `npx`, so there is nothing to install. This gives you the read tools (`exec`, `search`) over the docs plus the fs-direct write tools (`write` / `edit` for markdown and skills).
This rung does **not** start a collaboration server, so it does not give you live CRDT co-editing or the browser preview — those need rung 2.
## Rung 2 — Full editing + live preview, via the CLI
Install the OK CLI and start a local server:
```bash
npm install -g @inkeep/open-knowledge # or run it ad hoc with: npx @inkeep/open-knowledge
ok start
```
`ok start` runs the collaboration server and serves a browser preview at the URL it prints — no GUI required. Markdown writes now land in the live CRDT and the preview updates as you (or an agent) edit.
## Rung 3 — Desktop app, optional
For a native editor with the file tree, property panel, and built-in preview, install the OK Desktop app. The quickstart link above has the current download — it tracks the latest release, so grab it there rather than from a pinned URL.
## Tool visibility — before you conclude the MCP is missing
**MCP tool visibility — not seeing `exec` is NOT the escape hatch.** MCP wiring varies by client (Claude Code, Cursor, Codex, Windsurf, VS Code). Server labels are user-defined; tools may not appear as top-level symbols named `exec`. If OpenKnowledge is registered as an MCP server here, route markdown reads through its `exec` / `search` via your client's documented MCP invocation (including any generic "call MCP tool" flow). Registration is the test, not top-level-symbol visibility.
**Your initial tool list is NOT exhaustive — run tool discovery before concluding the MCP is missing.** Some clients (notably Codex) defer MCP tools behind a lazy `tool_search` / tool-discovery step, so `mcp__open-knowledge__*` is absent from the upfront tool set and only appears after you search for it. Absence from the visible list means "I have not discovered it yet," NOT "it is not registered." If you don't see the tools, your first move is to run your client's tool-discovery / `tool_search` for `open-knowledge` (or `exec` / `search` / `write`) — do not infer unavailability from the initial list.
references/starter-packs.md
# Knowledge layers + starter packs — depth
(Core carries the layer table. This file carries the operating detail.)
## The layer model
Three of the layers correspond to [Karpathy's three-layer knowledge-base pattern](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f): **ingest** (raw sources, immutable) → **research** (wiki, provisional) → **consolidate** (wiki, canonical). Onboarding an existing repo operates one level up, at the project-metadata layer, and is the brownfield counterpart to the greenfield `ok seed` CLI.
Typical day-2 flow: user shares a URL → ingest (preserve) → user asks "now research this" → research (provisional article; ingests more sources as needed) → decision lands → consolidate (canonical article, supersedes the research).
**None of these are MCP tools.** They are procedures that ship as skill guidance, in one of two shapes: a **reference file** inside a skill's bundle, which you open by path once that skill is loaded; or a **skill of its own**, which loads on description match when its pack is seeded.
- ingest → reference file `ingest-and-sources.md`, here in this bundle. Ships with every `ok init` because Core's Grounding rule depends on it, pack or no pack.
- onboard a repo that already has content → reference file `onboard-existing-repo.md`, here in this bundle.
- research / consolidate → their own skills, `/research-with-sources` and `/consolidate-notes`, installed alongside their pack by `ok seed --pack knowledge-base`. **Not available otherwise** — if the pack isn't seeded, say so rather than improvising a lookalike pipeline.
- generate / refresh a codebase wiki → reference file `generate-and-refresh.md`, inside the `codebase-wiki` skill's bundle and readable once that skill loads. Installed by `ok seed --pack codebase-wiki`.
Read the relevant procedure and execute its numbered steps with the OK verbs; don't skip its STOP gates.
**Autonomy gates vs session-level autonomy.** A procedure's STOP gates (research's scoping gate, consolidate's decision-confirmation gate) are not overridden by session-level "work without stopping for clarifying questions" hints. The session-level hint covers trivial back-and-forth ("which file did you mean?"); the gates exist for one-way-door decisions where the procedure deliberately wants confirmation before continuing. When in doubt, treat the gate as authoritative and the session-level autonomy hint as a default for the in-between turns.
**Do not chain silently.** After ingest, ask the user whether to proceed to research. After research, let the user decide whether the findings are ready to consolidate. Each procedure completes on its own terms — the user drives the transitions.
**Project scaffolding — two paths.** **Empty repo:** run `ok seed` once from a terminal (scaffolds the layout + seeds `log.md` + folder defaults). **Existing content:** work through `onboard-existing-repo.md`. Neither is required; the layer procedures work against any folder structure. Only mention each when explicitly relevant.
## Starter packs — reference for inspiration
The `ok` CLI (a Bash surface beside the MCP tools; other verbs `ok start` / `ok open` are documented in the core) ships proven layouts you can study to build a *similar* structure of your own — adapt the idea, don't clone the pack:
- `knowledge-base` — source-grounded research articles
- `software-lifecycle` — proposals, decisions, specs
- `codebase-wiki` — agent-authored wiki of your codebase
- `plain-notes` — notes + daily journal
- `worldbuilding` — fiction story wiki
- `writing-pipeline` — drafts → published
- `entity-vault` — people / companies / meetings (personal CRM)
- `okf` — Open Knowledge Format–conformant base
A pack that ships skills installs them project-local when seeded. Those skills are where the pack's procedures live; a pack may ship one skill or several.
To reference one **without installing it**: `ok seed --list-packs` (the menu) → `ok seed --pack <name> --dry-run` (its folders + the *why* of each folder + templates; writes nothing). Then either adapt the ideas into your own folders (`write({ folder })` + a template) or adopt the pack as-is by re-running without `--dry-run`. Reach for this when a user wants structure and an archetype fits — propose a tailored variant, not a verbatim copy.
references/template-authoring.md
# Template authoring + folder editing
## When to create a template
Templates make folder structure durable. Create them proactively:
- 2+ sibling docs share a skeleton in a folder with no template → extract via `write({ template })`.
- About to write a doc in a folder where no template fits, AND the shape is reusable → save as template the same turn.
- Scaffolding a new folder for a doc category → pair `write({ folder })` (or `edit({ folder })`) with `write({ template })` in the same turn.
- The user describes a recurring doc shape ("we always log meetings with attendees, agenda, action items") → author the template once.
Note new templates in chat ("saved as a template at `meetings/.ok/templates/prep-notes.md` for next time") so the user sees the discipline grew.
**Keep starter content clean (MUST).** A template body is a reusable skeleton, not a meta-prompt: section headings, real frontmatter, and SHORT `{Stub}` placeholders (e.g. `# {Meeting Title}`). Do NOT bake a workflow's verbose `{...}` prompt-paragraphs (the `research` / `consolidate` shape guidance is for filling ONE doc, not for persisting into every new one), do NOT duplicate sections, and do NOT save a half-filled or in-progress doc as a template. Long "how to fill this" guidance belongs in the folder description, not in the body each new doc inherits. After saving, `exec("cat <folder>/.ok/templates/<name>.md")` and eyeball it — a template propagates to every doc made from it, so a garbled one is a recurring defect, not a one-off.
## Editing a folder's own description
```ts
edit({
folder: {
path: "meetings",
frontmatter: { title: "Meetings", description: "Meeting notes", tags: ["meeting"] },
},
})
```
`frontmatter` is open-shape — any key about the folder itself, exactly like a doc's frontmatter (`title` / `description` / `tags` are conventional keys the UI surfaces). It's self-only: it describes the folder and does NOT flow into child docs — put per-doc starting values in a template instead. Each call targets a SINGLE folder by its own `path` (no globs). Use `write({ folder })` to create a new folder, `edit({ folder })` to change an existing one (merge-patch). Clear the folder's frontmatter by passing `frontmatter: {}`, or drop one key with `frontmatter: { key: null }` — the file deletes when empty and `.ok/` auto-cleans if no other tenant remains.
## Creating templates
```ts
write({
template: {
path: "meetings/prep-notes",
content: "# {Meeting Title}\n\n**Attendees:** \n**Date:** \n\n## Agenda\n- \n",
frontmatter: {
title: "Meeting Prep Notes", // REQUIRED — TEMPLATE_TITLE_REQUIRED if missing
description: "Use before a meeting.", // recommended — soft warning if absent
tags: ["meeting", "prep"],
},
},
})
```
**Substitution allowlist:** template bodies MAY use exactly two server-side substitutions — `{{date}}` (today's ISO-8601 date) and `{{user}}` (calling principal display name). Other `{{...}}` tokens are rejected at write time with `TEMPLATE_UNKNOWN_VARIABLE`. Plain `{shape}` placeholders (e.g., `{Meeting Title}`) are LITERAL — agents fill via subsequent `edit` calls. Delete a template via `delete({ template: { path } })` (auto-cleans empty `.ok/templates/` and `.ok/`).
## Creating a doc from a template
```ts
// Inspect the menu (already done in the pre-write checklist).
exec("ls -A meetings/")
// → templates_available: [{ name: "prep-notes", title: "Meeting Prep Notes", scope: "local" }, ...]
// Instantiate. `template` and `content` are mutually exclusive.
write({
document: {
path: "meetings/2026-05-02-roadmap-sync",
template: "prep-notes",
},
})
// Fill the `{shape}` placeholders via follow-up edit calls.
```
Templates resolve via leaf → root walk-up at the target's parent folder, closest-wins on filename collision. **`template` and `content` are mutually exclusive** — passing both errors with `TEMPLATE_AND_CONTENT_BOTH_SET`. Substitution happens at instantiation time only; templates on disk show the raw `{{date}}` token.
references/writing.md
# Writing — depth
(Core carries the MUSTs: route through `write`/`edit` never native, persist incrementally, pass a `summary`. This file carries the supporting detail.)
**Pass a `summary` on every content write (SHOULD).** `write`, `edit`, and `move` each take a one-line `summary` (≤80 chars) describing the user-facing outcome of the change — "Add gear list and permit info", not "edited trip doc". It renders as a bullet under your name in the document timeline and is the only human-readable change-note persisted to the shadow-repo history; omit it and the timeline shows *that* you wrote but not *what changed*. Write it from the reader's perspective, keep it specific, and avoid secrets or PII (it lands in git history). Each entry in the batch `documents:` form carries its own `summary`.
**Reach for visual structure where it aids comprehension.** Default to the right OK primitive over flat prose: a Callout (`> [!NOTE]`) for a key caveat, a ` ```mermaid ` diagram for a process or relationship, a table for options or comparisons, an `html preview` chart for numbers. **Call the `palette` MCP tool as you draft** (and `palette({ components })` for a canonical's JSX schema) — it returns copy-ready markdown-native forms, themed `html preview` embed starters, and the theme tokens, so the visual lands themed and in the content graph instead of hand-rolled. Don't decorate — use a visual only when it carries the point better than prose would. Full catalog: see `references/components-and-visuals.md`.
For the write-response advisory warnings (`content-divergence`, `disk-edit-reconciled`, `mermaid-parse-error`), MDX authoring, and delete/move mechanics, see `references/doc-editing.md`.
SKILL.md
---
name: open-knowledge
description: "Authoritative agent-runtime contract for working inside an OpenKnowledge project — a markdown-CRDT knowledge base exposed over MCP. Use whenever reading, listing, searching, editing, or linting any `.md` or `.mdx` file in the project, and before any `mcp__open-knowledge__*` tool call (`exec`, `search`, `write`, `edit`, `lint`, and the rest). Installed by `ok init`, so its presence means this is an OpenKnowledge project and it governs every markdown file here. Covers the read/write tool surface, grounding and linking rules, folder/template conventions, the live browser preview, and the rule that OK's MCP tools — never native file tools — handle in-scope markdown."
compatibility: "Any agent host with the OpenKnowledge MCP server configured; `ok init` wires the agents it detects on your machine. Some steps shell out to the `ok` CLI. Hosts that take an uploaded skill bundle instead of reading a project directory (Claude Desktop, Cowork, claude.ai) are covered by `ok cowork`."
metadata:
author: "Inkeep"
repository: "https://github.com/inkeep/open-knowledge-skills"
---
# OpenKnowledge — agent guidance
OpenKnowledge (OK) is a markdown-CRDT collaboration platform exposed via MCP. This skill is the single source of OK agent guidance. Every rule below is a MUST unless marked otherwise. **Depth lives in `references/*.md` — one level deep; load a reference when its task comes up.**
> Skill version tracks `@inkeep/open-knowledge-server`. `cat ~/.ok/skill-state.yml` shows what's installed. `ok seed` needs `@inkeep/open-knowledge` >= 0.4.0; if it errors `unknown command`, `npm install -g @inkeep/open-knowledge`.
> **Setup (not connected yet?).** If the `mcp__open-knowledge__*` tools aren't available in your client, this project isn't wired up on this machine — see [`references/setup.md`](references/setup.md) for the rung ladder (approve `.mcp.json` → `ok start` CLI → optional desktop app) and the canonical quickstart.
## TL;DR — the 90% case
1. **Reads:** `exec("cat …")` for one doc, `exec("ls -A …")` for a directory (folder defaults + template menu), `exec("grep …")` for literal, `search` for ranked retrieval. Native `Read` / `Grep` only on source code (`.ts` / `.py` / …), never on in-scope `.md` / `.mdx`.
2. **Writes:** `write({ document: { path, content } })` for a new or full-replace doc; `edit({ document: { path, find, replace } })` for a body find/replace; `edit({ document: { path, frontmatter } })` for a frontmatter merge-patch (`null` deletes a key). `delete({ document })` removes, `move({ from, to })` moves/renames. Body find/replace is body-only. Pass a one-line `summary` (≤80 chars, user-facing outcome) on every content write.
3. **Preview / open a doc — determine your ONE surface FIRST (once per session).** Stop at first match: **`OK_DESKTOP_TERMINAL` or `OK_HOSTED_AGENT` set** → you're inside OpenKnowledge (desktop terminal / in-app agent panel) → `ok open <name>` (switches the window the user is already looking at); never paste a `localhost` URL into your reply here · in-app browser (Claude Code Desktop's Browser pane, Cursor, Codex) → `preview_url`, then open/navigate it to the doc · else plain CLI → `ok open <name>`. `ok open <name>` opens a doc or folder (auto-detected); `--skill <name>` for a skill. The `previewUrl` field is a route id, **not** your open mechanism. Don't `preview_screenshot` to confirm edits. Full Step-0 procedure + per-surface how-to: `references/preview.md`.
4. **Knowledge layers:** capturing a source (ingest), synthesizing findings (research), promoting a decision (consolidate) — procedures, **not tool calls**; there is no `ingest` tool. Ingest ships here (`references/ingest-and-sources.md`); research + consolidate come with the `knowledge-base` pack. Layer model + packs: `references/starter-packs.md`.
5. **Direct questions:** a plain business question ("which customers…", "what did we decide about…") routes to `search` / `exec` + a cited chat answer — no "research" keyword needed. Persist only when durable + multi-doc + not already covered, and *offer* first. See `references/corpus-qa.md`.
6. **Authoring or improving a skill** ("write/make/improve a skill", "turn this into a skill"): STOP and invoke **`/open-knowledge-write-skill`** for scope (project/global), contract, evaluation, and install. Author through `write({ skill })`, never a document path. Skills are real folders under editor `skills/` dirs (`.claude` · `.cursor` · `.codex` · `.github` · `.opencode` · `.pi` · `.agents`): one source plus managed copies/symlinks. **Read/edit via `skills` and `edit({ skill })` — they route to the source.** Never hand-edit a non-source copy: managed copies refresh from the source; editing one forks it and stops refresh.
## Tool index — 21 tools (router; the MCP tool descriptions carry each tool's full contract)
- **Reads** — `exec` (primary; `cat`/`ls`/`grep`/… on a read-only filesystem, plus frontmatter/backlink/history enrichment; one command or one pipe, not a shell), `search` (ranked BM25 + recency), `history` (doc versions), `links` (`kind: backlinks|forward|dead|orphans|hubs|suggest`, or an array for one call), `skills` (search + read: `query` → skills.sh; omit `name` to LIST managed (Project + Global); `name` READs one — by `name`+`scope`, never path), `config` (resolved config), `palette` (authoring forms + `html preview` starters + theme tokens; `palette({ components })` for JSX schemas), `preview_url` (browser preview URL on demand), `share_link` (GitHub-substrate share URL; read-only, errors without a GitHub remote), `lint` (markdown-lint violations: `document` for one doc, omit for the project; `fix: true` with `document` auto-fixes fixable rules in place — attributed, live in the preview; the rest need `edit`/`write`), `audit` (every lint violation + broken internal link in one read-only report, by source file with lines; `path` scopes; for link VALIDATION use this, not `links`; caveats in `references/linking.md`). **Read `ran` on successful `lint`/`audit` results to see which enabled source families were selected. A family absent from `ran` was not checked, and `[]` means no checks were selected at all.**
- **Writes** — four native CRUD verbs, polymorphic over `document` / `folder` / `template` / `skill` / `asset` (pass EXACTLY ONE target, nested under its address key): `write` (create/overwrite; `write({ skill: {…} })` authors a skill as a REAL folder at the project's default skill home — live immediately for that folder's agent), `edit` (body find/replace/frontmatter merge-patch; no asset), `delete` (remove), `move` (move/rename, rewrites referrers; a skill also takes `scope`/`toScope` for Project↔Global — history resets, re-`install`). Output mirrors the input key; the preview envelope (`previewUrl`, `warning`) stays top-level. Plus `install` (WHERE a `skill` lives: `add`/`remove` locations additively — editor ids, `agents`, or custom roots; `mode` + `convert` re-form ONLY the locations named; `source` moves the real folder. The source folder IS the skill — no "uninstall everywhere"; a skill dies only via `delete`), `import` (acquire a skill-dir into `add`'s locations; scripts never run), `checkpoint` (named version), and `restore_version` (roll back). A folder's frontmatter is open-shape and self-only (does NOT cascade); templates are what new docs start with.
- **Conflicts** — `conflicts` (`kind: list|content`), `resolve_conflict` (write a resolution + commit; destructive). See `references/conflict-resolution.md`.
**Self-correcting on misuse:** constraints JSON Schema can't express ("exactly one target", "`find` needs a `replace`", body-XOR-frontmatter) return `isError: true` with a one-line corrective shape. Read it and retry with that shape; don't guess.
Tools NOT in OK MCP (your host's): `preview_start`, `preview_screenshot`, `WebFetch`, `WebSearch`, native `Read` / `Grep` / `Glob` / `Edit`. The STOP rule governs which you may use on in-scope markdown.
## STOP — native tools on in-scope `.md` / `.mdx`
**Route every in-scope markdown read and write through OK's MCP tools — never your host's native file tools.** Native `Edit` / `sed` / direct `Write` on in-scope markdown bypasses the CRDT and loses agent attribution in the shadow repo; native reads skip frontmatter, backlinks, shadow-repo activity, and project git history that OK returns for every matched file. When this workspace has OpenKnowledge MCP configured, do **not** use native file tools on markdown paths inside the content directory. The ban covers every common rationalization:
- **Native `Read` / `Grep` / `Glob` on in-scope `.md` / `.mdx`** — the original case.
- **`Bash ls` / `Bash find` / `Bash cat` on dirs containing in-scope markdown** — use `exec("ls -A …")` / `exec("find … -name '*.md'")` / `exec("cat …")`. Native returns bare names; `exec` adds frontmatter, backlinks, and recent activity. `-A` shows hidden entries without `.`/`..`.
- **Glob patterns that target markdown** — `exec` expands file operands (`cat specs/*.md`); quoted patterns and a command's own pattern (`find -name`) stay literal.
- **Dispatching the Explore / general-purpose subagent for markdown-heavy exploration** — subagents use native tools internally and bypass OK. Do markdown exploration yourself via `exec` / `search`. Subagents remain appropriate for **source-code** exploration.
- **Native `Read` / `Grep` on in-scope markdown inside `.ok/`** — `.ok/` is in-scope; treat its `.md` / `.mdx` like any other KB file.
- **`ls` / `cat` / `find` on skill folders to discover or read a skill** — skills are addressed by `name`+`scope`, not by path (a skill can live in any editor dir, the `.agents/skills/` hub, or a custom root, with copies elsewhere). Use the `skills` tool.
**Not seeing `exec` is NOT the escape hatch.** Wiring, labels, and tool visibility vary by client; some (notably Codex) defer MCP tools behind lazy discovery. Registration is the test, not top-level-symbol visibility — run tool discovery for `open-knowledge` first. Detail: `references/setup.md`.
**Escape hatch.** Native `Read` / `Grep` / `Glob` on `.md` / `.mdx` is allowed **only** when, after running tool discovery (above), no OpenKnowledge MCP server is registered for this project, **or** immediately after you actually invoked an MCP call and it failed — then begin a user-visible sentence with `OpenKnowledge MCP unavailable:`. "Not registered" is a conclusion you may only reach after tool discovery turned it up empty — never from the initial tool list alone. Never use the hatch because you skipped your client's MCP path, didn't see `exec` as a top-level tool, didn't run tool discovery, or rationalized the skill wasn't necessary.
**Source code and non-markdown files** (`.ts`, `.py`, `package.json`, …): native `Read` / `Grep` / `Glob` always.
## Reads — examples
- Read a file: `exec("cat <path>.md")` — contents + full enrichment.
- List a directory: `exec("ls -A <dir>")` — per-child frontmatter, recursive markdown counts, most-recently-updated doc per subdir, the folder's own `title`/`description`/`tags` + `templates_available`. Prefer `-A` over plain `ls`.
- Literal search: `exec("grep -rn <term> <dir> | head -5")` — matches + enrichment on matched files.
- Ranked search: `search({ query })` — title boost + body BM25 + recency; use when picking the best doc, not when listing every occurrence.
## Writing
Call `write` / `edit` as soon as you have content (route through MCP per the STOP rule).
**Persist incrementally — the knowledge base IS your checkpoint (MUST).** On any multi-step or long-running task — a research sweep, a multi-source synthesis, a batch of docs — write completed work to the KB as you finish each unit: per section, per source, per doc. Never hold finished findings only in your context waiting for one final write at the end. A rate limit, crash, or context compaction mid-task discards everything still unwritten; work already persisted survives, and you resume by reading the doc back. Create the target doc early (skeleton + frontmatter), then `edit` each section in as it firms up.
**Pass a `summary` on every content write (SHOULD)** — a one-line (≤80 char) user-facing note; it becomes the timeline entry. **Reach for visual structure** (Callout, `mermaid`, table, `html preview`) where it carries the point better than prose; call `palette` as you draft. Advisory write-warnings, MDX authoring, delete/move mechanics, and visual authoring: `references/writing.md` + `references/components-and-visuals.md` + `references/media-and-assets.md`.
## Grounding — every factual claim needs a source (MUST)
KB docs are factual artifacts: every claim traceable, and **the source lives inside the knowledge base**, not on the public web.
**Ingest is a procedure, not a tool** — binary-vs-text classification, SSRF-safe fetch flags, size + executable gates, wrapper frontmatter — in [`references/ingest-and-sources.md`](references/ingest-and-sources.md). Read it before your first capture; a naive fetch-and-paste skips every gate.
- **Closed loop.** External sources are pulled in by the ingest procedure, then cited locally. A bare `[source](https://...)` inside a KB doc is **not** a citation — it is a TODO meaning "still needs ingesting". The chain only works if every leaf is a local doc.
- **Every factual claim MUST cite its source at the point of claim.** No unsourced speculation.
- **Web sources** → fetch the page (host `WebFetch` / `WebSearch`), ingest it, then cite the path: `[source name](./path/to/source.md)` (the local doc carries `source_url:`). Inline `[source](URL)` is a chat affordance, not a KB one.
- **Self-fetched counts.** A URL YOU fetched to ground a claim gets the same ingest — no inline-URL downgrade.
- **Internal cross-refs** → link the OK doc holding the authoritative claim; that doc cites its own sources (chains terminate in preserved local docs).
- **No evidence?** Search and ingest the result, OR mark `(TODO: needs source)`, OR don't write the claim. Do NOT fabricate — unsourced speculation rots into untraceable tribal lore.
## Linking — standard markdown links (MUST)
Link every noun-phrase that names another document — `[text](./relative/path.md)` — and link liberally. **Every link must resolve to a doc that exists by the time you're done** (a same-pass forward-reference you create later in the pass is fine; for one that genuinely won't exist, leave the mention as plain prose + a tracked task). Never backtick a link (`` `[text](./foo.md)` `` is a bug) and never use HTML `<a>`. **Read `brokenLinks` on every `write`/`edit` response: `[]` means all links resolve; a populated list names each broken `href` + `reason` (`no-such-doc` / `no-such-file` / `unresolvable`) — fix them in a follow-up `edit`.** `audit` is the authoritative end-state link check (the editor's red-underline is slug-tolerant and lies, so trust the tool). External web sources are NOT inline body links (see Grounding). Full rule set + the `[[Page]]` legacy note: `references/linking.md`.
## Folders, frontmatter, templates
Every `.md` / `.mdx` needs YAML frontmatter — `title` + `description` required, `tags` recommended. **OKF projects (`okf` pack) are the exception:** pack rules win — a non-root `index.md` carries NO frontmatter (the `frontmatter-reserved-index` lint warns on any key), `log.md` needs none, and concept docs need only a non-empty `type`; `title`/`description` are optional there. Two **opt-in, nested** folder mechanisms: folder frontmatter (`<folder>/.ok/frontmatter.yml` — the folder's own open-shape properties; self-only, does NOT cascade into child docs) and templates (`<folder>/.ok/templates/` — what new docs start with). Most folders have NO `.ok/`. A doc's frontmatter is exactly its own on-disk YAML. Structural model + the full pre-write checklist: `references/folder-model.md`. Template authoring + folder editing: `references/template-authoring.md`. Frontmatter-vs-body edit rules: `references/doc-editing.md`.
- **Read the folder before writing (MUST).** Before creating/editing docs in a folder, call `exec("ls -A <folder>")` once per folder per session — it returns the folder's `title`/`description`/`tags` + `templates_available`. Skipping it lands docs that violate folder discipline. (If a folder has no frontmatter AND no templates AND the repo has substantial content elsewhere, it isn't onboarded — run `references/onboard-existing-repo.md` first.)
- **Use a template when one fits (MUST).** Instantiate via `write({ document: { path, template } })`; inherited templates count. Skip only when none match or the user asked for free-form (note why in chat). Create templates proactively when a shape recurs.
- **When recurring per-doc properties emerge (MUST).** Writing the same frontmatter on multiple siblings → bake those starting values into a template (`write({ template })`). Folder frontmatter does not cascade values into docs.
## Conflict-aware writes
Projects with GitHub sync may carry docs in merge-conflict state; mutating calls against them return RFC 9457 `urn:ok:error:doc-in-conflict` (409). Detect proactively — `exec("cat <path>.md")` returns `lifecycle: {status, reason} | null`; on `status === 'conflict'` switch to the `conflicts` + `resolve_conflict` flow. Full flow: `references/conflict-resolution.md`.
## Anti-patterns — the top offenders
| Task | Don't | Do |
| --- | --- | --- |
| List / find / read markdown | `Bash: ls`/`Glob: **/*.md`/`Read: foo.md` | `exec("ls -A …")` / `exec("find …")` / `exec("cat …")` |
| Explore a markdown-heavy dir | `Agent(Explore)` (bypasses OK) | `exec`/`search` yourself |
| Reference another doc | `` `[text](./p.md)` `` (backticked) or HTML `<a>` | `[text](./p.md)` |
| Embed an image | `<img>`, a `localhost`/`preview_url` URL, hot-link | save locally + `` |
| Factual claim in a KB doc | prose with no citation, OR inline `[src](https://…)` | ingest the source (`references/ingest-and-sources.md`), cite the local path |
| Confirm an edit landed | `preview_screenshot` / verification loop | trust the CRDT tool response |
| Delete a markdown doc | `Bash: rm` / native deletion | `delete({ document })` (`checkpoint()` first if risky) |
| Write in an unfamiliar folder | straight to `write` | `exec("ls -A <folder>")` first |
Full table: `references/anti-patterns.md`.
## Knowledge layers — the shape most KB work takes
Three recurring practices, not tool calls — each a full procedure that ships as skill guidance.
| Layer | When | Procedure |
| --- | --- | --- |
| **ingest** | Preserve a shared URL/PDF/file verbatim, or you fetched a URL to ground a claim (binary sources preserved, not scraped). | `references/ingest-and-sources.md` — ships here, §Grounding depends on it |
| **research** | Investigate / compare / synthesize sources → `status: provisional` article + `sources:`. | `/research-with-sources` skill |
| **consolidate** | A decision was made → canonical source-of-truth with a `supersedes:` chain. | `/consolidate-notes` skill |
Research and consolidate arrive with `ok seed --pack knowledge-base`. **Without that pack you do not have those procedures** — don't improvise one; do the work as an ordinary grounded `write`, or offer to seed it (`ok seed --pack knowledge-base --dry-run` shows what it would add).
Don't chain silently: let the user drive ingest → research → consolidate, and a procedure's STOP gates override session-level "don't stop to ask" hints. After any turn that changes KB content, check for a `log.md` and follow its contract (`references/cadence-and-logs.md`). Interleave a multi-doc batch so the preview shows narrative progress.
Onboarding a repo that already has content: `references/onboard-existing-repo.md`. Layer model + packs: `references/starter-packs.md`.
## Capabilities beyond this skill
OK does more than this skill describes, and what it does changes between releases. When a request implies something not covered here, read rather than guess:
- **Docs** — <https://openknowledge.ai/docs>
- **Source** — <https://github.com/inkeep/open-knowledge>
## Server lifecycle
If `write` / `edit` returns `"Hocuspocus server is not running"`, run `ok start` (via Bash) and retry. Never fall back to native `Edit` / `Write` for in-scope markdown.
## Scope recap
OK looks for documents under the resolved `content.dir` (runtime: `config({ key: 'content.dir' })`); `.gitignore` and `.okignore` (at root or any folder depth) define exclusions. **Every `.md` / `.mdx` under `content.dir` not excluded is an OpenKnowledge document** — including under `specs/`, `reports/`, `docs/`. Folder metadata + templates live in nested `<folder>/.ok/`, not in `.ok/config.yml`. **Working in a git worktree?** Pass the worktree's absolute path as `cwd` on your OK tool calls once — it sticks for the session, so reads, writes, and the preview all target that worktree.