spec 1.0 · normative
The Leji Specification
The complete normative specification on one page, in reading order, for reading straight through or searching. Each section links to its own page, where every heading carries a citation anchor.
The Leji Specification
Leji is an open specification for the shared context layer of AI-native teams. It defines how a team stores, governs, loads, and maintains the repo-owned context that people and AI agents both read on every task.
| Spec version | 1.0.0 |
| Status | GA, frozen at the v1.3.0 reference-tooling release. Breaking changes require a new major version. |
| Editor | Vuong Nguyen |
| One page | The full specification on a single page |
Principles (non-normative)#
- Intent over instructions. Leji captures durable intent (what things mean, what must hold, why it is so) instead of imperative, per-vendor instructions. People and agents derive actions from declared intent plus task context.
- A circle, not a tier. Human-to-human, human-to-AI, and human-to-AI-to-human are first-class flows around one shared context layer. Equal access, not equal authority: everyone with access to a context layer reads all of it, anyone proposes, people approve. Participation is role-based, not tool-based: a participant who never touches git directly is first-class in the circle. Access itself is the version control system’s to grant, not Leji’s; the circle is scoped to a context layer’s audience.
- Mechanism over goodwill. Shared context decays by default: reality moves, documents don’t, and nothing forces a wiki current. Leji’s forcing functions are mechanical, not goodwill: changes ride the same review gate as code, tooling fails on mechanical drift, freshness horizons flag what has aged, and stale context is never silently treated as current (normatively, governance.md → Freshness).
The rest of this specification is the normative consequence of those three principles.
Conformance language#
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this specification are to be interpreted as described in RFC 2119.
Citing this specification (non-normative)#
Cite a section by its title and the spec version, with a permalink to the section anchor. On the spec site every heading reveals its anchor on hover.
- Format: Leji 1.0, §Section:
https://leji.org/spec/<document>/#<anchor> - Example: Leji 1.0, §The circle, normatively:
https://leji.org/spec/governance/#the-circle-normatively
Always cite the version (Leji 1.0): breaking changes ship as a new major version, so a version-pinned citation stays accurate after the spec evolves.
Vocabulary#
These terms are used consistently across all normative documents:
| Term | Meaning |
|---|---|
| context layer | The artifact this specification governs: a repo-owned, versioned set of human-readable documents and machine-readable artifacts encoding a team’s durable operating context. “Leji context layer” is the full disambiguating form. Always write “context layer”; bare “layer” is reserved for naming a countable federation instance (a sibling, host, mounted, restricted, companion, or inaccessible context layer). |
| agent | An AI system that acts: it loads repository context, performs or assists work, and may propose changes. The normative actor noun. |
| person / people | Human participants. People hold approval authority. |
| participant | A person or an agent. |
| audience | The people and agents admitted to read a context layer by its repository permissions and any filesystem or shared-drive permissions that expose the checkout. “Everyone reads” is scoped to a context layer’s audience; different audiences are served by separate context layers, never by gating content within one. |
| agent host | The product or runtime an agent operates through (for example Claude Code, Codex, Cursor). Vendor adapters configure agent hosts. |
| tool | A callable capability an agent uses (shell, search, an MCP server). Never a product name. |
| vendor adapter | An agent-host entrypoint file that redirects to the boot profile and never holds canonical content. Some are portable across hosts (AGENTS.md); others serve a single host (CLAUDE.md, .cursor/rules). The rule is the same for both; the difference changes only what tooling generates by default. |
| boot profile | The agent-agnostic entrypoint of the context layer, for people and agents alike. |
| agent profile | A role-specific loading-and-posture document for agents. |
| AI | Used adjectivally (AI-native) and in the flow names human-to-human, human-to-AI, human-to-AI-to-human. In flow names, “AI” refers to agents operating through an agent host. |
| model | The predictive engine an agent runs on. Models don’t read the context layer; agents do. Appears only where the engine must be distinguished from the actor (for example, model selection as a host-specific mechanic). |
The hierarchy, in one line: a model powers an agent; an agent operates through an agent host and calls tools; the context layer addresses agents and hosts, never models directly. The specification is agnostic at every level of that stack: any model, powering any agent, operating through any host, reading the same context layer. “LLM” is deliberately not part of this vocabulary: it names one class of model, and the specification is model-agnostic by the same principle.
Scope boundary. Leji 1.0 governs agents and the agent hosts that load repository context. Non-agentic AI (autocomplete, inline suggestions, chat without repository context) is out of normative scope, except where it operates as part of an agent host that loads the context layer.
Normative documents#
In reading order:
| Document | Defines |
|---|---|
| context-layer.md | The context layer, the manifest, the root, the vendor-adapter rule |
| content-categories.md | The five logical content categories and how index files map content to them |
| boot-profile.md | The agent-agnostic entrypoint every agent host loads |
| machine-readable-surface.md | Manifest, index, changelog, profiles, decision records |
| decisions.md | Decision records |
| governance.md | Propose/approve, ownership, inclusion and removal, freshness |
| distribution.md | Monorepo, multi-repo submodule, federation |
| conformance.md | The four conformance levels and the checklist |
| versioning.md | Spec and schema versioning |
The JSON Schemas in ../schemas/ are normative for the machine-readable artifacts. The documents in ../rationale/ and ../adoption/ are non-normative.
Scope of 1.0#
In scope: providing context, setting constraints, recording decisions, reviewing changes, and capturing reusable patterns; agent-agnostic wiring and vendor adapters (lightly); ownership and continuity semantics (lightly).
Extension boundary. Leji 1.0 specifies the canonical shared context layer: how team context is written, owned, versioned, proposed, approved, indexed, and read. It deliberately does not specify the execution protocols that operate around that context layer: task envelopes, a generalized evidence protocol, agent-to-agent handoff, tool-permission protocols, and orchestration. These are extension protocols, not prerequisites: a conforming 1.0 context layer MUST remain useful without them, and an implementation MUST NOT require them to read, propose, review, approve, or validate the context layer. They complete the language as live practice proves them; they aren’t invented in the abstract.
Leji is not a programming language, a DSL, a runtime, or a SaaS. It is markdown conventions, small JSON schemas, and governance semantics.
The Context Layer
A Leji context layer is a versioned, governed set of human-readable documents that encodes how a team thinks about its work: domain language, system invariants, conventions, guardrails, and decision records. People and agents both read it in the course of real work, and both propose changes to it through the same review gate. It lives under version control so that history, currency, and approval stay verifiable; the mechanics are in Requirements below, and who takes part and how is in Participation.
Participation#
Participation in a context layer is role-based, not tool-based. Reading, proposing, reviewing, and approving happen through whatever interface preserves the repository’s review and approval semantics; direct git or command-line knowledge is not required to take part.
- Everyone with access reads. Access means practical access through the team’s normal tools, not shell access to the repository.
- Anyone proposes; people approve. A proposal is an intentional request to change the context layer. It MAY be authored directly by a person, generated by an agent from a person’s request, or generated by an agent from observed work. In practice agents draft most context changes; the irreducibly human contribution is governance: proposing intent and approving what becomes canonical. A person approving a change is accountable for its meaning and consequence, not for personally operating the version control system.
- Human meaning, machine-readable surface. The human-readable documents are the normative source of a team’s operating context. The machine-readable files (manifest, index, changelog) exist so tools can locate, index, validate, and synchronize that meaning; they never replace it.
The normative form of these flows, the circle (everyone reads, anyone proposes, people approve), is defined in governance.md.
Reading a record#
Governed content comes in two kinds, defined in content-categories.md: intent, maintained as present truth, and records, dated evidence that later state supersedes rather than corrects. Both are equally governed; what differs is what a reader may do with what it loads. A reader MUST NOT treat a record as current intent: a record informs with the standing of evidence true within its stated boundary, and a reader that presents a record’s claims as the present state of things, without saying so, is manufacturing currency the document does not have. This mirrors the degraded-mode rule below: in both cases, the reader’s obligation is to know, and to say, what kind of currency it is holding.
Requirements#
-
The context layer MUST live in a git repository and MUST be versioned with the work it describes (same repo, or a dedicated context repository consumed per distribution.md). The git repository is what makes the context layer’s history, checkout currency, and append-only changelog integrity verifiable; conforming tooling derives all three from it. Reading the context layer without that repository is a supported but degraded mode, defined in Reading modes.
-
A repository adopting Leji MUST carry a manifest file,
leji.json, at the repository root, valid againstcontext-manifest.schema.json. The manifest is the machine entrypoint: it declares the spec version (the self-naminglejikey), the context layer name, the context root, the boot profile path, the category mappings, an optional conformance claim, and ownership. It MAY also carry anagentsmap binding role identifiers (e.g.thought-partner,reviewer) to agent profile documents: protocols engage roles; the map decides who fills them. The map is a directory of roles, not a load order: a binding, including one at thedefaultkey, never causes a profile to be read; only the boot profile’s Loading section does that. -
The manifest MAY declare actors: named participants that can fill roles. Each actor declares the roles it is eligible for and a command template per role. Keying the command by role is the point: one actor can require a different invocation depending on which role it is filling, so a single command per actor cannot express it. An actor’s declared roles and its command keys MUST be the same set. Where a role has actors, its bound agent profile MUST NOT also declare
invocation: two authoritative commands with no stated precedence is a contradiction, and the layer resolves it by declaring the command in one place. Actors are optional and most layers need none. They earn their place when a role has more than one eligible actor, or when one actor needs a different invocation depending on the role it is filling; either alone is reason enough, and a role whose single actor needs only one command is served by the profile’s ownhostandinvocation. Declaring an actor grants no authority: it says who may be asked to fill a role, never who may approve.Command templates, wherever they appear (an actor’s
commandsvalues and an agent profile’sinvocation.command), follow one rule. A template is a command line for the shell the invoker selects; engagement that is not shell-shaped (a structured argv call, an in-process spawn) is not representable by these fields in the 1.0 line. Every template MUST carry the<prompt>placeholder, and each occurrence MUST stand as its own unquoted shell word in an argument position, never inside quotes or joined to other text. Substitution is one-pass: the occurrences present in the authored template are replaced simultaneously, exactly once, so the literal string<prompt>inside prompt text stays data and is never re-expanded. The invoker owns delivery, and the required outcome, not the quoting algorithm, is the contract: each occurrence yields exactly one argument whose value equals the prompt text, with none of it evaluated as shell syntax. What the schemas verify is the presence of a substitution site; placement and delivery are this rule’s to require and the invoker’s to honor. -
The manifest MUST declare a context root (
rootPath). The RECOMMENDED default isdocs/. All context layer paths are POSIX-style, relative to the repository root.rootPathdeclares where the context layer lives; it does not re-base other paths: every path in every Leji artifact resolves from the repository root, including those that repeat therootPathprefix. Category andmachinepaths SHOULD fall underrootPath; validators warn when they don’t. -
The context layer MUST have a boot profile per boot-profile.md. The RECOMMENDED default location is
docs/boot-profile.md; the manifest’sbootProfilePathdeclares the actual location. -
Context layer content MUST be human-readable first. Markdown is the RECOMMENDED format for prose; structured metadata uses YAML frontmatter or the JSON artifacts defined in machine-readable-surface.md. A document that only a machine can read doesn’t belong in the context layer.
-
The context layer MUST have a named owner (
owners.primaryin the manifest): a person accountable for its currency. Ownerless context layers rot.
Reading modes: canonical and degraded#
A context layer is read in two modes; a reader MUST know which mode it is in, because the guarantees differ. A reader determines its mode from what it can resolve: a reachable leji.json at the repository root and either a git working tree or the host platform’s repository revision identity is canonical; content reached as plain files with neither is degraded.
- Canonical. The reader resolves the context layer through its git repository: a checkout, or the repository view of the host platform. History, checkout currency, and changelog integrity are verifiable, and approved content is known current to the revision read.
- Degraded. The reader reaches the context layer as plain file content with no accessible git working tree or version metadata: files uploaded, synced, or copied into another interface without the repository. Plain-file reading is first-class for reading (the documents are human-readable by requirement, and the machine-readable changelog still conveys declared recency), but a degraded reader MUST treat checkout currency and approval state as unknown, never as current (see governance.md, Freshness). The changelog is the portable declared-recency surface in this mode; it does not by itself establish that the copy matches the canonical repository.
Degraded reading widens who and what can consume a context layer; it is never a path to canonical authority. A change becomes canonical only through the git-backed review gate, and a degraded copy cannot satisfy the canonical-mode checks federation depends on (pin currency, stale-pin status, ownership intactness, and restricted-mount access per distribution.md).
The vendor-adapter rule#
Agent-host configuration files (for example CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules, .cursor/rules, .windsurfrules, .github/copilot-instructions.md):
- MUST NOT hold canonical context layer content.
- If present, MUST redirect to the boot profile (typically a one-line pointer).
- MAY carry host-specific mechanics that have no meaning outside that agent host (model selection, runner settings), provided no team knowledge lives there.
- Tooling discovers which entrypoints to check from two sources: the manifest’s optional
vendorAdapterslist, and a published well-known set (the files named above). The example list above is that well-known set for this line; a host whose entrypoint is not in it is checked only when the manifest names it invendorAdapters.
The entrypoint conventions that exist tell an agent host where to look; Leji defines what the agent finds there. One source of truth, every participant reading it.
What the context layer is not (non-normative)#
- Not a wiki. Nothing forces a wiki current. The context layer stays alive because agents read it on every task (wrong context produces wrong output that is felt immediately), changes ride review like code, and tooling makes staleness visible.
- Not documentation in the traditional sense. Documentation describes what the system does, after the fact. The context layer describes how the team thinks, in the present tense, and is read constantly by people and agents alike.
- Not a template to import. Borrowed context goes stale immediately. The value of a context layer is that it is the team’s own representation; Leji standardizes the shape and the governance, not the contents.
Content Categories
Leji defines five logical content categories. They classify what a document is for, not where it lives: category names are stable identifiers used by the manifest, the index, and tooling. Directory names are the team’s own.
The five categories#
| Category | What belongs in it |
|---|---|
domain |
Business language and product semantics, in the team’s own words: what the core nouns mean, how they relate, the terms with local meaning. Business-state records (an engagement status, a market snapshot) classify here too, as records. |
system |
Architecture and its invariants: service boundaries, data ownership, integration contracts, consistency models, failure contracts, the constraints every change lives with. Technical assessments and system readouts classify here, as records. |
practice |
Conventions and patterns applied automatically: code conventions, testing patterns, and the prompt and workflow patterns that have proven out (see capture gate below). Records of applying a method (a retro, a runbook execution log) classify here, as records. |
governance |
Agent guardrails and operating rules: what agents may do unprompted, what needs a human gate, data-handling rules, escalation triggers, compliance controls. Governance evidence (an audit log, a review report) classifies here, as records. |
decisions |
Dated records of why things are the way they are, per decisions.md. |
Intent and records#
Every governed document is either intent or a record, independent of its category:
- Intent is maintained present truth: glossaries, invariants, conventions, guardrails. Readers rely on it as current, so when reality moves, the document is corrected. Intent is what review horizons and the freshness mechanism exist for (see governance.md).
- A record preserves claims within an explicit temporal or event boundary: statuses, assessments, ledgers, readouts, meeting outcomes, archives. Later state supersedes a record rather than correcting it; the original stays a valid account of its time. A record’s currency surface is its date, never a review horizon.
The classification test is one question: if later information disagrees with this document, must the document be corrected because readers rely on it as current, or does the new information supersede it while the original remains a valid account of its time? Corrected means intent; superseded means record.
A record is governed exactly like intent: indexed, reviewed, owned, and routed. What differs is what a reader may do with it: a reader MUST NOT treat a record as current intent; it is dated evidence (see context-layer.md, Reading a record). Decision records are the formal record subtype: they are inherently records, with their own schema and lifecycle per decisions.md.
Some questions about records are deliberately out of 1.0 and acknowledged rather than hidden: there is no machine notion of a record series (so tooling never certifies which record is “the latest”), no stream-recency mechanism (whether the next expected record is overdue), and no section-level kinds for documents that materially mix intent and record content. A mixed document SHOULD be split; where splitting is disproportionate, classify by the contract downstream readers principally rely on. Content that honestly fits no category stays reference; classification is not promised to be judgment-free.
Requirements#
- The manifest MUST map each category it claims to one or more repository-root-relative index files (
categories.<id>.indexes); each index file SHOULD fall under the declared context root, per context-layer.md. An index file declares inclusion, it does not relocate: content stays where the team already keeps it (for examplebusiness/,technology/,architecture/), and one directory may contribute documents to more than one category without renaming anything. - An index file is curated markdown carrying one or more fenced
leji-indexcode blocks. A block opens with a line of three or more backticks followed by the block’s info string and closes with the next line of three or more backticks; the closing fence’s backtick count need not match the opening fence’s. Exactly three info strings are valid:leji-index(an intent block),leji-index intent(the same, explicit), andleji-index record(a record block, whose entries resolve as records). Any other token afterleji-indexis a parse error, never silently ignored: the grammar is finite by design. Each block lists content one entry per line as- path: <repository-root-relative-path>, where a path is a directory (its markdown is included recursively) or a single markdown file. A path MUST be repository-root-relative POSIX: a leading/, a..segment, or a backslash is invalid and rejected. Blank lines and full-line#comments are ignored, and an entry MAY carry a trailing whitespace-preceded# comment. Whitespace in this grammar is ASCII space (U+0020) and tab (U+0009) and nothing else, everywhere the grammar consults it: around the fence backticks and the info string, as leading and trailing padding on an entry line, and before the#that opens a trailing comment. A leading UTF-8 byte order mark is stripped before parsing. Lines split on LF with a trailing CR tolerated, and the file is UTF-8. Implementations MUST NOT use a runtime whitespace class here: any other character a runtime happens to classify as whitespace, U+0085 and U+00A0 among them, is ordinary path content, so an entry whose path carries one is reported missing rather than silently trimmed. Theleji-mountsblocks of boot-profile.md are frozen on the same alphabet, so one scanner reads both grammars and three implementations cannot disagree about whether a fence is even there. Multiple blocks in one file are concatenated in document order. Prose and headings around the blocks are allowed, so an index file doubles as a human-readable map of the category. The scan is line based and does not consult markdown structure: a line carrying three or more backticks and the tag, after optional space or tab indent, opens a real block wherever it sits in the document, including inside a longer fenced example or inside a list item. An example meant to illustrate rather than to declare is therefore fenced with a different tag, never with an extra token afterleji-index: the tag is what the scanner matches on, soleji-index exampleopens a real block and reports a parse error, while a fence taggedtextopens nothing. The RECOMMENDED location iscontext/<id>.mdunder the context root; the location is configurable and the tooling never hardcodes it. - A context layer MUST map at least
domainorsystem, plusdecisions, to claim any conformance level (see conformance.md), and the populateddomain/systemminimum MUST include at least one intent document: a context layer of records alone preserves history but carries no operating context. The other categories accrete as the team hits real questions; an empty category (one whose index files resolve to no documents) MUST NOT be mapped to satisfy a checklist. - A document resolves to exactly one category and one kind. Index entries are selectors, and resolution follows selector specificity: a direct file selector beats any directory selector, and a deeper directory selector beats an ancestor directory selector. The most-specific selector covering a document determines its category and its block kind; a document a broader selector covers but a more-specific selector wins is simply not that broader selector’s content (which is how one kept-current file inside a record directory, or one team’s decision log inside a broader mapped tree, is expressed without moving anything). Selectors of equal specificity that disagree on category or kind are an error, never resolved by index order; identical equal-specificity assignments resolve once, while a literally duplicated entry within one index file is rejected. Tooling SHOULD surface a selector whose every covered document was won by more-specific selectors (a shadowed selector): dead weight in the curated map, never an error. Resolution is otherwise deterministic: a directory entry expands to its markdown in POSIX-lexical order (by Unicode code point; RECOMMENDED paths stay ASCII so order is unambiguous across implementations), and any path whose real location (after resolving symlinks) escapes the repository root is excluded rather than followed. Index entries (see machine-readable-surface.md) carry the category identifier and the kind.
- A document MAY declare its kind in frontmatter (
kind: intentorkind: record); frontmatter overrides the winning selector’s block kind and never the category. Any otherkindvalue is an error. Decision records take nokindkey (their schema is closed and they are inherently records). A record MAY carry a frontmatterdate(YYYY-MM-DD); tooling reads a record’s date only from that field, never from prose, header conventions, or filenames. A record MUST NOT carryfreshness.reviewAfter(a review horizon is an intent mechanism; on a record it promises a currency the document cannot have, and it is an error). - Practice content describing prompt or workflow patterns SHOULD be captured only after the pattern has worked at least twice (the proven-twice gate). Premature capture is how practice directories fill with aspiration.
Notes (non-normative)#
Not every category is present on day one. The minimum viable context layer is whatever the first month of work actually relies on. Categories exist so that a human or an agent can ask “what kind of truth is this?” and load the slice that matters for the task at hand, instead of the whole tree.
The two kinds exist because a real repository’s documentation is two interleaved corpora with different truth models, and forcing the operational half under intent semantics fails both ways: freshness promises that cannot be honored, or the majority of the repository exiled outside governance. A worked shape, with one intent exception inside a record directory:
# Domain context
```leji-index
- path: docs/glossary.md
```
Operational state is governed as records; the escalation policy stays intent.
```leji-index record
- path: docs/operations/
```
```leji-index intent
- path: docs/operations/escalation-policy.md
```
The Boot Profile
The boot profile is the agent-agnostic entrypoint of the context layer: one human-readable document that every agent host, and every person, can start from. It answers “what is this context layer, what do I load, and how do I behave here.”
Requirements#
-
The context layer MUST have exactly one boot profile, located at the path declared by
bootProfilePathin the manifest. The RECOMMENDED default isdocs/boot-profile.md. -
The boot profile MUST be plain markdown, readable by a person with no tooling. It MUST NOT depend on any vendor’s configuration syntax.
-
The boot profile MUST cover:
- Identity: what this repository or product is, in a paragraph.
- Loading: what context to read for which kind of task. This MUST give an unconditional set (what to read before any task), then task-typed selectors that route by path, by category, or via the context index, and a defined fallback for a task that matches no selector. Stated in task language, this is the boot-profile-level expression of the Task routing algorithm (machine-readable-surface.md); following it requires no knowledge of that algorithm.
- Posture: the agent’s operating expectations (when to proceed, when to ask, what to never do). This MAY be carried by reference to governance content or a core agent profile.
-
The boot profile SHOULD link to the manifest, the index (if present), and the agent profiles (if present), so that an agent entering through any host can discover the whole machine-readable surface.
-
The boot profile MUST speak task language: it names literal paths and concrete load order, and following it requires no knowledge of this specification. The manifest and schemas exist for tooling, not for agents; a boot profile that requires spec literacy to follow is a conformance smell.
-
The boot profile SHOULD state the context layer’s maintenance duties: where its changes are recorded (the declared changelog) and how decisions are captured (the declared decision-records location). Validators warn when the boot profile references neither.
-
Vendor entrypoint files redirect to the boot profile per the vendor-adapter rule in context-layer.md.
-
The boot profile’s unconditional load set (what it says to read before any task) SHOULD be bounded to what every task needs. Context only some tasks need SHOULD be routed by task, category, or the index rather than preloaded; and decision records SHOULD be routed by their declared
affectedPaths/affectedCategoriesrather than loaded as a whole directory, since they accrue without bound. Everything in the unconditional set is paid on every task. -
Federated siblings. A context layer that declares
federation.mounts(per distribution.md) MUST surface those siblings in the boot profile in a machine-checkable form: one or more fenced blocks whose info string isleji-mounts, placed anywhere in the document, whose entries concatenate in document order and carry exactly one entry per declared mount. An entry names the sibling, its owner, what it carries, and when to read it, the last two in the author’s task language. The worked example is below the requirements.The grammar is fixed so every implementation reads it identically. A block opens with a line of three or more backticks followed by the info string and closes with the next line of three or more backticks; the closing fence’s backtick count need not match the opening fence’s. The info string is
leji-mountsalone; a fence carrying any token after it is an error, never an ignored fence. The fence lines MAY carry space or tab indent and padding, and the records between them MUST NOT: a record begins at column 1 with- mount:, and its fields are indented exactly two ASCII spaces. Within a recordowner,carries, andread-wheneach appear exactly once, in any order; unknown fields, duplicate fields, and missing fields are errors. A value is the nonempty remainder of its line after thekey:prefix, with no leading or trailing space or tab and no control or line-separator character. Whitespace in this grammar is ASCII space (U+0020) and tab (U+0009) and nothing else, in the fence line’s indent and padding as much as in a content line’s; implementations MUST NOT use a runtime whitespace class here, since those disagree about characters such as U+0085 and U+00A0 and would disagree about whether a block exists. A leading UTF-8 byte order mark is stripped before parsing. Lines split on LF with a trailing CR tolerated, blank lines and full lines starting#are ignored (as in the category index blocks of content-categories.md), and the file is UTF-8. The scan is line based and does not consult markdown structure: a line carrying three or more backticks and the tag, after optional space or tab indent, opens a real block wherever it sits in the document, including inside a longer fenced example or inside a list item. An example meant to illustrate rather than to declare is therefore fenced with a different tag, never with an extra token afterleji-mounts: the tag is what the scanner matches on, soleji-mounts exampleopens a real block and reports a parse error, while a fence taggedtextopens nothing.mountMUST match a declared mount’snameandownerMUST match that mount’s declaredowner.name, compared as decoded strings; an entry for an undeclared mount, a second entry for one mount, and a declared mount with no entry are all errors. A layer that declares no mounts MUST NOT carry aleji-mountsblock.The sibling’s location is deliberately not an element: a mount is materialized in a machine-local, content-addressed projection, so a reader resolves it with
leji mounts locate <name>rather than inferring a path (per distribution.md). Prose around the block SHOULD explain the routing naturally; the block is the checkable core, never a replacement for that prose or for the declaration inleji.json. Mounted siblings are distinct, named sources, never merged into the host’s categories; the boot profile routes the agent into a sibling only when the task matches its routing or the profile requires it. What stays unchecked is deliberate:carriesandread-whenare free text, and their fidelity to the mount’s routing metadata is attested by the team rather than verified by tooling, which checks enumeration, identity, and presence. Surfacing siblings here keeps mount discovery in the agent’s task-language entrypoint, so following requirement 5 still needs no manifest reading.
A worked leji-mounts block#
One entry, for a host that declares a single mount named acme-product-context. The block sits at column 1 in the boot profile, exactly as it reads here; the outer four-backtick fence is this document’s wrapper and is not part of it.
```leji-mounts
- mount: acme-product-context
owner: Product team
carries: product-side domain language and the decisions behind the customer-facing surface
read-when: a task touches product behavior, product terminology, or billing
```
Agent profiles#
A context layer MAY define role-specific profiles (for example a reviewer profile, a release profile, a QA profile) under a directory declared by machine.agentProfilesPath. Each profile:
-
MUST be markdown with YAML frontmatter valid against
agent-profile.schema.json. -
MUST, once inheritance is resolved, carry what the role reads first (
requiredRead) and when it must stop and ask (mustAskWhen). A profile that declaresinheritsMAY omit either one where its base supplies it; a profile that does not MUST declare both itself. -
MAY declare
inherits, which is operative in the 1.0 line: it names exactly one other profile in the layer’s profile set, whoseroleMUST becore, and whose posture and body this profile extends. The layer’s profile set is every document under the declaredmachine.agentProfilesPathtogether with every document named in the manifest’sagentsmap, wherever that document sits. Resolution is single level, so a profile whoseroleiscoreMUST NOT declareinherits, and the named target MUST exist, MUST be unique byid, and MUST NOT declareinheritsitself. Resolution composes:- Posture arrays (
requiredRead,defaultContext,mustAskWhen,mustRefuseWhen): the base’s entries in their authored order, then the derived profile’s entries in theirs, dropping any the base already carries. Authored order is loading intent, so nothing is sorted. - Every other field (
id,name,role,purpose,version,host,invocation,escalation,owners,freshness): the derived profile’s own, never inherited.inheritsis a resolution directive and is not itself part of the resolved profile. - Body: both bodies are normative, the base’s first, then the derived profile’s.
A consumer that cannot resolve an inherited profile MUST NOT apply the derived file on its own; the derived file is one half of a profile, so the consumer reports it unsupported instead. Where an ask condition and a refuse condition both apply to the same situation, refusal governs.
Resolution guarantees composition, not semantic narrowing: derived prose that contradicts or weakens the base is nonconforming, and no tooling detects a natural-language contradiction.
- Posture arrays (
Profiles tune what a role loads and how it behaves; they don’t duplicate context layer content.
A profile’s optional host and invocation are the single-actor shorthand: they say how to engage the one participant that fills this role. Its command is a template following the same rule as actor command templates, including the <prompt> placeholder and its placement (see context-layer.md, Requirements). Where a role has more than one eligible participant, or where the same participant needs a different invocation depending on which role it is filling, the manifest’s optional actors registry carries that instead (same section). A role uses one mechanism or the other, never both.
Notes (non-normative)#
The boot profile is deliberately boring: a map and a posture, not a knowledge base. If the boot profile grows past a few screens, content is living in the entrypoint that belongs in a category.
The failure mode this design guards against is indirection: every hop between an agent’s first context and the actual constraint costs attention. A context layer implemented well needs no vendor entrypoints at all (invocation can point straight at the boot profile), and the boot profile walks straight to content. Depth belongs in the context layer’s documents, never in the path to them.
Every document the boot profile says to read before any task is paid on every task, so the unconditional set is the context layer’s most expensive space. Keep it to what is genuinely universal, and route the rest through task-typed loads, the categories, the index, and the scope each decision record declares. The index exists so an agent can load the slice a task needs instead of the whole tree; decisions accrue without bound, so they are routed, never preloaded as a directory.
The Machine-Readable Surface
Five artifacts make the context layer legible to tooling. Everything else in the context layer is prose for humans that agents happen to read; these five are the contract tools build against.
| Artifact | Default location | Schema |
|---|---|---|
| Manifest | leji.json (repository root, fixed) |
context-manifest.schema.json |
| Context index | <root>/context-index.json |
context-index.schema.json |
| Context changelog | <root>/context-changelog.json |
context-changelog.schema.json |
| Agent profiles | <root>/agents/*.md (frontmatter) |
agent-profile.schema.json |
| Decision records | <root>/decisions/*.md (frontmatter) |
decision-record.schema.json |
All locations except the manifest are manifest-declared; the table shows defaults.
Requirements#
- Manifest.
leji.jsonMUST exist at the repository root and validate against its schema. It is the only fixed filename in Leji: the file tooling reliably looks for. - Index. A context layer claiming
indexedconformance or above MUST carry a context index that is generated, never hand-maintained: tooling resolves the category index files (categories.<id>.indexes, per content-categories.md) to the documents they list and writes one entry per governed document. Each entry carries a stableid, apath, atitle, and acategoryidentifier. A generator SHOULD also emit the document’skind(intentorrecord); it is optional in the schema so an index written before kinds existed stays valid, and a consumer treats an absent value asintent. A record’s entry additionally carries itsdatewhen the document declares a valid frontmatterdate; the generator sources dates from frontmatter only, never from prose or filename conventions. A stale index (one that no longer matches what the index files resolve to) MUST be treated as a validation failure. A host that declaresfederation.mountsalso carries, in the same index, a top-levelmountsarray: one routing record per mount (name,source,pin,trackingRefwhen declared,owner,rolewhen declared, and the routing metadatacategories/topics/requiredWhen). These are routing records only; tooling MUST NOT copy a sibling’s entries or prose into the host index, and a mount record carries nothing the host’s audience may not see (per distribution.md, Restricted mounts). - Changelog. A context layer claiming
indexedconformance or above MUST carry a machine-readable changelog of context layer changes. Entries carry a stableid, a UTCdate, atype, a one-linesummary, and the affectedpaths. Canonical order is derived, not positional: tooling MUST order entries by(date, id)ascending, and array position carries no meaning. Becauseidis unique within the changelog (Identifiers),(date, id)is a total order even when two changes share adate. Surviving entries are immutable: tooling MUST treat modification of a published entry as a validation failure wherever it can establish the prior state, and reordering the array is not a modification. Establishing that state needs a distinct baseline to compare against; where the reference tooling has only the current revision, as in an ordinary continuous-integration checkout, the modification is not visible to it and the review of the change set is what catches it (see conformance.md). The changelog is a recency surface, not an archive: a long-lived context layer SHOULD compact it rather than let it grow without bound, and MAY compact it at any time by removing entries from the oldest end of that order, provided the same change set appends an entry of typecompactionwhosecompactedfield records the count and the first and last removed ids. Removal of anything but the oldest entries, removal without a compaction entry, and compaction to an empty file are validation failures. Append-only discipline is set-keyed byidand checked against the prior committed state, so it needs git at authoring time; the file itself stays git-free for consumers, and git history holds the full record. A change set that touches governed documents (those the category index files resolve to) MUST append an entry whosepathscover the governed paths it changed, so every changed governed document falls under some appended entry: append-only discipline keeps published entries immutable, and this coverage rule keeps the record complete. A human-readable changelog MAY exist alongside; the JSON record is the one tooling reads. - Frontmatter artifacts. Agent profiles and decision records are markdown documents whose YAML frontmatter validates against their schemas. The prose body stays free-form; the frontmatter is the machine contract. Pure-JSON profiles or decisions MUST NOT be required: people read these documents.
- Identifiers. All
idvalues MUST be stable once published: renames and moves updatepath, neverid. Identifiers are lowercase, hyphen-separated, unique within their artifact type. A generated index entry’sidis derived in priority order: the document’s frontmatteridif it declares one; else theidthe stored index already carries for that same path, or, for a pure content-preserving move, for that same content; else a slug of the filename, de-collided against its parent directory. The first that exists wins, so a publishedidsurvives a rename or a move and only a brand-new document mints a fresh one. A document that might be moved and edited in one change set SHOULD declare a frontmatterid: only frontmatter pins the id through a simultaneous path and content change (the carry-by-path and carry-by-hash fallbacks both miss it), and tooling warns (id-vanished) when a stored id disappears so the dangling references it leaves are caught. - Timestamps. Changelog
datevalues are ISO 8601 in UTC: either a calendar dateYYYY-MM-DD(ordered as that day’s start,T00:00:00Z) or a whole-second timestamp ending inZ(for example2026-06-13T15:04:05Z). Zoneless times, non-UTC offsets and fractional seconds are not permitted: fractional seconds would break the guarantee that a lexical sort ofdateis a chronological sort, since…05.1Zsorts before…05Zwhile being later. Every date field in every artifact is calendar-ranged, so a month of13or a day of99is invalid. Other artifacts’ dates follow ISO 8601 and MAY be date-only. Paths are POSIX-style, relative to the repository root, no leading./. - Every JSON artifact except the manifest MUST declare the schema line it was written against (
schemaVersion), per versioning.md; the manifest declares its target spec line with the self-naminglejikey. - Derived surfaces inherit access constraints. The index, the changelog, the generated viewer, and any compiled or exported view built from context layer content are derived surfaces, as is the output an agent produces from that content. A derived surface carries the access constraints of the most restricted content it draws from. A derived surface MUST NOT be written or copied to a location with a broader audience than that content without an explicit, reviewed redaction step that produces a separate surface for that audience, and an agent MUST NOT quote or summarize restricted context into a broader-audience or less-restricted surface (a pull request, ticket, chat, commit message, or public context layer). The index of a restricted context layer can be as sensitive as its prose: titles, paths, and summaries all describe it. This is a constraint on the people and agents who operate the tooling, not a check the tooling performs: Leji defines no audience model a tool can read to compute “broader audience” (access is the version control system’s, per governance.md, Access boundary), so the reference SDK does not enforce it and the most any tool does is warn (the viewer export warns to host it privately).
Task routing#
The index, the category assignments, and the decision records exist so an agent can load the slice of context a task needs instead of the whole tree. This section defines, normatively, how a task’s scope selects that slice. It is the one routing algorithm the rest of the specification refers to: the boot profile’s Loading section (boot-profile.md) points an agent here in task language, decision-record scope (decisions.md) is matched by it, and federated reading (distribution.md) reuses it to decide which siblings a task touches. Routing reads context; it is not a task envelope or an execution protocol, which stay outside 1.0 (see README.md, Extension boundary).
- Input. A task’s scope is the set of repository-root-relative POSIX paths the task reads or changes (normalized per Requirement 6: POSIX-style, root-relative, no leading
./), together with any categories the task explicitly names and any topics the task explicitly names. Topics are explicit inputs: the algorithm never derives them from paths, categories, prose, or content. How an agent or tool derives the scope from the task is out of normative scope; the matching below is not. - Path match (lexical, bidirectional). A declared path and a task path match when, after normalization (POSIX-style, root-relative, no leading
./, any trailing/removed), the two strings are equal, or one is a path-prefix ancestor of the other: the shorter equals the longer truncated at a/boundary. Matching is purely lexical: it never consults the filesystem, and it draws no distinction between a path that names a file and one that names a directory, because after normalization the two are indistinguishable. This is containment in either direction (theunderPathrelation the reference implementations share), so a broadly-scoped task and a narrowly-declared selector find each other whichever side is broader. - Category match (narrow), and the two category sets. A task’s categories divide into expanded and signalled. A category the task explicitly names enters both sets. A task path that is itself a governed document (matching its generated index entry by exact equality, never by containment) contributes that entry’s category to the signalled set only. Expanded categories load their intent documents and record candidates; signalled categories are a matching signal for decisions and federation mounts and load nothing on their own. A category selector MUST NOT infer a category for an arbitrary repository file, and a task path that is not itself a governed document, including any ancestor directory of one, contributes no category at all. Path scope reaches files; category expansion does not follow it.
- Topic match (exact, mount-only). A topic is a non-empty string of Unicode scalar values, compared by its UTF-8 encoding; a lone surrogate is not a valid topic. Both sides are held to that rule: a task topic or a mount
topicsentry that is not a non-empty string of Unicode scalar values is an input error, and an implementation MUST reject it rather than return it as a silent non-match. A task topic matches a declared topic when the two decoded strings are exactly equal. Implementations MUST NOT case-convert, Unicode-normalize, locale-compare, trim, tokenize, substring-match, or fuzzy-match either side, so canonically equivalent spellings that differ in bytes do not match; this equality rule is separate from the bytewise ordering of results below. Duplicate task topics form one signal, so naming a topic twice matches exactly as naming it once does. A federation mount matches when any task topic equals any topic the mount declares. A topic match selects the mount only: it MUST NOT enter the expanded or signalled category sets, load any document or record, route any decision, evaluaterequiredWhen, or make a mount required. - Status filter. Only a decision record whose
statusbinds is routed as current guidance.acceptedanddeprecatedbind; adeprecatedrecord binds with a stale posture, and an agent MUST treat it as guidance on its way out rather than settled current practice. Asupersededrecord MUST NOT bind except as history and MUST carrysupersededBy;proposedandrejectedrecords MUST NOT bind. A record that binds is live. - Unscoped decisions. A live decision record that declares neither
affectedPathsnoraffectedCategoriesis org-wide: it is routed for every task, whatever the task’s scope. Scoped live decisions are routed only when the task matches them by path (2) or category (3). - Empty path scope, and empty scope. When the task’s path set is empty, path matching contributes nothing and an agent MUST state that path-scoped routing was not evaluated; explicitly named categories are still honored and still expand, and explicitly named topics are still matched. Named categories and named topics both count toward a non-empty scope. The task’s whole scope is empty only when it names no paths, no categories, and no topics; then an agent routes only the unconditional boot-profile and agent-profile context plus the org-wide unscoped live decisions. An agent MUST NOT present an unrouted load as though it were scoped.
- Records route as candidates. An expanded category routes its intent documents as required context; that category’s records are returned separately, each with its kind and date, as candidates the reader loads by judgment. A record becomes required only when the task’s paths select it directly under item 2, an agent or boot profile names it, or a person asks for it; being a category match, or carrying the newest date, never makes a record required. Where a record is both a category candidate and directly path-selected, the direct selection wins and it is required. Routing MUST NOT certify any record as “the latest” or “the current one”: 1.0 defines no record-series identity or ordering guarantee, so recency judgments belong to the reader, made against the dates the index surfaces. Decision records keep their own routing (items 5 and 6) and never route as generic records. Naming a decision file as a task path does not route that decision; its declared scope does.
- Citation. An agent that loads routed decision records MUST cite which matched records it loaded, so a reader can see which guidance the agent applied and infer what it did not.
The routed slice is what an agent loads for a task. It is the union of: the boot profile’s unconditional load set and the active agent profile’s requiredRead, which the agent holds as its baseline independent of any scope; every governed intent document in an expanded category; every governed entry the task’s paths select under item 2; every record directly path-selected under item 8; and every live decision the task matches by path or by a signalled or expanded category, plus the org-wide unscoped live decisions.
Signalled categories contribute matching only, to decisions and to federation mounts, and never expand a corpus.
The slice is not the envelope. A tool that computes routing returns the slice alongside material the agent must not load unprompted: record candidates, and the routed-candidate metadata around them. Loading the envelope wholesale defeats the point of routing.
Result ordering is normative where a tool emits one, so independent implementations agree byte for byte: categories in the canonical category order of this specification; documents, records and decisions by ascending path; mounts by ascending name. String comparison is bytewise over UTF-8, not locale- or codepoint-collation dependent.
Tooling MAY offer a helper that computes this slice from a path set; the reference implementations expose one (route). Such a helper computes the scope-dependent portion and is not required to emit the baseline, which the caller already holds; the agent’s obligation to load that baseline is unchanged. Routing conforms whenever a raw reader follows this algorithm.
Notes (non-normative)#
The reference tooling checks the changelog’s schema and append-only discipline today (leji validate runs both); verifying changelog coverage against a base revision, that every changed governed path appears in an appended entry, is a reported check on the roadmap, not yet a blocking gate. Until it ships, coverage rides the process-attested review and CI discipline (see conformance.md).
The index is the navigation source for the governed context: leji viewer renders that governed spine from the index, and below it the repository’s own directory tree as a browsable reference zone, so one view surfaces both the governed context and the team’s existing navigation. Any docs tool can project the index the same way. Presentation is non-normative. The surface is deliberately small. Five shapes are enough for tooling to validate a context layer, diff it, score its freshness, and route an agent to the right slice, and few enough that a team can hold the whole surface in mind. Anything beyond these five is post-1.0 territory, gated on lived practice.
Decisions
Decision records are the context layer’s dated account of why: architecture decisions, vendor selections, scope boundaries, intentional non-decisions. They stop re-litigation, and they give agents the reasoning rather than just the rule.
Decision records are the formal record subtype (see content-categories.md, Intent and records): inherently records, with a uniform schema and lifecycle generic records do not have. Their generated index entries carry kind: record; a decision record declares no kind key of its own (the schema is closed, so an explicit kind fails validation).
Requirements#
- Decision records are markdown with YAML frontmatter valid against
decision-record.schema.json, one record per file. The decision corpus is the union of two manifest-declared surfaces, and a context layer MAY use either or both: the declared records path (machine.decisionRecordsPath, default<root>/decisions/) and the entries thedecisionscategory’s index files resolve to. A record MUST be reachable through at least one of the two. - Frontmatter MUST carry:
id(stable),title,status, anddate.statusis one ofproposed,accepted,superseded,deprecated,rejected. - The body MUST state, in prose: the context (what situation forced a decision), the decision itself, and its consequences. The RECOMMENDED section headings are
## Context,## Decision,## Consequences; a record MAY add## Alternatives. - Records are append-only history: a record MUST NOT be edited into a different decision. Two frontmatter fields are mutable as a decision ages,
status(its lifecycle) andsupersededBy(set when it is superseded); everything else, theid, the originaltitleanddate, the declared scope, and the prose body, is immutable once published. A reversal or change is a new record whose frontmatter setssupersedes, and the old record’sstatusbecomessupersededwithsupersededByset. The supersession link MUST stay consistent in both directions: when record B setssupersedes: A, record A carriesstatus: supersededandsupersededBy: B, and asupersededrecord MUST name its successor insupersededBy. Both records remain. The reference tooling enforces the bidirectional supersession consistency today. It does not yet verify immutability itself (that a published record’s frozen fields and body are unchanged against a base revision); that is a reported check on the roadmap, not yet a blocking gate. Until it ships, immutability rides the process-attested review discipline (see conformance.md). - A record MAY declare
affectedPathsandaffectedCategories, so tooling can route from a task’s scope to the decisions that govern it. How a task’s scope selects records (overlap-aware path containment, narrow category match, theaccepted/deprecatedbind, and the org-wide treatment of a record that declares neither) is the Task routing algorithm in machine-readable-surface.md. - Rejected proposals are records too (
status: rejected). A decision not taken, written down, is the cheapest re-litigation insurance there is.
ADR compatibility (non-normative)#
Leji decision records are deliberately compatible with Architecture Decision Records: an existing ADR directory satisfies decisions by adding the frontmatter fields to each record (or to new records going forward) and mapping the directory in the manifest. No ADR tooling is required, and none is excluded.
Governance
Governance is what separates a context layer from a wiki. The semantics are the circle: equal access, not equal authority.
The circle, normatively#
- Everyone reads. All participants, people and agents alike, with access to a context layer MUST be able to read the whole of it. A context layer with role-gated reading within it isn’t a shared context layer; where different people may read different material, that material belongs in separate context layers (see Access boundary below and distribution.md).
- Anyone proposes. Any participant, person or agent, MAY propose changes to the context layer. Agent-authored proposals are first-class: an agent that discovers missing or wrong context while working SHOULD propose the fix in the same change set as the work that surfaced it. A proposal SHOULD carry rationale sufficient for a reviewer to understand its intent and expected effect; that rationale is the minimum a person needs in order to approve. Leji 1.0 defines no generalized evidence protocol (see Scope of 1.0).
- People approve. Every change to the context layer MUST be approved by a person before it becomes canonical. Approval rides the repository’s existing review mechanism (pull requests); Leji introduces no separate process. Participation MAY happen through any interface, but canonical approval MUST be an auditable review record in that mechanism: attributable to the approving person and bound to the change set under review. An approval expressed only in external discussion, chat, ticket state, or document comments does not count until it becomes such a record; mirroring it into a comment does not. Widening how people take part never moves where authority is recorded.
Requirements#
- Ownership, not authorship. The manifest MUST name a primary owner (
owners.primary) and MAY name a continuity owner (owners.continuity): a different person who carries the same accountability when the primary is unavailable or steps away. An assisted adoption SHOULD name the continuity owner before outside help leaves; a solo context layer MAY have none, which honestly signals it has no succession. Owners are accountable people: an agent proposes and reviews but never owns, and naming the primary again as continuity provides none. The owner is accountable for the context layer’s health: that it stays current, that stale or contradictory content gets pruned, and that every area has someone who tends it. Owners are not curators. The content is written and kept true by the whole circle as it works; concentrating that in one keeper is the bottleneck this model exists to avoid. - Review scope. Context layer changes SHOULD be reviewed by the people closest to the affected content, the area owners, not funneled through a single gatekeeper. Area ownership is the repository’s existing ownership map (a
CODEOWNERSfile, a team convention), not a new manifest field; Leji reuses it the way it reuses pull requests for approval. The primary owner is accountable for ensuring every area has one. Review asks more than “is this true?”: why this belongs in the context layer, who will rely on it, what shows it holds, and when it should be revisited. A change that cannot answer those is a link or a note, not canonical context. The standing review question for any change set is did this change alter the context?; if yes, the context delta belongs in the same change set. - Inclusion and removal. Proposing is open; inclusion is not. Content belongs in the context layer only if it changes how future work is done: it sets a constraint, encodes a decision, defines an interface or ownership boundary, or stops a repeated mistake. Everything else is linked, not absorbed. The context layer MUST have a removal path as deliberate as its approval path: stale, superseded, and duplicated content is pruned in ordinary reviewed change sets, and pruning is part of each area owner’s duty, not a separate cleanup project. A context layer that only ever grows is one that rots while passing review. Durable guidance SHOULD clear the proven-twice gate (per content-categories.md): a one-off fix can be merged, but a norm becomes canonical only after it has held across at least two real tasks. Capture what is true, not what is hoped for.
- Changelog discipline. At
indexedconformance and above, every approved context layer change MUST append a machine-readable changelog entry per machine-readable-surface.md. - Freshness. Freshness is an intent mechanism: intent documents and agent profiles SHOULD carry review horizons (
freshness.reviewAfterin index entries and profiles), and a record carries none (its date is its currency, per content-categories.md; a horizon declared on a record is a validation error). Tooling SHOULD report intent content whose horizon has passed, and MUST NOT silently treat stale content as current. A reader that loads context for a task MUST surface, in that task’s output, any loaded item whose review horizon has passed, so the staleness is visible to the person rather than buried. Whether the next expected record in an operational series is overdue is a distinct notion (stream recency); 1.0 names it and defines no mechanism for it. A task’s required context is the union of the boot profile’s unconditional load set, the active agent profile’srequiredRead, and the slice the Task routing algorithm selects for the task (its routed live decisions and routed governed intent documents, plus any record the task’s paths select directly, per machine-readable-surface.md). When a required item’s horizon has expired, the reader MUST stop or ask rather than proceed on it; acting on context known to be past its review is the silent-staleness failure this rule exists to prevent. A stale item that is not required MAY be used with its staleness noted. Atgovernedconformance, freshness horizons MUST be declared and checked (per the conformance checklist, report-only checking is acceptable); running the check in CI is RECOMMENDED. Review freshness (above) is distinct from checkout currency: whether the copy a reader holds matches the canonical repository. A reader establishes checkout currency from the version control system (git); the working tree is current only to the revision checked out, and tooling MUST NOT silently treat an unverified copy as current. A reader that reaches the context layer as plain file content, with no accessible git working tree or version metadata (file content uploaded or synced into another interface, without the repository), MUST treat checkout currency as unknown rather than current. - Canonical content lives in the context layer. Knowledge that governs how work is done MUST NOT exist only in a vendor configuration file, a chat thread, or an individual’s notes. If it governs work, it belongs in the context layer, under review.
Access boundary#
Leji defines no access-control mechanism of its own. Access to a context layer is governed by the version control system (git) and the platform the repository lives on: the repository host’s permissions, and the filesystem or shared drive that exposes the working tree. The context layer is the unit of access.
- A context layer MAY live in an access-controlled repository. Leji does not grant, check, or enforce that access; the version control system and its host do.
- A conforming context layer MUST NOT require role-gated reading within itself. “Everyone reads” is scoped to a context layer’s audience: everyone the version control system admits reads all of that context layer.
- Content that needs a narrower audience (an executive, finance, security, or incident-response context) MUST live in a separate context layer with its own repository, manifest, owner, and review gate, permissioned by the version control system. Restricted context is a separate context layer, never a restricted region of a shared one.
- Composing a restricted layer into another team’s context is the federation case, with the additional rules for restricted mounts in distribution.md.
The maintenance model (non-normative)#
The context layer is maintained one delta at a time, riding work that is happening anyway: a task surfaces missing or wrong context; the fix travels in the same reviewed change set; the changelog records it. There’s no separate documentation sprint, and there never needs to be one. Wrong context produces wrong output that someone feels the same day; that, plus review, plus CI on the mechanics, is the entire forcing system.
That fast feedback catches wrong content quickly. Slow accumulation, content that is merely mediocre or redundant, is caught by the inclusion bar and the removal path above, applied by the people who own each area. This is curation, and Leji distributes it on purpose. A single curator looks like the safe way to hold quality and is the opposite: they become the slowest path in the system, changes queue behind them or route around them, and they hold less context than the area experts do. The context layer either stalls or forks. Every area owner pruning and gating their own slice is what keeps the whole context layer small and true without a chokepoint. The owner tends the health system; the circle tends the content.
Distribution
Where the context layer lives relative to the work it describes. Three patterns; one rule throughout: the context layer is docs-only and MUST NOT introduce a build or runtime dependency into any consuming repository.
Pattern 1: Monorepo (default)#
The context layer lives in the same repository as the code and infrastructure it describes, at the context root. This is the RECOMMENDED pattern wherever the team’s work lives in one repository: code, infra, and context version together, and drift is structurally hard.
Pattern 2: the docs-only submodule for multi-repo setups#
When work spans many repositories, the context layer lives in a dedicated context repository, and consuming repositories mount it as a git submodule.
- The context repository is a normal git repository with its own
leji.json, branch policies, and review gate. - Consuming repositories MUST mount it at a fixed path (RECOMMENDED:
context/) and MUST NOT couple any build or runtime step to its presence: a missing or stale mount degrades knowledge, never the build. - Each consuming repository pins a specific version of the context layer. Pin updates MUST arrive as reviewable change sets (scripted or bot-raised pull requests), so context changes are visible, reviewable, and attributable per repository.
- Tooling SHOULD report stale pins (how far each consuming repository is behind the context layer). Stale-pin reporting MUST precede any blocking enforcement: visibility first, gates later. The 1.0 reference SDK’s pin reporting covers federation mounts (pattern 3); for this pattern’s consumer-side pins it ships no check, so at
federatedthis item is process-attested (see conformance.md); a team or its own tooling reports it until a reference check lands.
Pattern 3: federation of sibling context layers#
Patterns 1 and 2 each have a single context layer: a monorepo owns one, and a multi-repo organization consumes one. Federation is the pattern for an organization where more than one team already owns a context layer of its own, and the goal is to make those context layers legible to each other without anyone surrendering control.
The instinct is to merge them: one context repository, every team’s knowledge pulled in. Resist it. A context layer stays current because the people who own it read it on every task and fix what’s wrong in the same change set. Pull product’s context into platform’s repository and you have separated product’s content from product’s accountability; it rots while everyone assumes someone else now owns it. Centralizing knowledge recreates the bottleneck that put it in heads and threads to begin with.
Federation composes the context layers instead of absorbing them. A team’s context layer joins another team’s graph as a sibling: mounted, referenced, and read, never copied.
-
A sibling context layer joins as a pinned mount declared in the host manifest’s
federation.mounts: the sibling’snameandowner, itssourcerepository locator, and apinnaming the full immutable commit id of the sibling revision the host reads. The pin is the mount’s version of record, held in the manifest itself so pin updates arrive as reviewable change sets: a mount records which version of another team’s truth this repository was reading, not a fork of it. A mount MAY declaretrackingRef, a fully qualified branch or tag on the source that staleness and reachability are judged against; absent, the source’s advertised default branch is used at check time and named in the report. -
The sibling keeps everything that makes it alive: its own repository, owner, review gate, changelog, and conformance claim. The host MUST NOT copy sibling content into itself. Content separated from the team that owns it goes stale with nobody responsible, which is the exact failure federation exists to prevent.
-
Mounted content materializes as a resolver-hydrated layer projection, never a committed copy. A sibling is usually a layer embedded in a larger repository (pattern 1), so checking out the sibling wholesale would vendor a product to read its context. Instead, tooling extracts the layer projection at the pin, the deduplicated union of everything the sibling’s own manifest makes readable (the root
leji.json, the tree under the declared context root, the boot profile, the machine index and changelog files when present at the pin, the agent-profiles and decision-records trees when present, every agent profile named by theagentsbindings, every category index file, and every governed path the pinned generated context index lists, wherever those live; the sibling’s own manifest defines its projection, the host never curates it), into an ephemeral cache the host’s version control ignores. The failure boundary follows the same line: a referenced or schema-required file absent at the pin (the boot profile, a category index, a bound agent profile, an indexed governed path) fails the projection with a stable code naming the declaring artifact and the missing path, while an absent directory or an absent machine artifact contributes nothing and fails nothing, whether its effective location was declared or defaulted; git cannot represent an empty directory, and a layer without a generated index simply has no content closure beyond its root tree. An availability-class projection failure (the pin’s content is missing or malformed) leaves the mount unavailable on this machine and never fails ordinary host validation or the host’s product build. A safety or internal projection failure (an escaping path, a malformed string, an exceeded limit) aborts hydration with a nonzero exit, and no partial projection is ever published. The pinned bytes are resolved from a git object store (a machine-local hint repository, the resolver-managed store, or a host submodule’s object database), never from any working tree, and network access happens only as an explicit, consented step. A host repository MAY carry a submodule of the sibling for its own reasons; tooling treats it only as one more local object store, and a checkout is never readable mounted content. Committing sibling content into the host, including cache contents, does not conform. The docs-only rule from pattern 2 holds by construction: nothing in the host builds or runs against the projection.A machine path that resolves to the repository root selects nothing. A declared
machine.agentProfilesPathormachine.decisionRecordsPaththat resolves to the sibling’s repository root contributes no directory selection to the projection: honoring it would vendor the entire sibling repository, which is the outcome the layer projection exists to avoid. Nothing referenced is lost, because profiles and decision records named individually, through theagentsbindings or the pinned generated index, still travel; only the blanket root selection is dropped.Projection limits. A resolver MUST enforce four bounds, so that an independent implementation refuses the same inputs rather than each one choosing its own ceiling. A projection carries at most 65,536 entries, counted after deduplication. Its content totals at most 2 GiB (2,147,483,648 bytes). No single projected path exceeds 4,096 bytes, measured as the path’s UTF-8 encoding rather than its characters, code points, or any runtime’s native string units, which differ across implementations and would otherwise accept different paths. One whole-tree listing occupies at most 256 MiB (268,435,456 bytes) in transport; this bounds the enumeration metadata a resolver reads in order to select, not the projected content the byte limit caps, and the two are deliberately different numbers because a large repository may hold a perfectly small valid projection. Exceeding any of the four is a safety-class projection failure.
-
A mount that is not materialized degrades knowledge, never the build. Validation separates three concerns. A manifest that lies is an error: duplicate mount names, a mount reusing the host’s own
name, or a missing or malformedsourceorpin. A declared mount that is simply not hydrated on this machine is a warning: honest degraded availability, reported and skipped. Integrity of a materialized projection against its pin is a diagnostic surfaced by tooling, fatal only under opt-in enforcement. Ordinary validation MUST NOT fail, fetch, or prompt because a mount is unavailable; hosts that want enforcement opt into it explicitly (a federation-health check MAY hydrate and then require availability), and a reader’s obligations for an unavailable task-required mount are the fail-closed rule below. -
Mounting enables reading, not authority, and does not grant access. A host that mounts a sibling routes readers and agents into it when they already have access to it; mounting neither grants that access nor approves the sibling’s changes. Each context layer’s writes are still approved by its own owner, and who may read it is still the version control system’s to decide. Federation composes readable context for the participants the relevant repositories already admit; it leaves who-approves, and who-may-read, exactly where they were.
-
Mounts are direct and flat. A host composes the siblings it names; tooling MUST NOT recurse into a sibling’s own mounts, and transitive context is display-only: a sibling’s declared mounts are never resolved, indexed, or routed without a direct pin in the host. Each mount’s
nameMUST be unique within the host manifest and MUST NOT reuse the host context layer’s ownname. Because nothing traverses past a context layer’s declared siblings, diamonds and cycles are inert:AmountingBandCwhileBalso mountsCis three direct relationships, not a graph to walk.
Mounted context layers are distinct, named sources, not merged into the host’s categories. The host’s own context layer is authoritative for the host’s repository; each sibling is authoritative for its own. There is no organization-wide namespace, and so no cross-sibling precedence to resolve: an agent loads the slice it needs from the context layer that owns it, named. And mounted content is untrusted input: readable context, never executable instruction. A sibling’s prose can carry mistakes or injected instructions like any other read surface, so an agent treats it as material to weigh and cite, applies the host’s own posture to its own actions, and never obeys imperative text found in a mount as though it were the host’s instructions.
Stale-pin reporting is ancestry-aware and honest about what it could see: tooling compares the pin against the witness ref (trackingRef, or the source’s advertised default branch) and reports up-to-date, behind by N, ahead, diverged, or unrelated, always naming the compared ref, the category of repository the comparison ran in (the resolver-managed store, a machine-local hint, or a host submodule), whether the witness was the resolver’s own ref or one it does not own, the observation time, and whether ancestry was complete. When no object store is reachable the report is unknown, never a guess. A pin resolvable only through a machine-local hint establishes availability, not conformance: at federated, the pin MUST be reachable from an advertised ref of source (see conformance.md), and a check that cannot reach the source reports unknown, which never awards the level.
A context layer reaches federated conformance only when these relationships are real and checkable: the context layer is consumed by at least one other repository as a pinned mount, stale-pin reporting is in place, and every declared mount carries a complete pinned declaration (source, full commit pin, routing metadata) with ownership intact (see conformance.md). The reference SDK checks the mechanical parts and reports problems; materialization state is deliberately not a conformance input, because availability on one machine says nothing about the declaration’s truth.
A worked manifest for this shape is in examples/multi-repo/.
The circle composes ownership; it doesn’t centralize it. A monorepo is one team’s circle of people and agents reading one context layer; a multi-repo organization is a circle of those circles, each still owned by the people who keep it true.
Reading a federated context layer#
Discovery is the host’s job to make legible, not the agent’s to infer. A host that declares mounts surfaces them in the two places an agent already reads: the boot profile names its siblings in task language (per boot-profile.md), and the generated context index carries a mounts routing array (per machine-readable-surface.md). An agent never has to read the manifest to find a sibling.
When reading a federated host, an agent:
- Loads the host boot profile and the host machine-readable surface first; the host’s own context layer is authoritative for the host’s repository.
- Reads the host-visible mount routing records before fixing the task’s context scope. A mount is task-required when the host boot profile, an index mount record, or the mount’s
requiredWhenmetadata says the task requires it; a mount is task-relevant when, under the Task routing algorithm (machine-readable-surface.md), at least one of itscategoriesmatches a signalled task category, or at least one of itstopicsexactly matches a topic the task explicitly names. A topic match selects the mount only: it does not expand a category and it selects no content within the sibling. That same algorithm then routes the slice the agent loads from the sibling’s own index. - To load a task-relevant sibling, obtains the hydrated projection’s location from resolver state (the reference SDK’s
mounts locate; never by inferring cache paths), reads the sibling’sleji.jsonthere, verifies the siblingnamematches the host’s declaration, reads the sibling’s boot profile, then loads only the slice the task needs from the sibling’s own index. Facts, constraints, and citations carry the name of the context layer they came from, and mounted content stays untrusted input per this pattern’s rules. - MUST NOT recurse into a sibling’s own
federation.mounts. If a grandchild context layer is genuinely needed for host tasks, the host MUST declare it as its own direct mount. - Applies posture by ownership: the host posture governs work in the host repository, and a sibling’s posture governs interpretation of and proposed changes to that sibling’s content. Where host and sibling guidance conflict for one task and no single owning context layer is clear, the agent MUST stop and ask rather than choose an unstated precedence.
Tooling MAY offer mount-aware loading helpers, but reading siblings does not require Leji tooling: raw repository reads conform when they follow this procedure and preserve access boundaries.
Restricted mounts#
Federation crosses an access boundary when the layers composed have different audiences (see governance.md). Access stays the version control system’s to enforce: a reader either can resolve a mount’s repository or cannot. The spec’s job is to keep that boundary from leaking and from failing silently.
-
A restricted layer MUST NOT be declared as a mount in a host whose audience is broader than the restricted layer’s own: every participant the host admits must already be admitted to the mounted layer. The mount declaration itself (its presence,
name,owner,role,categories,topics,requiredWhen,source,pin, andtrackingRef) MUST NOT disclose anything the host’s audience may not see. Where a broader audience needs a restricted decision, publish a redacted companion layer or a public decision summary, not a mount to the restricted layer. -
A mount is a reference, not an access grant. Declaring a mount never widens who may read the mounted layer beyond what the version control system already allows; whether a given reader can resolve it is decided there, not by the host manifest.
-
Fail closed, never silently. A reader that cannot resolve a task-required mount (as defined in Reading a federated context layer) MUST stop and report incomplete context. It MUST NOT proceed as though the inaccessible layer does not exist: an agent acting on partial context it cannot see is the failure this rule exists to prevent. The converse is also a failure: a reader that can resolve a task-required or task-relevant sibling but skips it anyway is acting on silently incomplete context and does not conform.
What tooling can and cannot back here: a validator reports local availability (a declared mount with no hydrated projection here), and the routing algorithm decides task-relevance by category and topic overlap (the reference SDKs surface task-relevant mounts; see machine-readable-surface.md, Task routing). But task-requiredness turns on
requiredWhen, which is free-text task conditions, and runtime reachability turns on the reader’s own access at read time; both are the agent’s to judge, not a tool’s. The fail-closed MUST is therefore agent-attested: tooling surfaces what it can see, the agent enforces the stop.
Notes (non-normative)#
The submodule pattern’s bad reputation comes from code submodules with build coupling. A docs-only leaf has none of those failure modes: nothing compiles against it, nothing breaks when it lags, and the pin is just a recorded “which version of the truth was this repository working from”, which is information, not risk.
Federation looks like more moving parts than a merge, and is less. A merge is cheap once and expensive forever: every cross-team edit thereafter routes through whoever owns the central repository, and the parts no single team reads daily are the parts that rot. Sibling mounts keep each context layer small, owned, and read, and pay only the price of a pin update, which is a reviewable diff, not a meeting.
Conformance
Partial adoption is by design. Four levels, each containing the previous; a team claims its level in the manifest (conformance.claimedLevel). Self-attestation only: there is no certification program.
Conformance is evaluated against the context layer as it is materialized where the check runs, not against a canonical layer that a copy might represent. A copy reached without its repository is read in the degraded mode of context-layer.md, and degraded reading is never a path to canonical authority: such a copy does not verify, and the tooling says so rather than leaving the question open.
Most checklist items are machine-verified: the reference tooling checks them against the layer and fails a claim they do not hold. Four outcomes are reported, and they are deliberately not interchangeable:
fail: the evidence was gathered and the requirement is not met.- (process-attested), reported as
manual: the item describes a team practice (a review gate, a CI job, an external consumer) that no tool can confirm from the repository alone, so the team stands behind it. Only items tagged (process-attested) below are ever reported this way. unknown: a machine item whose evidence was unobtainable in this run, such as the federated pin-reachability check without access to the source, or append-only discipline with no git baseline to compare against.unknownnever awards a level, and never refutes a claim that an evidence-bearing run could confirm.not applicable: a conditional machine item that does not apply to this layer, such as the federated mount items on a layer that declares no mounts. It is not scored, and it is not evidence in either direction.
The verifiedLevel the tooling reports is the highest level whose applicable machine-verified items all pass, never above the level the layer claims; fail and unknown both prevent an award, and items that are process-attested or not applicable are not scored. The cap on the claim is deliberate: verification answers whether the claim holds, not what the layer could claim, so a layer claiming core whose evidence would carry it to governed still reports core, and the way to raise the reported level is to raise the claim. verifiedLevel never asserts the process-attested items, so a passing verifiedLevel is necessary but not sufficient for a level that carries them. Each item below is machine-verified unless tagged (process-attested).
Two machine-verified items behave differently in a degraded copy, and the difference follows from what evidence each has. Git presence is answered: a copy that is not in a git repository does not meet the core requirement that the context layer lives in one, so the item is a fail. Changelog append-only discipline is not answered: the file may be entirely well-formed while the prior committed state needed to compare against is unreachable, so the item is unknown and the layer simply does not verify at indexed from that copy. Neither is reported manual, which is reserved for the tagged process-attested items. Separately, the freshness reader rule (surface stale loaded context, and stop or ask on an expired required item, per governance.md) is reader-behavioral, not a conformance gate: the reference leji route stamps each routed document with its review horizon and expiry so an agent can apply it.
Three items are verified today at less depth than their stated intent, and the gap is named here rather than left for a reader to discover. The boot-profile item is verified as presence at the declared path; whether it actually covers identity, loading, and posture is reported by the opt-in --content lint as warnings, not gated. The real-decision item is verified as schema-valid frontmatter on at least one resolved record; body substance (an actual decision, not a stub) likewise rides --content. The changelog item is the third: append-only discipline is checked against the file’s state at HEAD, which catches a rewrite still in the working tree, the case a pre-commit hook exists for. In a continuous-integration checkout the working tree is HEAD, so a rewrite that arrives already committed is not visible to the check, and the review of the change set is what covers it. The item therefore verifies the working tree, not the history. The intent stated in each of the three items remains normative for what a conforming context layer carries; deepening the machine checks, and comparing the changelog against an explicit base revision, are on the reference-tooling roadmap. Verification of federated additionally requires at least one declared federation.mounts entry: a provider-only context layer (one that is consumed by other repositories but declares no mounts of its own) verifies at governed, and its federated standing rests on the process-attested consumption items.
Level 1: core#
A context layer exists and both people and agents can work from it.
- The context layer lives in a git repository, versioned with the work it describes (per context-layer.md, Requirements).
-
leji.jsonat the repository root, valid against the manifest schema. - A boot profile at the declared path, covering identity, loading, and posture.
- At least
domainorsystemmapped (via its index files) and populated with at least one resolved intent document (records alone carry no operating context), plusdecisionswith at least one real decision record: a record carrying a concretestatusand an actual decision in its body, not an empty stub or placeholder. - A named primary owner.
- Vendor entrypoint files, if present, redirect to the boot profile.
Level 2: indexed#
The context layer is legible to tooling.
- All of
core. - A generated context index, current with the tree.
- A machine-readable changelog; context layer changes append entries.
Level 3: governed#
The forcing functions are mechanical, not goodwill.
- All of
indexed. - Context layer changes ride the repository’s review gate; people approve. (process-attested)
- Agent profiles (at least a core profile) valid against the profile schema.
- CI validates the surface: manifest, index matches the tree, changelog discipline, profile frontmatter, declared paths resolve. (process-attested)
- Freshness horizons are declared and checked (report-only is acceptable).
Level 4: federated#
The context layer spans a multi-repo organization.
- All of
governed. - The context layer is consumed by at least one other repository as a pinned mount, with pin updates arriving as reviewable change sets. (process-attested)
- Stale-pin reporting is in place: consumers can see how far their pins trail the witness ref. The reference SDK’s ancestry-aware report covers declared federation mounts; consumption-side reporting beyond that is the team’s. (process-attested)
- Any sibling context layers are declared as complete pinned mounts per distribution.md: a normalized
sourceand a full commitpin, ownership intact. Materialization state on any one machine is not a conformance input. - Each declared mount’s pin is reachable from an advertised ref of its
source(the declaredtrackingRef, or the source’s default branch). This check needs source access: without it the result isunknown, andunknownnever awards the level. A pin resolvable only through a machine-local hint is availability, not conformance. - Each declared mount carries routing metadata: at least
categories, plustopicsorrequiredWhen, so an agent can decide relevance without reading the sibling. - The boot profile surfaces every mounted sibling, and the generated index carries the
mountsrouting array, so an agent discovers and loads siblings without reading the manifest (per boot-profile.md, machine-readable-surface.md).
Notes (non-normative)#
core is the minimum that makes a context layer real, indexed adds the generated surface tooling reads, governed is where the context layer stops depending on anyone’s discipline, and federated is for organizations where more than one team already owns a context layer worth keeping whole. Most teams should reach governed and stop; federated exists for those organizations, not as a maturity badge.
Versioning
Three things version independently: the specification, the schemas, and any implementing tooling.
The specification#
- The spec carries a SemVer version (currently 1.0.0). Breaking changes require a major version; every change is recorded in the repository changelog.
- A context layer declares the spec line it targets in
leji.jsonvia the self-naminglejikey (e.g."leji": "1.0"), following the OpenAPI convention. The value is the spec line (major.minor), never the spec’s patch version: a patch release (1.0.0to1.0.1) refines wording or tooling without moving the line, so the manifest stays"1.0"across every patch. Tooling MUST validate a context layer against the declared line, not the newest one.
Preview lines#
A spec line MAY be designated preview. A preview line is revisable in place: it MAY change in ways that would otherwise be breaking, rather than being bumped to a new version, until it is frozen at general availability (GA). The “breaking changes require a major version” rule (item 1) and the “$id moves on an incompatible shape change” rule (item 3) apply from the GA freeze onward, not while a line is in preview. At GA the line is frozen and both rules take effect.
A line that ships before general availability MUST declare that at its initial release.
The 1.0 line is frozen at the v1.3.0 reference-tooling release. Within the line, schema changes are additive only and the $id stays on v1.0; any incompatible change ships as a new line, never in place.
The schemas#
- Each schema carries a stable
$idof the formhttps://leji.org/schemas/v<major>.<minor>/<name>.schema.json. The$idline moves only when the schema’s shape changes incompatibly. - Within a published line, schema changes MUST be additive (new optional fields). Field removals or semantic changes require a new line.
- Machine-readable artifacts other than the manifest declare the schema line they were written against via
schemaVersion; the manifest declares its target spec line via the self-naminglejikey (item 2).
Stability set#
The following are frozen within a spec line; tooling (including future commercial implementations) builds against them with no parallel schema:
- the manifest shape and its fixed filename
leji.json, - the category identifiers (
domain,system,practice,governance,decisions), - the conformance level identifiers (
core,indexed,governed,federated), - identifier and path normalization rules per machine-readable-surface.md,
- the index entry, changelog entry, agent profile, and decision record shapes.
Implementing tooling (non-normative)#
SDKs and CLIs version on their own SemVer and declare which spec lines they support. The reference SDKs in this repository are the @leji-org/leji npm package (packages/sdk), the leji PyPI package (packages/sdk-py), and the leji Go module (packages/sdk-go, a single static binary); they are behaviorally identical and tested against one shared fixture suite.