The shared memory your AI agent fleet keeps forgetting it needs.
A git-backed, searchable knowledge base and incident tracker for fleets of AI agents. Which servers exist, how services are wired, what broke and how it ended — the operational truth that survives between sessions and across machines.
Website · By Alexander Panasenko
Quick start · How it works · Incidents · Agent skill · MCP · Web UI · HTTP API · Citation
Agents forget everything between sessions. Multiple agents — Claude Code, Codex, Antigravity — on multiple machines rediscover the same infrastructure over and over, and every hard-won incident lesson evaporates the moment the context window closes.
KYB is the shared memory that survives: which servers exist and what runs on them, how services are wired, how they deploy, which decisions were made and why — plus incident reports: what broke, the impact, how to live with it, and how it ended.
- Git is the canon — there is no database. One markdown file (YAML frontmatter + body)
per entry — under
knowledge/,incidents/ortasks/by kind — one commit per change, a commit sha is a version id. History, diff and rollback come for free; the canon can be read and edited with any text editor. (Flat pre-v2 canons migrate themselves on start.) - The tree holds only what is live. Closing an incident or a task archives it: the
file leaves the working tree, but the final version — resolution included — stays in the
default search, in the listings and in
GET(markedarchived). Deleting plain knowledge is a retraction and drops it from the default search. Git keeps everything either way. - Tantivy is the index — a disposable cache rebuilt from git on every start. It covers
the latest version of every key and every historical version, so agents can search what
the knowledge said before it changed (
--history: "what moved where"). - Search is hybrid — BM25 fused (reciprocal rank) with vector search over every entry (multilingual-e5-small, int8 ONNX, runs locally on CPU in ~5 ms). Ask in one language about a base written in another and it lands; exact technical terms still rank first. No model on disk → the service runs lexical-only.
- Writes are upserts by key — a no-op when content did not change. Flat key space plus tags; no projects, no namespaces.
- Secrets never enter the base — writes are rejected if the body looks like a token, a private key or a password. Store pointers instead.
Agents use the kyb CLI over HTTP. The Rust/axum server stores Markdown and history in Git,
then builds search state from that history. Tantivy provides BM25 search; an optional
multilingual-e5-small ONNX model supplies in-memory vectors. Reciprocal rank fusion combines
lexical and semantic candidates. All search modules run inside the server. Without a model,
lexical search still works.
Incidents are first-class entries (kind: incident, keys prefixed inc-) with a lifecycle
open → mitigated → resolved and a rule: closing requires a resolution — an incident
that ends with "it just went away" teaches nobody anything. A report is a control panel, not
a story:
| Field | What it gives an agent |
|---|---|
detection |
an executable "is it still happening?" check, with the expected healthy result |
affected |
machine-readable poisoned windows [{scope, from, to}] — a backtest excludes them programmatically |
knowledge |
links to the knowledge entries the incident concerns |
resolution |
how it ended — searchable, so "how did we fix this last time" has an answer |
| timeline | started/detected/mitigated/resolved_at; the server stamps status transitions |
| follow-ups | - [ ] checkboxes in the body; the server counts them and warns when closing over them |
The server teaches structure instead of gating on it: a bare report is accepted but the
reply carries hints naming the missing actionable parts. kyb incident --template prints
the canonical skeleton.
Resolving archives the report: the canon stays clean (open things only), the record stays searchable forever.
The third kind (kind: task, keys prefixed task-): short actionable notes and ideas with
the same close-with-an-outcome discipline and none of the incident ceremony.
- Lifecycle —
open → in_progress → blocked → done | dropped. Onlydoneanddroppedare terminal: they require a resolution ("dropped: obsolete after the rewrite" is knowledge too) and archive the task.in_progressandblockedare work in flight — the task stays live in the canon, inkyb tasksand inopen_tasks. priority— optional,low | medium | high | critical; empty means unranked and stays unranked, nothing infers one. Exact filter onGET /tasksand/search.blocked_reason— optional, what the task waits on. It belongs tostatus: blockedonly: setting it on any other status is rejected, and moving offblockedclears it, so a task never reports a block it is no longer in.assignee— optional, who holds the task right now: a short public label (≤80 chars, single line, secret-scanned like every other field). Empty means unclaimed and stays unclaimed. Exact filter onGET /tasksand/search.parent_task— optional, thetask-key this one hangs under; empty means top-level. Must be a validtask-key and cannot create a parent cycle. A parent that does not exist yet is allowed — a child can be filed before its parent — and is reported back asunknown_parent.- Partial transitions —
POST /tasks/{key}/transition(kyb task-status) moves a task between the live statuses and optionally changes its owner or parent without resending title, body, tags, priority or links. Leavingblockedclears the reason; the terminal statuses are refused with a pointer tokyb done, because closing demands an outcome.
Closing archives the task; kyb tasks and the search keep the full record. Every write is a
commit, so kyb history <key> + kyb get <key> --at <sha> reconstruct who held a task, in
which status, at any point in time.
# install the server binary from crates.io
cargo install knowyourbusiness --locked
# run natively — reindexes from git on start, listens on 127.0.0.1:9310
kyb-serverNote:
cargo install knowyourbusiness --lockedinstalls thekyb-serverbinary. The separatekybclient CLI (skills/kyb/bin/kybviabash skills/install.sh) and model setup for hybrid search remain separate steps (see CLI, The agent skill, and Search quality).
# build and run from source
cargo run --release
# docker — data (git canon + index) lives in ./data
docker compose up -d
# remote private-network clients: bind one exact interface, never every NIC
KYB_PUBLISH_ADDR=10.0.0.10 docker compose up -dConfiguration — env, all optional
| Variable | Default | Notes |
|---|---|---|
KYB_DATA |
./kyb-data |
git canon directory |
KYB_INDEX |
./index |
Tantivy cache (safe to delete) |
KYB_ADDR |
127.0.0.1:9310 |
no auth by design — run on a private network |
KYB_PUBLISH_ADDR |
127.0.0.1 |
Docker Compose host interface; set one exact private IP for remote clients |
KYB_MODEL |
(unset) | dir with model.onnx + tokenizer.json; absent = lexical-only |
KYB_AUDIT |
audit.jsonl |
JSONL request log |
skills/kyb/bin/kyb (installed to ~/.local/bin/kyb; point it anywhere with KYB_ADDR=host:port):
kyb query "nats streams" [--tag infra] [--history] [--recent] [--kind incident] [--status open] [--service X]
kyb query "nats streams" --as-of 2026-08-01 # the base as it stood then: one version per key,
# the value that was current, not today's
kyb query "" --changed-between 2026-08-01,2026-08-07 # what moved while you were away
kyb diff nats-streams [--from <rev>] [--to <rev>] # what changed in it (defaults to the two newest)
# The three above ask a server-version-dependent question, so the CLI checks
# /healthz first and refuses rather than let an older server answer a question
# about the past with today's data. Plain queries are unaffected.
kyb tags # which topics the base covers
kyb add --key nats-streams --title "..." --tags nats,infra <<< "body" # upsert by key
kyb get nats-streams [--at <sha>] # current or any historical version
kyb history nats-streams # the whole chain of changes
kyb incident --template # print the report skeleton
kyb incident --key inc-2026-07-22-orders-api-oom --title "..." --service orders_api \
--severity high [--hosts host-a] [--knowledge orders-api-architecture] \
[--detection "check + healthy result"] \
[--affected '[{"scope":"...","from":"...","to":"..."}]'] <<< "body"
kyb incidents [--status X] [--service X] [--open-followups] [--all] # live by default, --all adds archived
kyb resolve inc-2026-07-22-orders-api-oom <<< "what fixed it" # resolution is mandatory; closing archives
kyb task --key task-raise-log-retention --title "..." [--tags idea] \
[--priority high] [--status in_progress] [--assignee agent-a] <<< "body"
kyb task --key task-swap-disk --title "..." --status blocked \
--blocked-reason "waiting on the replacement disk" [--parent task-migrate-logs] <<< "body"
kyb task-status task-raise-log-retention --status in_progress --assignee agent-a # partial: nothing resent
kyb task-status task-swap-disk --status blocked --blocked-reason "waiting on the disk"
kyb tasks [--status X] [--priority P] [--assignee A] [--parent K] [--open-followups] [--all]
kyb done task-raise-log-retention <<< "what came of it" # or --status dropped + whybash skills/install.sh installs, for every agent found on the machine:
- the CLI and the MCP bridge (one copy of each on PATH),
- the manual
SKILL.md, - a pointer section in each agent's always-loaded global instructions — a skill an agent never opens is a skill it never uses.
| Agent | Skill path | Pointer |
|---|---|---|
| Claude Code | ~/.claude/skills/kyb |
CLAUDE.md |
| Codex | ~/.codex/skills/kyb |
AGENTS.md |
| Antigravity | ~/.gemini/config/skills/kyb |
GEMINI.md |
Idempotent: sections are delimited by markers and updated in place. The skill encodes the governance that keeps a shared base alive — always query before adding, overwrite the same key instead of inventing synonyms, only verified facts, English entries, file incidents when something breaks and fold the lesson back into knowledge after resolving.
Alternatively, install the skill into any supported agent workspace using the open skills CLI:
npx skills add alex09x/kybThe server speaks MCP itself, at POST /mcp. Nothing is installed on the client:
claude mcp add --transport http kyb http://<host>:9310/mcpThat is the whole setup. The tool list comes from the server, so upgrading the server is how every connected agent gets new tools — there is no client to update, and no version of a client that can disagree with the server about what exists.
A tool call is replayed through the same router that serves the public API, so there is one
implementation of what POST /knowledge means, the audit log records MCP-driven writes like
any other write, and a route that changes changes here with it.
It is stateless: no sessions, no server-initiated stream, GET and DELETE answer 405 and
say so. Batches work. ?readonly=1 on the URL drops the six writing tools for that
registration — a guard rail for an agent, not a security boundary, since nothing stops a
caller from omitting it.
rm and reindex are not exposed over MCP at all: retracting an entry and rebuilding the
index should not be something an agent reaches for mid-thought.
This adds no reach that the API did not already have. POST /knowledge and
DELETE /knowledge/{key} already answer unauthenticated on this port; /mcp is exactly as
open as they are and no more. Bind accordingly — see the note in src/config.rs.
A deploy script, an ssh one-liner on a fleet node, cron and a person at a terminal all
need the CLI, and MCP reaches none of them. Anything that cannot speak MCP uses kyb.
kyb-server embeds a zero-dependency, dark-mode Web UI served directly at http://<host>:9310/:
- Live Fleet Feed: real-time activity stream of agent reads, writes, and tool executions from the audit log.
- Interactive Topology Graph: 2D force-directed map visualizing services, hosts, dependencies, and linked incidents.
- Incident Control Center: status board (Open / Mitigated / Resolved), executable detection check commands, and mandatory post-mortem resolutions.
- Agent Task Kanban: columns for Backlog, In Progress, Blocked (with reasons highlighted), and Closed tasks.
- Knowledge Explorer: instant search, full Markdown document rendering, revision histories, and line-by-line visual git diffs.
| Method | Path | What it does |
|---|---|---|
GET |
/ |
Web UI & Control Room (zero-dependency single-page application) |
GET |
/api/audit |
?limit=50 — recent operations from the JSONL audit log, newest first |
GET |
/knowledge/{key}/diff |
?from=<sha>&to=<sha> — unified git diff between two revisions |
POST |
/knowledge |
upsert by key. Body: {key, title, body, tags?, refs?} → {key, sha, changed, action}. Identical content = changed:false, no commit |
GET |
/knowledge/{key} |
the entry (kind-specific fields included); archived incidents/tasks come back with archived:true; ?at=<sha> returns a version from history |
GET |
/knowledge/{key}/history |
{key, versions:[{sha, committed_at, message, change}]}, newest first |
POST |
/incidents |
upsert a report; reply carries unknown_knowledge for dangling links and hints for missing structure. status:resolved requires resolution and archives |
GET |
/incidents |
?status=&service=&followups=open&all=true&limit= — live reports by default, open first, freshest on top; all=true, an explicit status= or followups=open include archived ones |
POST |
/incidents/{key}/resolve |
{resolution, status?=resolved} — flips status, records the outcome, stamps the timeline, archives on close |
POST |
/tasks |
upsert a task: {key, title, body, status?, priority?, blocked_reason?, assignee?, parent_task?, knowledge?, resolution?, tags?, refs?}; status is open|in_progress|blocked|done|dropped, priority is ""|low|medium|high|critical, blocked_reason requires status:blocked (400 otherwise), parent_task must be "" or another task- key and cannot create a cycle; the terminal statuses require resolution and archive |
GET |
/tasks |
?status=&priority=&assignee=&parent_task=&followups=open&all=true&limit= — live tasks (open, in_progress, blocked) by default, freshest on top; same archive rules as /incidents |
POST |
/tasks/{key}/transition |
partial update: {status, assignee?, parent_task?, blocked_reason?} — status is a live one (open|in_progress|blocked); omitted fields keep their stored value, leaving blocked clears blocked_reason, and a terminal status is refused with a pointer to /tasks/{key}/resolve |
POST |
/tasks/{key}/resolve |
{resolution, status?=done} — flips status, records the outcome; a non-blocked status clears blocked_reason; done/dropped archive |
GET |
/search |
?q=&tag=&history=&limit=&sort=recent&kind=&status=&service=&priority=&assignee=&parent_task= → ranked hits with full bodies; an empty q lists newest first |
GET |
/tags |
which topics the base covers, most used first |
DELETE |
/knowledge/{key} |
knowledge: retract (drops from the default search); incident/task: archive (stays searchable) |
POST |
/reindex |
full index rebuild from git |
GET |
/healthz |
{ok, entries, open_incidents, open_tasks, index_docs, last_commit} — open_tasks counts every live status (open, in_progress, blocked) |
Every request except /healthz is appended to a JSONL audit log: timestamp, client ip,
method, path, query, status, duration.
scripts/eval-search.sh [addr] [--lexical] measures retrieval: paraphrase questions that
share few or no tokens with the entries they should land on, scored top-1 / top-3.
Measured 2026-09-22 against a 643-entry base, e5-small int8, round trip over LAN:
| setup | top-1 | top-3 | latency |
|---|---|---|---|
| lexical only | 1/14 | 2/14 | ~7 ms |
| + e5-small int8 (118 MB, shipped) | 3/14 | 4/14 | ~9 ms |
Read the absolute numbers with care and the comparison as the point. The fourteen questions that ship in the script are examples written against a different base — three of the keys they expect do not exist in the base measured above, so those cases cannot be hit by either mode and the ceiling is well under 14/14. What the run does establish, on identical cases against an identical base, is that the vector side roughly doubles the hit rate over BM25 alone.
Replace CASES in the script with questions and keys from your own base before drawing any
conclusion about your own setup. An earlier edition of this table reported 9/14 and 12/14
for the hybrid row; those numbers came from the base the example questions were written
for and do not reproduce elsewhere, which is exactly the trap this paragraph exists to
flag.
e5-small ships in the Docker image (MODEL_REPO build arg to swap). Without a model on
disk the service degrades to lexical-only by design.
Rust: axum + tantivy 0.22 + git2 + ort (ONNX Runtime). A single write mutex
(Tantivy allows one IndexWriter and git commits are sequential anyway); reads are lock-free.
cargo test # 321 casesCI builds the image and smoke-tests that the container starts and answers /healthz.
For academic or archival reference, cite the published v0.2.3 release:
@misc{panasenko2026kyb,
doi = {10.5281/zenodo.23027682},
url = {https://zenodo.org/records/23027682},
author = {Panasenko, Alexander},
title = {KYB (Know Your Business): Git-backed knowledge base and incident tracker for AI agents},
version = {0.2.3},
publisher = {Zenodo},
year = {2026}
}- Alexander Panasenko. KYB (Know Your Business): Git-backed knowledge base and incident tracker for AI agents (v0.2.3). Zenodo. https://doi.org/10.5281/zenodo.23027682 (concept DOI: 10.5281/zenodo.22850723)
