A compiler over what this repository already states, and the writer of what an episode
learned. sources, nodes, edges, search, show, related, candidates and check
are read-only; derive, promote and reject write knowledge records and ledger lines, and
nothing else.
It is not a wiki, not a database, not a memory service, and not a second place to write things down. Every source it reads is a file somebody already maintains, everything it produces is derived and regenerable, and none of it outranks the file it came from. A result is a pointer to canonical knowledge, never a rewritten answer.
knowledge is not search. search is a literal scan over the durable operational
records — handovers, checkpoints, decisions, questions, prompts, the ledger — with no
index, because that corpus is a handful of files. knowledge covers the repository's
canonical artifacts: the policy, the profiles, the prompts, the milestone and issue
contracts, the claims matrix and the documents. Two corpora, two contracts, and neither
changes the other.
-
sources [--scope shared|operational|all]lists the curated source classes and the files each one discovers, with the class, the scope, the kind, a content hash and the repository-relative path. -
nodes [--scope ...] [--kind <k>]derives one node per canonical object: its identity, its kind, the source it came from and that source's hash. Exits10when two objects claim one identity. -
edges [--scope ...] [--type <t>]derives one edge per stated relationship, with the file and the field or line it was observed in. -
search <query> [--scope ...]returns the nodes the query matches, in an order that is a rule rather than a score. A node is placed on the first rung it reaches:id(the query is its canonical id),title(the query is its title),title-prefix(its title begins with the query),path(its source path contains the query),body(its source file contains the query literally); every comparison ignores case. Within a rung the order is byte order of the id. Each result names the rung it was placed on (matched, andrungin--json), so the reason for the order is in the answer. Nodes carry no tags, so there is no tag rung. -
show <id> [--scope ...]prints one node with its kind, scope, title, source path and content hash. Exits12when no node has that id. -
related <id> [--scope ...]prints the edges out of and into one node, each with its type and the file and field or line it was observed in. Exits12when no node has that id.None of the three writes, and none rebuilds anything: there is no stored index to go stale, so each call compiles what the checkout holds at that moment, exactly as
nodesdoes. Case 3037 asserts the ladder, the fields of every result, and that the working tree reads the same after all three. -
derive [--episode <id>] [--dry-run] [--json]derives the knowledge the episode produced into candidate records under.ai/repo/knowledge/candidates/, from the ledger and git and never from a conversation: a recorded decision becomes aconvention, a resolved question afact, a blocked or failed task alesson, a completion with a verification command afact. The open episode, or the one named. Deterministic: same ledger, same git, same bytes, and a second run reports every recordunchanged.--dry-runsays what it would write and touches neither the tree nor the ledger. -
candidates [--json]lists every record awaiting review — id, class, date, the branch of the episode it came from, title — reading the directory rather than the index, so a candidate the hook just wrote and nobody has added is listed. -
promote <id> [--class <c>] < evidence.mdmoves a candidate tocurated/asverified, with the evidence on standard input appended under# Evidenceand the class the person chose. The deriver never writesverified; this is the act that does. -
reject <id> --reason "<why>" [--by <id>]marks a candidatesupersededin place with the reason under# Rejected, naming the record that replaces it when--bygives one. -
check [--json]validates every record undercandidates/andcurated/: the schema, no unknown key, unique ids equal to the file name, provenance besideverified, every reference resolving, a reason besidesuperseded, no candidate claimingverified, no conversation. The same checkcheckanddoctordispatch.
The read side of the same store is served by the executable — bin/majordomus-cli knowledge candidates, knowledge record <id> and knowledge status, each a capability declared once
and answering identically over HTTP, MCP and the command line (CAPABILITIES.md); status
reports the last derivation, the newest close and whether the writer has stopped.
KNOWLEDGE.md has the derivation table, the two moments and the rules.
Writes: derive writes .ai/repo/knowledge/candidates/<id>.md, one per record, through a
temporary file in the same directory renamed over the id-named file, never staged, and prints
written, unchanged or skipped per record with the summary last; it appends
knowledge.derived to the ledger — episode, counts, paths — even when it wrote nothing.
promote writes .ai/repo/knowledge/curated/<id>.md the same way, removes the candidate,
appends knowledge.promoted and prints the curated path last. reject rewrites the
candidate in place and appends knowledge.rejected. The record id is the episode id and a
digest of the evidence, so the same fact always lands in the same file.
Never stages, commits, or modifies any other file. Never reads a conversation, a prompt or
a handover; never calls a model or a network. Never writes verified.
Refuses (10) promote with empty stdin, with stdin carrying a transcript marker, on a
record whose status is not candidate, or with a class outside the enumeration; reject on a
record whose status is not candidate, or when --by names no record; derive when the
policy does not parse; check on any failing record. (12) promote or reject when no
candidate carries the id. (2) reject without a non-empty --reason. (13) a write that
did not complete. derive with no episode to derive from says so and exits 0.
$ majordomus decision add "Tabs are refused in the parser" --why "two encodings of one thing"
$ majordomus knowledge derive
written .ai/repo/knowledge/candidates/s-20260912101500-7f1a-3b9c2e4d1a05.md
knowledge derive: 1 written, 0 unchanged, 0 skipped for episode s-20260912101500-7f1a
$ majordomus knowledge derive
unchanged .ai/repo/knowledge/candidates/s-20260912101500-7f1a-3b9c2e4d1a05.md
knowledge derive: 0 written, 1 unchanged, 0 skipped for episode s-20260912101500-7f1a
$ printf 'Verified by reading lib/yaml.sh.\n' | majordomus knowledge promote s-20260912101500-7f1a-3b9c2e4d1a05 --class constraint
.ai/repo/knowledge/curated/s-20260912101500-7f1a-3b9c2e4d1a05.md
No edge without provenance. Every edge names where the relationship was stated, and an edge missing any of from, to, type or provenance is refused rather than emitted with a blank — a blank source reads as "unknown" and is indistinguishable from one nobody recorded. An edge nobody can trace to a line is not a fact, it is a guess wearing a fact's clothes, and it is a guess the person best placed to notice it is wrong will never see.
Nothing is inferred from prose. The repository already states its relationships explicitly, in fields somebody maintains, and those are both free and correct. The edge types are a closed set; an undeclared one is an error rather than a new vocabulary word:
| type | from → to | stated in |
|---|---|---|
part_of | issue → milestone | the issue's milestone |
depends_on | issue → issue, milestone → milestone | the record's depends_on |
declares | milestone → claim | the milestone's claims |
specified_by | claim → document | docs/CLAIMS.yaml's source |
implemented_by | claim → implementation | its implementation |
tested_by | claim → test, doctrine → test | its test |
supports | doctrine → claim | the doctrine's claims |
references | document → document | an inline Markdown link |
Links are the one edge source that is not curated, and they are read conservatively. Fenced
code is dropped first, because a path inside a code sample is an example of a path and not a
reference to one. A target with a scheme, a protocol-relative target and an absolute path are
all skipped; a fragment is trimmed, because docs/CLI.md#session refers to docs/CLI.md.
Severity distinguishes what Majordomus owns from what an author wrote. A declared
relationship pointing at a file the repository does not contain is a FAIL: it is a broken
promise. A link in a document pointing at nothing is a WARN: a document may deliberately
point outside the repository and the target alone cannot tell the two apart. A relationship
pointing at a real file that this compiler does not model as one node — a container of many
objects, or something outside the curated set — is silent, because a report that is large by
design is a report people stop reading.
nodes and edges are two views of one derivation, so a defect in either exits 10 on
both. There is no clean node listing over a graph that is broken.
A node's identity is never its content hash. It is the object's own canonical id where
it has one — claim:policy-parse, issue:I0801, milestone:M003, profile:debugging —
and its repository path where it does not, as in document:docs/CONTINUITY.md. A hash says
whether something changed; it can never say what something is, because then every edit
would delete a node and create a stranger, and every reference to it would point at nothing
without anything saying so. The hash rides along on the node and is what freshness is
measured with.
The consequence is deliberate: editing a document keeps its node and moves its hash; renaming one is a delete and an add, and disturbs no other node.
A kind comes from structure, never from prose. It is decided by the source class the
file was discovered in and by fields the file itself declares. Nothing reads a body looking
for a word that suggests a type: a document that discusses roadmaps and milestones is a
document that discusses them. Where no rule applies the kind is unknown and the node is
still emitted, with one finding naming the class — an explicit unknown is information, and a
confident wrong answer is not.
A title is taken or left empty, never invented. From the record's own title or
description, or from a document's first level-one heading. A document with no heading gets
no title rather than its filename, because a filename standing in for a title reads like a
fact and is a guess.
Two stores have no id field of their own. A decision is keyed by its title and a question by its text — in both cases exactly what the ledger records and what a session envelope references, so three places name the same thing the same way and nothing maps between them. Answering a question rewrites the line it lives on, and the identity survives that: the appended answer is stripped, and only when what remains ends in the entry's opening date, so a question whose own text contains a dash is left whole rather than cut at a guess.
Discovery is driven by the repository index, not by a filesystem walk. A walk returns
build output, vendored trees and editor droppings; it returns them in an order that
differs between two machines; and it can return a file nobody meant to publish. Listing
tracked files instead gives repository truth in a stable order, and an untracked file is
never a source. Operational records are discovered from the state directory Majordomus
itself owns under .ai/local/, which is never tracked — and from nowhere else. No hidden
directory is scanned because it happens to exist.
The source classes are declared twice, by two owners: the repository declares its shared
sources in its AI layer, .ai/repo/knowledge/sources.yaml, and the tool ships the
operational classes, the records it writes itself, in share/knowledge-sources.yaml. The
scope of a class is decided by which file declared it; neither file names the other:
| scope | meaning |
|---|---|
shared | repository knowledge; may be projected to a public surface |
operational | this checkout's own working records; never part of a shared projection |
The scope is a property of the class, so the publication boundary is decided in one place
rather than at each producer. Every tracked pathspec carries the :(glob) prefix, under
which * does not cross a directory separator. That is not tidiness: without it,
docs/*.md also matches docs/claims/*.md, two classes silently overlap, and one file
becomes two nodes. test/cases/64_knowledge_discovery.sh fails on that mutation.
One overlap is settled by the file rather than by the classes. A section's README.md matches
both .ai/**/README.md, the context class, and its section's own pathspec
(.ai/repo/features/*.md, .ai/repo/rules/**/*.md, and so on). It declares kind: context, so
it is a context node, once: a second class discovering it adds nothing. The index applies the
same precedence in apps/majordomus-cli/src/discovery/mod.rs, and
test/cases/330_a_contract_is_the_kind_it_declares.sh proves it for knowledge nodes. Where no
file declares a kind, two classes over one path remain a refusal (case 73's twin).
init declares the context class first, so a new repository's contracts are objects of its
index from the start. Without it context resolve, which reads the files, and every capability
that reads the index disagreed about what applies to a path;
test/cases/130_context_compiler.sh fails without the class.
A class marked required that discovers nothing is reported as a WARN. The cost of a
curated list is that a path can be forgotten, and a forgotten path is indistinguishable
from a repository that does not have that file unless something says so.
Whether a file conforms to the schema of its kind is the executable's index's verdict, not the
reader's: knowledge nodes asks the index which files it refused, drops them and reports each
refusal as a FAIL (test/cases/405_the_knowledge_reader_honours_the_schema.sh). With no
executable built, or no jq, it reports a WARN that the schema went unchecked; it never
reads as a pass. MAJORDOMUS_KNOWLEDGE_SCHEMA=unasked says not to ask, and the reader then
reports an INFO that the check was not asked for. That is what a recorded use-case scenario
sets: whether the index could be asked is a fact about the recording machine, and evidence
that must read the same on every machine cannot carry it (#713,
test/cases/777_a_recorded_scenario_does_not_depend_on_the_recorder.sh).
$ majordomus knowledge sources --scope shared
policy shared policy 4f2a9c1d8b30 .ai/repo/policy.yaml
profile shared profile a1b2c3d4e5f6 .ai/repo/profiles/debugging.yaml
...
issue shared issue 0e5a7b9c3f51 .ai/repo/project/issues/I0801.yaml
claims shared claim 7c9e1b3d5f70 docs/CLAIMS.yaml
document shared document 9b1e2d4f8c3a docs/CONTINUITY.md
knowledge sources: 169 file(s) in scope shared