Content
<div align="center">
<img src="https://img.shields.io/badge/K-Y_B-00b3c4?style=for-the-badge&labelColor=1a1d24" alt="KYB" height="34"/>
# KYB — Know Your Business
**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.
<br/>
[**Website**](https://kybmemory.com) · By [Alexander Panasenko](https://prod.codes/about/)
<br/>
[](https://crates.io/crates/knowyourbusiness)
[](LICENSE)
[](#stack--tests)
[](#stack--tests)
[](Cargo.toml)
[](#search-quality)
[](https://skills.sh/alex09x/kyb/kyb)
<br/>
[**Quick start**](#quick-start) · [**How it works**](#how-it-works) · [**Incidents**](#incident-reports) · [**Agent skill**](#the-agent-skill) · [**MCP**](#mcp) · [**Web UI**](#web-ui-and-control-room) · [**HTTP API**](#http-api) · [**Citation**](#citation)
</div>
---
## Why
> 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.
---
## How it works
- **Git is the canon — there is no database.** One markdown file (YAML frontmatter + body)
per entry — under `knowledge/`, `incidents/` or `tasks/` 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` (marked `archived`). 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.
### Architecture
[](docs/assets/kyb-architecture.png)
[View the full-size diagram](docs/assets/kyb-architecture.png).
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.
---
## Incident reports
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.
---
## Tasks
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`. Only `done` and `dropped`
are terminal: they require a resolution ("dropped: obsolete after the rewrite" is knowledge
too) and archive the task. `in_progress` and `blocked` are work in flight — the task stays
live in the canon, in `kyb tasks` and in `open_tasks`.
- **`priority`** — optional, `low | medium | high | critical`; empty means unranked and stays
unranked, nothing infers one. Exact filter on `GET /tasks` and `/search`.
- **`blocked_reason`** — optional, what the task waits on. It belongs to `status: blocked`
only: setting it on any other status is rejected, and moving off `blocked` clears 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 on `GET /tasks` and `/search`.
- **`parent_task`** — optional, the `task-` key this one hangs under; empty means top-level.
Must be a valid `task-` 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 as
`unknown_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**. Leaving `blocked` clears the reason; the terminal
statuses are refused with a pointer to `kyb 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.
---
## Quick start
```bash
# 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-server
```
> **Note:** `cargo install knowyourbusiness --locked` installs the `kyb-server` binary. The separate `kyb` client CLI (`skills/kyb/bin/kyb` via `bash skills/install.sh`) and model setup for hybrid search remain separate steps (see [CLI](#cli), [The agent skill](#the-agent-skill), and [Search quality](#search-quality)).
### From repository checkout
```bash
# 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 -d
```
<details>
<summary><b>Configuration</b> — env, all optional</summary>
| 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 |
</details>
---
## CLI
`skills/kyb/bin/kyb` (installed to `~/.local/bin/kyb`; point it anywhere with `KYB_ADDR=host:port`):
```bash
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 + why
```
---
## The agent skill
`bash 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:
```sh
npx skills add alex09x/kyb
```
---
## MCP
The server speaks MCP itself, at `POST /mcp`. Nothing is installed on the client:
```sh
claude mcp add --transport http kyb http://<host>:9310/mcp
```
That 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`.
### It does not replace the CLI
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`.
---
## Web UI and Control Room
`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.
---
## HTTP API
| 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.
---
## Search quality
`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.
---
## Stack & tests
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.
```bash
cargo test # 321 cases
```
CI builds the image and smoke-tests that the container starts and answers `/healthz`.
---
## Citation
For academic or archival reference, cite the published v0.2.3 release:
```bibtex
@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](https://doi.org/10.5281/zenodo.22850723))
---
## License
[MIT](LICENSE)
Connection Info
You Might Also Like
No recommendations available