witan
Pronounced
WIT-ən(/ˈwɪtən/) — rhymes with "written" minus the r. From Old English witan, the king's council of advisors.
A team-wide shared knowledge graph for coding agents, backed by Omnigraph. Stores coding patterns, project/repo facts, lessons, and agent context. Exposed over MCP so every agent platform (pi, Claude Desktop, GitHub Copilot) can read and write without platform-specific code.
Documentation
See the User Guide
for a task-oriented walkthrough (install, setup, day-to-day loop, operating
modes, troubleshooting) and the CLI Reference
for the full witan command surface.
Quick Start
witan setup installs the omnigraph binary, copies hooks/skills, and wires the
MCP server entry into your agent config in one step. Run it once; re-run after
upgrades to refresh installed files.
The omnigraph binary step always downloads the release pinned by
_OMNIGRAPH_VERSION in the shared installer
witan_core.omnigraph_install
straight from GitHub releases into ~/.local/bin/omnigraph — there is no
build-time bundling, so re-running witan setup is also how you pick up an
omnigraph version bump (a Renovate PR bumps the pin; see renovate.json's
omnigraph-version customManager). When witan-code is also installed
(importable in this environment — e.g. via the --with in the uv tool install/MCP server's uvx invocation below), witan setup folds its skill
and hooks into the same install pass — registered as witan code …, so the
hooks only need witan on PATH, not a separate witan-code binary — and
skips a separate MCP entry, since witan serve already mounts witan-code's
tools in-process. One witan setup then covers both packages. Installed
standalone (without witan), witan-code has its own equivalent witan-code setup (or the mounted witan code setup) — see its
README.
With persistent CLI — required for Claude Code and Pi (and --agent all),
whose hooks/extensions call the witan command directly, so it must stay on your PATH.
Install witan-code alongside it with --with so its witan code … hooks
work too (omit --with for a witan-only install):
# Install the latest release from PyPI …
uv tool install --with witan-code witan-council
# … or, to track pre-release/unreleased code, from the git repo directly:
uv tool install --with git+https://github.com/mitodl/agent-kit#subdirectory=mcp/servers/witan-code \
git+https://github.com/mitodl/agent-kit#subdirectory=mcp/servers/witan
# … or editable from a local checkout, at the repo root:
uv tool install --editable --with ./mcp/servers/witan-code mcp/servers/witan
# then run setup:
witan setup --agent claude # or: pi | all
On these two platforms the MCP server entry witan setup writes is witan serve — the same install the hooks use. The MCP-only agents below get a
self-contained uvx --from … entry instead, since they have no CLI to point
at. That difference is deliberate: registering the uvx form here would leave
Claude Code and Pi running two witan installs from two different sources (the
hooks on your PATH install, the MCP server on git main), which skew silently
as those lineages diverge.
Without persistent CLI — enough for the MCP-only agents (Copilot, OpenCode,
Kilo), whose server runs via uvx, so no install is needed:
# From PyPI …
uvx --from witan-council witan setup --agent copilot # or: opencode | kilo
# … or, to track pre-release/unreleased code, from the git repo directly:
uvx --from git+https://github.com/mitodl/agent-kit#subdirectory=mcp/servers/witan \
witan setup --agent copilot # or: opencode | kilo
# … or from a local checkout, at the repo root:
uvx --from mcp/servers/witan witan setup --agent copilot
# Optional — set your author name (defaults to git config user.name or $USER):
export WITAN_AUTHOR="Your Name"
Manual wiring (if you prefer to configure agents by hand): run
./install.shto initialise the local graph, then copy the appropriate config snippet fromconfig/into your agent's MCP config file.
Local development setup (both servers, hooks, and skills wired from your checkout): see Local Development Setup.
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
WITAN_MEMORY_URI |
No | ~/.local/share/witan/graph.omni |
Graph URI — local path, s3://, or http:// |
WITAN_MEMORY_TOKEN |
Only for http:// |
— | Bearer token for remote server auth |
WITAN_S3_PROFILE |
No | — | AWS CLI profile exported for credentials when the graph URI is s3:// |
WITAN_S3_REGION |
No | ambient AWS config | Region passed to omnigraph with a configured S3 profile |
WITAN_AUTHOR |
No | $USER |
Attribution on every insert. Local stdio only — a deployed server attributes each write to the calling user's JWT instead (see ADR-0004) |
WITAN_REPO |
No | — | Repo slug override (bypasses git detection) |
WITAN_SCAN_ENABLED |
No | true |
Write-path secret/PII scanning; set to false to opt out — see Write-path content scanning |
WITAN_OPTIMIZE_INTERVAL |
No | 86400 |
Throttle window (seconds) for the Stop hook's opportunistic background store compaction; 0 disables it |
WITAN_REMOTE_WRITE_MAX_INFLIGHT |
No | 4 |
Concurrent writes this process will send to one graph on a remote server before refusing. Sized from the measured write rate against the deployment's 30s deadline; 0 refuses every remote write |
WITAN_REMOTE_WRITE_QUEUE_SECONDS |
No | 10 |
How long a write waits for one of those slots before it is refused outright rather than admitted into a queue it cannot clear in time |
For an S3-backed personal target, put the profile selection next to the store
instead of wrapping witan in a credential-export shell script:
[targets.personal]
server = "s3://personal-witan/graph.omni"
s3_profile = "personal"
s3_region = "us-east-1"
match_paths = ["~/src/personal"]
Before each omnigraph subprocess, witan runs aws configure export-credentials --profile personal --format process and supplies the
resulting access key, secret, and optional session token only in that child
process's environment. The profile can therefore use static keys, SSO, role
assumption, or credential_process; credentials are never written into
config.toml. The AWS CLI must be installed when s3_profile is configured.
Local and HTTP graph stores ignore these settings.
witan migrate merge --from <name> --to <name> carries each named target's own
profile for its end of the merge, so two buckets under two profiles work in one
command. A bare store URI on either end gets the ambient profile only when it
is the configured store; otherwise it inherits whatever AWS credentials the
environment already has, so name a target when the bucket needs its own.
MCP Tools
witan memory vs. your agent's built-in/session memory. witan is the shared, synced, team-wide store; a coding agent's built-in memory (e.g. Claude Code's
memory/files) is private to one machine and user. Durable, shareable engineering knowledge — project facts, patterns, lessons, decisions — belongs in witan so other agents and future sessions can find it; keep only private/ephemeral notes in built-in memory. Thewitan-memoryskill and the MCP server instructions steer the agent accordingly.
Memory Tools
| Tool | Description |
|---|---|
memory_search |
Full-text BM25 search across all memories, with optional repo/kind filters |
memory_list |
List memories (no search), optionally filtered by kind and/or repo; ordered most-recent first |
memory_store |
Insert a new memory (pattern, project_fact, lesson, or agent_context) |
memory_get |
Fetch a single memory by slug |
memory_get_project_facts |
Return all project facts for the current (or specified) repo |
memory_list_patterns |
List coding patterns, optionally scoped to a repo and/or language |
Workflow Tracking Tools
Track engineering projects end-to-end across multiple Claude Code sessions.
See mcp/servers/witan/witan/skills/witan-project-tracker/SKILL.md for usage.
| Tool | Description |
|---|---|
workflow_project_create |
Create a new project (wp- slug) to track an engineering objective |
workflow_project_get |
Fetch a single project by slug |
workflow_project_list |
List projects filtered by repo, status, or phase; defaults to active |
workflow_project_update |
Correct title/description/repos/tags/issue post-creation, or mark a project abandoned |
workflow_project_advance |
Advance a project to the next phase (discovery → spec → implementation → delivery) |
workflow_project_complete |
Mark project done; assembles a WorkflowTrace corpus record from all sessions |
workflow_project_link_memory |
Attach a memory (pattern/lesson/project_fact) to a project via an Informed edge |
workflow_project_block |
Declare that one project must complete before another can begin |
workflow_project_unblock |
Remove a project dependency declared with workflow_project_block |
workflow_project_get_blockers |
Return all projects currently blocking the given project |
workflow_session_start |
Link the current agent session to a project; writes a state file for the Stop hook. Re-entrant — a retry returns the open session rather than minting a second one |
workflow_session_end |
Close the session with a summary, tools used, and files changed |
workflow_trace_get |
Fetch a completed project's corpus WorkflowTrace by its wt-/wp- slug |
Task Tracking Tools
A dependency-aware, hierarchical task tracker (beads-like) in the same graph, so
tasks hard-link to projects, sessions, and memories. See
mcp/servers/witan/witan/skills/witan-task/SKILL.md
(the /witan-task skill) for usage.
| Tool | Description |
|---|---|
task_create |
Create a task (tk- slug). Supports parent (epic → sub-issue), blocked_by, project_slug, external_uri (e.g. a GitHub issue), and symbol_refs |
task_get |
Fetch a single task by slug, including its comments |
task_list |
List tasks filtered by repo, status, project, parent (epic children), or assignee ("@me" resolves to the caller) |
task_comment |
Leave an attributed, append-only note on a task without changing it — for telling the holder their premise is wrong instead of overwriting their description or filing a task that is not work |
task_update |
Update mutable fields — reassign, re-prioritise, re-parent, attach a URI (to claim for work, prefer task_claim) |
task_claim |
Advisory claim for parallel/multi-user agents: set in_progress under an assignee with a lease; refuses if actively held (use force to steal). Read-check-write, not an atomic lock — see claimed_at lease + the CAS follow-up |
task_release |
Drop a claim: clear assignee/lease, return the task to open |
task_close |
Close a task with an optional resolution; unblocks its dependents |
task_ready |
Ready work: open/unblocked tasks (plus in_progress tasks whose lease has lapsed) ordered by priority — the core coordination primitive |
task_link |
Link tasks: blocks / parent / discovered_from, or addresses a Memory node |
task_unlink |
Remove any of those links — for one recorded backwards or against the wrong slug. A no-op when the link isn't there |
context_for_symbol |
Reverse lookup: given a code-graph symbol id (repo#path::Name), return the memories and tasks whose symbolRefs include it |
Tasks are hierarchical (an epic decomposes into sub-issues via parent, with
parentSlug denormalized for fast child lookup) and dependency-aware (blocked_by
maintains a denormalized blockedBy list that drives the task_ready query without
graph traversal). external_uri links a task to a GitHub issue/PR or any reference.
Comments (TaskComment) are the way to say something about a task rather
than to it. Every other write primitive either mutates the task (task_update,
which overwrites another author's text) or creates a node (task_create, which
puts a non-work item into the ready-work list) — so a one-paragraph correction
used to cost a whole p0 task plus a parent edge written onto someone else's
claimed row. task_comment is flat and append-only: no threading, no editing,
no deleting, no resolution. It deliberately leaves the task's own row untouched,
updated_at included, since that field doubles as the advisory-lease start for
an in_progress task with no claimed_at — bumping it would silently renew a
stranger's claim. Comments come back oldest-first in task_get, and the
UserPromptSubmit hook surfaces unread ones on a task you hold (see below).
Code Branch Tracking
Links a git branch (repo + raw branch name, e.g. feature/new-api — never
witan-code's sanitized omnigraph branch name) to the task/project it's
carrying, so "which branch carries task X" and "which tasks are in flight on
branch B" are one-hop graph queries. Coordination state that lives in witan
(shared, durable), not witan-code's per-repo/bridge omnigraph stores — those
are shareable now, but they stay re-derivable caches, rebuildable from the
source tree by reindexing, and that is the distinction that matters here.
(branches --prune is the narrow case: it deletes this checkout's own local
branch views, refuses a shared graph outright, and never touches the bridge
store.)
Wired in automatically, best-effort — no dedicated tool call needed:
workflow_session_startupserts aCodeBranchfor the current checkout's repo+branch (when detected) and links itForProjectto the session's project.task_claimupserts aCodeBranchon success and links itWorksOnthe claimed task; re-callingtask_claim(lease renewal) does not duplicate the edge.- The
UserPromptSubmitcontext-injection hook surfaces an In-Flight Branch section when the current checkout's branch already carries an open task — a signal that this session should likely continue that work rather than pick up something new. - The same hook surfaces a New Comments on Work You Hold section, ahead of
everything else, when an
in_progresstask held by this identity has a comment it has not shown yet. "Unread" is a local watermark in the session state dir, not a read receipt in the graph: it records what was rendered into this machine's prompt, which is a fact about a client rather than about shared work. Losing the file re-shows a comment, which is the harmless direction to fail in. Held tasks are found viacfg.authorlocally andtask_list(assignee="@me")against a deployment, where the identity comes from the caller's token and the client cannot know it.
Both call sites no-op silently with no repo/branch context (detached HEAD,
outside a git repo, or a store that predates this feature and hasn't run
witan migrate schema yet) — this is metadata riding alongside a task/
workflow tool call, never a hard requirement for the tool it's attached to.
Write-path content scanning
Pluggable secret/PII scanning on every write (memories, tasks, projects,
sessions, traces), with block/redact/warn enforcement and a witan.scanners
entry-point mechanism so other organizations can add their own detection
rules without forking. Enabled by default (opt-out) — see
docs/write-path-scanning.md for the full
config surface, the witan scan test/witan scan rules CLI, the audit
trail, and how to write a plugin, and
ADR 0001 for the design
rationale.
Migrating or merging stores
Moving a store to a new machine, merging two machines' local stores, or moving a local store onto the shared multi-tenant server — same command in all three cases:
witan migrate merge [source] [--from <name>] [--to <name>] [--target-uri <uri>] [--dry-run]
Reconciles slug collisions newest-record-wins (by timestamp) rather than
omnigraph load --mode merge's raw last-loaded-wins overwrite, and is
repeatable — re-running against an already-merged target loads nothing new.
--from/--to name [targets.<name>] config blocks (--to ol is how a local
store moves onto the deployment); --target-uri names a store URI. Never
mv/copy a store directly (Lance embeds absolute paths). See
docs/migration-runbook.md for the procedure and
docs/store-merge-findings.md for what was
verified about --mode merge's actual collision behavior.
Tests
Integration tests spin up throwaway omnigraph graphs and exercise the real query
files end-to-end (they skip automatically if the omnigraph binary is absent):
uv run --group test pytest
Exploring the graph (witan)
A cyclopts CLI for manual inspection of the work-coordination graph and the
indexed code graphs. Installed alongside the server (uv run witan …,
or uv tool install the package to get it on PATH):
Pass --output-format json|toml|yaml (default txt; env WITAN_OUTPUT_FORMAT)
anywhere on the command line to get a machine-readable dump instead of the
rich view, e.g. witan tasks --all-repos --output-format json. The table
commands (tasks, projects, memory, traces, scan test, scan rules,
target list, and with witan-code installed code repos, code symbols
and code stitch) print {title, rows}. For witan's own commands an empty
result is rows: [], never a sentence; the code commands still print prose
when empty. task <slug> (also task show <slug>) and project status
print the tool's record whole, and project tasks and session list print
the tool's rows. Those four keep nulls in JSON and YAML and omit them in TOML,
which has no null; the table commands still print a null as "" in every
format. project <slug>, trace <slug> and graph are unaffected.
A structured run writes one document to stdout and nothing else; notices and
log lines go to stderr, including on success. Branch on the exit code, not
on whether stderr is empty: task <slug>, project <slug>, project status
and project tasks exit 1 on a missing slug.
| Command | Description |
|---|---|
setup [--agent claude|pi|…|all] |
Install witan for one or all supported coding agents; also writes a starter ~/.config/witan/config.toml if one doesn't exist yet |
tasks [--ready] [--status …] [--project wp-…] [--all-repos] |
Tasks for the current repo (closed elided by default — pass --status closed to see them); --ready = open with no open blockers |
task [show] <tk-slug> |
One task's details, blockers, and sub-tasks |
task create <title> |
Create a task from the CLI |
task close|claim|release|update|link <tk-slug> … |
Drive task state transitions from the CLI |
run <tk-slug> [--agent claude|pi] [--dry-run] |
Claim a task and launch an agent to execute it |
projects [--status …] [--all-repos] |
Workflow projects (default: active in this repo) |
project <wp-slug> |
A project with its sessions, tasks, and corpus trace |
project status <wp-slug> [--json] |
"What next" resume view — phase, ready tasks, last session, blockers |
project tasks <wp-slug> [--status …] [--detail] |
A project's tasks; --detail expands each task's blockers and dependents |
project create <title> |
Create a workflow project from the CLI |
project advance|complete|block|unblock <wp-slug> … |
Drive project phase/dependency transitions from the CLI |
session start|end|list … |
Start/close a workflow session, or list a project's sessions |
memory [QUERY] [--kind …] |
BM25 memory search, or (with no query) list memories |
code repos |
Repositories with a code graph indexed (requires witan-code) |
scan test <text> |
Dry-run active detectors against an ad-hoc string; prints findings (never the matched text) |
scan rules |
List active write-path scan detectors, their category, source, and enforcement mode |
inject-context [--debug] |
The UserPromptSubmit hook body; --debug prints detection/read diagnostics to stderr (repo, branch, graph reads, counts, swallowed-failure reasons) to explain a blank block |
optimize [--store URI] |
Compact the store's Lance fragments (non-destructive) so query latency doesn't bloat; safe to run on a cron/systemd-timer |
cleanup [--keep N] [--older-than 7d] --yes |
Reclaim disk by GC-ing old Lance versions (destructive; requires --yes) |
serve |
Start the MCP server (memory + code tools when witan-code is installed) |
run claims the task (in_progress + your author), then hands the terminal to
the agent seeded with the task description and a reminder to task_close when
done — invoke it from the task's repo checkout. Use --dry-run to print the
prompt without launching.
Operating Modes
Local Disk (default)
No extra infrastructure. Memories persist at
~/.local/share/witan/graph.omni.
# No env vars required.
export WITAN_AUTHOR="Alice Smith"
Local RustFS (S3-compatible, for testing team mode)
RUSTFS=1 ./install.sh
# Follow the printed export instructions.
Remote Team Server
export WITAN_MEMORY_URI=http://witan.internal:8080
export WITAN_MEMORY_TOKEN=<bearer-token>
export WITAN_AUTHOR="Alice Smith"
See docs/internals/agent-memory.md for full
deployment instructions, the graph schema, and the v2 roadmap.
Project Structure
witan/
├── README.md # This file
├── install.sh # Initialise local graph (manual alternative to `witan setup`)
├── pyproject.toml # Python package metadata
├── schema/
│ └── schema.pg # Omnigraph graph schema
├── queries/
│ ├── read.gq # Read queries
│ └── mutations.gq # Insert/update queries
├── tests/ # Integration tests
├── witan/ # Python package
│ ├── __init__.py
│ ├── __main__.py # Entry point
│ ├── server.py # FastMCP app + tool definitions
│ ├── cli.py # `witan` umbrella CLI (cyclopts)
│ ├── config.py # Config loaded from env vars
│ ├── context.py # inject-context / session-checkpoint helpers
│ ├── repo.py # Git remote → canonical repo slug
│ ├── graph.py # OmnigraphClient (CLI subprocess wrapper)
│ ├── setup.py # `witan setup` agent-installer + omnigraph fetch logic
│ ├── extensions/ # Agent extension configs (bundled by setup)
│ ├── hooks/ # Shell hooks (bundled by setup)
│ └── skills/ # Bundled agent skills
└── config/
├── pi.json # Snippet for ~/.pi/agent/mcp.json
├── claude.json # Snippet for claude_desktop_config.json
└── copilot.json # Snippet for .vscode/mcp.json
Release files for witan-council 0.37.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| witan_council-0.37.0.tar.gz | 1.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| witan_council-0.37.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.7 MB
Release files / witan_council-0.37.0.tar.gz
| Download URL | witan_council-0.37.0.tar.gz |
|---|---|
| Size | 1.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
830c4a7f44e1dddf0db375c0d743c31086850a3725c9c3fc3035bc2a13e11c8c
|
|
BLAKE2b-256 checksum How to use checksums |
1ecb88a07f691bea92042d2db6b06e955c812e446b5fd8f646d0ec5343289ad8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency logRelease files / witan_council-0.37.0-py3-none-any.whl
| Download URL | witan_council-0.37.0-py3-none-any.whl |
|---|---|
| Size | 1.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e3a74d9b0550a5f5f000fb12013ed32922bf56700a6257f166e3ca84a15837b9
|
|
BLAKE2b-256 checksum How to use checksums |
de56852182996947a109e60c3f4000f5f58e5d4c8532171bf398b2a8a726c9c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency log