██╗ ██╗ ██████╗ ██╗ ██╗███████╗ ╚██╗ ██╔╝██╔═══██╗██║ ██╔╝██╔════╝ ╚████╔╝ ██║ ██║█████╔╝ █████╗ ╚██╔╝ ██║ ██║██╔═██╗ ██╔══╝ ██║ ╚██████╔╝██║ ██╗███████╗ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
Knowledge your AI can trust.
ontology-based knowledge database · governed context injection for AI agents · MCP-native
MIT · feature-complete through v5.9 · visual overview
English | 한국어
An AI agent with memory will repeat whatever it heard. An AI agent on yoke speaks only knowledge that carries a source, survived review, and is still current — and it cites its sources. Memory layers automate what your AI remembers; yoke governs what your AI is allowed to believe.
Trust isn't a promise here — it's five mechanisms, each enforced in code:
- Nothing enters without a source. Every write passes through a single commit gate that rejects knowledge with no provenance (who said it, where, when). Knowledge without a source is just a rumor, and rumors don't get in.
- Nothing is believed until a human verifies it. New knowledge lands as a
draft, quarantined from injection. AI agents can record knowledge over MCP, but they cannot promote it — verification is deliberately a human act (yoke verify), and there is no MCP tool that does it. By default onlyverifiedknowledge reaches your AI's context; an agent can ask for drafts explicitly (includeDraft) and they arrive labelled[draft]. One documented exception:connect rdbmaps an existing database that is already the org's system of record, so mapped rows land verified — see docs/BACKENDS.md. - Nothing is silently overwritten. Storage is append-only: an edit is a new
version, and there is no delete at all — retirement is a status (
deprecated). You can always reconstruct what the system believed at any point in time, and every injected item carries a citation —[type:id@vN] author (confirmed by promoter), occurred_at— so every claim is auditable and names both who wrote it and who vouched for it. The one mutation history cannot record isrename-type, which rewrites the type on existing version rows; it leaves an audit row saying so. - Contradictions are surfaced, never auto-resolved. When new knowledge
conflicts with what's already verified, yoke keeps both and links them with
a
conflicts_withedge for a human to settle, and injection serves both sides marked as disputed. A disagreement is itself knowledge; deciding the winner is not the database's job. Automatic detection compares embeddings, so it needs an embedding provider configured (below) — without one, records you link yourself are still surfaced, but nothing is detected for you. - Knowledge expires. Verified isn't forever — entries lose freshness past
their type's TTL and are demoted to
staleat read time, out of the injection path until someone re-confirms them. Stale truths are the politest form of misinformation, and yoke treats them that way.
And it's measured, not asserted: the injection-quality eval reports 0%
contamination (no draft, stale or retired record reaching an injection) and
0% missed contradictions on its planted pairs (see below). Read it for what
it covers: a synthetic corpus and a stub embedder, so it measures the filter and
the detection wiring rather than retrieval quality on your data — npm run eval:retrieval is the one that measures that, against a gold set.
Runs local and embedded — better-sqlite3 + FTS5 + sqlite-vec, no server required.
| One-line summary | A database optimized for knowledge: structure it as an ontology, then inject only the verified subset relevant to the current context into your AI — with citations. |
| Front adapters | An MCP server (inject · commit · record_decision · overview · persona · use_scope) and a thin CLI. Every AI tool is just an MCP client — no per-tool adapter. |
| Storage backends | sqlite (default, FTS5 + sqlite-vec) · postgres (native scored FTS + pgvector, no extra dependency) · opensearch (native BM25 + k-NN, no extra dependency) — point either remote one at the server your company already runs · sharded (federation by tenant). All four pass one conformance suite. |
| Capture connectors | github-pr (review comments), slack (channels + threads), notes (local transcripts) — external sources → draft knowledge, dated from the source. rdb (Postgres/MySQL read-mapping) maps a database that is already the system of record, so its rows land verified. |
| Anchored injection | One mechanism, two entry points: anchor on a collaboration for the team's shared working context, or on a person for a persona. |
| Persona | "How would a teammate decide?" → their recorded, verified judgments, cited and generated live. Citation, not impersonation. |
| Shared working context | Pin a collaboration and a team shares one context; scope prioritizes without hiding org-wide knowledge. |
| Enterprise | Namespaced multi-tenancy · OIDC/SSO + API tokens · RBAC (the verify permission is the governance permission) · read replicas · online backup + point-in-time export. |
| License | MIT |
curl -fsSL https://raw.githubusercontent.com/jhso-dev/yoke/main/scripts/install.sh | bash
# clones to ~/.yoke/app, builds, and links the global `yoke` command
# (--skip-link to skip the link, --dir PATH to change the location)
yoke init # create ./yoke.db + seed the ontology
yoke add fact --attr statement="Deployments only happen Tuesday mornings"
yoke review # inspect the draft queue
yoke verify <id> # promote it (or: yoke verify --all-drafts)
yoke inject "when do we deploy" # inject only verified knowledge, with citationsAnything added via add starts as a draft. It won't show up in inject until
you promote it with verify — that gate is the whole point of the governance
model. Use yoke verify --all-drafts to promote in bulk on a cold start.
Prefer to build from source (contributors)? Clone and link directly:
git clone https://github.com/jhso-dev/yoke && cd yoke
npm install && npm run build && npm link # provides the global `yoke` commandRecording a decision:
yoke add decision \
--attr conclusion="Cache with Redis" \
--attr rationale="P99 latency exceeded our target" \
--attr rejected_alternatives="in-process cache" \
--attr rejected_alternatives="memcached"rejected_alternatives is a list, and a repeated --attr is how the CLI builds one — a single
occurrence is a string, which the gate rejects for a list-typed attribute.
Attach yoke to an agent (Claude Code and friends) as a stdio MCP server. In your
project root .mcp.json:
{
"mcpServers": {
"yoke": {
"command": "yoke",
"args": ["mcp", "--db", "./yoke.db"]
}
}
}Tools exposed:
yoke_inject— query a context → inject verified knowledge, with citationsyoke_commit— stage knowledge (enters asdraft)yoke_record_decision— decision shortcut (conclusion + rationale + rejected alternatives)yoke_persona— person-scoped injection ("how would a teammate decide?")yoke_overview— the corpus at a glance: counts by type, the most-connected records, who authored whatyoke_use_scope— pin the current collaboration so the whole session shares one working context
No model ships with yoke, and none will. One provider setting — an OpenAI-compatible
/embeddings endpoint — which is why the same three lines reach OpenAI, Azure, Ollama, vLLM, TEI and
LiteLLM. Bundling an ONNX runtime would add 258MB of platform binaries and a second cross-platform
prebuild trap to a CLI people install globally; every practice we surveyed keeps the model out of the
application process.
Free, local, and keyless via Ollama:
ollama pull bge-m3
export YOKE_EMBED_URL=http://localhost:11434/v1
export YOKE_EMBED_MODEL=bge-m3 # no key neededbge-m3 is the recommended default: 100+ languages in one model, 8192-token context, 1024
dimensions, MIT. If your knowledge is not mostly English, do not use nomic-embed-text — it is
English-centric, so semantic matching on other languages quietly degrades to roughly what keyword
search already gave you, which looks like a working setup and is not.
A hosted provider instead:
export YOKE_EMBED_URL=https://api.openai.com/v1
export YOKE_EMBED_MODEL=text-embedding-3-large
export YOKE_EMBED_KEY=sk-...Set it in the shell, not only in .mcp.json. MCP server env applies to that process alone, so a
.mcp.json-only setup leaves yoke add, yoke ui and every connector writing records with no
vector — measured at 1 of 3 entities in this repo's own database before it was fixed.
| with a provider | without | |
|---|---|---|
| Knowledge is stored | yes | yes — a provider being down never rejects a record |
| Duplicate candidates on commit | yes | no, and yoke add says so — there is no keyword fallback for this, because treating every FTS hit as a duplicate is mostly false positives |
conflicts_with auto-detection |
yes | no |
| Keyword search / injection | yes | yes, unaffected |
Coverage is repairable at any time — the vector is a derived index, not knowledge, so this writes no new version and changes no citation:
yoke backfill --embeddings # index every record with the current model
yoke backfill --embeddings --rebuild # after CHANGING model (dimension differs)A database holds one vector space. Switching models without --rebuild fails loudly with the
dimension it found and the command above — a mixed space would return confidently wrong neighbours
instead.
Point yoke at a Postgres or an OpenSearch you already operate. The knowledge goes there; this client's audit trail and API tokens stay in a local sqlite — yoke's own bookkeeping does not belong in someone else's database, and asking for a place to put it would get a no.
# Postgres — the database most orgs already have. pgvector gives `similar` when present.
export YOKE_POSTGRES_URL=postgres://user:pass@localhost:5432/db
export YOKE_POSTGRES_SCHEMA=team_a # optional: two yoke DBs in one database
# — or OpenSearch (setting both is an error, not a precedence order)
export YOKE_OPENSEARCH_URL=http://localhost:9200
export YOKE_OPENSEARCH_USER=admin YOKE_OPENSEARCH_PASSWORD=… # a secured cluster only
export YOKE_OPENSEARCH_PREFIX=team_a_ # optional: two yoke DBs in one cluster
yoke init # creates the schema/indices, seeds the ontologyOr put the same lines in a .env in the working directory — cp .env.example .env and uncomment.
Node's own parser reads it, so there is no dependency and no format of ours, and an actual environment
variable still wins over the file, which keeps a CI secret ahead of anything left on disk. .env is
gitignored; .env.example is committed and lists every YOKE_* variable yoke itself reads.
--db still names the local sqlite. Everything else is unchanged — add, review, verify,
inject, yoke ui, MCP. Both backends rank search natively and scored (Postgres ts_rank,
OpenSearch BM25) and both serve similar from the engine (pgvector / k-NN), so retrieval needs no
second service. Neither adds a dependency: pg was already in the tree for the RDB connector, and
the OpenSearch adapter is plain REST.
What lives where, and why: docs/BACKENDS.md. The short version is that YokeStore's extension
surface is synchronous (better-sqlite3 shaped it), so a networked backend is composed with a local
sqlite rather than swapped in — and an adapter that cannot satisfy those synchronous signatures is not
selectable at all.
docker run -d --name yoke-opensearch -p 9200:9200 \
-e discovery.type=single-node -e DISABLE_SECURITY_PLUGIN=true \
-e DISABLE_INSTALL_DEMO_CONFIG=true -e "OPENSEARCH_JAVA_OPTS=-Xms256m -Xmx256m" \
opensearchproject/opensearch:2A governance workbench in the browser — the human side of the same core functions the CLI exposes. Not a place to ask questions of your knowledge (that's your AI's job, over MCP): every screen shows records, typed and versioned and cited.
yoke ui # http://127.0.0.1:4800 — local, single-user, ungated
yoke serve --auth --host 0.0.0.0 # a team; log in with a token from `yoke token create`Screens: the review queue, conflicts, the ontology browser, persona preview, entity
detail, injection preview ("what would my agent actually receive for this query?"), a
force-directed graph explorer, and the audit log. One static bundle, one port. Under
yoke serve the same process also answers POST /mcp, so a team deployment needs
nothing extra; yoke ui serves the workbench only.
Servers bind loopback by default. yoke ui has no authentication, so widening it is an
explicit --host and it warns that anyone reachable can read, create, retire and rename
this database's knowledge; issuing credentials is refused to non-loopback callers there.
yoke serve refuses a non-loopback bind without --auth outright, since it can
authenticate and therefore has no reason not to.
yoke init | add | get | search | list | link | verify | deprecate
yoke review [--stale] # drafts awaiting review / verified past their TTL
yoke inject <query> [--include-draft] [--limit n] [--scope <id>] [--depth n] [--as-of ts]
yoke overview | graph [--limit n] # the corpus at a glance / as edges
yoke conflicts | ontology <list|add-type> | rename-type <from> <to> | persona <person-id> [--check f]
yoke history <id> | audit [--since ts] [--until ts] [--limit n] [--shape]
yoke connect github-pr|slack|notes|rdb ...
yoke mcp | ui | serve [--auth] [--host addr] | token <create|list|revoke>
yoke backup <dest.db> [--force] | restore <src.db> [--force]
yoke export --until <ts> --out <new.db> # --shards <file> federates backends
yoke backfill [--embeddings [--rebuild]] # repair authorship edges / the vector index
Common options: --db (> YOKE_DB env > ./yoke.db), --actor
(> YOKE_ACTOR env > yoke:system), and --json (machine-readable output).
A team builds one knowledge space together, in real time. When the user says
"this is PAY-42 work", the agent declares it once with yoke_use_scope, and the
whole session defaults to that collaboration — injections lead with its knowledge,
and anything recorded links to it automatically. A decision one person records
(and a human verifies) is in every other session's context the next time they ask.
Scope prioritizes, it doesn't imprison: a pinned collaboration leads, but org-wide facts and personas still flow in on a query. And the context outlives the work — when the collaboration wraps, its knowledge stays in the graph as org memory rather than vanishing into a closed ticket.
A persona is the same mechanism anchored on a person instead of a collaboration — authorship is a graph edge, so "what does this person know" and "what do we know about this work" are one walk with two names. The one difference is deliberate: a persona is strict, because presenting knowledge someone didn't author as their judgment would be impersonation.
yoke measures two different things, and they answer different questions.
Injection quality (npm run eval) — does the filter hold, and is the detection
wired up:
| Metric | Definition | Target | Measured |
|---|---|---|---|
| Contamination rate | Share of draft entries among inject results | 0% | 0.0% (only the 20 verified of 40 candidates were injected) |
| Missed-contradiction rate | Share of opposing-conclusion decision pairs with no conflicts_with edge | 0% | 0.0% (5/5 detected) |
Read those two numbers for what they cover: a 45-record synthetic corpus and a stub embedder whose vectors are built from the planted topic word, so the contradiction figure measures that stage 4 runs and files the edge — not that a real embedding model would notice. Precision is not measured on either axis.
Retrieval quality (npm run eval:retrieval -- <db>) — does the right record come
back, over eval/gold-set.json on a loaded corpus. This is the one that measures search
against real text, and it reports its own weak spots rather than a headline: on the demo
corpus, keyword-only retrieval scores recall@10 59.2% overall but 52.0% on
question-shaped queries against 95.5% on keyword-shaped ones.
| Doc | What's in it |
|---|---|
| VISION | Why yoke exists, the version scope, persona & shared context |
| ARCHITECTURE | The ports-and-adapters boundary |
| KNOWLEDGE-POLICY | The gate, lifecycle, and injection-filter rules |
| SPEC | The implementation contract — schema, port, gate, MCP tools, CLI |
| WEB-UI | The governance workbench — the twelve screens and the line we don't cross |
| ROADMAP | v0.1 → v5.9 built, in order, each section a record |
| BACKENDS | Adapter extension + RDB read-mapping (with live-verification notes) |
| ENTERPRISE | Multi-tenancy, auth, RBAC, replication, sharding |
| MARKET | Competitive landscape and positioning |
MIT