Skip to main content
Glama
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
```