On Your Machine
Last updated: 2 September 2026
Every plate on this page was produced for it on 2 September 2026, from
a release build at commit ea43d1d on Windows 11, and quotes
what the command printed. Nothing here calls a model. The two commands
that can when the manifest names one, kb answer and
kb boot, are shown on the paths that do not, and
kb promote, which always does, is described and not run,
so every line you read below is one you can reproduce with the binary
alone.
Build
Requires Rust. There is one dependency, rusqlite 0.37 with
its bundled feature, and no build scripts of our own. That
dependency compiles SQLite in from the amalgamation, so a fresh checkout
needs a C toolchain: the MSVC Build Tools on Windows. There is no network
at runtime.
git clone https://github.com/richard-wollyce/ulpia && cd ulpia
cargo build --release --manifest-path tools/kb/Cargo.toml
sh tools/kb/install.sh
Cargo writes the binary into tools/kb/target/release/ and the
install step copies it to tools/kb/bin/kb
(kb.exe on Windows), which is the path every tracked config
file in the repository names. Put that on your PATH or
call it by that path; every command below says kb.
cargo test in
tools/kb
runs the suite, and the
count is left out here on purpose, because more than one session
writes the repository and the number moves between them.
The source is public at
github.com/richard-wollyce/ulpia,
under Apache 2.0, with
LICENSE
and
NOTICE
at the root, and the first line above clones it. Building it yourself
is the honest default for a tool that reads your notes, and it is not
the only way: tagged releases carry kb-linux-x64,
kb-windows-x64.exe, kb-macos-arm64 and
kb-macos-x64, each with a .sha256 beside it,
plus a checksums.txt holding all of them and a signed
attestation.jsonl.
gh release download -R richard-wollyce/ulpia -p "kb-linux-x64*"
sha256sum -c kb-linux-x64.sha256
gh attestation verify kb-linux-x64 --repo richard-wollyce/ulpia
There is no tag in those commands on purpose. Without one
gh resolves to the latest release, so the line cannot rot
into fetching an old binary while this page describes a newer one. Name
a tag only when you want a specific version:
gh release download kb-v0.2.2 -R ....
The second line and the third answer different questions, and
only the third is worth much to a stranger. A
.sha256 downloaded from the same release as the binary
says the download was not truncated, and nothing more: whoever can
publish one can publish the other, so it is integrity and never
provenance. gh attestation verify asks a public
transparency log whether that exact digest came out of this
repository's release workflow, from a named commit, and needs no trust
in whoever uploaded it. It wants a gh logged in to any
GitHub account; a consumer that cannot log in checks the same signed
statement offline by adding
--bundle attestation.jsonl, which ships as a release
asset. The release job proves those instructions work rather than
printing them: it downloads the published assets back and verifies
them before the run is allowed to go green.
On macOS the download is quarantined, so run
xattr -d com.apple.quarantine kb-macos-arm64 once before
the first use. Whatever binary you end up with,
kb --version names the commit it was built from, so a copy
found on a machine later can be placed without guessing.
The demo fleet
Three tiny invented agents ship with the source under
examples/demo:
zed, a software architect;
steve, a marketing analyst; yaron, a nutrition
assistant. Each holds a few textbook notes, and a note is a book: one
markdown file about one subject, with its label, the Search
for line, near the top. Beside them sits
gold.tsv, the
answer key: thirteen questions, ten the fleet should answer and three it
should refuse. Everything in the demo is invented. The fleet its authors
run is private, which is the product working as designed.
$ kb index examples/demo
steve 3 reindexed, 0 unchanged, 0 removed, 3 chunks
yaron 4 reindexed, 0 unchanged, 0 removed, 4 chunks
zed 4 reindexed, 0 unchanged, 0 removed, 4 chunks
total: 11 files, 11 chunks, 3 indexes
kb index builds one SQLite index per agent, beside the
agent at <agent>/.kb/index.db, and prints one line per
base: how many files it reindexed, left unchanged and removed, and how
many chunks it wrote on that run, then a total. The index is derived and
disposable. Delete .kb/ and you have lost a rebuild, not a
fact.
$ kb route "quem decide se um deploy pode ir pra producao" examples/demo
question: quem decide se um deploy pode ir pra producao
indexed: 11 entries across 3 agents, 0 aliases
1. 33.69 zed/knowledge/deploy-checklist.md
matched: deploy, producao
2. 7.49 steve/knowledge/hooks-that-hold.md
matched: decide
That is Vesta answering which book to open. She prints the question back, what she had to work with (eleven entries across three agents, no aliases), then the ranked files with the score and, under each, the words that matched, so a bad ranking can be diagnosed instead of guessed at. The question is Portuguese over English notes on purpose: the catalogue matches the keys each file declares, in whatever language they were written.
$ kb route "quem aprova um deploi" examples/demo
question: quem aprova um deploi
indexed: 11 entries across 3 agents, 0 aliases
nothing matched. Either the base does not cover it, or the
Search for lines do not carry the words a real question uses.
the base does know these, and they look like words you used:
quem aprova deploy, aprovar mudanca, checklist de deploy, deploy, deploy checklist, deploy window, janela de deploy, reverter deploy
that is spelling and not meaning, so it finds a typo or a cognate
and never finds a translation.
And when she does not have the book, she says so, and answers with the
library's own vocabulary. The comparison is character trigram overlap,
so it reaches a typo (deploi) or a cognate and never a
translation, and the reply says which kind of help it is.
$ kb eval examples/demo/gold.tsv examples/demo
gold: 13 questions, 10 answerable
indexed: 11 entries across 3 agents
file agent kw score marg verdict question
ok ok ok 33.7 4.50 hit quem decide se um deploy pode ir pra producao
ok ok ok 11.2 1.00 hit como faco rollback sem downtime
ok ok ok 22.5 1.00 hit devo usar sqlite ou postgres no comeco
ok ok ok 29.3 1.00 hit what makes a review comment blocking
ok ok ok 24.8 1.00 hit como prender a atencao nos primeiros segundos do video
ok ok ok 24.8 1.00 hit qual a melhor frequencia de postagem
ok ok ok 66.9 5.95 hit how much protein per meal
ok ok ok 11.2 1.00 hit quanto tempo um musculo leva pra recuperar
ok ok ok 24.8 1.00 hit onde consulto a tabela nutricional de um alimento
ok ok ok 33.0 1.00 hit quantos gramas de proteina por quilo
ok - - 0.0 0.00 none qual acao da bolsa devo comprar
ok - - 0.0 0.00 none me conta uma piada
ok - - 0.0 0.00 none qual o sentido da vida
FILE fused 10/10 (100%)
keyword 10/10 (100%), the same question asked of the keyword scorer alone
[the five-line AGENT block, quoted in the paragraph below]
GATE flagged 0/0 of its own misses as a guess
demoted 0/10 correct answers to a guess
of 3 question(s) the set says to decline: refused 3, hedged 0, answered 0
SPEED 2176 us per question fused, 750 us keyword only. No model, no network.
Release binary.
kb eval grades the catalogue against the answer key in
front of you. Run for this page under the floor that scales with the
corpus, 6.9 for these eleven entries: the right file 10 of 10, and in
the AGENT block the plate leaves out, the right agent 10 of 10 on every
row: what kb boot hands over, the deterministic fold alone,
and the same fold over the fused list, against 4 of 10 for
always-yaron, the best a fixed choice can do on this set,
so routing beats the fixed choice by six questions. All 3 refusals
refused, and no correct answer demoted to a guess. An earlier version of
the README told the reader to expect routes 8/10 and two
demotions, and it was right at the time: the floor was one fixed number,
17.5, measured on a fleet of 226 entries, and on eleven entries it was
too high by the arithmetic of inverse document frequency. This is an
eval a reader can re-run. Every figure on this page that came from the
private fleet says so, because that fleet is one a reader does not
have.
The abstention line separates three outcomes, refused, hedged and answered, and a test holds that they sum back to the denominator. Before that split, anything that was not a hit counted as an abstention, so a guess that served two files scored as silence, and one run could support both it stayed quiet and it answered while quoting the same figure.
Your own agent and its first book
The walk below was made in an empty folder, with nothing in it before the first command. A folder holding markdown is a base; no repository is needed, and none is created.
What kb init writes
$ kb init yaron
created .\fleet\yaron
12 files and directories
served as it stands: no repository is needed, and none was created
Next: fill in agent.txt's role, then index.md. Both are placeholders,
and a generated file left unedited is a file nobody owns.
Twelve files and directories: agent.txt,
index.md, MAP.md, AGENTS.md,
blocks.txt, kb-aliases.txt and a
.gitignore, then knowledge/,
inbox/, decisions/, protocols/
and templates/, each holding a .gitkeep.
That is the full shape and nothing else. agent.txt carries
name = Yaron, an empty role = for you to fill,
an empty ends = for where the agent stops, and one
commented line, private = profile/, projects/,
records/, which is the default private layer written out so it is
there to be edited and not there to be needed. The
.gitignore mirrors that line, a courtesy to anyone who later
versions the fleet: kb check warns when the two disagree,
and nothing requires that git ever reads it.
The note, then the question
Three steps, and only one order between them matters. Write the book,
fleet/yaron/knowledge/creatina.md, with a Search
for line at the top. Run kb index .. Ask
kb route "quanto de creatina por dia" .. The note written
for this page:
# Creatina: a dose de manutencao
**Search for:** `creatina`, `creatine`, `quanto de creatina`, `creatina por dia`, `dose de creatina`, `creatine dose`, `3 g`, `5 g`, `saturacao`, `loading phase`, `fase de carga`, `monohidratada`, `monohydrate`, `retencao de agua`, `water retention`, `quando tomar creatina`, `when to take creatine`
**Exists to:** The maintenance dose of creatine and whether a loading phase is needed
A maintenance dose of 3 to 5 g of creatine monohydrate per day is the textbook figure
for adults who train. A loading phase of 20 g per day for five to seven days saturates
the muscle faster and is not required: the same saturation arrives in about four weeks
at the maintenance dose. Take it at any time of day, with a meal if it upsets the stomach.
These are population numbers from published guidelines, not a prescription.
For the plate below the order was deliberately wrong: kb index
. ran first, over the freshly generated agent, and the note was
written after it. Then the question was asked twice, once to the
catalogue alone and once with --hybrid.
$ kb route "quanto de creatina por dia" .
question: quanto de creatina por dia
indexed: 2 entries across 1 agents, 0 aliases
1. 17.28 yaron/knowledge/creatina.md
matched: creatina por dia, creatina
$ kb route "quanto de creatina por dia" . --hybrid
question: quanto de creatina por dia
indexed: 2 entries across 1 agents, 0 aliases
NOTE: only one scorer ranked any of these, so this is a guess rather
than an answer. The base may not cover the question at all.
0.016 yaron knowledge/creatina.md keywords #1
A note is served the moment it is on disk. The catalogue, the keyword
scorer, re-reads the files on every run, so the first command found the
new book before the index had seen it. What kb index buys is
the second scorer, full text search over the content, and a note written
after the last index has no chunks in it. So --hybrid on
that note drops to one scorer and reports a weak guess with no passage:
keywords #1 with no text rank beside it, the NOTE above,
and nothing quoted. That is the only silent failure left in this flow,
and it is the quiet kind. The answer is not wrong; it is thinner than it
should be.
An earlier version of the README had a fourth step between writing and
indexing, kb commit, because the router once asked git what
it may serve, and a note that was not yet tracked was invisible. That
rule is gone. kb never consults git, a directory holding
markdown is a base, and kb commit remains, optional, for
whoever versions a fleet. It is not in the path between a note and an
answer.
What kb index buys
$ kb index .
yaron 1 reindexed, 2 unchanged, 0 removed, 1 chunks
total: 4 files, 1 chunks, 1 indexes
$ kb route "quanto de creatina por dia" . --hybrid
question: quanto de creatina por dia
indexed: 2 entries across 1 agents, 0 aliases
0.033 yaron knowledge/creatina.md keywords #1 + text #1
Creatina: a dose de manutencao: **Exists to:** The maintenance dose of creatine and whether a loading phase is needed ...
The index touched one file, the new one, and left two unchanged, because
files are content hashed and an unchanged file costs nothing on
reindex. With both scorers holding the note, --hybrid
prints the fusion score, the agent, the file, and why: keywords
#1 + text #1, one rank from each scorer, then the first passage.
The fusion is Reciprocal Rank Fusion, each list contributing
1 / (60 + rank) to every file it ranks, so two first places
sum to 0.033 and a lone first place to 0.016. It reads agreement, not
magnitude, which is why the two lines in the demo's hybrid run tie at
0.033 while the catalogue alone had them at 33.69 and 7.49: fusion is
the right rule for assembling passages a person reads and the wrong one
for choosing a single winner, and the verdict comes from the catalogue.
Where a passage comes from the deposit, inbox/, the line
ends in [short memory].
$ kb route "quem decide se um deploy pode ir pra producao" examples/demo --hybrid
question: quem decide se um deploy pode ir pra producao
indexed: 11 entries across 3 agents, 0 aliases
0.033 zed knowledge/deploy-checklist.md keywords #1 + text #2
What a deploy needs before production: ... A deploy without a rollback plan is a bet, not a release. Canary first ...
0.033 steve knowledge/hooks-that-hold.md keywords #2 + text #1
Hooks: the first three seconds decide the rest: **Exists to:** Why openings decide retention, and the three hook shapes that hold A ...
Check and fleet
$ kb check .
yaron (4 public files, map: MAP.md)
warn W07 knowledge/creatina.md unsearchable key(s): quanto de creatina, 3 g, 5 g. Several written words that reduce to one after stopwords, so the term reaches neither the keyword index nor the phrase index and no question can find it. Rewrite so the word that carries the meaning is the one that survives: `nao contestar` indexed as `contestar`, its own opposite, until it became `proibido contestar`
0 errors, 1 warnings
kb check lints every base without touching a file. It
prints a header per base, the base's name with how many files it read
and which map it found, then the findings, then clean or
the error and warning counts. It exits 1 on any error, and with
--strict on any warning; the run above exits 0 as shown and
1 with --strict. The note written for this page earned its
warning honestly: quanto de creatina reduces to
creatina once the stopwords go, and 3 g and
5 g reduce to one word each, so none of the three can be
found by a question, and the subject each of them names is already on
the keys beside them. Drop those three keys and the check is clean,
which was run for this page, as the demo's is:
$ kb check examples/demo
steve (3 public files, map: none)
clean
yaron (4 public files, map: none)
clean
zed (4 public files, map: none)
clean
$ kb fleet .
FLEET: .
ROLE: not set. Add a `role = ` line to fleet.txt.
AGENTS: 1
- Yaron
root: .\fleet\yaron
role: not set in agent.txt
SOURCE: fleet.txt, each agent's agent.txt, and the directory names under fleet/. No index was queried and no ranking took place, so none of the above is a retrieved passage and none of it can be attributed to a knowledge file.
kb fleet reads and does not rank. The fleet's name and
role come from a fleet.txt at the root, each agent's from
its agent.txt, and the output says so in its last line,
because a model that cannot tell a looked-up fact from a retrieved
passage will eventually cite one as the other. The folder above has no
fleet.txt yet, so the fleet is named by the path it was
given and the role line tells you what to add. The demo's
manifest carries
name = Vesta and role = The demo librarian,
and its roster prints each agent's role from the line in its own
agent.txt.
The private layer
--all, on any command, includes the private layer:
profile/, projects/ and records/
unless the base declares otherwise with a private = line in
its agent.txt, where . declares the whole
base. Nothing outside the base is consulted. It is the right flag for
your own agents and the wrong one for a consumer that other people talk
to, because it turns the privacy filter off. The deposit,
inbox/, is served in both scopes, and every passage from it
is labelled short memory on every surface.
Prose after the verdict: kb answer
kb answer runs retrieval first, unchanged, and only then
hands the served passages and the gate's own evidence line to the
model named by answerer = in fleet.txt. The
demo's line is answerer = ../../tools/answer-claude.cmd, a
command that reads a prompt on stdin and answers on stdout; any command
with that shape will do. The prompt states the grounding rules as hard
instructions: every claim must be supported by a served passage and
cite the file after it; the library does not hold this is a correct and
complete answer; a passage marked SHORT MEMORY may be used and, if it
is, the answer must say the claim comes from short memory; and the
answer is given in the language of the question.
Three refusals wrap the one model call, in order. A nothing verdict
never reaches the model, because fabrication needs a vacuum and gets
none. A missing answerer prints the reading list kb route
would have printed. A failed call prints the reading list out loud, on
stderr, and exits 1. Under every answer the caller prints
sources served: from retrieval's own list, so a fabricated
citation has nowhere to hide.
$ kb answer "qual acao da bolsa devo comprar" examples/demo
nothing in the library matched. Either it does not cover this, or the
Search for lines do not carry the words the question used.
$ kb answer "quanto de creatina por dia" .
no `answerer = ...` in the fleet manifest, so here is the reading list:
1. yaron/knowledge/creatina.md
Three reading modes exist, chosen by the caller and never guessed. The
default reads the top five files in one call, a table sized for a
personal question. --expanded reads up to twelve, one
call, for evidence spread across several files. --complete
reads every keyed file the base serves, in batches of ten, one call per
batch plus one to compose, because a question whose answer is crumbs
across many files is starved by any top-k table. A whole-base read costs
minutes, so complete mode prints its cost before anything runs, the
count of files, batches and model calls, then the pace and a whole-read
estimate in minutes as soon as the first batch lands.
Measured on a release build with Sonnet as the pen, dated 24 August
2026 by the
decision record
and the same day as its 23 August run at commit 35322d2
by the
benchmark record:
the abstention
benchmark over examples/demo had left exactly two
out-of-scope questions answered confidently by the deterministic layer,
both deliberate medical baits sharing vocabulary with textbook
nutrition. Passed through kb answer, both were refused, 2
of 2: the model stated the served passages hold no renal or dosing
content and referred the asker to a professional. That is a sample of
two, and the catch rate at scale is its own instrument.
From Claude Code and Claude Desktop
kb serve . speaks the Model Context Protocol over stdio.
Five tools, none of which writes to the library: kb_route (a question, and
optionally top), kb_retrieve (the same two),
kb_remember (a claim), kb_fleet, which
takes nothing, and kb_list, whose facets are every one of
them optional. The first returns ranked paths with the words that
matched and no file contents; the second returns the passages
themselves with their heading path and provenance; the third measures a
claim against the base and proposes ADD, UPDATE or NOOP with its
evidence, writes nothing and never proposes DELETE; the fourth is the
roster read from fleet.txt and each agent.txt,
not a search; the fifth lists what the shelves hold, narrowed by base,
folder, species, stage or provenance, and it takes no question, so
nothing it returns carries a score or a verdict. One file is written all the same, and
the source
says that hiding it was a mistake: a refusal from kb_route or
kb_retrieve appends the question and the terms offered
back to kb-misses.txt beside the fleet manifest, a log of
what could not be answered rather than knowledge.
$ printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"kb_route","arguments":{"question":"quem decide se um deploy pode ir pra producao"}}}' | kb serve examples/demo
kb serve: steve (examples/demo\steve)
kb serve: yaron (examples/demo\yaron)
kb serve: zed (examples/demo\zed)
kb serve: 11 map entries, 0 aliases, scope Public. Ready on stdio.
[stdout carried one JSON line; its text field, with the escaped newlines rendered:]
Files to open for: quem decide se um deploy pode ir pra producao
EVIDENCE: keyword score 33.7 against a floor of 6.9; both of the two independent scorers ranked the top file; it leads the runner-up by 4.50x. Verdict: something here matches.
1. zed/knowledge/deploy-checklist.md (What a deploy needs before production)
matched: deploy, producao
The checklist a change passes before production, and who decides
2. steve/knowledge/hooks-that-hold.md (Hooks: the first three seconds decide the rest)
matched: decide
Why openings decide retention, and the three hook shapes that hold
Every kb_route and kb_retrieve answer carries
that EVIDENCE line: the keyword score against the floor that applied to
this fleet, how many of the two scorers ranked the top file, its lead
over the runner-up, and the verdict in words. A miss offers the base's
vocabulary back and tells the model, in the same text, that if the
question was asked in one language and the base written in another it
should rewrite the question with those terms or the canonical ones it
expects and ask again; the tool's own description adds that this comes
before concluding the base does not cover the subject.
There is no write tool, deliberately. A write reached by a model is a
different security surface and gets built on purpose, not while the
retrieval side is still warm. The private layer stays out unless
--all is in the client's configuration, a deliberate act
visible in a file; the deposit is served in both scopes, and its
passages say [short memory: ...] in the tool's text.
Registering it with Claude Code at project scope is a
.mcp.json at the root of the folder you open. The
repository's own reads:
{
"mcpServers": {
"vesta": {
"command": "./tools/kb/bin/kb.exe",
"args": ["serve", "."]
}
}
}
The installed path, not the build directory. This server stays mapped for the whole life of a session, so the path named here is a file some process is holding open. Point it at target/release/ and the next cargo build --release fails its own final step on Windows, because a running image cannot have its file removed and cargo installs by remove-then-hardlink. Build into target/, which nothing maps, then sh tools/kb/install.sh to place it in bin/.
At user scope, which stays out of any repository, one command
registers several bases at once, and the -- is mandatory:
everything after it is passed to the server untouched.
claude mcp add --transport stdio --scope user fleet-memory -- \
C:\fleets\zed\tools\kb\bin\kb.exe serve \
C:\fleets\zed C:\fleets\steve C:\fleets\yaron
Claude Desktop takes the same command and arguments in
claude_desktop_config.json:
{
"mcpServers": {
"vesta": {
"command": "/path/to/kb",
"args": ["serve", "/path/to/your/fleet"]
}
}
}
Careful. claude.ai in a browser cannot use this. A web page cannot spawn a local process, and custom connectors dial out from Anthropic's infrastructure rather than from your machine, so a localhost address resolves to their servers. Claude Code and Claude Desktop both run locally and both work.
The server is dual-era. One protocol revision removed the
initialize handshake and moved the version into each
request; the earlier ones require it. So the server answers
initialize when asked, echoing back whatever version the
client named, and never requires it, which the plate above already
showed by getting an answer to a bare tools/call. And
stdout belongs to the protocol: nothing is written there that is not a
message, and every diagnostic goes to stderr, where a client is told
not to read it as failure.
$ printf '%s\n' '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"docs","version":"0"}}}' | kb serve examples/demo 2>/dev/null
{"id":0,"jsonrpc":"2.0","result":{"capabilities":{"tools":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"kb","version":"0.1.0"}}}
The fleet boots the agent
Which agent answers is not decided by a file the model reads. It is
decided before the model sees the message, by kb boot on a
UserPromptSubmit hook: the runtime hands the hook the
prompt as JSON on stdin and injects whatever it prints on stdout into
the model's context. The repository registers it in its own
.claude/settings.json,
together with the session-end hook
described further down:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/tools/kb/bin/kb.exe\" boot \"$CLAUDE_PROJECT_DIR\" --top 5 --all",
"timeout": 30
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/promote-on-idle.sh\"",
"timeout": 10
}
]
}
]
}
}
$ printf '%s\n' '{"session_id":"docs-local-demo","prompt":"quem decide se um deploy pode ir pra producao","hook_event_name":"UserPromptSubmit"}' | kb boot examples/demo
VESTA: this message routes to zed. You are zed for as long as that holds. This was decided by the fleet's router before you saw the message, not by a file you read, so it is not yours to override: if it is wrong, say so rather than answering as somebody else.
Open these before answering:
zed/knowledge/deploy-checklist.md
steve/knowledge/hooks-that-hold.md
$ printf '%s\n' '{"session_id":"docs-local-demo","prompt":"como faco rollback sem downtime","hook_event_name":"UserPromptSubmit"}' | kb boot examples/demo
VESTA: still zed.
Open these before answering:
zed/knowledge/deploy-checklist.md
$ printf '%s\n' '{"session_id":"docs-local-demo","prompt":"how much protein per meal","hook_event_name":"UserPromptSubmit"}' | kb boot examples/demo
VESTA: this message routes to yaron. You are yaron for as long as that holds. This was decided by the fleet's router before you saw the message, not by a file you read, so it is not yours to override: if it is wrong, say so rather than answering as somebody else.
(the conversation was with zed until this message)
Open these before answering:
yaron/knowledge/protein-basics.md
yaron/index.md
The constitution is injected only when the routed agent changes,
tracked per session id under .kb/sessions/; between
changes the hook says VESTA: still zed. and lists the
files, because a constitution on the private fleet this was built for
was roughly 55 KB when
the record
was written on 18 August 2026, and
repeating it every turn would make the router the most expensive thing
in the loop. An
empty prompt is a session opening and gets the roster. Machine text,
the runtime's own notifications on the same hook, gets silence: run for
this page with a system-reminder envelope as the prompt, the hook
printed nothing and exited 0. Below the gate it picks nobody and says
so:
$ printf '%s\n' '{"session_id":"docs-local-demo","prompt":"","hook_event_name":"UserPromptSubmit"}' | kb boot examples/demo
The fleet:
steve
yaron
zed
$ printf '%s\n' '{"session_id":"docs-local-demo","prompt":"qual acao da bolsa devo comprar","hook_event_name":"UserPromptSubmit"}' | kb boot examples/demo
VESTA: routed this message and found no owner (top keyword score 0.0, floor 6.9).
Do not assume an agent. Either ask which one this belongs to, or answer as the fleet's librarian and say the base does not appear to cover it.
The fleet:
steve
yaron
zed
It always exits 0. On this event, exit 2 blocks the prompt and erases it, and exit 1 shows the person a hook error; neither is acceptable for a routing step that failed, because the message belongs to the person and must reach the model whatever the router thinks. If nothing was injected, the hook is not installed or the router abstained, and the session is the librarian rather than one of the agents.
A host with no hook
Everything above is one runtime's envelope, and a memory layer that
only speaks one runtime's envelope is that runtime's feature. So
kb boot takes a second shape: the message alone on stdin,
with --session and --cwd carrying what the
envelope would have carried, and --text saying stdin is
never an envelope. Every shell, every editor and every runtime without
a prompt hook has exactly that, and the adapter is one line.
$ printf '%s' 'how much protein per meal' | kb boot examples/demo --text --session s1
VESTA: this message routes to yaron. You are yaron for as long as that holds.
Open these before answering:
yaron/knowledge/protein-basics.md
yaron/index.md
$ printf '%s' 'como faco rollback sem downtime' | kb boot examples/demo --text --session s1
VESTA: this message routes to zed. You are zed for as long as that holds.
(the conversation was with yaron until this message)
Open these before answering:
zed/knowledge/deploy-checklist.md
--session is what makes the constitution arrive once per
conversation rather than on every message, because it is the key the
last routed agent is remembered under. Without it there is nothing to
remember against, so the constitution is emitted every time: the cost
is repetition, and the alternative is an agent handed a name with none
of the rules that go with it.
The two shapes are told apart by the first non-space byte. A
{ that then parses as JSON and carries one of the
envelope's own fields is an envelope; everything else is the message,
verbatim. That has a failure mode and it is worth stating rather than
hiding: a message that is itself a JSON object with a top-level
prompt key is read as an envelope, so pasting a hook
payload as your whole message routes on the value inside it.
--text skips the test entirely, which is why an adapter
that never speaks JSON should always pass it.
Measured on 18 August 2026 on the release binary over five bases of a private fleet, which is one a reader does not have: 355 ms per message, of which about 60 ms is process start, about 280 ms is opening the five bases and about 5 ms is the routing itself. The record names no commit. That opening cost included one call to git per base, which the router stopped making on 1 September 2026, and no record in the repository re-measures the figure after that. The model that can sit in front of the deterministic choice, a command named in the manifest that reads a briefing and names the owner, is a separate cost: 9 to 15 seconds through the Claude Code CLI on the author's machine, a figure that read 13 to 16 until it was corrected on 20 August 2026, again with no commit named, and it is named on How the Catalogue Ranks. The demo has no such line, so every boot above was the arithmetic alone.
$ kb blocks fleet/yaron
fleet/yaron/blocks.txt
# block mode files bytes ~tokens cumulative
1 identity resident 2 2342 586 586
2 user resident 0 0 0 586
missing file: ../person/core.md
3 map resident 1 978 245 831
4 session on-demand 0 0 0 -
resident total: about 831 tokens
cost of changing a block, in tokens that have to be prefilled again:
identity 831
user 245
map 245
A change invalidates its own block and everything after it, so the
first block is the most expensive to touch. That is why the order is
by how often a block changes, most stable first.
kb blocks prints what the agent wakes up knowing and what
it costs: each block of the constitution in stability order, resident or
on demand, its size in bytes and rough tokens, then the cost of changing
each block in tokens that have to be prefilled again, because prefix
caching reuses a prompt only up to the first token that differs. The
README's
own example, from the private fleet and carrying no date or
commit in the README: identity 5,911 tokens,
user 2,399, map 6,404, and the map is the largest resident block and
the one that changes most often, which is the worst combination
available. The README's projection, with its condition: once
kb route is wired into the loop, the map becomes on-demand
and the resident set drops by 46 percent across that fleet, because
routing is the map's whole job and it will be happening outside the
model by then.
$ printf '%s\n' '{"session_id":"first-session","prompt":"quanto de creatina por dia","hook_event_name":"UserPromptSubmit"}' | kb boot .
VESTA: this message routes to yaron. You are yaron for as long as that holds. This was decided by the fleet's router before you saw the message, not by a file you read, so it is not yours to override: if it is wrong, say so rather than answering as somebody else.
[the constitution kb init wrote follows, left out here]
$ kb capture . --session first-session
captured session first-session into .\fleet\yaron\inbox\2026-09-02-session-first-session.md
$ cat fleet/yaron/inbox/2026-09-02-session-first-session.md
---
provenance: agent
stage: raw
---
# Session first-session, 2026-09-02
What this session produced without a model, written by `kb capture` when it ended. Nothing here has been judged. `kb promote` reads it and decides what, if anything, enters the library.
## Where the conversation went
yaron
$ kb capture . --session first-session
nothing captured for session first-session: the session produced nothing to capture
At SessionEnd the second hook runs kb capture
synchronously, then a detached kb promote. Capture turns
the session's record, which kb boot appended to on every
message, into one markdown file in the last routed agent's
inbox/: provenance: agent,
stage: raw, no model run and no Search for line,
so the catalogue never names a raw session as an answer while the text
scorer can still surface it, labelled short memory. The deposit is
written before the record is deleted, so a crash between the two
captures again rather than losing the session. A session no agent was
routed in is not captured, the record is kept, and the reason is
printed; the second capture above found nothing left to take.
The reading room
$ kb ui examples/demo
kb ui: http://127.0.0.1:4114/ (3 agents, 11 entries)
kb ui serves one embedded page plus three Garamond faces
on 127.0.0.1:4114; the page holds the fleet, the
catalogue, the stacks, the desk, the block budgets and the doctor. It
binds the loopback address and nothing else: the flags it takes are
--port and --all, there is no flag for another
address, and the listener in
the source
is bound to
127.0.0.1 by name, so a page that has no business leaving
the machine cannot be asked to. The tray application over the same
library is Windows only, and young.
Previous: How the Catalogue Ranks · Next: Somewhere That Is Not Your Machine · All of the docs · Back to the library