The engine contract for Profile package format 3 and OKF 0.2 exactly.
Status: Accepted for wayfinder 0.5.0 (unreleased)
The 0.5 stability contract identifies the public compatibility and support boundaries. Acceptance of this contract is not publication or a claim that release verification has completed.
Wayfinder checks an OKF bundle in two independent steps. It runs okf's own validation first, then the rules of the Profile the project selects. A Profile is a package of vocabulary and rules that adds requirements to OKF (§5). This guide is the engine contract. It says what a Profile package is, how the engine selects and evaluates one, what results it reports, and how tools that generate, validate, and adopt bundles behave.
Precedence is a chain, and every link is one-directional:
OKF wins over the engine contract, which wins over each Profile package.
The engine contract is this guide plus the JSON Schemas it publishes under
docs/schemas/. A Profile package can add rules within
what the contract allows (§5.3). It can never change OKF or the contract.
This guide holds no Profile's rules. Bitwild, the first Profile, appears here
only as an example; its own rules, rationale, and history live in
profiles/bitwild/.
Conformance means what a tool decides, never what a reader infers. A bundle
conforms to OKF when okf reports no error. A bundle conforms to a Profile when
wayfinder validate with that Profile selected ends with the gate PASS
(§4.1). No prose adds a requirement that the validator does not check. A
Profile's rationale and its skill explain and guide; they are not conformance
requirements (§5.1).
"Guide" does not mean advisory. The requirements below carry their RFC 2119 force. They constrain programs and packages, not bundles. "A generator MUST be idempotent" constrains a tool; "a package MUST NOT declare an OKF key" constrains a Profile author. A bundle meets the rules of the Profile it selects, read the way this guide says.
The audience is whoever writes a tool, writes a Profile, or stands up a repository. That is a handful of people per project, and they read it once.
Conventions. MUST, MUST NOT, SHOULD, and MAY carry their RFC 2119 senses.
"Tool" means any program that reads or writes a bundle. "Bundle" means the
directory a wayfinder.json entry names in applies_to.
Adopting a Profile creates one project binding, two bundle root files, and one agent-instruction paragraph, in this order:
- Select the Profile. Write
wayfinder.jsonat the project root with one entry keyed by the Profile's id. Give it the Profile's Git source and the explicit bundle path inapplies_to(§4.7). Bitwild's entry is keyedbitwild-profilewithpathprofiles/bitwild. Resolve it withwayfinder getand commit the metadata-onlywayfinder.lockand the Profile skill directoriesgetinstalls. Add only project-specific types, tags, relationship names, and actors that actual knowledge uses. - Seed the bundle. Create
index.mddeclaring the package's OKF release asokf_version, andlog.md. A Profile's skill may carry literal templates in its adoption guidance. The adoption skill'sSEEDING.mdcarries OKF's own for a Profile without them;KnowledgeSeedsinpackage:wayfinder/adoption.dartholds the same text. When the Profile requires generated indexes,wayfinder validate --fixwrites them once the bundle holds concepts (§3.1). - Write the repository's agent instruction paragraph.
AGENTS.md(or the equivalent) MUST say that durable documentation lives in the bundle and that the reader starts at its rootindex.md. A Profile's adoption guidance may add to it. Bitwild's adds that execution records stay in the tracker.
Adoption is not a migration. A bundle with no wayfinder.json above it
cannot be assessed (§4.4). A bundle that used the Bitwild 2026.2 in-bundle
selector keeps validating on wayfinder 0.1.x until it migrates, as the
Bitwild changelog describes.
Seed only what the Profile names. Seeding writes what the Profile's adoption guidance names. Without that guidance it writes only the OKF root files. Bitwild's adoption guidance, for example, says to create no subject directories.
A host can adopt in process with Wayfinder's KnowledgeAdoption. It plans
steps 1 and 2 without writing in the project, so a person sees every file
first, then applies them whole or not at all
(ADR-0020). It returns step 3's
paragraph as text, because a repository's agent configuration is its own,
and adds the bundle's .okf.lock, which okf keeps at the bundle root, to
.gitignore.
A repository does not migrate its existing documents as part of adoption.
Adoption gives new knowledge a home. Converting old knowledge is a separate,
scheduled piece of work that follows the selected Profile's own guidance, such
as Bitwild's migration reference.
A repository MAY run for months with a thin bundle beside an unconverted
docs/ tree, provided §2.1's paragraph says which is authoritative for what.
Adoption is complete when a validator run ends with the gate PASS, every
generated index is current, and someone who has never seen the repository can
find the authoritative home for a new decision without asking. The third is
the real test and it is not mechanical.
A Profile can require every index to be okf's generated output by naming the
matches-generated builtin with generator: "okf-index" (§5.6). Bitwild
does. This section binds the tools that write and check that output.
The okf-index generator is OkfIndexGenerator from the okf release the
engine depends on, run over the loaded bundle with the root index declaring
the package's implements.release as okf_version. A tool that writes or
checks those indexes MUST produce exactly that generator's text. okf names
the bytes its generator writes with a generator revision,
okfIndexGeneratorRevision: the okf release that last changed that output
for any bundle. okf changes the output only in a minor release, so every
release in one minor series writes the same bytes. Wayfinder depends on a
range of okf releases within one revision (^0.7.0-beta.1, revision 0.5.0) and
reports the release it runs, okf's okfVersion, as JSON engine.okf and a
SARIF tool.extensions entry (§4.1). Golden reports over fixtures with
generated indexes fail when a resolved okf release changes the generator's
output. The generated text is engine contract, so §5.6 says what an engine
release that changes it owes its users.
A validator MUST report every path the generator writes whose file is missing
or whose text differs from the generated text. The comparison is exact apart
from line endings: the generator's rendering is the contract, so no
presentation is left to an implementation and no semantic normalization
applies. A CRLF line ending, which a checkout may write, compares equal to LF,
and --fix leaves such an index unchanged.
wayfinder validate <bundle> --fix is the safe fix, in the convention of
eslint --fix and ruff --fix. When a rule in the selected chain uses
matches-generated, it first writes the generator's output, then validates
and reports as usual. It MUST:
- Write only generated
index.mdfiles, and only those whose bytes differ. It never deletes a file: anindex.mdthe generator does not write stays a finding until the author deletes it. - Write nothing when OKF fails (
BLOCKED BY OKF), when no Profile is selected, or when the selected chain has no fixable rule. It reports why with thewayfinder/fix-not-appliedwarning diagnostic (§4.1). - Be idempotent. A second run over its own output writes nothing.
- Refuse to write through a symbolic link at any path segment below the bundle root, and replace each file atomically.
- Report a failed write. When a write fails it stops, reports the files
already written, and reports the failure as the
wayfinder/fix-failederror diagnostic. A partly written bundle cannot pass the gate (§4.1). - Hold okf's exclusive bundle lock (
.okf.lock) while it loads the bundle again, writes, and validates, whenever it has something to write, so it never interleaves with an okf write or read. A fix with nothing to write takes no lock. A lock it cannot take is a failed write: it writes nothing and reportswayfinder/fix-failed. Taking the lock leaves.okf.lockin the bundle, which okf's loader, and any inventory that stands for what okf loads, ignores at any depth.
When a fix ran, its JSON report adds fix.written, the bundle-relative paths
it wrote in path order, between diagnostics and gate (§4.1). Without
--fix the report is unchanged.
okf index <bundle> --declare-version 0.2 --check reports the same stale
paths from the okf command line.
The root log is not an index input or output. A generator MUST preserve it unchanged; an author or authoring workflow records meaningful history separately. Current concepts and version-control diffs may assist that workflow, but an implementation MUST NOT claim they can reconstruct the log's significance or completeness mechanically.
A validator checks the result a generator maintains. A repository MAY write indexes by any means and rely on validation to catch drift. The generator is an implementation convenience, not a required bundle artifact.
A tool that changes a bundle on its author's behalf, such as KnowledgeWriter
in package:wayfinder/authoring.dart, applies the change through okf's write
path and judges the result before it writes (ADR-0019).
It MUST:
- Rewrite only the generated files the change affects. When a rule in the
selected chain uses
matches-generated, write the generator's text (§3.1) for exactly the indexes whose generated text differs between the bundle and the candidate. Every other index keeps its bytes, even when it is stale; rewriting every stale index is the--fixrepair, and a tool that offers it writes exactly what--fixwrites. - Leave history to the author (§3.2). Record only a log entry the author supplies, or okf's own entry for the change, and keep every entry already there.
- Judge the candidate as validation would after the commit, in memory and with the same selection: OKF over the whole candidate, and the Profile's findings compared with the bundle's by rule id and path. Refuse a candidate OKF rejects, or one that adds a Profile error or an error diagnostic, and write nothing. When no Profile can be selected, the candidate is judged by OKF alone, and the tool says so instead of implying a Profile passed it.
- Write exactly the bytes it judged under okf's exclusive bundle lock, and refuse when the bundle, or the configuration or lock its selection came from, changed since it judged them.
The command surface is wayfinder validate <bundle> [--config <file>] [--fix] [--output text|json|sarif]; §3.1 defines --fix. The bundle path is required and implementations MUST
inspect exactly that directory. Config discovery MAY walk up to find the
project's wayfinder.json, but MUST NOT select a different bundle. They MUST NOT
accept a caller-selected Profile or rule set, or provide --strict or
another switch that promotes recommendations into requirements.
Text and JSON MUST expose three distinct results and the gate they derive:
| Result | Values |
|---|---|
| OKF conformance | PASS, FAIL |
| Profile assessment | PASS, FAIL, BLOCKED BY OKF, NOT ASSESSED |
| Diagnostics | a list, each entry at level error, warning, or note |
| Gate | PASS, FAIL, INCOMPLETE |
The independent OKF report MUST remain intact and Profile findings MUST NOT
reclassify it. The OKF component is okf's own report projection: one
canonically ordered findings array with namespaced identifiers and the
two-tier error/advisory severity model, in which a file that failed to load
is an error finding like any other. OKF conformance is PASS exactly when
that report holds no error-severity finding; the validator judges it
non-strict, so an OKF advisory never fails OKF conformance. If OKF fails,
deterministic Profile validation MUST stop as BLOCKED BY OKF without
cascading findings from partial content. NOT ASSESSED means no Profile was
selected, and an error diagnostic always says why.
A diagnostic is the engine reporting on its own run, never on the bundle. Its
id is wayfinder/<code> from a closed set the engine owns, so no Profile
package can declare, suppress, or reuse one. A diagnostic carries a message
and, when it has one, a location that is either a bundle-relative path or the project
configuration file as the caller named it. Configuration diagnostics say why
a Profile could not be selected or what the configuration adds to it.
Execution diagnostics say what the run could not do. Configuration
diagnostics are reported even when OKF fails, so one run names every problem
that blocks a complete assessment.
| Code | Level | Reported when |
|---|---|---|
config-missing |
error | no wayfinder.json is found above the bundle |
config-invalid |
error | the configuration cannot be read |
bundle-unbound |
error | the configuration does not apply to the bundle |
profile-unresolved |
error | the lock is missing, stale, or lacks the bundle's chain, or a package is not in the local cache or disagrees with the release or parent the lock records |
profile-invalid |
error | a package in the chain is malformed, such as a schema violation, a bad id, a repeated name, an OKF frontmatter key, or rule examples that disagree with their check |
profile-unsupported |
error | a package in the chain is well-formed for another engine, with another format, an OKF release this okf cannot read, or a builtin this engine lacks, or a rule schema keyword, $schema dialect, format or $ref form outside the engine subset; use a matching engine or migrate the package (§5.4) |
profile-timeout |
error | reading a package in the chain, its rule tests included, did not finish within the engine's bound (§5.6) |
profile-composition |
error | the chain and the project additions do not compose (§5.7) |
profile-skill-stale |
warning | a chain member ships a skill whose installed copy is missing or not at the locked commit (§4.7) |
project-type |
note | the configuration adds a type to the Profile types |
link-graph-unavailable |
error | the OKF link graph could not be built, so link rules were not assessed |
fix-failed |
error | a --fix write failed |
fix-not-applied |
warning | --fix was requested but did not run |
internal-error |
error | the run stopped before it had a result |
The gate is derived, never stored. It is FAIL when OKF fails or any Profile
finding is an error, because one witness proves non-conformance and missing
assessment cannot unfind it. Otherwise it is INCOMPLETE when any diagnostic
is an error, because a rule that did not run cannot pass. Otherwise it is
PASS, so a PASS always means every selected rule ran. Exit 0 means
PASS, exit 1 means FAIL, and exit 2 means INCOMPLETE, including
usage and I/O outcomes. Advisories, warnings, and notes MUST NOT change the
gate or exit status. The validator assesses a Profile's rules and nothing
else. Guidance in a Profile's skill is never assessed, so no output may claim
it was.
The result model also carries summary entries beside the findings. A rule of
severity note reports something its Profile permits but wants visible, such
as Bitwild's unresolved internal link. Its entries MUST NOT appear among the
findings or change any component state or the exit status. JSON carries them
as profile.summary, a canonically ordered list whose entries have a
finding's id, message, location, profile_release, and help_uri but
no severity. The list is present, even when empty, whenever the selected
chain declares a note rule. Text prints a Summary: block after the
findings when there is an entry.
JSON carries the results as okf, profile, diagnostics, gate, and
engine, in that order, with fix between diagnostics and gate when a fix
ran (§3.1). diagnostics is always present, even when empty, and gate is
{"state": ...}. Whenever a Profile was selected, including under BLOCKED BY OKF, profile names it. Its id and release are the bundle's own
package, and chain lists every package in the selected chain, root ancestor
first, as {id, release, commit} with the locked commit. Only NOT ASSESSED
omits all three. An agent loads one Profile skill per chain entry, so it can
read a Profile's guidance while it repairs an OKF failure. engine is {"okf": "<version>"}, the okf package version
the engine generates indexes with, so a byte change in generated output reads
as an engine upgrade rather than a Profile change. A finding or summary entry
whose package names docs carries help_uri, that URI with the fragment set
to the rule id. Text prints a Diagnostics: block after the summary when
there is one, and ends with Gate: and the gate's value. When --output is
json or sarif and the run stops before it has a result, the output is
still a parseable result. It holds only the wayfinder/internal-error
diagnostic and the INCOMPLETE gate, so a machine reader never mistakes
empty output for a pass. Its SARIF log has no rule descriptors and no
results, carries the diagnostic as its one execution notification, and has
only gate among the run properties, because no OKF or Profile result
exists.
sarif carries the same findings as JSON in a SARIF 2.1.0 log for code
scanning. Each OKF and Profile finding is one result whose ruleId is its
finding ID; error maps to SARIF error and advisory to warning. Each
summary entry follows the findings as a result of kind informational with
level none, as SARIF §3.27.10 requires for a kind other than fail.
The selected chain's rules are the run's rule descriptors, each with a
helpUri when its package names docs. The okf package version is a
tool.extensions entry named okf. Each diagnostic
is a notification on the run's invocation, in toolConfigurationNotifications
or toolExecutionNotifications by its kind, with one driver.notifications
descriptor per code reported. executionSuccessful is false exactly when a
diagnostic is an error. The OKF state, Profile id and release, Profile
state, and gate are run properties because SARIF has no field for them. The exit status is the same
as for text and JSON.
Every deterministic Profile finding MUST carry a stable machine-readable ID
alongside its prose, <namespace>/<rule-slug>, where the namespace is the
id of the package that declares the rule, such as bitwild-profile
(§5.4). It MUST name the release of the package that assessed it. The ID
MUST describe the semantic rule rather than a section number, implementation
class, or message text, so integrations can depend on it across refactoring.
The validator MUST NOT accept suppressions or exceptions. A bundle cannot change the rules its project binding selects, and a caller cannot change them through command options or repository configuration.
The engine, its builtins, and its diagnostics MUST NOT report, at any severity:
- a concept with no
verifiedevent, or any derived trust tier; - an external URL that does not resolve;
- a concept using an OKF mechanism that no rule in the selected chain names;
- an unknown type, tag, relationship name, or actor as invalid OKF.
Only a Profile rule reports vocabulary, and it reports a Profile finding. A Profile package MUST NOT contain a rule that reports either of the first two items (§5.3). The first matters most. The only way an author can clear a "missing verification" report is to record a verification that did not happen, which turns a diagnostic into a corruption of OKF's evidence model.
A tool MAY summarize trust tiers and organizational provenance. A summary MUST NOT affect exit status.
Validation keeps syntax separate from contextual truth. A rule decides from the bundle, the packages in the chain, and the project binding. It never decides whether a type fits a concept, whether metadata is truthful, or whether a placement is right. Those are judgment, and a Profile carries judgment in its skill (§5.1). A rule that reports a frontmatter key no package declares does not break tolerant reading. The engine still loads the concept, keeps the unknown key, and leaves the OKF result unchanged. Tolerant reading governs consumers. A Profile rule governs what that Profile's producers write.
The implementation MUST use the ordinary OKF graph and MUST NOT enrich,
reinterpret, or replace its edges. An implementation that projects a graph
or expands search context MAY read each relationships entry as an
additional edge named by its relationship and resolved as the OKF graph
resolves a link target (§5.5). OKF's nodes and edges and their meanings stay
unchanged. A graph output adds such edges under a key of its own, never
inside OKF's edge list, so a consumer of the OKF graph contract reads it
unchanged.
Validation MUST NOT probe the network. An external resource that no longer resolves remains ordinary OKF provenance or a body link.
A validator MUST dispatch through wayfinder.json alone. For a bundle named
by exactly one applies_to path, it follows the lock's extends pointers
from that entry's id to the root, reads each package from the local cache at
its locked commit, and dispatches on each package's format, which MUST be
3, the package format this engine reads. A Profile's release is its own
name and never selects engine behavior. The chain need not reach any
particular Profile. The engine verifies safe paths, each package's OKF
binding, and that the chain composes (§5.7). A package is data the engine evaluates, read from
the cache at the locked commit, never from the network.
A bundle with no wayfinder.json above it leaves the Profile NOT ASSESSED
with the wayfinder/config-missing diagnostic and exit 2. A bundle that an
existing wayfinder.json does not list gets wayfinder/bundle-unbound the
same way. A file in the bundle never selects a Profile, so a Bitwild 2026.2
in-bundle declaration changes neither outcome. Such a bundle validates on
wayfinder 0.1.x or migrates to a project binding as the
Bitwild changelog describes. One engine
carries one dispatch path, not a frozen copy of every Profile release.
An explicit --config path MUST exist and name the requested bundle. A
package this engine cannot read leaves the Profile NOT ASSESSED with the
wayfinder/profile-unsupported diagnostic and exit 2, preserving any
independent OKF result. A malformed or unreadable selector also prevents
dispatch, with the configuration diagnostic §4.1 names. The
validator MUST NOT silently apply the newest rules or reinterpret an old bundle
because a neighboring project config exists. Generic OKF reading remains
available even when Profile dispatch fails.
A validator SHOULD run in CI on any change touching the bundle. One wayfinder validate <bundle> invocation is the complete model-independent gate; a
separate OKF command MAY still be useful for focused upstream diagnostics.
CI MUST NOT treat INCOMPLETE as a pass, and MUST NOT present a Profile
skill's guidance as checked.
docs/compatibility-review.md records the
engine's review against its okf package and the pinned OKF specification.
It answers whether the engine keeps OKF's result and meaning intact. Each
Profile records the OKF review of its own rules and declared keys in its
changelog (§5.10).
wayfinder.schema.json describes the
project configuration, and §5 describes the package it points at. A
version-1 project file has one form: direct Profile sources and the
bundle paths they apply to. It has no bundles, implements, or
default_bundle key.
{
"version": 1,
"profiles": {
"bitwild-profile": {
"source": {
"git": "https://github.com/btwld/wayfinder",
"ref": "main",
"path": "profiles/bitwild"
},
"applies_to": ["./knowledge"],
"types": [
{"name": "Project Note", "description": "A project-specific durable note"}
]
}
}
}The map key is the package id, not an alias, and a package whose id
differs from its key is invalid. source.path names the package directory in
the locked revision. Types, topic tags, relationship names, and actor lookup
are the only project additions. A project entry cannot declare frontmatter
keys or rules, and it never wires a chain, because a package names its own
parent (§5.7). A project that needs its own rules writes a package, which can
live in the same repository. A registered project type is reported as the
wayfinder/project-type note diagnostic (§4.1). Whether the type fits its
concepts is judgment for the Profile's skill.
Every entry applies to at least one bundle. applies_to paths MUST be
relative, unique, inside the project after symlink resolution, and not nested
within one another. Several bundles MAY share an entry. A directory inside a
bundle inherits that bundle's Profile and never selects another, and
ambiguous bundle application fails Profile dispatch. wayfinder.json and
wayfinder.lock are project configuration at the project root, never OKF
concepts. A bundle sits strictly inside the project, so the engine never reads
its configuration from inside a bundle or writes it into an index.md.
A relative local source.git is resolved from the directory containing
wayfinder.json, not the process working directory. Cache identity uses that
resolved location; the lock preserves the declared source spelling. A
drive-relative path is invalid.
get resolves every entry's chain and writes wayfinder.lock atomically.
The lock is flat. Its packages map holds each Profile id's source,
requested_ref, resolved_commit, path, release, and, for a child,
extends, the parent's id. A project locks one source per Profile id, its
git, ref, and path, resolved to one commit. An id therefore names one
ruleset and one finding namespace everywhere in the project. get checks
that when it adds each package. When two chains describe one id with
different sources, get fails and names both sources, even when they
resolve to the same commit. get converges.
With an unchanged configuration, lock and cache, it fetches nothing and
leaves the lock's bytes alone. upgrade deliberately refreshes mutable refs.
A canonical-JSON hash invalidates the lock on semantic config changes, not
formatting changes; an unchanged source retains its locked commit during
get. The lock contains revision metadata, never project knowledge,
credentials, vocabulary or rules. The local Git cache holds fetched
objects.
If a current lock's selected commit is absent, a read-only command does not
substitute the current branch tip: run get to recover the exact commit or
upgrade to select a new revision deliberately. Failed resolution leaves the
previous lock intact.
A package MAY name its agent skill directory in skill, relative to the
package. After writing the lock, get and upgrade install each locked
package's skill from its locked commit into .claude/skills/<id>/ and
.agents/skills/<id>/ under the project root, with a .wayfinder-profile
marker recording {id, release, commit}. The skill therefore always matches
the rules it accompanies. Only regular files at paths inside the skill are
installed, and the skill MUST hold SKILL.md; anything else fails get. Each directory is replaced by
renaming a staged sibling into place. A directory whose marker already names
the locked revision is not rewritten, so get still converges. get MUST
NOT replace or remove a directory without a marker for its id, and fails
before writing the lock when one is in the way. It removes a marked directory
whose Profile no longer ships a skill or is no longer locked. Projects commit
the installed directories, so agents without wayfinder read the same skill.
validate reads each chain member's marker without writing; a missing
directory or a marker for another revision is the wayfinder/profile-skill-stale
warning (§4.1), which never changes the gate.
Validation operates on one explicit bundle. The implementation first
inspects it under OKF 0.2. A failing OKF result blocks Profile assessment but
is never reclassified. It parses the selected project config, whose
diagnostics are reported even when OKF fails, checks safe
path/identity/release and reads only the selected Profile chain from a
current lock/cache, checking each package's release and parent against
the lock. Missing or stale source state leaves
the Profile NOT ASSESSED with the wayfinder/profile-unresolved diagnostic
alongside the independent OKF report; neither
CLI validate nor read-only MCP validate fetches or writes a lock. The
engine then evaluates every rule in the chain (§5.6). An ancestor project
config that does not list the bundle never applies to it silently (§4.4).
An unknown package never falls back to the newest rules.
graph projects the ordinary OKF graph, plus the relationships edges §4.3
permits, without Profile-source resolution.
Embedding index and search consume either an explicit bundle or the project's
configured applies_to paths without resolving unused Profile sources. The
explicit-path forms do not consult neighboring configuration; project discovery
reports a malformed configuration rather than omitting bundles. These commands
do not generate Profile navigation indexes. A neighboring malformed wayfinder.json cannot make ordinary OKF
graph reading fail. Those command boundaries keep OKF reading independent of
Profile selection (§5.2).
The parser checks wayfinder.json and each Profile package against their
published JSON Schemas, evaluated by Ack within the engine's keyword subset
and embedded in the binary, then runs the cross-document checks a schema cannot
express. Those cover normalized and canonical paths, package parents, package
identity, rule tests, composition of the effective vocabulary, and actor
lookup. A schema alone cannot prove filesystem safety, Git availability, or
whether a concept truthfully uses a type or topic tag. A directory beside the
bundle, such as a captures/ directory, stays outside it unless a binding
names it as another bundle.
Frontmatter is checked in two layers. The okf package validates the pinned
OKF 0.2 field shapes and keeps unknown content for tolerant reading. The rules
of the selected chain then add each Profile's constraints, such as Bitwild's
required status. §5.5 says which frontmatter keys a package may declare.
The engine has no type-specific property schema. OKF frontmatter is an
upstream contract, not a project-owned object model. A project type records
only a name and a description. It MUST NOT add properties, required
fields, or frontmatter keys, so wayfinder.json never becomes a second
frontmatter schema. A Profile that needs a type-specific constraint writes a
rule over existing OKF fields or over a key it declares. Information with no
OKF field belongs in the concept body or in a key a package declares under
OKF §4.1. A new meaning for an OKF field needs an upstream OKF change first.
Tags stay OKF topic strings, and a generic OKF consumer tolerates any tag. A
package's types, tags, and relationships lists declare vocabulary, and
the project binding adds names to each. Composition rejects a repeated name
and a tag that collides with another vocabulary (§5.7). What an undeclared
used name means is a Profile rule's choice. Bitwild reports an undeclared tag
as an error. A Profile that wants an open vocabulary writes no such rule.
A Profile is one package: a directory at one Git revision whose
wayfinder-profile.json holds everything wayfinder validate enforces for it.
wayfinder-profile.schema.json
publishes its shape. One parser reads every package, Bitwild's included. The
engine embeds no Profile, holds no installed rules, and treats no Profile as a
base or a default. The
create-profile skill writes and
revises packages. This section is the contract every package meets.
A package holds its identity and OKF binding (§5.4), its vocabulary (§5.5), its
rules (§5.6), and optionally its parent (§5.7). It MAY also name docs, a URI
for its rationale, and skill, a directory of agent guidance that get
installs (§4.7).
Conformance to a Profile is the gate PASS with that Profile selected: OKF
passes, every rule in the chain ran, and no rule reported an error. Nothing
else is a conformance requirement. A package's README and skill explain its
rules and guide the judgment its rules cannot decide, such as whether
knowledge earns a concept or a placement fits its subject. Where the README
or the skill disagrees with the package, the package governs and the prose is
in error. A Profile that wants a requirement enforced writes it as a rule. A
requirement that no rule can check is guidance, and no tool reports it as
assessed.
The engine runs okf's validation before any Profile rule. When OKF fails, no
Profile rule runs and the Profile state is BLOCKED BY OKF, so a Profile
never reports cascading findings from content OKF rejected. A Profile result
never alters, reclassifies, or replaces the OKF result. Every Profile
therefore holds OKF conformance as a precondition. No package can relax it.
OKF reading never depends on a Profile. graph, index, and search read
the explicit bundle without resolving Profile sources (§4.7), and a broken
Profile selection leaves the OKF report intact.
A Profile is stricter than OKF and never contradicts it. Every rule and every declared key MUST pass one compatibility test:
- it uses only constructs OKF 0.2 permits;
- it keeps the upstream meaning of every OKF field and reserved file;
- it leaves the OKF graph contract uninterpreted;
- it leaves the independent OKF result unchanged;
- it leaves the bundle readable by a generic OKF consumer that knows nothing of the Profile.
A rule that needs a new OKF field or a new meaning for one fails the test. Its author pursues the change upstream before any Profile depends on it.
A rule MUST NOT report a concept's missing verified event or a derived trust
tier, and MUST NOT require a value that only a false statement could supply.
OKF §5.3 gives the absence of verification meaning, and an author can clear
such a finding only by recording a verification that did not happen.
A Profile narrows OKF only through its rules. Where no rule applies, OKF governs, and anything OKF permits stays permitted. Silence defers to OKF; it is never a gap to fill with a local convention. A Profile's skill follows the same rule. Where the skill is silent, an author reads the pinned OKF specification and follows it.
id is the Profile's one name. It is lowercase kebab-case starting with a
letter, at most 64 characters, the Agent Skills name limit. The names okf,
wayfinder, and those of wayfinder's own skills are reserved. The id is the
finding namespace (§4.2), the key in wayfinder.json and the lock, and the
name of the installed skill directory, so no mapping between them exists.
release names the package's content. The engine never branches on it. Each
Profile picks its own scheme; Bitwild uses <year>.<serial>.
format names what the engine must understand to read the package. This
engine reads format 3, and the value is a JSON integer. A non-integer
number such as 3.0 is malformed, not an alternate spelling of format 3.
A package with another integer format, including 2, is reported as
wayfinder/profile-unsupported, never read the old way or partly applied.
Format 3 removes directory-name policy from the sibling_directory fact
(§5.6). A format 2 package needs an explicit migration, not merely a new
engine: its author reviews uses of that fact, publishes a format 3 revision,
and the project moves its binding's source ref to that revision before
refreshing the lock with get or upgrade (§4.7).
implements binds the package to one OKF release, {"id": "okf", "release": "0.2"}. Every package in a chain binds the same release, and this engine
reads OKF 0.2. The root index of a bundle declares that release as
okf_version.
docs is a URI. Each finding's help_uri, and each SARIF rule's helpUri,
is that URI with the fragment set to the rule's id. The page at docs
SHOULD carry a heading per rule id so every help link lands on its rule.
types, tags, and relationships each list names with a one-sentence
description. A project binding adds names to each list (§4.7). These lists
are producer-side declarations. They let a Profile's rules see drift, and
they never license a generic OKF consumer to reject a concept.
frontmatter_keys lists the producer keys a package adds under OKF §4.1,
each with the sentence that defines it. A declared key MUST NOT be a key OKF
0.2 defines, and the parser rejects a package that declares one. A declared
key MUST NOT give an OKF field another meaning, which the package's author
reviews (§5.10). Keys are unique along a chain. A project binding cannot
declare keys.
relationships is the engine's one link field. Each entry of a concept's
relationships list names a relationship and a resource, and the engine
reads it as a typed edge resolved as the OKF graph resolves a link target
(§4.3). A Profile that wants typed links declares the relationships key
and names its relationships.
A rule has an id slug, a category, a severity, a status, a
description, a message, and a check. A schema rule also has tests. A
builtin rule has none, because exercising a builtin needs a bundle. Severity is error,
advisory, or note. An error finding fails the gate. An advisory asks for
action and never changes the gate or the exit status. A note is a summary
entry (§4.1). A check is either a JSON Schema over one subject's facts or a
builtin. The parser runs each rule's tests and reports a package whose
examples disagree with its check as wayfinder/profile-invalid. A rule's
pattern runs on a backtracking regular-expression engine, where a pattern
can take far longer than any rule needs, so the engine bounds reading a
package: one whose parse, its tests included, does not finish within the
engine's bound, ten seconds in this engine, is refused whole and reported as
wayfinder/profile-timeout, and one whose parse stops on an error, such as a
stack overflow, as wayfinder/profile-invalid. A package's tests should
finish in milliseconds.
Current implementation gap: ProfilePackage.parseWithin normally enforces
that bound with a killable isolate, but falls back to synchronous parsing without
a timeout after IsolateSpawnException. The fallback does not meet the bounded-read
guarantee above; hosts needing a hard limit must isolate the work themselves.
This documents the existing gap, without changing package format or the contract;
see host operating boundaries.
A builtin is a check the engine implements and versions with its release. Any
package may call one by name with params. The rule's id, severity, message,
and docs belong to the package; the capability belongs to the engine. A check
becomes a builtin only when a schema over one subject's facts cannot express
it, because it needs disk I/O beyond the parsed bundle, a comparison with
generated output or a --fix, a finding about something absent, or more than
one finding per subject. A builtin is named for what it checks. It never
holds policy, which is its params, never names a Profile, and never reports
engine health, which is a diagnostic. The create-profile
builtins reference lists
each builtin with its params.
Facts describe the loaded bundle without any Profile's directory conventions.
The concept fact sibling_directory is true exactly when the loaded file
inventory contains a directory beside the concept whose name is the concept's
filename without .md. For billing.md this is billing/; for
references/vendor.md it is references/vendor/. Every path is treated the
same. Directories are those implied by files in the loaded inventory, not
empty directories found by additional disk I/O. This definition also applies
to an in-memory candidate that has not been written. A Profile that excludes
particular names expresses that policy in its rule schema over path and
sibling_directory; the engine excludes none.
Path-target checks use the loaded inventory for every in-bundle file or
implied directory, including in-memory candidates; they never recover a removed
candidate file from disk. Disk lookup applies only to relative targets outside
the bundle. Bundle-root parent escapes, raw or decoded backslashes, encoded
separators and malformed percent escapes report a missing target. A literal
percent in a filename must be encoded as %25. URLs and prose source
descriptors remain outside the path check; an explicit /, ./ or ../
prefix identifies a path even when it contains whitespace.
Generated text is engine contract. matches-generated compares a bundle with
the output of the generator in the engine's okf release (§3.1), so the
bytes a Profile accepts depend on that release's generator revision as well
as on the package. The engine reports the okf release as engine.okf. A
wayfinder release that moves to an okf generator revision with different
output MUST say so in its changelog as a breaking change for every Profile
that uses matches-generated, and its users regenerate with --fix. A
wayfinder release that moves to another okf release with the same revision
changes engine.okf and no verdict. A Profile that omits the version param
never has to change its own release for either. One that sets version
names a generator revision and fails to load on an engine whose generator
has any other revision, so its verdicts change only when the Profile moves
the pin.
A package names its own parent in extends, and a project entry never wires
a chain, so a Profile means the same thing in every project that uses it. A
parent without git, {"path": ...}, is the package at that
repository-root-relative path in the same repository at the same commit, so
Profiles hosted together release together. A parent with git, {"git", "ref", "path"}, resolves from its own repository and ref. Its git is a URL
or an absolute path, never a path relative to one project. A chain need not
reach any particular Profile. A cycle fails get.
Composition only adds. A child adds rules in its own namespace and adds
vocabulary. It cannot remove, replace, or re-grade an inherited rule, so an
ancestor's rule definition stays unchanged. Format 3 has no field that
changes an inherited rule. A later format may add an explicit one in the
child package, never in wayfinder.json.
The engine composes the chain and the entry's project additions into one
effective Profile before evaluating anything. Composition depends on nothing
else, so the same inputs always compose the same way. Every rule uses that
effective vocabulary, including rules inherited from an ancestor. A child
or project addition can therefore change an inherited rule's findings: for
example, a tag-declaration check accepts a tag the child adds. The rule's
definition, severity, namespace, and release do not change. A type, tag, or
relationship name MUST be unique across the chain and the project additions.
A frontmatter key MUST be unique along the chain. A tag MUST NOT equal a
type, an OKF status, an OKF trust tier, or a relationship name. A violation
is a composition error. get composes every chain before it writes the lock,
so it reports the error and leaves the lock as it was. validate reports it
as the wayfinder/profile-composition diagnostic. A project entry that
repeats one of its own names is a configuration error (config-invalid).
Validation evaluates the rules of every package in the chain, root ancestor
first. Each finding names its package's id as namespace and its package's
release, including when it uses vocabulary a child adds. A project
locks one source per Profile id (§4.7), so a namespace in its reports
always means one ruleset.
The engine selects a Profile only through wayfinder.json and its lock
(§4.4). It never falls back to the newest rules, never guesses a Profile from
bundle content, and never applies part of a package. When it cannot select
and read the whole chain, the Profile state is NOT ASSESSED and an error
diagnostic says why: config-missing, config-invalid, bundle-unbound,
profile-unresolved, profile-invalid, profile-unsupported,
profile-timeout, or profile-composition (§4.1). Only then does JSON
omit profile.id, profile.release, and profile.chain. The gate is then
INCOMPLETE with exit 2, unless OKF or an evaluated rule already failed
it. A PASS always means every selected rule ran.
Consumers MUST keep OKF's tolerant-reader behavior (OKF §11). Unknown concept types, unknown frontmatter keys, unknown relationship names, unregistered actors, missing optional content, and broken links MUST stay loadable. Unknown frontmatter SHOULD stay available to downstream consumers rather than being dropped on round-trip. Valid OKF that a Profile does not describe is content to carry forward untouched.
A Profile's rules judge what its producers write. They never change how a consumer reads. The engine's graph, index, and search commands read a bundle the same way whichever Profile a project selects. External resource availability is never a check (§4.3).
The engine contract changes through an ADR, an entry in this guide's change
record (§9), and a wayfinder release. A change that would make the engine
read an existing package differently bumps format, so an older package is
reported as unsupported rather than reinterpreted. An upstream OKF
specification release needs an engine compatibility review
(docs/compatibility-review.md) before
the engine reads it.
Each Profile changes through its own release process, in its own repository. A release MUST carry a change record that names the rules and vocabulary it changes, the real-use driver, and the migration impact in words: whether bundles that passed still pass, and what they must do if not. It MUST record the compatibility test of §5.3 for each new or changed rule and declared key, and MUST NOT claim compatibility with an OKF release it was not reviewed against. A published release is a Git ref that never moves, so a lock always reproduces it. Bitwild's records are its changelog.
This repository distributes the engine and the generic skills, as the root
README.md and skills/README.md
describe. Each Profile distributes itself as a package in a Git repository,
and a project names it from wayfinder.json. No project vendors a copy of a
Profile. Two requirements bear on bundles and so are stated here:
- A Profile's skill must reach every environment that writes to a bundle.
An agent that never read the skill cannot apply the judgment the Profile's
rules leave open, such as where a concept belongs. A Profile's skill ships
in its package and reaches a project through
get, pinned to the locked commit and committed with the lock (§4.7). - The tools must be pinned per repository and dispatch on the packages
wayfinder.jsonselects (§4.4), so repositories on different Profiles and releases can share one implementation.
Deferred, and it stays deferred until a second bundle exists to design against — a referencing mechanism designed against one bundle would encode that bundle's shape.
Until then, a bundle referencing another bundle's concept uses an ordinary URL link, which OKF already tolerates and which a graph loader treats as an external node. A project MUST NOT invent a cross-bundle identifier scheme in the meantime; a URL that later becomes a first-class reference is a rewrite of one link kind, while a private scheme is a migration.
A tool claims conformance to this guide by satisfying, for the package format it reads:
- the generator contract (§3), if it writes indexes;
- the exit codes (§4.1), finding IDs (§4.2), the prohibitions (§4.3), and version dispatch (§4.4), if it validates;
- tolerant reading (§5.9) in both cases.
A tool MAY implement a subset — validation without generation is the common case — and states which. Nothing here licenses a tool to reject a bundle that is valid OKF.
Entries below are newest-first decision history. Earlier format numbers and "next wayfinder release" wording describe the decision at that time, not additional formats this engine reads. The current contract in §5.4 reads format 3 only; historical entries are not compatibility promises for format 2.
Package format is a JSON integer, 2026-11-05.
Affected section: §5.4. Driver: accepting a finite whole-number JSON number
such as 3.0 would make a second spelling of format 3 part of the 0.5
surface. Implementations report a non-integer format as malformed and an
integer other than 3 as unsupported. No package migration: a package encodes
format as an integer.
Retrieval source and ingestion corrections, 2026-11-04.
Affected surfaces: retrieval unresolved-source, citation Markdown and ingestion.
Review found copied path classifiers disagreeing with validation; retrieval now
uses the §5.6 inventory-first resolver and carries its decisions into display.
Missing or malformed targets remain raw labels, while valid targets receive a
bundle prefix. Freshness rejects malformed UTF-8 as invalid bundle input.
A retained encoder is discarded when its own call throws or returns a vector
of the wrong dimension or a non-finite component.
Ingestion validates IDs before writes with digests and temporary disk storage;
metadata duplicates write one final value per batch. Store-batch atomicity
remains the transaction boundary. No Profile format or OKF meaning changes;
callers should fix malformed citations, budget temporary disk, and rebuild
indexes whose source citations or constructor chunking changed.
Adoption request and Git cleanup corrections, 2026-11-04.
Affected surface: adoption host requests and Git subprocess cleanup. Review
found normalized parent aliases and any-case .git segments accepted at the
request boundary; they now fail with adoption-invalid before planning.
Hosts must pass a direct project-relative bundle path, including seed
concepts. Git cleanup keeps the root frozen through its kill, discovers late
forks and uses one inspection budget. A failed listing kills verified
descendants instead of resuming them. Package format and OKF semantics do
not change.
Path-target candidate and decoding corrections, 2026-11-04.
Affected sections: §5.6, the builtin reference and public resolvePathTarget.
Driver: no-space source paths escaped the graph's invalid-target branch, and
candidate deletions could be hidden by stale disk files. Use one inventory-first
resolver for validation and retrieval, with disk lookup only outside the bundle.
Packages keep format 3; malformed URI escapes now report rather than borrowing
a literal filename, and candidates require every cited internal target in their
own inventory. Encode literal percent signs and fix affected citations before
writing. No OKF field meaning changes.
Profile-neutral directory facts, package format 3, wayfinder 0.5.0.
Affected sections: the guide header, §§4.1, 4.4, 5.4, 5.6, 5.7, and 9; the package
schema and generic authoring guidance are updated with them (ADR-0029).
Driver: independent Profiles and Bitwild are both resolved through the
shipped example/CI workflows, but the shared sibling_directory fact
excluded interactions/ and references/ even for a Profile that chose no
such directory convention. Bitwild's existing mirrored-source and time-axis
exceptions belong in its package, not in all Profiles' input facts.
Migration for implementations: accept only format 3, report format 2 as
profile-unsupported before reading its rules, and compute the same-stem
sibling-directory fact from the whole loaded inventory without name
exclusions. No OKF field, graph, loader or generator meaning changes.
Migration for packages: review every use of sibling_directory, encode any
chosen directory exemptions in the rule itself, and publish a format 3
revision through the package's release process. A package whose own rules
do not use this fact needs no rule changes, but its parents must also be
migrated. The chain's verdicts stay unchanged only when no rule in it uses
the fact or every affected package explicitly preserves its prior policy.
Bitwild's unpublished 2026.3 revision moves its existing exceptions
into concept-area-name-collision, preserving its findings. Migration for
projects: move the selected package's source ref, and every pinned parent
ref, to migrated revisions, then refresh and commit the lock and installed
skills with get or upgrade. An immutable old ref cannot advance merely
because upgrade was run. Until migration, a format 2 selection is
NOT ASSESSED with an INCOMPLETE gate (exit 2) when OKF passes, never a
silently different Profile verdict. An older engine remains an option for
an intentionally unmigrated package.
Shared effective vocabulary, next wayfinder release. Clarifies §§5.7
and 9 and the package schema descriptions (ADR-0027). Driver: the shipped
examples/profiles/two-rule-child declares billing; an inherited
tag-declaration check accepts that tag when the child is selected, contrary
to §5.7's former claim that ancestor findings never change. Every rule
already uses the whole chain's and project's vocabulary. Migration for
implementations: retain that composition and evaluation order; do not bind
an ancestor to an ancestor-only vocabulary. Migration for packages and
bundles: none; existing verdicts and package interpretation are unchanged,
so format stays 2. Rule definitions cannot be overridden, but their results
can depend on added vocabulary. The generic package-authoring guidance and
a composition golden now make that distinction explicit.
Bounded package reading, next wayfinder release. Binds no Profile and
keeps package format 2. §5.6 bounds reading a Profile package, its rule
tests included, and refuses one that runs past the bound as the new
wayfinder/profile-timeout configuration diagnostic, which §§4.1 and 5.8
add to the closed set. Affected sections: §§4.1, 5.6, 5.8, and 9. Driver: a
host that embeds resolution found that a package whose tests run a pattern
that backtracks without end, such as ^(a+)+$ on a near miss, held get,
validate and every selection forever, and nothing on the parsing isolate
could stop it. Migration for implementations: read a package where its
parse can be stopped, refuse it whole once it runs past a bound well above
what real packages take, and report profile-timeout; report a parse that
stops on an error as profile-invalid. Migration for
packages: none. Bitwild, the largest shipped package, parses with its tests
in about five milliseconds, so no package reads differently, and a package
that would take ten seconds was already unusable.
Profile-aware writes, next wayfinder release. Binds no Profile and keeps
package format 2. The new §3.5 binds a tool that writes a bundle on its
author's behalf; §3.4, the 2026.2 projection removed below, stays unused so
earlier entries keep their meaning. §3.1 adds that --fix holds okf's
exclusive bundle lock while it writes, reports a lock it cannot take as
wayfinder/fix-failed, and leaves .okf.lock, which an inventory of what
okf loads ignores. Affected sections: §§3.1, 3.5, and 9. Driver: agents and
hosts wrote Profile bundles with raw file edits, because okf's own writes
regenerate indexes and the log without the Profile and judge only OKF (#45),
and --fix could interleave with an okf write. Migration for
implementations: a writer regenerates only the indexes whose generated text
the change alters, records only author-supplied or okf lifecycle log
entries, judges the candidate in memory with a delta Profile gate keyed by
rule id and path, says when no Profile judged it, and commits under okf's
lock; --fix takes that lock when it writes, and a freshness inventory
leaves .okf.lock out. Migration for packages: none. No verdict changes, and
a bundle needs no edit; a project ignores .okf.lock in Git.
okf generator revision, next wayfinder release. Binds no Profile and
keeps package format 2. These entries change the engine contract that the
first wayfinder release to depend on okf ^0.5.1 implements.
matches-generated compares its version param with okf's index generator
revision, okfIndexGeneratorRevision, instead of with the okf release the
engine runs. The engine depends on okf ^0.5.1, a range within generator
revision 0.5.0, instead of exactly 0.5.0, and engine.okf and the SARIF
tool.extensions entry report okf's own okfVersion. The core library no
longer exports okfPackageVersion. Affected sections: §§3.1, 5.6, and 9.
Driver: Profile-aware writes need okf 0.5.1's reserved-file projection, and
a host that embeds the engine resolves okf and wayfinder together. Under
the exact pin, an engine on any later okf, even one whose generator writes
the same bytes, would have stopped every package that sets version,
Bitwild 2026.3 included, from loading. Migration for implementations: compare
version with the generator revision of the okf the engine runs, and report
that okf's release in engine.okf. Migration for packages: none. Revision
0.5.0 names the bytes okf 0.5.0 wrote, which is what a version of 0.5.0
named before, so such a package loads and reads exactly as it did and keeps
format 2 (ADR-0015). A bundle's verdicts are unchanged, and engine.okf
reads 0.5.1 or a later okf 0.5 release.
Package format 2, next wayfinder release. Binds no Profile. These entries change the engine contract that the release after wayfinder 0.1.x implements.
Revised in place before publication: §4.1 replaces the four result components
with OKF, Profile assessment, diagnostics, and a derived gate. The engine's own
reports become wayfinder/* diagnostics: release dispatch problems, the
project-type note, a link graph that cannot be built, --fix failure or
refusal, and a run that stopped. They are no longer Profile findings, summary
entries, or fix states. Profile state UNSUPPORTED becomes NOT ASSESSED,
gate UNSUPPORTED becomes INCOMPLETE, and the Judgment Rules component is
removed because no validator assesses it. SARIF reports diagnostics as
invocation notifications. Affected sections: §§3.1, 4.1, 4.3, 4.4, 4.5, 4.7,
and 9. Driver: a link graph failure let link rules pass unassessed, and engine
failures dressed as rules could not be told apart from bundle findings.
Migration for implementations: emit diagnostics and gate, derive the gate
from OKF, error findings, and error diagnostics, and drop judgment_rules and
automated_gate. A bundle is affected only where its result relied on a
removed rule id or on a graph failure being a FAIL; it is now INCOMPLETE.
Revised in place before publication: the engine dispatches only through
wayfinder.json. §4.4 drops the root profile.md selector, so a bundle with
no wayfinder.json above it gets the wayfinder/config-missing diagnostic
and the INCOMPLETE gate whatever its root files say. §3.4's 2026.2
projection and the 2026.2 rows, columns, and asides elsewhere are removed.
Affected sections: §§2.1, 3, 3.1, 3.4, 4.1, 4.2, 4.3, 4.4, 4.7, 4.8, 5.5, 6,
and 9. Driver: the maintainers chose a clean break so the engine carries no
Profile-specific legacy code; the 2026.2 declaration path was what kept
in-bundle registries and frozen 2026.2 rules in the engine. Migration for
implementations: delete the in-bundle declaration reader and the 2026.2 rules,
and report an unconfigured bundle as config-missing. A 2026.2 bundle keeps
validating on wayfinder 0.1.x, or migrates to a wayfinder.json binding as
Profile §15.3 describes. No configured bundle is affected.
Revised in place before publication (2026-10-02): the engine embeds no
Profile. A Profile is one package file, wayfinder-profile.json in package
format 2, read by one parser for every Profile; the separate manifest and
rule catalog are gone, standard_types is types, and frontmatter_keys is
a list any package may declare. There is no base Profile, so a chain need not
reach Bitwild. Bitwild's package is bitwild-profile at profiles/bitwild/,
and its findings are bitwild-profile/*. Builtins are engine capabilities
with params, named for what they check: files-present,
path-targets-exist, and matches-generated. The per-rule ref is removed;
a package's docs URI yields each finding's help_uri in JSON and helpUri
in SARIF. JSON adds engine.okf and SARIF a tool.extensions entry naming
the okf version used to generate indexes. The new rule
bitwild-profile/root-index-lists-log requires the root index of a bundle
with no concepts to link only log.md. Composition collisions become the
wayfinder/profile-composition diagnostic, and wayfinder/profile-invalid
joins the closed set. Affected sections: §§2.1, 4.1, 4.2, 4.4, 4.7, 4.8, and
9. Driver: an engine that embedded one Profile and required every chain to
reach it could not assess a Profile that stands alone, such as one a second
knowledge base ships, and a change in generated output could not be told
apart from a Profile change. Migration for implementations: parse format 2
packages through one boundary, compose the chain before evaluating it, report
the new diagnostics, and emit help_uri, helpUri, and engine.okf. A
configured bundle must rename its wayfinder.json key to bitwild-profile,
point source.path at profiles/bitwild, and run wayfinder get; its
finding ids change namespace from concepta-profile/* to
bitwild-profile/*.
Revised again in place before publication (2026-10-02): composition lives in
the package. A package names its parent in extends, either at the same
revision ({"path"}) or at its own ({"git", "ref", "path"}), and
wayfinder.json loses extends; every entry applies to at least one bundle.
The lock's profiles map becomes a flat packages map keyed by Profile id,
each entry with release and an optional extends, holding one revision per
id. get composes each chain before writing the lock and leaves an unchanged
lock's bytes alone. JSON profile adds id and chain, and SARIF adds the
profile_id run property. Affected sections: §§4.1, 4.4, 4.7, and 9.
Driver: a Profile whose parent each consumer wired could mean different
things in different projects, so a Profile's own guidance could not be
written against a fixed parent. Migration for implementations: read
extends from the package, lock packages, and select a bundle's chain by
the lock's extends pointers. A configured bundle whose wayfinder.json
used extends names only the child entry, moves the parent into the child
package's extends, and runs wayfinder get; an older lock is rewritten.
Revised again in place before publication (2026-10-02): a package MAY name
its agent skill in skill. get and upgrade install it from the locked
commit into the project's .claude/skills/<id>/ and .agents/skills/<id>/
with a .wayfinder-profile marker, never touching a directory without one,
and validate reports a missing or outdated copy as the
wayfinder/profile-skill-stale warning. A Profile id has at most 64
characters, the Agent Skills name limit, so every id names its skill directory
validly. Affected sections:
§§4.1, 4.7, 6, and 9. Driver: a Profile's judgment lived in a user-level
skill family pinned to no Profile revision, so an agent could read guidance
for other rules than the ones validate ran. Migration for implementations:
read skill, install it after the lock with the marker, warn on a stale
copy, and refuse an id over 64 characters. No configured bundle is affected; a
Profile with a longer id must shorten it, and a project whose Profile ships a
skill commits the installed directories after its next get.
Revised again in place before publication (2026-10-02): this guide becomes
the wayfinder engine contract and binds no single Profile. The prose Bitwild
Profile text is deleted. Its framework rules move to the new §5, Profile
packages: what a package is, OKF first and independent, the compatibility
test that keeps every Profile stricter than OKF and never contrary to it,
silence as deference, identity, release and format, frontmatter keys,
builtins, generated text as engine contract, composition, exact selection,
tolerant reading, and how the contract and each Profile change. Precedence
becomes OKF, then the engine contract, then each Profile package. Conformance
to a Profile is the gate PASS with it selected, so a Profile's README and
skill no longer carry requirements of their own. Four statements of the old
text are reversed rather than moved: the prose no longer governs the rules,
a chain no longer has to reach Bitwild, the ban on changing an inherited
rule is a limit of format 2 rather than a permanent rule, and Profile
conformance no longer includes requirements no rule checks. The old §5
migration method moves to Bitwild's skill, the coverage matrix and its §4.6
release gate are deleted, and §§2, 3, 4.3, 4.7, 4.8, 6, and 8 drop
Bitwild-specific text. Affected sections: §§1 through 8, and 9. Driver: a
Profile is what the validator enforces, so a prose Profile above the guide
and a coverage map between them described requirements no tool decided.
Migration for implementations: none; engine behavior is unchanged. A Profile
author reads §5 instead of the Bitwild text.
Revised again in place before publication (2026-10-03). §4.7 locks one
source per Profile id, its git, ref, and path, instead of one
revision. get fails and names both sources when two chains describe one id
differently, even when both resolve to the same commit. §§4.1 and 5.8 report
profile.id, profile.release, and profile.chain whenever a Profile was
selected, including under BLOCKED BY OKF, and only NOT ASSESSED omits
them. §2.1 seeds what the Profile's adoption guidance names instead of
forbidding directories for every Profile. §5.6 states the whole builtin
definition and gives tests to schema rules only. Affected sections: §§2.1,
4.1, 4.2, 4.7, 5.6, 5.7, 5.8, and 9. Driver: a commit does not say where a
package came from, so one id could be locked from two sources, and an agent
repairing an OKF failure could not read the chain to load the Profile skills.
Migration for implementations: compare locked sources rather than commits,
and emit the selected Profile under BLOCKED BY OKF. A configured bundle is
affected only when two of its chains name one id from different sources. It
makes them agree and runs wayfinder get.
Revised again in place before publication (2026-10-07). Ack, not an
evaluator the engine owns, evaluates rule schemas and the published schemas
within the engine's keyword subset, as §4.7 states. The §4.1 diagnostic table
names a rule schema keyword, $schema dialect, format or $ref form
outside that subset as a cause of profile-unsupported. A rule schema now
follows 2020-12 where the owned evaluator did not: an empty enum matches
nothing; a $ref below a $defs entry, a $id or $schema below the
root and a $schema that does not name 2020-12 are unsupported; and a
malformed annotation, a duplicate required or type entry and an empty
type list are refused. A published-schema violation is
the first in Ack's keyword order, and a root null is reported as "must not
be null". Affected sections: §§4.1, 4.7, and 9. Driver: the migration to Ack
as the only JSON Schema evaluator. Migration for implementations: evaluate
rule schemas with a 2020-12 evaluator restricted to the subset, and refuse the
forms above. Every shipped package stays conformant, as the migration's
parity corpora and a scan of the shipped schemas showed. Format 2 is
unreleased, so §5.10 needs no format bump.
Revised again in place before publication (2026-10-22). §2.1 names the
library home of the generic seed, KnowledgeSeeds in
package:wayfinder/adoption.dart, beside the adoption skill's SEEDING.md,
and describes KnowledgeAdoption, which plans before it writes, leaves
nothing half done, returns the agent paragraph as text and ignores the
bundle's .okf.lock in .gitignore. Affected sections: §§2.1 and 9.
Driver: hosts that set projects up in process could only replay the skill's
prose, with no preview and no way back from a failed step. Migration for
implementations: none; what an adopted repository holds is unchanged, and no
bundle or package reads differently.
2026.3. Binds Profile 2026.3. Implements explicit wayfinder.json
release dispatch while preserving the 2026.2 in-bundle path. Affected sections:
§§2, 4.1, 4.4, 4.7–4.8, and 9. Driver: reusable Profile bindings,
reproducible source revisions, and removal of repeated in-bundle configuration.
It also adds --output sarif (§4.1), the same findings as a SARIF 2.1.0 log,
so CI can upload them to code scanning. Migration for implementations: add direct-source configuration, explicit
get/upgrade lock management, read-only validation of a selected Profile,
safe path and registry checks, and per-release index projection; retain legacy
dispatch and independent OKF results. Existing 2026.2 bundles remain
supported without edits.
Revised in place before publication: §3 makes 2026.3 indexes the output of
okf's reference generator, pins okf 0.5.0, replaces semantic comparison with
exact comparison for 2026.3, and adds validate --fix; the 2026.2 generator
contract and semantic comparison move unchanged to §3.4. Affected sections:
§§1, 2.1, 2.3, 3, and 8. Driver: Profile 2026.3 §9 now defers to okf's
generator. Migration for implementations: generate and compare 2026.3 indexes
with the pinned generator, and keep the 2026.2 projection for 2026.2 bundles.
Revised in place before publication: §§4.3 and 4.8 check the 2026.3
relationships frontmatter key and its declared names instead of the
# Relationships body section, accept the frontmatter keys a release declares,
and let a graph or search projection add relationship edges beside the OKF
graph without changing it; §4.7 adds relationship names to the binding
vocabulary, and §4.1 names --fix in the command surface. Affected sections:
§§4.1, 4.3, 4.7, 4.8, 5.5, and 9. Driver: Profile 2026.3 §7.2 moves typed
relationships into frontmatter. Migration for implementations: parse a
release's declared keys and relationship vocabulary, resolve relationship
targets as the OKF graph resolves links, and keep the body-section checks for
2026.2 bundles.
Revised in place before publication: §4.1 adds summary entries to the result
model beside the findings, as profile.summary in JSON, a Summary: text
block, and SARIF informational results; §4.7 reports a registered custom type as a
summary entry. A graph that cannot be built is reported at index.md, which
every 2026.3 bundle has, instead of profile.md. Affected sections: §§4.1, 4.7,
and 9. Driver: Profile 2026.3 §14.1 now reports what it permits as summary
entries, and §5.1 checks colliding tag names where the binding declares them.
Migration for implementations: route a release's summary rules to the summary,
emit the new JSON key and SARIF informational results, and reject a colliding tag declaration
while reading and composing the binding; 2026.2 output is unchanged.
Revised in place before publication: §§4.2, 4.4 and 4.7 read a non-base
source's rule catalog at its locked commit and evaluate the catalog chain,
base first, with each finding in its catalog's namespace; the lock is
unchanged. Affected sections: §§4.2, 4.4, 4.7, and 9. Driver: Profile 2026.3
§11 lets a non-base entry ship a rule catalog. Migration for implementations:
parse a named catalog whole and fail dispatch as UNSUPPORTED on anything the
engine lacks, keep the installed base catalog authoritative, and list every
catalog's descriptors in SARIF output.
Revised in place before publication: Profile 2026.3 §9 now names the
generator's okf release, so §§1 and 3 cite it there and keep only how a tool
pins that release and detects drift from it, and §3.1 states that --fix
never deletes a leftover index. §4.3 reports a registered project type and an
unresolved internal link or relationship target under 2026.3 as summary
entries, as §4.1 and Profile §14.1 already did, and cites Profile §7.2 for the
relationship shape instead of restating it; §4.8 states that a colliding tag
declaration fails at configuration. Affected sections: §§1, 3, 3.1, 4.3, 4.8,
and 9. Driver: a guide that pinned the release decided which index bytes
conform, which is a bundle rule the Profile owns; §§4.3 and 4.8 still
described the advisories and per-concept tag check that the summary-entry
revision replaced.
Migration for implementations: none; the release and the checks are
unchanged.
2026.2. Binds Profile 2026.2. The Profile adopted the upstream OKF 0.2
revision in which every timestamp is an ISO 8601 datetime with an explicit UTC
offset, and moved the pinned specification to the canonical
open-knowledge-format repository. No guide contract changes: adoption,
the generator contract, the validation process contract with its release
dispatch, migration, and distribution are all unchanged. Release dispatch now
accepts "2026.2" as the supported release and reports any other declared
release as unsupported, which is the behavior §4 already specified. Migration
for an implementation is the release string alone; a bundle migration is the
optional okf format --migrate-timestamps pass the Profile's §15.3 describes.
2026.1. First release. Binds Profile 2026.1. Establishes adoption (§2), the index generator contract (§3), the validation process contract (§4) with its closed result model and release dispatch, the migration method (§5), and distribution (§6). Cross-bundle references (§7) remain deferred, as the profile leaves them. Amended in place when the okf toolchain released 0.2.0: §4.1's OKF-component wording is restated over okf's finding contract (ADR-0008) — a toolchain clarification, with the result model, exit codes, and rules unchanged.
The file keeps its name and path, implementation/okf-implementation-guide.md,
so links into it stay stable. It was titled Implementation Guide while it sat
below the prose Profile, which was the specification. It now sits above every
Profile package and keeps the name "guide". §1 states why "guide" does not mean
advisory.