Cairn
README.md
# Cairn
A local MCP server that checkpoints AI coding sessions and — when things go
wrong — recovers them with context. Not just a rollback. An informed restart.
See [DESIGN.md](DESIGN.md) for the full design document.
## Install
```sh
pip install cairn-mcp # heuristic-only
pip install cairn-mcp[ai] # with LiteLLM for auto-labels, handoffs, embeddings
```
## Setup
```sh
cd your-project
cairn init # writes cairn.toml, gitignores .cairn/, installs ~/.cairn
claude mcp add cairn -- cairn start
```
Then add `@~/.cairn/cairn_agent.md` to your tool's always-loaded context file
(`CLAUDE.md`, `.cursorrules`, `.windsurfrules`, or `.roo/rules/cairn.md`).
Full requirements and per-tool setup (Claude Code, Codex CLI, OpenCode,
Roo Code, and others) are in [INSTALL.md](INSTALL.md).
## CLI
| Command | What it does |
|---|---|
| `cairn start` | Run the MCP server (stdio); a session starts on connect |
| `cairn stop` | Mark active sessions for this directory as ended |
| `cairn history [--archived]` | List sessions and checkpoints (or purged sessions) |
| `cairn dismiss` | Dismiss current drift signals (false positive) |
| `cairn init` | Set up `cairn.toml`, `.gitignore`, and `~/.cairn` |
| `cairn eval run [--judge]` | Score drift detection against synthetic scenarios |
| `cairn eval results` | List past eval runs |
## MCP tools
Cairn exposes exactly six tools. Descriptions are terse and responses are
compact JSON — the whole Cairn footprint in the model's context is budgeted
at under 300 tokens.
| Tool | Mode | What it does | Returns |
|---|---|---|---|
| `cairn_checkpoint(label?, trigger?)` | async, fire-and-forget | Snapshot the working tree onto a shadow branch; `trigger` is `"manual"` (user asked) or `"ai"` (model judged, default) | `{"status":"queued","id":"..."}` |
| `cairn_rollback(checkpoint_id, inspect?)` | sync, atomic | Auto-stash, reset to checkpoint, write handoff + recovery gate (`inspect=true` creates a `cairn/recovery/*` branch instead) | `{"status":"restored","checkpoint":"...","stash_ref":"stash@{0}","handoff_written":true}` |
| `cairn_check_drift()` | sync (drains queue first) | Run all drift signals: scope drift, write loops, error recurrence, semantic shift, token pressure | `{"drift":false}` or `{"drift":true,"evidence":[...],"suggested_restore":"...","label":"..."}` |
| `cairn_log_usage(tokens, limit)` | async | Record reported context usage for the token-pressure signal | `{"status":"logged"}` |
| `cairn_log_event(type, data)` | async | Record a write/error/decision event for drift analysis | `{"status":"logged"}` |
| `cairn_acknowledge_handoff()` | sync | Delete the `RECOVERY_ACTIVE` gate and archive `HANDOFF.md` | `{"status":"acknowledged","archived":"..."}` |
## How an MCP call flows
```mermaid
flowchart TD
AI["AI coding tool<br/>(Claude Code / Cursor / Roo / Windsurf)"] -->|MCP protocol| SRV["Cairn MCP server<br/>(session starts on connect)"]
SRV --> CP["cairn_checkpoint(label?)"]
SRV --> LOG["cairn_log_event / cairn_log_usage"]
SRV --> DRIFT["cairn_check_drift()"]
SRV --> RB["cairn_rollback(checkpoint_id)"]
SRV --> ACK["cairn_acknowledge_handoff()"]
%% checkpoint path (async)
CP -->|"ACK immediately: {status: queued, id}"| AI
CP -.enqueue.-> Q["operation queue<br/>(worker thread)"]
Q --> SNAP["git snapshot on shadow branch<br/>cairn/snapshots/{session}/{checkpoint}"]
SNAP --> ROW["insert checkpoint row in SQLite"]
ROW -->|"on failure: revert git ref"| Q
%% logging path
LOG --> DB[("SQLite<br/>.cairn/cairn.db")]
%% drift path
DRIFT --> DET{"drift signals?<br/>scope drift · write loops ·<br/>error recurrence · semantic shift"}
DET -->|no| CLEAN["{drift: false}"] --> AI
DET -->|yes| EV["{drift: true, evidence, suggested_restore}"] --> AI
EV --> USER{"user decides"}
USER -->|dismiss| DISMISS["cairn dismiss<br/>(signals reset)"]
USER -->|inspect| INSPECT["rollback with inspect=true<br/>creates cairn/recovery/* branch"]
USER -->|rollback| RB
%% rollback path (sync)
RB --> STASH["git stash push -u<br/>(uncommitted work preserved)"]
STASH --> RESET["git reset --hard to checkpoint<br/>(user stays on their branch)"]
RESET --> HANDOFF["generate HANDOFF.md<br/>(LiteLLM or heuristic)"]
HANDOFF --> GATE["write .cairn/RECOVERY_ACTIVE gate"]
GATE -->|"{status: restored, stash_ref}"| AI
%% recovery path (next session)
GATE -.next session, branch matches.-> ACK
ACK --> ARCHIVE["delete gate, archive handoff,<br/>AI reads HANDOFF.md and resumes informed"]
```
## Development
```sh
pip install -e .[dev]
pytest
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues