Where does a project’s knowledge actually live? Which port the server listens on. Why that one flag exists and what broke the day it was added. The fix somebody found at two in the morning and pasted into a chat that scrolled away. The number that was true in March. Most of it lives in people’s heads, in closed conversations, and in the prompt of whichever agent happened to be running, and all three of those places forget. Module 02 showed why a fact in a model’s context drifts and dies with the session. This module builds the place where the app’s facts go to survive: a wiki, meaning a folder of small pages that link to each other, one page for each thing worth knowing.
You build it by hand, before any agent touches it, and the order matters. The pages themselves are cheap; agents will write most of them later. What agents cannot be trusted to invent is the rulebook, because an agent asked to keep notes will happily keep bad ones, duplicated, undated, confident, and wrong in ways nothing checks. So this module builds the container and its rules, writes exactly one page as the worked example, and hands the pen to the agents only in module 35, when everything they must obey is already true.
Nothing in this module imports the SDK. It is three markdown files, one worked page, and one lint script, and the whole thing would work identically for notes you keep for yourself.
The one idea: provenance beats timestamps
Here is the failure this module exists to prevent. A page in your knowledge base says the deploy takes four minutes, and its header says last-verified: 2026-08-30. Reassuring, but what does that date actually claim? That somebody timed a deploy that day? That an agent skimmed a config file? That a script bulk-stamped forty pages during a migration? The date cannot tell you, because a date only says when something happened. It never says what happened.
Provenance is the field that says what happened: I ran it and watched, I read it in a file, I reasoned it from other pages, a machine extracted it and nobody has looked, or the sources disagree. Pair that with the date and the header becomes a claim you can weigh. Leave it out and every page in the wiki wears the same confident face, and the pages most likely to be wrong, the machine-extracted ones, are indistinguishable from the ones a person checked an hour ago. That is the gist of the whole module, and everything below is machinery for keeping that pairing honest.
flowchart TD raw["raw sources<br/>code, logs, command output<br/>outside the wiki, read only"] -->|ingest| wiki["knowledge-base/<br/>one concept per page,<br/>one fact in one place"] wiki -->|lint| schema["_schema.md + kb-lint.mjs<br/>the contract, and its teeth"] schema -->|governs| wiki
The four files
knowledge-base/_schema.md The contract, whole. An agent reads this before writing or editing any page; a person does too.
# The knowledge base's schema
Read this before writing or editing any page here. This file is the contract.
There is no separate README; the contract is the introduction. See
`docs/decisions/0100`.
## Three layers
1. **Raw sources.** Source files, SDK type declarations, container logs,
command output, the decision records. They live outside this directory and
are never edited from here. A page cites them; it does not replace them.
2. **The wiki.** This directory. One concept per page, one fact in one place.
A fact that belongs on two pages belongs on one of them, and the other links
to it with `[[page]]`.
3. **The schema.** This file, and `tools/kb-lint.mjs`, which is the only thing
that checks it. The two are edited together. A rule stated here and not
checked there is a wish.
A page is one Markdown file at the root of this directory, named for its
concept in lower-case with hyphens. Files whose name starts with `_` are
machinery, not pages, and no page rule applies to them.
## Frontmatter
Every page opens with one fenced block of flat `key: value` lines. No nesting,
no lists, no quoting. The parser accepts exactly this and nothing more, so a
page that parses is a page the schema allows.
Required on every page:
- `owner`, the one person or role answerable for the page being true. Not a
list. A page nobody owns is a page nobody re-checks.
- `volatility`, how fast the fact rots. One of `static`, `low`, `medium`,
`high`. Each carries a time to live, counted from `last-verified`:
`static` has none and never goes stale, `low` is 365 days, `medium` is 90,
`high` is 30. Past the tier's life the lint reports STALE.
See `docs/decisions/0103`.
- `last-verified`, `YYYY-MM-DD`, the day somebody or something last checked
the fact against its source. Not the day the file was edited.
- `verified-by`, who or what did that checking. A person's handle, or the
word `machine`, or the word `unverified`.
- `provenance`, what kind of checking it was. One of the five below. This is
the field that carries the signal; `last-verified` on its own is a date with
no claim attached to it. See `docs/decisions/0101`.
Optional:
- `expires`, `YYYY-MM-DD`. An absolute date the page already knows it stops
being true on: a pinned version, a deadline, a temporary workaround. Past it
the lint reports EXPIRED regardless of volatility.
- `todo`, one line of open work on this page. A worklist row, not a defect.
The lint reports TODO and still exits 0 for it.
Copy `_template.md` rather than typing the block from memory.
## The five provenance values
- `observed`, somebody ran the thing and read what came back. Evidence: the
command and the output it produced, quoted in the body.
- `read`, taken from a file in this repository. Evidence: the path, and the
line number if the file is long enough for one to help.
- `derived`, reasoned from other pages here rather than from a source
directly. Evidence: the `[[wikilinks]]` it rests on, which the body must
carry. A derived page is only as true as its links, and it says which.
- `extracted`, machine-extracted and unreviewed. A tool wrote it; no person
has read it against its source. Evidence: what extracted it. This value is
the honest one for a page that is probably right and has not been checked.
- `mixed`, the page carries claims of more than one kind. Evidence: each
claim labelled inline with the tier it belongs to. Prefer splitting the page.
## The pairing rules
These are rules, not guidance. The lint reads the pair, because either field
alone is easy to satisfy and says almost nothing.
1. **A machine-extracted page must not name a human.** If `provenance` is
`extracted`, `verified-by` must be `machine`. A person's handle there
asserts a review that did not happen, and it does so on exactly the pages
most likely to be wrong. Finding: PROVENANCE.
2. **A page naming a human must say how they verified.** If `verified-by` is
anything other than `machine` or `unverified`, the body carries a
`## How verified` section stating what that person did. A name with no
method is a signature on a blank page. Finding: PROVENANCE.
3. **A derived page cites its sources.** `provenance: derived` requires at
least one `[[wikilink]]` in the body. Finding: PROVENANCE.
4. **Every tier except `extracted` needs a checker.** `verified-by:
unverified` on `observed`, `read`, `derived` or `mixed` is a contradiction:
the tier claims evidence the field says nobody gathered. Finding:
UNVERIFIED.
## Dates live in frontmatter
Do not write a date in the body of a page. `last-verified` and `expires` are
the only dates, and they are the only dates anything reads. A date in prose is
never re-checked, never reported, and drifts silently. Dates inside code
fences and inline code spans are quoted material and are left alone.
Finding: DATE-IN-PROSE. See `docs/decisions/0105`.
## Contradictions
When a new fact contradicts a page, do not overwrite the page and do not pick a
winner. Record both claims side by side under a `## Contradiction` heading,
each with its own provenance and its own source, and set `provenance: mixed`.
Then raise it with the `owner`.
Silently resolving a contradiction destroys the one piece of information that
matters, that two sources disagree, and leaves a page that reads as settled.
The disagreement is the finding.
## Cross-references
Link with `[[page]]`, the file name without its extension. Links are how a
reader gets from a fact to the facts around it, and they are the evidence a
`derived` page rests on. A link to a page that does not exist is a note that
the page should.
## The log
Every change to this directory gets one line in `_log.md`. Format is stated
there. knowledge-base/_template.md The copyable skeleton. Copy this instead of typing frontmatter from memory.
---
owner: a person or role, one of them, answerable for this page being true
volatility: static | low | medium | high, TTL none | 365d | 90d | 30d
last-verified: YYYY-MM-DD, the day it was last checked against its source
verified-by: a handle, or `machine`, or `unverified`, who did that checking
provenance: observed | read | derived | extracted | mixed, what the checking was
expires: optional YYYY-MM-DD, a date this page already knows it stops being true
todo: optional one line of open work; reported as a worklist row, never a defect
---
# Page title, the concept this page is about
The one fact, stated first, in as few sentences as it takes. One concept per
page; a second concept is a second file.
## Evidence
What backs the fact, in the form the page's `provenance` requires: the command
and its output, the file path and line, or the `[[pages]]` it was derived from.
## How verified
Required only when `verified-by` names a person rather than `machine` or
`unverified`. What that person actually did, ran what, read what, compared it
against what. Delete this section when nobody is named.
## See also
`[[other-page]]`, why a reader here would want to go there. knowledge-base/_log.md One line per change to the wiki, each with a Why. Empty except its own format sentence.
# Knowledge log
One line per change to this directory, newest last, in the form
`YYYY-MM-DD | page | what changed | Why: the reason it changed`. knowledge-base/app-port.md The first real page, whole. Walked line by line further down the page.
---
owner: server
volatility: low
last-verified: 2026-08-30
verified-by: machine
provenance: read
---
# The port the server listens on
The default is 3000. It is set in one place, `server/src/config.js`, and every
other appearance of the number in this repository is that default being
forwarded rather than a second decision.
`PORT` in the environment overrides it. The value must parse as a whole number
and be zero or greater, or the process throws at startup with `PORT` named in
the message; port 0 is legal and asks the operating system for a free port.
## Evidence
`provenance: read`. From `server/src/config.js`, in the object `loadConfig`
returns:
```js
port: readInt(env, "PORT", 3000, 0),
```
`readInt` in the same file takes the variable's name, the fallback, and the
minimum. It returns the fallback for an unset or empty value and throws for a
value that is not an integer or is below the minimum. The `Config` typedef in
that file documents the field as "TCP port. Default 3000."
Two other files carry the number and neither decides it:
- `docker-compose.yml` publishes `"3000:3000"` and sets `PORT=3000` in the
container's environment. Its own comment calls 3000 "the server's default",
which is this page's fact, restated at the place that depends on it.
- `Dockerfile` sets `PORT=3000` in its environment block and declares
`EXPOSE 3000`. `EXPOSE` documents the port; it publishes nothing on its own.
The host interface is a separate variable, `HOST`, defaulting to `127.0.0.1`,
with the container setting `0.0.0.0`. The port and the interface are not one
setting and changing one does not change the other. tools/kb-lint.mjs Zero-dependency Node. Its header and its tables; the checks themselves run to about four hundred lines.
// The knowledge base's linter. Zero dependencies, plain ES modules, run with
// `node tools/kb-lint.mjs [dir]`.
//
// Role: read every page in a wiki directory and report the ways its
// frontmatter and its body disagree with `knowledge-base/_schema.md`. The
// schema is the contract; this file is the only thing that checks it, so the
// two are edited together or the schema becomes a wish.
//
// The finding this tool exists for is not staleness. A date only says when
// somebody last touched a page; it cannot say what they did. The pairing of
// `provenance` and `verified-by` says what happened, and reading the pair is
// the difference between a lint whose signal is strongest on the pages most
// likely to be wrong and one whose signal is weakest there. See
// docs/decisions/0101 and 0102.
//
// Invariants:
// - The clock is an argument. `lintWiki` takes `now`; nothing below calls
// `Date.now()` or `new Date()` except the CLI at the bottom. Record 0090 in
// this directory's numbering established that seam for the mail rate limit
// and this is the second module to take it.
// - Files whose name starts with `_` are wiki machinery, not pages. They are
// exempt from every page rule. The one thing checked about them is that
// `_schema.md` exists at all.
// - A clean wiki prints nothing. Silence is the success message.
// - Exit 0 when the only findings are TODO. Every other category exits 1. A
// directory that is not a wiki exits 2. See docs/decisions/0104.
import { readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
/** Frontmatter keys every page must carry. */
export const REQUIRED_FIELDS = [
"owner",
"volatility",
"last-verified",
"verified-by",
"provenance",
];
/** Frontmatter keys a page may carry and need not. */
export const OPTIONAL_FIELDS = ["expires", "todo"];
/**
* How long a page of each volatility stays fresh, in days. `null` means the
* tier has no TTL: a static fact does not rot, so a date on it is a record of
* when it was checked and never an alarm.
*
* The tiers and these numbers are argued in docs/decisions/0103.
*/
export const VOLATILITY_TTL_DAYS = Object.freeze({
static: null,
low: 365,
medium: 90,
high: 30,
});
/**
* The five provenance values, and what the lint requires of each.
* What a page looks like, line by line
Here is the header and the first half of app-port.md , the page the Build ladder will have you imitate. The tree above carries the file whole:
---
owner: server
volatility: low
last-verified: 2026-08-30
verified-by: machine
provenance: read
---
# The port the server listens on
The default is 3000. It is set in one place, `server/src/config.js`, and every
other appearance of the number in this repository is that default being
forwarded rather than a second decision.
`PORT` in the environment overrides it. The value must parse as a whole number
and be zero or greater, or the process throws at startup with `PORT` named in
the message; port 0 is legal and asks the operating system for a free port.
## Evidence
`provenance: read`. From `server/src/config.js`, in the object `loadConfig`
returns:
```js
port: readInt(env, "PORT", 3000, 0),
```
Two other files carry the number and neither decides it: `docker-compose.yml`
publishes `"3000:3000"` and sets `PORT=3000`, and `Dockerfile` sets `PORT=3000`
and declares `EXPOSE 3000`, which documents the port and publishes nothing.
Walk the header, because each line answers a question a future reader will have:
owner: server, one owner, never a list. A page two people own is a page nobody re-checks. The owner is who you go to when the page is doubted.volatility: low, how fast this fact rots. Four tiers:staticnever goes stale (a definition, a thing that already happened),lowgets a year,mediumninety days,highthirty. Past its tier’s life sincelast-verified, the lint calls the page STALE. A port default changes rarely, solow. The three numbers are declared in the records as round guesses, because nothing in this young repository has been observed rotting yet; they are yours to tune when yours has.last-verified: 2026-08-30, the day the fact was checked against its source, not the day the file was edited. Fixing a typo does not move this date.verified-by: machine, who checked. Three kinds of value: a person’s handle, the wordmachine, or the wordunverified.machineis honest here, and it needs a sentence: the agent that wrote this page openedconfig.jsand quoted the line, real evidence gathered by a real reader that happens not to be human. Whatmachinemay never do is impersonate a review that did not happen, which is the trap at the bottom of this page.provenance: read, what kind of checking. The fact came out of a file in this repository, and the Evidence section quotes the path and the line. Notice what this page honestly does not claim: nobody started the server while writing it, so it states what the source decides, not what a socket bound. Choosingreadrather thanobservedis exactly that distinction, made visible.
One body rule you cannot see by looking: no date appears in the prose. last-verified and expires in the frontmatter are the only dates, because they are the only dates anything reads. A date written into a sentence is never re-checked and drifts silently, module 02’s lesson relocated to disk, and the lint has a category for it.
The five provenance values
| Value | What it claims | Evidence the schema demands |
|---|---|---|
observed | Somebody ran the thing and read what came back | The command and its output, quoted in the body |
read | Taken from a file in this repository | The path, and the line when it helps |
derived | Reasoned from other wiki pages, not from a source | The [[wikilinks]] it rests on, in the body |
extracted | A machine wrote it; no person has read it against its source | What extracted it |
mixed | Claims of more than one kind, or a recorded disagreement | Each claim labelled inline |
extracted is the value the whole field exists for. A knowledge base that agents maintain will fill with machine-written pages, most of them right, none of them reviewed, and extracted is the honest label for that state: probably true, nobody has looked. The schema’s job is to keep that label from quietly upgrading itself.
Two more rules round out the contract, and both are the kind you feel the first time they save you. One fact lives in one place: the port page exists so no other page ever states the port, and anything needing it writes [[app-port]] instead, because two copies of a fact agree right up until one of them is updated. Contradictions are recorded, never resolved: when a new fact disagrees with a page, both claims go side by side under a ## Contradiction heading, each with its own provenance, the page becomes provenance: mixed, and the owner gets told. The schema’s own sentence carries the why: silently resolving a contradiction destroys the one piece of information that matters, that two sources disagree. The disagreement is the finding.
The lint, category by category
tools/kb-lint.mjs is the schema’s teeth: zero dependencies, an injectable clock so transcripts are reproducible, one plain sentence per finding, silence on a clean wiki. Seven categories, and the right response to each is different, which is why there are seven and not one:
| Finding | It means | You do |
|---|---|---|
| NAKED | Required frontmatter is missing, or a value the schema does not define | Copy _template.md; the page cannot be judged until it parses |
| EXPIRED | The page named a date it would stop being true, and that date passed | Re-verify or delete; the page predicted its own death |
| STALE | Longer since last-verified than the volatility tier allows | Re-check the fact against its source, then move the date |
| UNVERIFIED | The tier claims evidence but verified-by says nobody gathered it | Go check the page, or downgrade its provenance honestly |
| PROVENANCE | The fields contradict each other as a pair | Fix the header; the page is lying about its own history |
| DATE-IN-PROSE | A date in the body, where nothing will ever re-check it | Move it to frontmatter or delete it |
| TODO | The page names open work | Nothing urgent: a worklist row, and alone it exits 0 |
That last row is a design decision: a TODO does not fail the lint, because a worklist row that breaks the build gets deleted, not done (record 0104). Everything else exits nonzero, and the lint is wired into the repo’s own npm run check, so a dishonest wiki fails the same gate a broken test does.
PROVENANCE is the category the module is named for, and it fires on pairings rather than fields. From the real fixture run:
PROVENANCE machine-named-a-human.md: is `provenance: extracted`, machine-extracted
and unreviewed, yet `verified-by: dana` names a person; nobody reviewed it,
so no name may sit there
PROVENANCE human-without-method.md: names `verified-by: dana` but has no
`## How verified` section saying what that person actually did
Read them together: the same rule pointing in opposite directions. A machine-written page may not borrow a person’s credibility, and a person’s name may not sit on a page without saying what the person did. Either field alone passes any field-at-a-time check; the lie lives only in the pair.
Run it
git checkout module-34
npm run kb # the lint, now part of the repo's own check
On the shipped wiki: nothing prints, exit 0. Point it at an empty folder and it prints one finding that is the module’s philosophy in a sentence:
NAKED _schema.md: does not exist, so this directory has no contract and no
page in it can be judged against one
Then run it where everything is wrong at once, with the clock pinned so your output matches this page:
KB_LINT_NOW=2026-08-30 node tools/kb-lint.mjs tools/kb-lint-fixtures
All seven categories fire, one sentence each, and the fixture names tell you what you are looking at: machine-named-a-human.md, stale.md, derived-without-links.md. The thirty tests behind the lint were validated by mutation, deleting each rule and watching its own test fail, so the categories you just saw firing are proven able to fire.
Making the model live in it
The wiki you built has a gap the lint cannot see: nothing tells an agent it exists. Claude Code does not open a folder because the folder is there. Start a session in this project tomorrow and the model will re-derive the port from config.js, state it in a reply, and let it scroll away, while app-port.md sits unread the whole time.
The fix is conditioning, and by that this page means something plain: instructions in the configuration the model reads at the start of every session, stating the working protocol in imperative sentences. Module 03 built that surface. A project CLAUDE.md conditions every session in this repository; a user-level CLAUDE.md or rules file makes the habit follow you into every project you open. Either way the protocol is short, and every sentence in it is there because the wiki dies a specific death without it:
- Read the wiki’s index before starting work, and
_schema.mdbefore writing or editing any page. A model cannot cite a page it does not know exists, and it cannot obey a contract it has not read. The index is a list of what the wiki holds, one line per page. At two pages the folder listing serves; when the wiki outgrows a listing, the index becomes a page of its own. - When a round of work produces something durable, write it to the owning page in the same round. Durable means a decision and its why, a gotcha, a fix, a status change. The banned word is “later”, because later means after the session ends, and module 02 showed what the end of a session does to everything that lived only inside it. Knowledge batched for later is knowledge lost.
- One fact, one page. The schema already demands this. The conditioning restates it because an agent deep in a task consults its standing instructions far more reliably than a file it has to remember to open.
- Link related pages with
[[wikilinks]]. A page nothing links to can be found only by someone who already knows its name. The double-bracket convention, and the whole shape of a vault of small linked pages, comes from Obsidian; its internal links documentation is where the syntax lives. - Convert relative dates to absolute. “Yesterday” is true for one day and wrong every day after, and nothing ever re-reads a sentence to fix it.
2026-08-30does not rot. - Never write a secret into the wiki. The pages are plaintext markdown, injected into sessions, read by the lint, and in most projects committed. A key or password that lands in one is published, and no schema field can un-publish it.
None of this was invented for this page. It is the distilled version of an Obsidian-style vault configuration the course’s authors run daily in their own projects, cut down to the smallest set of sentences a first-timer can adopt whole. What the conditioning does not carry is the mechanics of a single write, the walk through choosing the owning page, the provenance value, and the log line. That walk is a skill’s job, and module 35 takes this further, into the capture skill that steps the model through the schema at the moment it writes.
Keeping it alive with hooks
A rules file is read by the model and weighed by the model, and module 09 opened on what that means: on a bad day the instruction loses the argument. The same module built the surface that does not argue: the hook, a program Claude Code runs at a fixed moment in a session. Two of those moments carry the whole wiki habit: the moment a session starts, and the moment Claude tries to finish a turn.
The start is SessionStart. On that event, a hook that exits 0 with plain text on stdout, the stream an ordinary print writes to, puts that text in front of Claude as context for the session. That is the entire mechanism. Print the index and every session opens already knowing what pages exist, whether or not anything reminded it to look.
One documented behaviour turns that wiring into a leak. A session “starts” five ways, and one of them is compaction, Claude Code replacing the conversation so far with a summary when the context window fills. SessionStart fires for compaction too, so a hook that injects on every firing re-injects the index after every compaction, and the context accumulates copies with no error anywhere. The guard is one check: read the source field in the event’s payload and stay silent unless it says startup, or name startup in the matcher and let the configuration do the checking. The event’s page walks all five sources and this exact trap.
The finish is Stop, and it is the capture protocol’s conscience. Stop runs when Claude finishes its turn, before control comes back to you, and it is one of the events module 09 listed where exit code 2 blocks: instead of ending the turn, Claude reads what the hook wrote and continues working on it. A capture hook uses that veto to hold the door once per turn and put one question in front of the model: did this round produce something durable, and if it did, is it on its owning page with a _log.md line?
Be precise about what the hook knows here, which is nothing. It is a program, and “durable” is a judgment, so the judgment stays with the model. The hook’s contribution is that the question gets asked every round instead of only on the rounds somebody remembers it. A sharper trigger can narrow when it asks, for instance blocking only when files changed and _log.md did not, but the division of labour holds: the hook forces the question, the model answers it, and the conditioning above is what makes the answer worth having.
Two facts from module 09 keep this hook from misbehaving. The event’s payload carries stop_hook_active, true whenever the current run is already a continuation an earlier Stop hook caused; read it first and allow the stop, or the hook blocks its own continuation. And the loop is capped whatever you return: after eight consecutive blocks, Claude Code ends the turn regardless. Reading the field costs one line and gets you what you wanted, one forced check per turn. Forgetting it costs up to eight continuations of a model being asked a question it already answered.
The wiring is the three-level shape module 09 taught, event, then matcher group, then handlers:
{
"hooks": {
"SessionStart": [
{ "matcher": "startup",
"hooks": [ { "type": "command", "command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/inject_index.py"], "timeout": 5 } ] }
],
"Stop": [
{ "hooks": [ { "type": "command", "command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/capture_check.py"], "timeout": 10 } ] }
]
}
}
On SessionStart the matcher names which of the five sources the group applies to, so "matcher": "startup" is the compaction guard written into configuration; Stop takes no matcher at all. Where the block lives decides whom it wires. In the project’s .claude/settings.json it travels with the repository to everyone who clones it. In your user file at ~/.claude/settings.json it follows you into every project, and the wiki stops being one project’s habit and becomes yours. These two hooks are the basic wiring, and module 38 takes this further, into hooks that keep agents inside the lines once more than one of them holds the pen.
What this costs, in tokens
Everything the last two sections wired is always on, and always on has a price. The conditioning sentences load into every session. The injected index loads into every session. Every page the model opens on demand is read into the context window like any other file. All of it is measured in tokens, the unit module 27 defined as both the model’s limit and your bill, and all of it competes with the actual work for the same window.
Whatever you configure to load always, you pay for always. Three rules keep the overhead small. The always-loaded part is an index, one line per page, never page bodies. Bodies are read on demand, when a page becomes relevant to the round’s work. The schema is read when writing, not on every turn.
What the overhead adds up to in your project is not a number this page can state, because it scales with what you choose to auto-load and how large your pages grow. Measure yours: module 24 built the cost surfaces that report what a session spent.
The trade is the module’s opening problem run in reverse. Knowledge that lives only in a session is free until it is lost. Knowledge that lives in the wiki costs a little every session and survives all of them. The first time someone auto-loads page bodies to save the model a read, the index rule is what they broke, and the window the work needed is what pays for it. Module 37 takes the token thread further, into keeping the main window small.
The ladder:
- Run it. The three runs above: silence, the one-sentence philosophy, and all seven categories at once.
- Read one file.
knowledge-base/_schema.mdtop to bottom. It is about a hundred lines, and it is short because every sentence in it is enforced by the lint or read by a rule the lint applies. - Change one line and see it. Change
app-port.md’svolatilityfromlowtohigh, setKB_LINT_NOWtwo months ahead, and watch STALE appear: the tier is a promise about how fast the fact rots, and you shortened it. - Build. Write the wiki’s second page yourself: the mail ceiling from module 33, its default of 50, and where it lives in
config.js. Copy_template.md, choose the provenance honestly (you will be reading a file, soread), wikilink[[app-port]]under See also, add your_log.mdline with its Why, and finish with a silentnpm run kb.
A page’s body says the fact was machine-mined, its frontmatter says verified-by: dana with a recent date, and a lint that checks fields one at a time passes it without comment: the date is a valid date, the name is a valid name, every required field is present. Now scale the mistake the way an agent-maintained wiki scales it: forty machine-extracted pages, bulk-stamped with a person’s name during some migration, every one now wearing human credibility nobody spent. Readers trust them, other pages derive from them, and the wrong ones poison quietly, because nothing about any single field is invalid. The dishonesty lives entirely in the pairing, which is why a field-at-a-time lint is blindest exactly where an agent-maintained wiki is most likely to be wrong.
The pairing rule is the fix, and it cuts both ways: an extracted page may not name a human, and a page naming a human must say how they verified. Under those two rules the forty pages become what they honestly are, a worklist of extracted pages awaiting review, visible in one lint run, and every one a row rather than a counterfeit.
Entirely, and further than anything else in the track. There is no SDK call, no frame, and no Claude anything in this module: the three layers, the pairing rule, the volatility tiers, the one-fact-one-place rule, and the lint travel to Obsidian, to any markdown wiki, to a team runbook, and to the notes you keep for yourself. That is why it is built by hand first: module 35 hands agents the pen, and everything they must obey was already true before they arrived.
Check yourself
- A page has every required field, a recent date, and a real person’s name, and it is still dishonest. What combination of values makes that true, and which single lint category exists to catch it?
- Why does a TODO finding leave the exit code at 0 while a STALE finding does not, and what behavior would the other choice train into whoever runs the lint?
- Two sources disagree about a fact the wiki records. Write the frontmatter value and the body shape the schema demands, and say what is lost forever under the alternative.