Adopt and maintain an Open Knowledge Format v0.2 documentation bundle: frontmatter, generated indexes, a fail-closed conformance check, and coverage that names every unindexed document
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
The Open Knowledge Format is a directory of markdown files with YAML frontmatter. That is the whole format: no schema registry, no central authority, no runtime. Its value is not the frontmatter — it is that a corpus becomes enumerable, so an agent answers "which document covers X" from one generated index read instead of grepping a tree and guessing from prose.
This skill operates on the bundle: structure, frontmatter, indexes, and the entry points routing
agents into it. It never writes a document's body — authorship belongs to the repo's own templates,
and to the adr skill for decision records. Keeping that line sharp is what makes it safe to run
over documentation it did not write.
Target: OKF v0.2. Read references/frontmatter.md for the field families and
references/index-format.md for the frozen index grammar before writing either.
docs/okf.yaml is not part of OKF v0.2 — it is a convention some toolchains use to declare a
dialect, and a profile: key in it names which documents must carry which keys. Read it if it
exists and say what it declares, because it changes how a conformance verdict should be read.
It does not change what gets indexed, and this skill no longer refuses a repository for carrying one. The refusal it replaces rested on two claims nothing ever checked: that a profile ships its own index generator, and that a commit gate somewhere compares the index byte-for-byte and would reject v0.2 output. Where those hold, they are worth respecting — so check them rather than assume them: is there a generator, and is a gate actually armed? Where they do not hold, refusing means the repositories most likely to want an index are the ones guaranteed not to have one, and a guard whose condition nothing can satisfy is a defect wearing a guard's clothes.
If a repository really does regenerate its index from another tool, that path belongs in
.okfignore, which is the mechanism for "another tool owns this" and states it per path instead of
per repository. Full argument: references/adoption.md.
| Owns | Delegates |
|---|---|
| Bundle layout and the chained root indexes | A concept document's body and its claims |
| Frontmatter presence, shape, and repair | What type a new kind of document should be |
| Index generation, everywhere, bottom-up | Requirement text, acceptance criteria, ADR decisions |
Which paths are in scope (.okfignore) | Anything another tool generates or owns |
Removing log.md and in-document history | The repo's existing document templates |
Agent-entry wiring in CLAUDE.md / AGENTS.md / GEMINI.md | Navigation and lookup at read time |
| §11 conformance verdicts | Any profile-specific dialect |
typeOKF v0.2 §11 requires exactly one thing of a concept document: a parseable YAML frontmatter block
containing a non-empty type. A document carrying only type is fully conformant.
---
type: Playbook
title: "Incident response: data freshness alert"
description: Steps to triage a freshness alert on the orders pipeline.
tags: [oncall, incident]
---type is a free string — no central registry — so pick self-explanatory values and reuse them
consistently, because type is what the index groups by. Near-synonyms (Runbook and
Playbook) fracture one section into two.
title and description are optional to the spec but load-bearing here: they are the two fields
the index projects. A document with no description contributes a bare link and answers nothing
before it is opened. Derive it from the document's own opening sentence — never invent a summary of
content you have not read.
Stamp the optional families only when there is a real fact to record — generated, verified,
status (draft/stable/deprecated, absent means stable). Field families, the actor
convention, and why a date-only stale_after is ignored: references/frontmatter.md.
Every directory containing markdown gets an index.md, up to and including the repo root, listing
every markdown file in it — unless .okfignore excludes it, or it is plugin payload (both
below). A document is listed
because it exists, not because its folder was registered anywhere: a reader hunting for the
contributing guide or a stray plan cannot know the corpus filed it as furniture, and an index that
confidently omits it sends them back to ls.
Listing costs the document nothing. The row's title degrades — frontmatter title, then the first
body heading, then the filename stem — so a file with no frontmatter is listed exactly as well as
one with, and being listed never obliges it to carry keys. What must carry keys is a separate
question, answered by check and unchanged.
Generate them with the bundled script, always:
node ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs index <bundle-root>Pass the repo root as the bundle root. It walks deepest-first, so a subdirectory's description exists by the time its parent is written, and it is idempotent.
Two things it will not touch, both reported rather than done quietly:
.git entry is a submodule or a
nested clone, and the repo you invoked on only pins it. Writing there edits someone else's
repository, shows up in a git status nobody was looking at, and coverage cannot catch it
because git reports a submodule as a single gitlink. The walk stops at the boundary and says
separate-repo: <path>/. If that tree needs an index, generate it from inside that repository.index.md it did not write. Every generated index carries the marker in its first
content line; one without it is hand-maintained, or another tool's output, and its rows may carry
an id, a status or a shape v0.2 does not project. Overwriting is a silent lossy downgrade of the
exact catalog the index exists to be. It is left alone and reported as foreign-index: <path>.
Read it, then either delete it to hand this tool the directory, or name it in .okfignore to
leave it with its owner. Hand-rendering is the most reliable
way to introduce drift — sort order, separator and trailing newline vary between one writing and the
next, and nothing fails when they do. A stale-looking index is a regeneration task, never a reason
to grep the folder.A description over 160 characters is dropped rather than truncated, leaving the bare link
an absent one would leave, and named on stderr as long-description: <path>. A machine-cut
half-sentence would be a summary no author wrote, planted in the field consumers trust most.
The script cannot summarise a subdirectory, so it reports needs-description: <path>. Read
enough of it to write one honest line and supply it:
node ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs index docs --describe requirements="Functional and non-functional requirements."It then persists by round-tripping through the generated index, so it is written once. A directory holding exactly one described document inherits that description and is not reported.
At a Claude Code plugin root — a directory holding plugin.json or marketplace.json, either at
the package root or under .claude-plugin/, both of which the loader accepts — the commands/,
agents/ and skills/ children belong to the loader. Every
.md under commands/ is a slash command, every .md under agents/ is an agent definition,
and a skill folder's entry point is SKILL.md, carrying Claude Code's frontmatter schema rather
than OKF's. Writing an index.md there puts a document where the loader expects payload; demanding
type there asks a SKILL.md for keys that are not its schema and makes a progressively-disclosed
reference file pay context for keys nobody reads.
So the walk never descends into one, check never scans one, coverage never demands one, and each
pruned directory is reported as plugin-payload: <path>/. This is refused structurally rather than
left to an .okfignore line, for the same reason another repository's work tree is: the line can
only be written after the first run has already done the damage.
The anchor is the manifest, never the directory name — a docs/commands/ folder documenting a
CLI is ordinary knowledge and stays indexed. Delete the manifest and every one of those files is
enumerated again.
.okfignore — the paths this skill does not ownA real bundle root holds paths another tool owns: delivery logs, generated projections, vendored
docs. List them at the bundle root — one path per line, trailing / for a directory, # to comment.
docs/plans/
docs/TRACEABILITY.mdA listed path is not enumerated, not checked, and never stamped — three consequences of the one fact that it is not yours. Matching is exact, with no globs, so a blank line matches nothing rather than everything.
Exclusion fails green, so it is reported: every skip as ignored: <path> (.okfignore:N), every
line matching nothing as unused-ignore:, and a line broad enough to empty the corpus drops both
commands to 77 instead of a quiet success.
Prefer fixing the owner. A generated file whose generator stamps type needs no line here — it is
an ordinary concept document with a real description, which is where an important one belongs.
.okfignore is for what you cannot make conformant.
Never create log.md (§9), and git rm any that exist. Strip ## History, ## Changelog,
## Revision history, version tables, and Last updated: lines from concept bodies.
One reason covers both: each is a hand-maintained copy of what git already stores, they disagree the
first time someone commits without updating them, and nothing fails when they do — so the drift
is found by a reader who trusted the wrong one. Deleting loses nothing; the content is in history.
The spec's answer to "when" is generated.at and verified[].at (§5.2): point-in-time facts, not
an accumulating record. This is the one place the skill is more opinionated than the spec — say so,
and point at git log --follow.
An index nobody is told to read saves nothing.
node ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs wire <bundle-root>That writes a marker-delimited block into CLAUDE.md and AGENTS.md, creating either if absent and
leaving existing content alone:
<!-- okf:entry -->
## Documentation
Start at [index.md](index.md). Every documentation folder carries a generated `index.md` listing
each document's title and one-line description — answer "which doc covers X" and "does a doc for Y
exist" from that index in one read, and open a document only after the index names it. Do not grep
`docs/` for a document's identity; grep stays correct only for a literal phrase inside a body that
the index cannot carry.
<!-- /okf:entry -->The markers make the block idempotently updatable: a later run replaces what lies between them
rather than appending a second copy. GEMINI.md uses import lines, so it gets @index.md instead.
wire exits 1 until an index.md exists — pointing agents at a missing file is worse than not
pointing them anywhere.
It writes those files into the bundle root, so the bundle root must be the repo root: wire docs produces a docs/CLAUDE.md nothing reads. Chained roots are the design — ./index.md is the
address you give agents, and it links docs/ onward.
Keep the block's second half. Without the stated grep exception the rule reads as overreach and is ignored wholesale, costing more index reads than it buys.
docs/okf.yaml if it exists and report what it declares. A profile:
key scopes required keys; it is not a reason to stop..okfignore now, before it produces violations you would try to fix by hand.node ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs check . before writing anything. It prints one note: per
description the index will refuse — that is the worklist, shorten each to a sentence. A
corpus that already has frontmatter needs this, not a pass that overwrites it.type, title, and a description drawn
from the document's own opening. Read each file — one invented from a filename is a fabricated
claim in a machine-readable field.git rm any log.md; strip history sections and Last updated: lines.okf.mjs index at the repo root, so ./index.md, docs/index.md and every folder
index are written as one chain. Supply --describe for each reported directory.node ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs wire .node ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs check . must exit 0,
and okf.mjs coverage . must exit 0 too. check alone cannot tell you a document was never
walked — it and the index are the same projection..okfignore line and why, and any
description you could not derive without guessing — leave it blank and say so.Step 3 may follow step 6 instead. An index of titles and links with few descriptions is a correct intermediate state: every title still routes, where an index of paragraphs outgrows the documents it enumerates and sends readers back to grepping.
Run after any change to the corpus: a document added, renamed, retyped, or re-described.
check . — fix what it names; act on unused-ignore:, which means a declared path moved or
was deleted and the line now protects nothing.index . — regenerate, and commit it with the content change, never as a follow-up commit
someone forgets.coverage . — the added document is the one most likely to be orphaned, and it is the only
check that can say so.wire . — a no-op unless the block's wording changed, and safe to run every time for that
reason.Delete a removed document's file and regenerate; leave no tombstone entry. A superseded one gets
status: deprecated and stays — §5.4 exists so links do not break.
node ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs check <bundle-root>It checks §11 and nothing stricter: parseable frontmatter, non-empty type, reserved filenames used
only for their reserved purpose. It does not fail a bundle for missing optional fields, unknown
type values, extra keys, broken links, missing indexes, or an over-long description — the spec
forbids rejecting on those, and a check that invents its own strictness trains people to ignore it.
Over-long descriptions come back as note: lines: the repair worklist.
Exit codes: 0 conformant, 1 violations named, 77 nothing was evaluated, 64 usage error.
Treat 77 as a failure to verify, never as a pass. The scan found no concept document at all —
a wrong path, an empty tree, or an .okfignore broader than intended. A clean bill issued over zero
files is the most convincing wrong answer a checker can give.
Frontmatter is parsed by a bundled reader — no YAML library, so the verdict is identical on every
machine. It fails closed: an unterminated quote, flow sequence or flow mapping, a duplicate key,
or nested content it cannot parse is a violation, because silent tolerance is how a corpus rots
behind a green light. Block scalars (>, |) are folded into their text — the danger in a
hand-rolled reader is never the syntax it rejects, which is loud, but the syntax it misreads.
check cannot performnode ${CLAUDE_PLUGIN_ROOT}/skills/okf-maintain/scripts/okf.mjs coverage <bundle-root>check and "regenerate, then diff against the committed index" both read the corpus through the
same walk. A document that walk never reaches is therefore missing from the committed index and
from the regenerated one, the two agree perfectly, and the run is green. A projection compared
against itself cannot report a missing input — not because the comparison is sloppy, but because
the missing input is absent from both sides by construction.
coverage gets a second, independent enumerator: git ls-files --cached --others --exclude-standard, resolved from the repository root. Every markdown file it lists that no
index.md links to is named:
unindexed: .github/CONTRIBUTING.md
okf.mjs: 113 tracked document(s) in 44 index file(s), 1 reachable only by lsThose three flags are the contract, not defaults. Plain git ls-files reads the index, so a
document written but not yet staged is invisible to it — and that is exactly the document at risk,
the one being added right now. A check blind to it passes, the commit lands with no row, and the
next commit belatedly adds the previous document's row with nobody the wiser. --others covers the
working tree; --exclude-standard keeps the repo's own ignore rules authoritative so build output
stays out. Indexes are likewise read from disk, so running straight after index and before git add tells you the truth about what you are about to commit.
Exit 0 when every document is reachable and no index is stranded, 1 naming each finding, 77
outside a git work tree — which is a refusal to verify, never a pass.
Two ways to clear a finding, and the choice is the whole point: index the document, or declare in
.okfignore that something else owns it. There is no third option where it stays invisible, which
is what the old behaviour amounted to.
Only an index the root index reaches may vouch for a document. index writes but never
deletes, so anything that narrows what gets indexed — a new .okfignore line, a pruned plugin
payload directory, the last document leaving a folder — strands the index.md it stops maintaining
on disk with its rows intact. Counting those rows credits a document that nothing a reader can
follow leads to, and the debris is invisible to every other check: an orphan is not an unreached
document, it is a reached nothing, so the totals read identically before and after it is removed.
Generated strays are therefore named as orphan-index: and their rows do not count. Delete
them — the generation marker is the evidence they are this tool's own leavings. A stray with no
marker is somebody else's file and is never named, though its rows do not count either.
A row whose document git will not commit is named too. index reads the working tree, so a
document written and not yet added is listed on purpose — that is what lets the regeneration hook
index it on the edit that created it. What happens next is the hazard: git commit -a stages
modifications to tracked files, so the updated index goes into the commit and the document it now
vouches for does not. The result is self-consistent and wrong, and every other enumerator here reads
the working tree, where both files are present, so nothing else can see it. Such rows are reported
as dangling-row: naming the index that carries them; git add the document, or delete it and
regenerate. Scoped to indexes that are themselves tracked — an untracked index is not going into
that commit either, so there is no half-commit to warn about.
Dot-directories are the one place index will not go, and coverage says so when a finding lands
there. The walk skips them because they hold tooling — descending reaches .git, .venv, and every
editor's cache — so no amount of regenerating produces an index inside one. That leaves two honest
answers and one trap. Move the document out of the dot-directory if it is real documentation; name
the path in .okfignore if it is machine output. Do not hand-write the missing index.md: it is
outside the walk, so nothing regenerates it, and it rots unseen — which is the exact failure this
command exists to surface, reappearing at the one place the tool declines to reach.
The plugin ships a PostToolUse hook (hooks/okf-index-regen.mjs) that runs index whenever a
markdown document is written or edited inside an adopted bundle. A projection refreshed only by hand
is stale from the first time somebody forgets, and nothing reports it until coverage runs — so
every OKF repository is a mechanical client of the generator rather than a manual one.
What it refuses, all of it before any write:
okf.yaml (at the root or under docs/). Adoption is the opt-in; without
it the hook would stamp indexes into every repository you happen to edit markdown in..okfignore, and a file that is not markdown.ownsIndex rule index itself applies.okf_version than the installed generator writes. The marker is
versionless, so an older plugin would silently regenerate a richer catalog into a poorer one. The
declaration is read from okf.yaml and from the root index's own stamp, and the higher wins.The root is resolved from the edited file — git -C <dir of the file> rev-parse --show-toplevel,
with the file required to be inside the answer — never from the session's working directory. A cwd
default is how an edit aimed into a linked worktree or a submodule rewrites the session repository's
indexes while the edited one stays stale: the wrong tree written, the right tree not, and no error
on either side.
It is never gated on check. A repository can carry frontmatter violations and still owe its
readers an accurate index; the two questions are independent. It writes only indexes whose bytes
differ, and it fails open — every path exits 0, because a hook that can block an edit trades a
stale index for a stuck session.
The hook does not retire the manual command: it fires only on edits made through Claude Code, so a
human editor, a merge or a rebase still leaves the projection stale, and coverage is still what
proves it.
index.md. The next regeneration silently discards it. Change the
source frontmatter instead.foreign-index: file to make the report go away. That is the one action that
destroys what the report was protecting. Read the file first: if its rows carry an id or a status,
the directory belongs to whatever produces them, and the answer is an .okfignore line.description to fill a column. An empty one is a visible gap; a fabricated one is
a false claim in the field consumers trust most.## Changelog back because a reviewer asked. Point at git log --follow.77 from check or coverage as green. Both mean nothing was evaluated, and a
clean bill issued over zero files is the most convincing wrong answer a checker can give.check as "everything is indexed". It cannot mean that: it walks the same
tree the index came from. Only coverage compares against a list the walk did not produce..okfignore..okfignore to silence a violation. It declares who owns a path, not which
complaints you would rather not see. A document you own with no type is a document to fix.