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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo. The command line is longer than the plate and scrolls.

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.
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo. The vocabulary line scrolls.

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.
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo. The five AGENT lines between FILE and GATE are quoted in the paragraph below rather than in the plate, where the bracketed line stands in their place. The SPEED line is this laptop's on that run and is not a benchmark; the measured figures with their conditions are on Benchmarks. Long lines scroll.

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.
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, in an empty folder.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, in the folder above, with the note written after the last kb index. Long lines scroll.

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 ...
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, in the same folder, after the index caught up with the note. Long lines scroll.

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 ...
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo. Long lines scroll.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, in the folder above. The finding is one line and scrolls.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo.
$ 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.
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, in the folder above, which has no fleet.txt yet. The SOURCE line scrolls.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11: the first over examples/demo, whose manifest names an answerer that was not called; the second in the folder above, whose manifest does not exist. Zero model calls in either.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo. The four kb serve lines went to stderr as written; stdout was one JSON-RPC line, shown from the bracketed line down as its text field with the newlines rendered. No initialize was sent before the call. Long lines scroll.

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"}}}
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo, stderr discarded. Both lines scroll.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo, three messages in one session. The demo's agents carry no blocks.txt, so no constitution follows the first line; an agent kb init writes carries one, and it follows on every message that changes the agent. Long lines scroll.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo. Both commands exited 0. Long lines scroll.

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
Real output, 6 September 2026, local release build on Windows 11, over examples/demo, two messages in one session. The first line of each briefing is quoted to its first sentence; the full line continues with how to record a misroute. Long lines scroll.

--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.
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, in the folder above, on the blocks.txt kb init wrote. The missing file is the person's core, which kb init --person creates and this walk did not.

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
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, in the folder above. The bracketed line stands for the generated constitution the boot printed after its first line. Long lines scroll.

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)
Real output, 2 September 2026, commit ea43d1d, release build on Windows 11, over examples/demo, stopped after three seconds. The line is written to stderr.

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