Every claim about your system, entered as an exhibit and cited to its source, kept in the repo, and a Claude Code plugin that reads the case file first and enters new evidence as it works.
The case file · An exhibit · How to use it · Install · Browse it
Which services exist, who owns them, what depends on what, why the datastore
decision went the way it did: that knowledge is usually hearsay, scattered
across people's heads, stale wikis and chat threads. docket keeps it on the
record. Each fact is an exhibit, a plain Markdown file with YAML
frontmatter under .docket/, and each claim in it cites its evidence:
the file and lines, manifest key, endpoint, URL or conversation it was seen in.
docket weighs that evidence into a confidence score; nobody writes one by hand.
The files are the record. Indexes, vector stores and graphs are disposable projections of them, deleted and rebuilt byte-for-byte whenever you like:
rm -rf .docket/.index
docket rebuilddocket init opens the case: a config file at the root and a .docket/ tree
beside your code. Humans and agents edit the same files, and Git keeps the
history.
your-repo/
├── .docket.yaml config: where the case file lives, which adapters to feed
└── .docket/
├── entities.yaml the ontology: resource types, relationships, evidence sources
├── resources/ exhibits, one Markdown file per thing
│ ├── services/orders.md id: service.orders
│ ├── teams/platform-engineering.md
│ ├── datasources/orders-db.md
│ └── ... repositories, libraries, agents, systems, environments
├── decisions/ why things are the way they are, losers included
│ └── orders-on-postgres.md
├── constraints/ rules the system must obey
│ └── pci-scope.md
├── notes/ anything else worth keeping
├── .index/ projections: generated, gitignored, disposable
│ ├── documents.jsonl
│ ├── nodes.jsonl
│ ├── edges.jsonl
│ ├── manifests/ what sync handed each adapter instance: local.json, ...
│ └── adapters/ state an adapter instance keeps for itself, if any
└── .cache/ chat answers and when each adapter last synced: gitignored, disposable
Directories are only for people: an exhibit's identity is the id in its
frontmatter, never its path, so files can move freely. The ontology in
entities.yaml is per-repository. It starts as the default SDLC ontology
(services, APIs, pipelines, clusters, incidents, runbooks, policies, teams,
decisions, constraints and more) and you extend it when your case needs a type
it does not have. examples/acme-platform is a
complete worked case file: 21 resources and 37 links.
---
id: service.conversation-api # <type>.<semantic-name>; never changes when the file moves
type: service # a resource type from entities.yaml
title: Conversation API
attributes:
language: typescript
lifecycle: active
links: # typed relationships to other exhibits
- rel: owned_by
target: team.platform
- rel: depends_on
target: datasource.sessions
attributes:
criticality: high
runtime: true
evidence: # where this link was seen
- source: code
path: src/store/sessions.ts
lines: 12-40
observedAt: 2026-10-05
observedBy: claude
evidence: # where the service itself was seen
- source: manifest
path: services/conversation-api/package.json
key: name
observedAt: 2026-10-05
observedBy: claude
---
# Conversation API
Handles conversation persistence and retrieval.Only id, type and title are required. Links may point at exhibits no
file defines yet: the graph is built incrementally, so a dangling reference is
a warning until you ask for --strict. See
Evidence and confidence for how evidence becomes a
score.
1. Open the case. From the root of your repository:
npx @chrisjowen/docket setupThis installs the Claude Code plugin, offers to run docket init to create
.docket.yaml, .docket/ and the default ontology, and offers to install the
docket CLI globally. Restart Claude Code to load the plugin.
Install covers teams, CI and doing it by hand.
2. Enter evidence. Ask Claude to "remember" a fact, decision or constraint,
or let it notice one: the plugin's remember skill writes the exhibit and cites
where it saw it, and an end-of-session review captures what a session settled.
You can write or edit exhibits by hand just as well. Either way, check them
against the ontology:
docket validate --strict3. Index the record. Projections are what make the case file searchable:
docket sync # project changed files into .docket/.index (and mem0 or Neo4j, if configured)
docket watch # or keep projecting continuously as files change
docket rebuild # throw every projection away and rebuild it from the files4. Question the witness. Search every projection at once; each hit points at its canonical file:
docket search who owns checkout
docket ask "how many services depend on the ledger?"
docket ontology show service # a service's attributes, links and what each source is worthThe plugin teaches Claude to ask the docket before it greps the code.
5. Review the case. docket open serves a web UI to browse the graph, read
each exhibit with its evidence and confidence, and ask questions. See
Browse it.
One command, from the root of the repository you want to remember things:
npx @chrisjowen/docket setupIt installs the Claude Code plugin (through the claude CLI, for your user),
offers to run docket init if the repository has no .docket.yaml, and offers
to install the CLI globally so docket works in your shell too. Run it again
whenever you like: it updates what is already installed and leaves an existing
.docket.yaml alone. Restart Claude Code afterwards to load the plugin.
Without a terminal (CI, scripts) it asks nothing: it installs the plugin and
skips the rest unless told otherwise. --yes takes the recommended answer to
every question (set up the repository, but no global install), --no-init
and --no-global skip those offers, and --global installs the CLI without
asking. Node 22 or later.
npx @chrisjowen/docket setup --teaminstalls the plugin for the repository instead of for you: it records the
marketplace and the plugin in .claude/settings.json. Commit that file, and
everyone who clones the repository and trusts it in Claude Code is offered the
plugin.
Without the claude CLI on your path, or to do it by hand, install the plugin
from inside Claude Code:
/plugin marketplace add chrisjowen/docket
/plugin install docket@docket
The plugin needs no separate CLI install. It runs the repository's own
docket, or one on your path, and otherwise the CLI release it pins, through
npx (so Node 22 or later is still needed). Set up a repository with
npx @chrisjowen/docket init.
pnpm add -D @chrisjowen/docket # or: npm install --save-dev @chrisjowen/docket
npx docket initThe command is docket; run it through npx (or a package.json script) when
it is installed as a dev dependency. The plugin uses this copy first.
docket setup # install the Claude Code plugin and set up this repository
docket init # scaffold .docket.yaml, .docket/ and the default ontology
docket validate # check files against the ontology
docket validate --strict # unresolved links become errors
docket sync # project changed files into .docket/.index
docket sync --adapter <id> # sync only that adapter instance (repeatable)
docket rebuild # reset and reproject everything
docket rebuild --adapter <id> # reset and reproject only that instance, in its own namespace
docket watch # reconcile continuously as files change
docket ask <question...> # ask the adapters through the coordinator: each answer with its evidence, and a summary
docket ask --json <question...> # the coordinated answer as JSON: blocks, evidence, diagnostics (no summary unless --synthesis)
docket ask --adapter <id> --no-synthesis <question...> # one adapter (repeatable), without a summary
docket search <query...> # the documents each adapter found: ask's compatibility view
docket adapters list # the configured adapter instances, without connecting to any
docket adapters status # each instance's health and how far its index lags the files (--json)
docket graph --json # every exhibit and link, read from the files, as versioned JSON (--root <dir>; docs/graph-json.md)
docket open # browse, search, ask and chat in a web UI on 127.0.0.1 (--host to widen)
docket ontology list # resource types, relationships and evidence sources
docket ontology show service # attributes, relationships and confidence by source
docket adapter add <provider> # set up jsonl, neo4j, mem0, memvid or mempalace: asks, shows the plan, applies it once confirmed
docket adapters list # configured adapter instances and whether each can load
docket config migrate --dry-run # show a version 1 .docket.yaml rewritten as version 2
docket config migrate # rewrite it, once confirmed (--write: without asking); the original is kept
docket runtime plan <id> # validate a runtimes entry and show what it would run, secrets redacted
docket runtime up <id> # start its containers, pulling only as its pullPolicy allows
docket runtime status <id> # state and health of its containers, changing nothing
docket runtime down <id> # stop and remove its containers; volumes are kept (--destroy-volumes deletes them)docket open serves a web UI for the repository on port 4380 and prints
http://127.0.0.1:4380/ (or any free port when that one is taken; --port
picks one, --no-open skips launching the browser). It reads the canonical
files on every request, so it needs no projection beyond the default jsonl.
It listens on 127.0.0.1 only, so nothing else on the network can reach it.
--host picks another interface: --host 0.0.0.0 serves every interface, and
then anyone who can reach the machine on that port can read the repository's
knowledge, whatever Host they ask for. Syncing stays limited to the page at
127.0.0.1 either way, so a server listening only on another address cannot
sync.
Behaviour change: earlier versions of
docket openlistened on every interface (0.0.0.0) by default. To keep reaching the UI from other machines, start it withdocket open --host 0.0.0.0.
The API never changes the canonical files; its writes are chat's answer cache and, when the page asks from this machine, a sync of the projections.
-
Graph - every entity and relationship as a force-directed graph, each type drawn with its own icon and colour, filtered by type and relationship (each with All / None switches). Links to entities no file defines yet show as dashed ghosts.
-
Inspector - an entity's frontmatter, notes and links in and out, with the confidence docket computes for it and for each link, the sources that corroborate it, and every piece of evidence - file and lines, endpoint, URLs, when and by whom - merged from all the files that declare its id. Each link expands to show the evidence for that relationship.
-
Quick search (
⌘Kor/) - instant type-ahead over ids, titles, types, tags, attributes, notes and relationship names.type:service,rel:depends_onandtag:corenarrow it down; picking a result focuses the graph on it. -
Table and History - beside the graph under Browse: every exhibit or observation as a sortable table, filtered by type, source, date observed and assessment; and the dated observations over time - when something happened (
eventAt) and when it was observed (observedAt), labelled apart - with each chain ofsupersedesdecisions. -
Sync - the header's Sync button runs
docket sync- the same code, not a copy - for every adapter enabled for projection or just one, showing each adapter's result as its pass finishes, when each last synced, and a hint when files changed since. One sync runs at a time. Only the page on the machine runningdocket open, at127.0.0.1, can start one: the server checks the request's address, Host and Origin, and a token it makes at launch. -
Ask - its own page: a question put to every adapter configured for query, each answer shown as it came - exhibits, passages, facts, counts, tables, timelines, graphs - with how the adapter read the question, how much it covered, and the evidence behind each result, which opens in the inspector beside it. One adapter failing leaves the others' answers in place, and counts that disagree are shown side by side. Ask goes through the same coordinator as
docket ask(see Asking questions), with the relationship paths that join what the adapters found; "Show on the board" puts the results, and only them, on the graph. Clicking evidence opens it with what the files hold at its reference, read when you click. Adapters answer from their projections, so rundocket syncfirst; the UI says when an adapter is behind the files. -
Adapters - each configured adapter's roles, connection health and how far its index lags the files. It never shows configuration, which can hold secrets, and viewing it never starts or pulls anything.
-
Chat - a conversation with the casebook. Each question runs the same search as Ask, then a model summarizes what it found in a few sentences, citing each exhibit it relies on; citations link to the exhibit, and the graph shows only the exhibits cited (everything found when it cites none). The model is Claude, through the
claudeCLI from Claude Code - nothing to configure, it uses your own login. docket runs it in print mode (claude -p) with the question on stdin, no tools, no MCP servers and no saved session, and gives up after two minutes. To pick its model, or to summarize with a local Ollama model instead, setsummarizein.docket.yaml:summarize: provider: claude model: sonnet # passed as `claude --model`; unset, Claude Code picks # timeoutMs: 120000
summarize: model: "qwen2.5:7b" # Ollama at http://localhost:11434 # provider: ollama # the default when `provider` is left out # url: http://localhost:11434 # timeoutMs: 60000
When
claudeis not installed and no Ollama model is set - or when the model fails - chat shows what search found, unsummarized, and says why.Summaries are cached in
.docket/.cache/chat/, one JSON file per question, keyed by a hash of the question (ignoring case and spacing), the model and its provider, its instructions, the content hash of every exhibit the summary was built from and the relationship paths connecting them. Asking the same question again returns the cached answer at once, marked as such, while those are unchanged; editing any file that declares one of the exhibits, finding a different set, a change in how they connect, or changing the model or provider asks the model afresh and replaces the entry. Like.index, the cache is disposable:docket initadds.docket/.cache/to.gitignore, and deleting it only costs the next answer a model call.
The UI is a SvelteKit app in packages/docket-ui,
built into the npm package, so npm i -g @chrisjowen/docket is all it needs.
The API it reads is plain JSON, for scripts too: GET /api/graph returns
the graph docket graph --json prints, without its schema and version
(scripts should read the command, whose contract is versioned:
docs/graph-json.md); POST /api/ask (a question,
optional adapters, conversation, synthesis, timezone and requestId)
returns what docket ask --json prints; DELETE /api/ask/<requestId>
cancels a question still being answered, as closing its connection does;
GET /api/adapters returns what docket adapters status --json prints; and
POST /api/evidence resolves up to 16 canonical references against the files,
with a bounded excerpt of each. The older GET /api/ask?q= and
GET /api/chat?q= still answer, translated from the coordinator's answers.
docket ask puts a question to every adapter enabled for query - or those
--adapter names - through one shared coordinator
(docs/adapter-spec.md §10). The UI's Ask page and
docket search use the same coordinator, so they agree.
- Each adapter answers in its own terms: entities and passages from keyword or semantic search, counts and typed tables from a graph query, facts, timelines. Nothing is merged into one score; a count from a graph is never turned into a list of documents.
- They are asked at most
query.maxConcurrentAdaptersat once, all within onequery.timeoutMsdeadline. One that fails, breaks the adapter contract or misses the deadline is reported as that adapter's failure; the others' answers stand. Ctrl-C (or closing the UI's request) cancels the adapters still answering. - Every answer is validated against the contract, and every canonical
reference in it is checked against the files as they stood when the
question was asked: a result naming a record the files do not hold in scope
is left out with a warning, a reference to another scope is dropped, and a
reference to an older revision is marked as coming from an index behind the
files. Block and evidence ids are prefixed with the adapter instance id, so
graph:row-1is unique across the answer. - Coverage is stated per adapter:
every matchonly when the engine enumerated everything matching in scope, otherwisetop matches only- a sample, never a count. - Unless
--no-synthesis(orquery.synthesis: false), the summarize model writes a short answer from the results, citing exhibits as[service.orders]and evidence as[graph:row-1]: it is told to say which facts were derived by an engine, what each result covers, and to give disagreeing counts side by side rather than choose one. Its prompt holds the most relevant parts of each exhibit and piece of evidence - including the entities on the paths between what was found - within one total budget. Summaries are cached in.docket/.cache/chat/, keyed by the question and how it was read (scope, time zone, the day, the conversation), each adapter's configuration, checkpoint and answer, the revisions of what it cites, the paths between them, the model and its instructions. An adapter whose freshness is unknown makes its summary uncacheable.
--json prints the whole coordinated answer - per-adapter blocks, evidence,
the status of each evidence reference, freshness and diagnostics, every
canonical reference found once with the adapters that found it, and the
paths between them - and leaves the summary out unless --synthesis asks for
it. --timezone sets the zone relative times are read in.
A captured resource or link says exactly where it was seen, as evidence: a
source kind and the location — file, line range and symbol; config key; API
method and endpoint; URL — with when, by whom and in which session.
evidence:
- source: infrastructure
path: deploy/k8s/orders/deployment.yaml
lines: 1-48
symbol: Deployment/orders-api
urls:
- https://github.com/acme/platform/blob/3f2c1d0/deploy/k8s/orders/deployment.yaml#L1-L48
observedAt: 2026-10-05
observedBy: claude
session: 6c1f0e2a
note: Deployment with 3 replicas of the orders-api image.Links take an evidence list of their own. Evidence is append-only: seeing
something again adds an entry and never rewrites an earlier one.
Confidence is computed, never written. The ontology says what one observation from each source is worth, per resource type and relationship — a dependency declared in a manifest is close to certain, a pod or secret named in code is not much until something independent confirms it:
# .docket/entities.yaml
evidence:
sources:
code: { confidence: 0.6, requires: [path] }
runtime: { confidence: 0.85, requiresAny: [urls, endpoint, symbol] }
resourceTypes:
pod:
confidence: { code: 0.3, infrastructure: 0.7, runtime: 0.9 }
relationships:
depends_on:
confidence: { manifest: 0.95, code: 0.65 }Observations of one source kind do not corroborate each other — the strongest
counts — while independent kinds combine as 1 - (1 - a)(1 - b): a pod read
from code (0.3) and seen running (0.9) is 0.93. A resource with no evidence
takes its provenance.confidence, or evidence.unevidenced (0.5). The
default ontology ships the source kinds code, manifest, config,
infrastructure, api, runtime, docs, conversation and human, and
rules for the types where it matters; an ontology written before evidence
existed gets the same defaults. docket ontology show <type> prints the
confidence each source earns for that type, and docket validate rejects
evidence that does not give the location its source requires.
Several files may declare the same id — two branches capturing the same
service, say. They are one resource: projections receive a single merged
entity with every file's evidence, one relationship per (source, rel, target),
and the confidence the combined evidence earns. The first file by path names
it and its attribute values stand; a file that disagrees gets a warning, and
one that gives the id another type is left out with an error.
ontology + canonical files
│ authoritative
▼
reconciliation
▼
projections
disposable / rebuildable
The filesystem is the durable memory protocol. The ontology defines what structured knowledge means. The watcher turns file changes into normalized resource changes. Projections provide query capability, and nothing in them is a source of truth.
Links may point at resources that do not exist yet — the graph is expected to
be built incrementally, so a dangling reference is a warning until you ask for
--strict.
The Markdown files are the source. A projection is a destination: sync and
watch push every change into each adapter .docket.yaml enables for
projection, and docket search asks each one enabled for query.
.docket/**/*.md ──► docket sync / watch ──┬──► jsonl → .docket/.index/*.jsonl
├──► mem0 → hosted or self-hosted mem0
└──► neo4j → a Neo4j graph
An adapter declares which canonical inputs it takes, and is handed only those
(docs/adapter-spec.md §8):
- entity — one per id, however many files declare it, each resource and link carrying its evidence, evidence count, corroborating source kinds and confidence. Its id and revision (the merged hash) are what they always were. The bundled jsonl, mem0 and neo4j adapters take entities only.
- observation — one per distinct evidence record, of a resource or of one
of its links: what was seen, its source locations (
pathandlines), the canonical files that record it, andobservedAtonly when the record states one. Canonical evidence records no event time, so none is given; a file's modification time is never used as one. Its id is its entity's id and a hash of the record, so a corrected record replaces the old observation. - document — one per canonical file with a Markdown body: the text, the
entities it declares and
[[mentions]], and the span of the file it is,startLinetoendLineat the file's hash.
.docket.yaml version 2 lists adapter instances
(docs/adapter-spec.md §5):
version: 2
adapters:
- id: local # unique; names the instance in answers and errors
module: "@docket/adapter-jsonl" # a package, or ./a/file.mjs relative to .docket.yaml
roles: [projection, query] # default: both; a role left out never runs
config: # the adapter's own settings
output: .docket/.index
- id: enterprise-graph
module: "@docket/adapter-neo4j"
roles: [query] # asked questions, never synced into
config:
uri: "neo4j+s://graph.internal.example"
database: project-memory
username: docket
passwordEnv: DOCKET_GRAPH_PASSWORD
scope: payments-project
- id: company-memory
module: "./tools/docket/company-memory.mjs"
config:
endpoint: "https://memory.internal.example"
tokenEnv: COMPANY_MEMORY_TOKEN
query:
defaultAdapters: [local, enterprise-graph, company-memory] # default: every query adapter
timeoutMs: 30000
maxConcurrentAdapters: 3
synthesis: trueAn instance's id is its identity, separate from its module, so one package
can serve several instances — two Neo4j databases, say. docket itself checks
only this envelope, strictly: an unknown field, a repeated id, a role other than
projection or query, a runtime that is not defined or a default adapter
that is not configured for query is an error naming the instance. Everything
under config belongs to the adapter, which receives it as written and
validates it when the docket opens; the bundled adapters reject a field they do
not know rather than ignore it, and their errors name the instance too.
Secrets never go in the file. An adapter takes the name of the environment
variable holding one — passwordEnv, apiKeyEnv, tokenEnv — and reads the
value itself; docket never writes a value into a manifest, error, log or API
response.
docket ask, docket search and the UI ask the instances
query.defaultAdapters names, or every instance with the query role, unless
a question names its own. timeoutMs is the deadline every adapter answers
within, maxConcurrentAdapters how many answer at once, and synthesis
whether a summary is written by default
(docs/adapter-spec.md §10). docket init writes a
version 2 file.
docket adapter add <provider> replaces the manual steps for the bundled
adapters. It asks what the adapter needs, shows the resolved plan - the
.docket.yaml entry, any runtime group and Compose file, the environment
variables to set, the driver package and the commands to run next - and
applies it only once you confirm:
docket adapter add neo4j # an existing or hosted server, or a local container
docket adapter add mem0 # hosted mem0 (app.mem0.ai) or a self-hosted mem0 server
docket adapter add jsonl # another JSONL index
docket adapter add memvid # a memvid .mv2 file, searched through the memvid CLI
docket adapter add mempalace # drawers in a MemPalace wing, through its MCP server
docket adapters list # what is configured, and whether each module and driver loadsFor Neo4j, choose an existing endpoint (its URI, username and the name of
the variable holding its password) or a local container. A local container
gets a runtime group in .docket.yaml and a Compose template you then own,
./infra/docket-<runtime>.compose.yaml, binding Bolt to 127.0.0.1. Its
image is the reference you give with --image (an internal registry tag or
digest), or, without one, a required ${DOCKET_NEO4J_IMAGE} Compose reads when
the runtime starts: docket never picks an image. For mem0, choose hosted
(an API key variable, MEM0_API_KEY by default) or a self-hosted server URL.
mem0's in-process oss mode needs an embedder, vector store and LLM, so write
that entry by hand.
memvid and MemPalace are embedded: no secrets and no runtime group,
only an optional file or palace directory, namespace or wing, and the command
docket runs. Their adapter packages are not bundled with the CLI and not
published to npm, so the plan reminds you to add the package to the project
(a workspace or file: dependency), along with the engine each drives and
docket never installs: npm install -g memvid-cli@2.0.160, or pip install mempalace==3.10.0 and its embedding model, downloaded beforehand.
What it will and will not do:
- It never overwrites a file. An existing Compose file at the chosen path is kept as it is, and the plan says so.
- It never asks for, shows or writes a secret: only the name of the environment variable holding one. A value given where a name belongs, or credentials in a URL, is refused.
- It writes version 2 entries. In a version 1
.docket.yamlit offers to rundocket config migratefirst (keeping the original as a backup) and otherwise explains how; it never migrates silently. - It installs the driver (
neo4j-driverormem0ai) only with your consent, with the project's package manager (frompackageManageror its lockfile): asked at a terminal, or--install. - It never pulls an image or starts a container. It prints
docket runtime up <id>for you to run, and can rundocket runtime plan <id>(which starts nothing) when asked (--plan-runtime). For an endpoint it can check the connection (--check), opening the adapter asdocket syncwould.
Scripts and CI pass the answers as options and --yes to apply without asking;
a required answer that is missing is an error naming its option, and an option
that does not apply to what was chosen is refused. --dry-run prints the plan
and changes nothing.
docket adapter add neo4j --mode local --image registry.internal/neo4j:5.26 --yes
docket adapter add neo4j --url neo4j+s://graph.internal.example --password-env DOCKET_GRAPH_PASSWORD --yes
docket adapter add mem0 --mode platform --api-key-env MEM0_API_KEY --install --yes
docket adapter add mem0 --mode server --url http://localhost:8888 --dry-rundocket adapter add --help lists every option. A project-local adapter module
is added to .docket.yaml by hand (Writing an adapter).
A version 1 .docket.yaml keeps working unchanged. Its projections are read
as adapter instances: each entry's type picks the module and names the
instance (jsonl, mem0, then mem0#2 for a second of a type; type: file
still means jsonl), its runtime
moves to the instance, and every other field becomes the instance's config
— every mem0 mode, Neo4j setting and scope included. Both roles are enabled,
as before.
docket config migrate --dry-run prints the file rewritten as version 2.
docket config migrate shows it and asks before writing; --write writes
without asking, which is also the only way it writes without a terminal. Either
way it first copies the original to .docket.yaml.v1.bak (or .v1.bak.2, never
over an existing backup). Only version and projections change, and comments
stay, though comments inside the old entries keep their version 1 wording. The
rewritten file loads as exactly the same configuration, so the next sync
reprojects nothing.
jsonl (@docket/adapter-jsonl) writes documents.jsonl, nodes.jsonl
and edges.jsonl under output (default .docket/.index) — a readable view
of exactly what projections receive. Output is deterministic, so rebuilds are
byte-identical and diffable. It is the default, and the only adapter docket ask has out of the box: a lexical keyword search over titles, ids, tags and
bodies, answering with each matching entity and the paragraphs of it that
matched, at the revision it was projected.
mem0 (@docket/adapter-mem0) stores each resource as one verbatim memory
(infer: false): the title, type, id, confidence, body, links, evidence and
tags as text; the id, type, paths, hash, tags, confidence and evidence count as
metadata. Rebuilds reproduce it exactly and cost no LLM calls. Documents with
index.vector: false are left out. It needs the optional mem0ai package
(pnpm add mem0ai).
adapters:
# Hosted mem0. The key comes from the environment, never the file.
- id: memories
module: "@docket/adapter-mem0"
config:
mode: platform
apiKeyEnv: MEM0_API_KEY # default
# host: https://api.mem0.ai
# mem0's self-hosted REST server.
- id: team-memories
module: "@docket/adapter-mem0"
config:
mode: server
url: http://localhost:8888
apiKeyEnv: MEM0_API_KEY # optional: the server can run without auth
# Or mem0ai/oss in-process. Its `config` goes to mem0's `Memory` constructor
# untouched, so any embedder, vector store or LLM mem0 supports works.
- id: local-memories
module: "@docket/adapter-mem0"
config:
mode: oss
config:
embedder: { provider: ollama, config: { model: nomic-embed-text } }
vectorStore: { provider: qdrant, config: { host: localhost, port: 6333 } }
llm: { provider: ollama, config: { model: "qwen2.5:7b" } }Every mode also takes minScore (drop hits below that similarity) and scope.
All modes file memories under one scope, by default
agentId: docket-<checkout directory>-<hash>, where the hash is taken over the
checkout's absolute path (symlinks and letter case resolved). Two clones or
worktrees with the same directory name therefore never share a scope. Moving a
checkout to a new path starts a new scope and leaves the old remote one behind.
The path is the only input, so checkouts on different machines at the same path
(devcontainers, Codespaces, CI runners) get the same default scope: if they
point at one shared mem0 or Neo4j server, each must set scope: (userId,
agentId and/or runId) explicitly. Set scope: too to choose your own.
docket rebuild deletes and repopulates the whole scope, so do not share it
with memories written by anything else. The neo4j adapter's
scope defaults the same way.
Scopes created by earlier versions (team-memory-<directory> in mem0, the bare
directory name in Neo4j) are left behind, not migrated or deleted: the first
docket sync projects everything into the new scope. Delete the old scope by
hand if you no longer want it, or set scope: to the old value to keep using
it. Set
MEM0_TELEMETRY=false to turn off the mem0 SDK's telemetry.
neo4j (@docket/adapter-neo4j) writes one (:Memory:<Type>) node per
resource and one relationship per (source, rel, target), with confidence,
evidenceCount, sources and evidence on both, so a query can ask for what
rests on code alone — and a full-text index over the documents. It needs the
optional neo4j-driver package and a running Neo4j server, local or hosted.
A question is answered with a full-text query, or, with cypher set, a local
Ollama model writes a read-only Cypher query against the graph's schema. The
query must bind every pattern it matches to the instance's scope or it is not
run; its rows are kept as the values they are - a single row of numbers as
metrics, anything else as a typed table, relationships as a graph - each row
cited as a derived fact. It counts as exhaustive only when it ran to
completion; full-text, or a cypher query that fails or finds nothing, answers
with the best matches instead.
adapters:
- id: graph
module: "@docket/adapter-neo4j"
config:
uri: bolt://localhost:7687 # default; any driver scheme, e.g. neo4j+s://
username: neo4j # default
passwordEnv: NEO4J_PASSWORD # unset: connect without auth
# database: neo4j
# scope: payments-project
# cypher:
# model: "qwen2.5:7b" # Ollama at http://localhost:11434uri is also accepted as url, its name in version 1 entries.
Each adapter instance enabled for projection has its own manifest,
<state.dir>/manifests/<id>.json (default .docket/.index), recording the
revision of every input it acknowledged and the files behind each entity. Sync
plans each instance against its own manifest, so an input is handed again
whenever its revision changes — one of its files, or a confidence rule in the
ontology — and an instance that is behind catches up on its own.
- Failures stay with their instance. Instances sync concurrently and
independently. A manifest is written only after its instance flushed, and
only with what the instance's
ApplyReceiptacknowledged: a change it failed, or left out of the receipt, keeps its previous entry and is retried (once more in the same pass when retryable, then on the next sync). An instance that cannot be reached, or throws, is reported and its manifest left as it was; every other instance still syncs.docket syncthen exits non-zero, naming what failed. - Batches are not transactions. Changes are handed over in batches of up to 100, each carrying the checkpoint the instance is at once every change in it is acknowledged — a hash identifying exactly the records it then holds. Replaying a change an instance already applied must not duplicate it, which is how a pass that failed before its manifest was written recovers.
- Configuration is fingerprinted. A manifest is tied to its instance's module, configuration (as the adapter read it) and scope. Changing an instance's endpoint, scope or any setting makes its next sync reset that instance's own namespace — never a shared store globally — and project everything into it; other instances are untouched. Adding an instance projects everything into it alone. Renaming an instance of a bundled adapter (jsonl, mem0, neo4j) keeps its manifest; renaming an instance of any other module starts it afresh, because its state directory moves with its id.
- Upgrading reprojects nothing. Earlier versions kept one manifest,
<state.dir>/manifest.json, for every projection together. When an instance has no manifest of its own and that file was written for exactly the projections configured now, the instance takes its entity records over and is not reset; input kinds it never received before are projected as new. The shared file is deleted once every configured instance has its own. If the projections changed in the same upgrade, the shared file vouches for none of them, and each instance is reset and reprojected as on a first sync.
New engines plug in as memory adapters, through the contracts in
packages/contracts (@docket/contracts, not published
on its own; its code ships bundled inside @chrisjowen/docket, which also
exports its types). The three adapters above are packages too, each with its
own configuration and driver dependency, sharing
@docket/adapter-kit. docket's core imports none of
them: an instance's module is loaded only when it is configured.
The standard @chrisjowen/docket package ships all three, bundled in
dist/bundled. The drivers stay optional: install neo4j-driver or mem0ai
next to docket only for the adapter you configure. A jsonl-only project needs
neither, nor Docker, Python or network access. A project's own install of an
adapter package (from a private registry or a workspace) is used in preference
to the bundled copy. For a minimal core, build docket with only the adapters
you want bundled — DOCKET_BUNDLED_ADAPTERS=jsonl pnpm build, or empty for
none. The @docket/adapter-* packages are not published to npm, so a project
using a minimal core cannot npm install the adapters it left out: a
configured adapter that is neither installed nor bundled fails with an error
saying it is not included in this build, and that a build with
DOCKET_BUNDLED_ADAPTERS unset or listing it includes it.
A project-local adapter is a compiled .js or .mjs module whose default
export is an AdapterDefinition
(docs/adapter-spec.md §7, §12), named by its path in
module. A program can also open a docket with more adapters than the file
lists:
import { createDocket } from '@chrisjowen/docket'
const docket = await createDocket({
projectRoot,
// A compiled module relative to .docket.yaml, or a package installed in the project.
adapters: [{ id: 'company-memory', module: './tools/docket/company-memory.mjs', config }],
// Or a definition passed in directly.
registrations: [{ id: 'other-memory', definition, config }]
})docket never installs an adapter package, and runs a .ts adapter only with a
TypeScript runner passed as typescript.
Two recall engines are adapter packages loaded this way, not bundled with the
CLI: @docket/adapter-memvid (a memvid .mv2
file, searched lexically through the memvid CLI) and
@docket/adapter-mempalace (drawers in a
MemPalace wing, recalled through its MCP server). Each answers with source
passages and the canonical records they came from; their READMEs list the
engine versions they were tested against, what each needs installed, and what
each does not do.
An adapter connects to a service; docket does not start one unless asked.
Hosted and already-running services need nothing more, and no command but
docket runtime ever calls Docker - not sync, watch, search, open or
loading an adapter, even with a runtimes section configured.
To have docket manage local containers explicitly, write your own Compose file
with the images you approve, and name it in a runtimes group - or have
docket adapter add neo4j --mode local write both from the image you give
(Set up a provider). An adapter
instance's runtime: says which group it connects to (in a version 1 file, a
projection's runtime: does):
adapters:
- id: dev-graph
module: "@docket/adapter-neo4j"
runtime: graph-dev
config:
uri: bolt://127.0.0.1:17687
passwordEnv: DOCKET_GRAPH_PASSWORD
runtimes:
graph-dev:
provider: docker-compose
composeFile: ./infra/docket-memory.compose.yaml # relative to .docket.yaml
projectName: docket-payments
pullPolicy: never # never (default) | missing | always
services: [graph] # default: every service in the file# infra/docket-memory.compose.yaml - yours; docket never rewrites it
services:
graph:
image: ${DOCKET_NEO4J_IMAGE:?Set the approved image reference}
ports:
- "127.0.0.1:17687:7687"
environment:
NEO4J_AUTH: ${DOCKET_NEO4J_AUTH:?Set the local authentication value}
volumes:
- graph-data:/data
volumes:
graph-data: {}docket has no images of its own: the image is exactly what the Compose file resolves, an internal registry tag or digest included, and registry logins are Docker's own. Commands, entrypoints, health checks, networks and extra services all go in the Compose file.
docket runtime plan <id>checks the group and the Compose file (throughdocker compose config, which starts nothing) and prints each service's image, ports, volumes and environment variable names - never their values - with the exact commandsup,statusanddownrun.docket runtime up <id>runsdocker compose up --detach --pull <pullPolicy> --no-build: Compose enforces the pull policy, and a Compose too old to support it fails rather than pulling. docket never builds images.docket runtime status <id>lists the project's containers by Compose label (docker ps), so it changes nothing and needs none of the Compose file's variables set.docket runtime down <id>runsdocker compose --project-name <projectName> downwithout the Compose file, so it too needs none of the file's variables set, and keeps named volumes. Only--destroy-volumesadds--volumes;docket rebuildnever touches containers or volumes.
packages/claude-plugin/ contains skills and hooks that let Claude capture
durable project knowledge by editing the same canonical files a human would.
Agents never write to an index. See its
README.
pnpm install
pnpm typecheck # sources and tests
pnpm build # the adapter contracts, kit and adapters and the web UI
# first, then docket, which ships copies of them; the
# end-to-end tests run the built CLI, so build first
pnpm test # the contracts', adapters', CLI's and plugin's suitesTo work on the web UI with hot reload, run docket open --no-open in a
repository with a .docket/, then pnpm -C packages/docket-ui dev; the dev
server proxies /api to it (set DOCKET_API if it is not on port 4380).
Layout, phases and track ownership are in PLAN.md. The full
specification is docs/SPEC.md.
.github/workflows/publish.yml stages
@chrisjowen/docket on npm, with provenance, when a v<version> tag is pushed
(or a GitHub release creates one). It typechecks, builds and tests first, and
refuses a tag that does not match packages/docket/package.json. npm does not
allow publishing from CI without 2FA, so the workflow runs npm stage publish:
the version lands in npm's staging area and goes live only once a maintainer
approves it.
- Bump
versioninpackages/docket/package.json, andCLI_VERSIONinpackages/claude-plugin/scripts/cli.jsto match (the plugin's tests fail until they agree), and merge it. - Tag that commit
v<version>and push the tag, or publish a GitHub release for it. - Approve the staged version with 2FA:
npm stage approve <stage-id>, or the Staged Packages tab on npmjs.com.
The workflow authenticates with the NPM_TOKEN repository secret (an npm token
with publish rights to @chrisjowen/docket, already configured) and signs
provenance through GitHub's OIDC token. The first stage of a new package also
creates a public 0.0.0-stage placeholder version on npm.
Check what a release will contain with npm pack --dry-run in
packages/docket.
The case-file illustration at the top, docs/assets/docket-hero.svg, is
original artwork drawn for this repository; it uses no third-party images. The
docket open screenshot shows the examples/acme-platform
case file.
