Metadata-Version: 2.4
Name: cairn-ai
Version: 0.1.0
Summary: Runtime signal capture and distillation for AI-assisted Python development
Project-URL: Homepage, https://github.com/Cope-Labs/cairn
Project-URL: Repository, https://github.com/Cope-Labs/cairn
Project-URL: Issues, https://github.com/Cope-Labs/cairn/issues
Author-email: Cope Labs LLC <dev@copelabs.dev>
License: AGPL-3.0-or-later
License-File: LICENSE
Keywords: agent,ai,instrumentation,mcp,observability,testing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: click>=8.0
Provides-Extra: compat
Requires-Dist: nox>=2024.3; extra == 'compat'
Provides-Extra: config
Requires-Dist: tomli>=1.2; (python_version < '3.11') and extra == 'config'
Provides-Extra: coverage
Requires-Dist: coverage>=7.0; extra == 'coverage'
Provides-Extra: dev
Requires-Dist: coverage>=7.0; extra == 'dev'
Requires-Dist: margin>=0.9.23; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.3; extra == 'dev'
Requires-Dist: tomli>=1.2; (python_version < '3.11') and extra == 'dev'
Provides-Extra: embed
Requires-Dist: sentence-transformers>=2.0; extra == 'embed'
Provides-Extra: health
Requires-Dist: margin>=0.9.23; extra == 'health'
Provides-Extra: pytest
Requires-Dist: pytest>=7.0; extra == 'pytest'
Description-Content-Type: text/markdown

# Cairn

**Runtime signal capture and distillation for AI-assisted Python development.**

Cairn instruments your running Python code, captures structured evidence from executions, tests, and failures, distills recurring patterns, and feeds those patterns back into your AI agent's context before the next coding session. It closes a loop that currently stays open in every codebase.

---

## The problem

AI coding agents — Cline, Claude Code, Copilot, Cursor — start every session cold. They have no memory of what failed last Tuesday, what pattern keeps recurring in your async code, what the last three refactors broke. The context window is their only memory, and it resets.

The workarounds that exist — `.cursorrules`, `CLAUDE.md`, `AGENTS.md`, codified context documents — are manual. A human or an agent writes something down. That document gets read at the start of the next session. The loop is: write, read, write, read.

Cairn writes its own documents. From evidence.

---

## What Cairn does

Every time your code runs, Cairn captures structured signal:

- **Execution traces** — function calls, return values, exceptions, duration, call depth. Via `sys.monitoring` (Python 3.12+, near-zero overhead) or a decorator-based fallback.
- **Test signal** — not just pass/fail, but assertion values, local variable state at failure, which source lines were implicated. Via a `conftest.py` plugin that requires no changes to existing tests.
- **AST structure** — untested branches, missing type annotations, deep nesting, common anti-patterns. Static, but correlated with runtime failures over time.
- **Git correlation** — commit → test delta → error delta linkage. Which changes broke what, and whether the fix held.

That signal gets normalized into `TruthRecord` objects and written to a local SQLite store with vector embeddings for semantic search. Records are versioned and idempotent — re-running never duplicates.

A distillation pass runs on demand or on a schedule. It clusters failure records by structural similarity, names the patterns, and produces either:

- **Context snippets** — injected into your agent's system prompt before the next session starts
- **Lint rules** — encoded as ruff/flake8 rules so the pattern can't recur without a warning
- **Test templates** — property-based test stubs generated from observed function behavior

The agent that writes the next piece of code knows what this specific codebase has actually failed on. Not general Python anti-patterns. Not what some other team's codebase does. This one.

---

## The flywheel

```
code runs
    ↓
runtime signal captured (sys.monitoring, pytest hooks, AST scanner, git log)
    ↓
normalized → TruthRecord (source, domain, confidence, timestamp, embedding)
    ↓
distillation pass (cluster failures → name patterns → encode as context/rules)
    ↓
agent context injection (pre-prompt snippets, confidence-weighted, decayed by age)
    ↓
agent writes better code
    ↓
better code runs → richer signal
    ↓
(repeat)
```

Each cycle, the clusters tighten. Each cycle, the agent context gets more specific to this codebase's actual failure modes. The compounding rate is proportional to eval quality — which is why Cairn captures assertion values, not just pass/fail booleans.

---

## Why this is different from existing tools

| Tool | What it does | What it doesn't do |
|---|---|---|
| Sentry / Datadog | Captures runtime errors in production | Feeds signal back into dev agent context |
| pytest-cov | Reports untested code paths | Persists or distills that signal over time |
| Cursor / Cline | Generates code from context | Remembers what failed last session |
| CLAUDE.md / AGENTS.md | Manual context for agents | Writes itself from runtime evidence |
| Live-SWE-agent | Creates tools on the fly during a session | Persists learned tools and patterns across sessions |
| ReasoningBank | Distills agent reasoning traces | Instruments the Python runtime directly |

Cairn is not a profiler. Not a logger. Not a linter. Not an eval framework. It is the connection layer between those things and the AI agent that writes your next function.

---

## Architecture

```
cairn/
├── capture/
│   ├── monitor.py        # sys.monitoring hook (3.12+) — call depth + duration
│   ├── decorator.py      # @cairn.watch fallback for older Python
│   ├── pytest_plugin.py  # conftest.py plugin, auto-registered
│   ├── ast_scanner.py    # structural analysis on import or demand
│   ├── git_linker.py     # commit → test delta correlation
│   └── coverage_reader.py # untested branch import from .coverage
│
├── store/
│   ├── schema.py         # TruthRecord dataclass + SQLite schema
│   ├── writer.py         # idempotent upsert engine
│   ├── embedder.py       # sentence-transformers, local by default
│   └── query.py          # semantic + structured search interface
│
├── distill/
│   ├── cluster.py        # failure pattern clustering
│   ├── namer.py          # LLM-assisted pattern naming
│   ├── rules.py          # encode patterns as ruff rules
│   ├── decay.py          # confidence decay model (age-weighted)
│   ├── templates.py      # property-based test stub generator
│   ├── health.py         # margin-powered drift + anomaly + health (optional)
│   ├── causality.py      # cross-cluster causal discovery (optional)
│   └── escalation.py     # policy-based LOG / ALERT / HALT (optional)
│
├── inject/
│   ├── context.py        # build pre-prompt snippet from top-N records
│   ├── cline.py          # Cline / VS Code integration
│   └── mcp_server.py     # MCP server for any MCP-compatible agent
│
└── cli.py                # cairn status / cairn distill / cairn annotate
```

---

## Installation

```bash
pip install cairn
```

For pytest auto-registration (zero config):

```bash
pip install cairn[pytest]
```

For margin-powered health analysis (drift tracking, anomaly detection, causal
discovery, and escalation policy on pattern clusters):

```bash
pip install cairn[health]
```

Cairn registers its pytest plugin automatically. Your first test run starts generating signal immediately.

---

## Quickstart

**Zero config — just install and run your tests:**

```bash
pip install cairn[pytest]
pytest
cairn status
```

**Full pipeline in four commands:**

```bash
pip install cairn[pytest,coverage]
coverage run --branch -m pytest
cairn scan . --with-coverage .coverage --with-branch-coverage .coverage --with-git HEAD~10
cairn distill
cairn context --output .cairn/context.md
```

Then reference `.cairn/context.md` in your `CLAUDE.md` or `AGENTS.md` with `@.cairn/context.md`.

**Watch a specific module:**

```python
import cairn

@cairn.watch
def parse_manifest(path: str) -> dict:
    ...
```

**Run the distillation pass:**

```bash
cairn distill
cairn distill --use-llm        # LLM-assisted naming via OpenRouter
```

**Inject context into your next Cline session:**

```bash
cairn context > .cairn/context.md
```

**Export the store for external tooling:**

```bash
cairn export --format json > records.json
cairn export --format csv  > records.csv
```

**Ingest signal from any language (nASTi-js, nASTi-go, nASTi-rs, …):**

```bash
# Any nASTi variant writes JSONL — only 'content' is required
nasti-js apply src/ | cairn ingest --source nasti-js --domain myapp/src

# Or from a file
cairn ingest records.jsonl --source nasti-go --domain myservice

# Backfill embeddings for records written by non-Python tools directly to SQLite
cairn embed --repair
```

All language variants write into the same `.cairn/store.db`. Vector embeddings are
generated at ingest time — records from nASTi-js and records from pytest are
semantically searchable together via `cairn query` or the MCP server.

**MCP server (for Claude Code, Cline, or any MCP-compatible agent):**

```bash
cairn serve
```

Add to your MCP config and your agent can query the truth store directly.

---

## TruthRecord schema

The unit of storage. Everything Cairn captures gets normalized to this:

```python
@dataclass
class TruthRecord:
    id: str                    # sha256(source:domain:content) — stable across reruns
    source: str                # "pytest", "monitor", "ast", "git"
    domain: str                # project namespace / module path
    content: str               # the fact: pattern description, error, observation
    confidence: float          # 0.0–1.0, decays with age unless re-confirmed
    occurred_at: datetime
    source_file: str | None
    source_line: int | None
    exception_type: str | None
    pattern_cluster: str | None  # assigned by distillation pass
    embedding: list[float]     # for semantic search
```

Records are idempotent on write. Re-running a test suite that produces the same failure updates `confidence` and `occurred_at` rather than creating a duplicate. Confidence decays on a configurable half-life — old truths that aren't re-confirmed by new runs fade out of active context.

---

## CLI reference

| Command | Description |
|---|---|
| `cairn status` | Store summary: counts, active vs stale, recent failures, distilled cluster names |
| `cairn stats` | Detailed breakdown by source, domain, confidence tier, and last-write age |
| `cairn scan [PATH]` | Run the AST scanner on a file or directory and persist findings |
| `cairn distill` | Cluster failures → name patterns → write distilled records |
| `cairn context` | Print the Markdown agent context snippet (or write to `--output` file) |
| `cairn query QUERY` | Search the store by keyword or semantic similarity |
| `cairn watch TARGET` | Instrument and run a script or module with `sys.monitoring` (Python 3.12+) |
| `cairn serve` | Start the MCP server (stdio for Cline/Claude Code, or HTTP+SSE for remote agents) |
| `cairn templates` | Generate property-based test stubs from observed function behaviour |
| `cairn export` | Export the store as JSON or CSV for external tooling |
| `cairn ingest [FILE]` | Ingest TruthRecords from a JSONL file or stdin — the cross-language bridge |
| `cairn embed` | Re-embed all records (after model upgrade); `--repair` backfills only NULL embeddings |
| `cairn sync SOURCE_DB` | Merge records from another store into this one (CI aggregation, multi-machine) |
| `cairn annotate [PATH]` | Insert `# cairn[source]: ...` inline comments into source files |
| `cairn prune` | Delete records older than a duration or below a confidence threshold |
| `cairn install` | Scaffold `.github/copilot-instructions.md` referencing the context file |
| `cairn install-hook` | Install a git hook that auto-runs `cairn distill` after commits |
| `cairn config show` | Print the resolved configuration (defaults merged with `pyproject.toml`) |

Run `cairn <command> --help` for full option details.

---

## Configuration

Cairn reads `[tool.cairn]` from the nearest `pyproject.toml`, merging with built-in defaults:

```toml
[tool.cairn]
db     = ".cairn/store.db"    # SQLite store path
domain = "myproject"          # default domain tag written on new records

[tool.cairn.decay]            # exponential confidence half-lives, in days
pytest  = 21.0                # test results go stale after ~3 weeks
monitor = 21.0
ast     = 14.0                # AST findings decay faster (code changes quickly)
git     = 30.0
distill = 60.0                # distilled patterns are more durable
```

Omitted keys inherit the defaults above. Print the resolved config at any time:

```bash
cairn config show
```

---

## Distillation

The distillation pass is the flywheel mechanism. Without it you have a log. With it, each failure makes the system slightly harder to fail the same way again.

```bash
# Run manually
cairn distill

# Schedule via cron (runs after every test suite)
cairn install-hook
```

The pass:

1. Pulls all `TruthRecord` objects from the last N days
2. Clusters by exception type + call stack shape + AST pattern similarity
3. Names each cluster (optionally via LLM, or deterministic heuristics)
4. Encodes high-confidence clusters as context snippets
5. Optionally generates ruff rules for structural patterns
6. Writes distilled output back to the store with `source="distill"`

---

## Health analysis (margin integration)

When [margin](https://github.com/copelabsllc/margin) is installed (`pip install cairn[health]`), the distillation pass gains three capabilities that replace the default exponential confidence decay with trajectory-aware analysis:

**Drift tracking** — each pattern cluster is fed through margin's `DriftTracker` as a weekly time series of failure counts. Instead of "confidence 0.72," you get: "this pattern is ACCELERATING WORSENING — failure rate is increasing and the rate of increase is itself increasing." Or STABLE, DRIFTING, DECELERATING, REVERTING, OSCILLATING — each with a polarity-normalized direction.

**Anomaly detection** — margin's `AnomalyTracker` classifies the latest week's failure count against the historical reference window. A new exception type in a module that's never failed before is NOVEL. The same ValueError in config.py for the 12th week is EXPECTED. The agent should respond very differently to those two.

**Cross-cluster causal discovery** — margin's correlation engine discovers temporal relationships between clusters. When config parsing failures consistently precede API validation failures by 2 weeks, a CORRELATES link is created automatically. The context output tells the agent: "ValueError::parse_config --CORRELATES--> KeyError::validate_request (strength 1.00, lag 2 weeks). Root cause: ValueError::parse_config."

**Escalation policy** — each cluster gets an escalation level:

| Level | Condition | Agent behavior |
| ----- | --------- | -------------- |
| **HALT** | ABLATED + WORSENING, or NOVEL anomaly with 3+ records | Flag as critical, address before other work |
| **ALERT** | DEGRADED, ACCELERATING worsening, or ANOMALOUS | Highlight prominently in context |
| **LOG** | Everything else | Mention in context (default) |

The context output changes from:

```markdown
## ValueError in config.py (12 occurrences) [pytest]
- FAIL test_config_0: ValueError in parse_config  _(conf: 0.87)_
```

To:

```markdown
## HALT — ValueError in config.py (12 occurrences) [pytest]
_Health: **ABLATED** | Drift: ACCELERATING WORSENING | Anomaly: NOVEL_
- FAIL test_config_0: ValueError in parse_config  _(conf: 0.87)_
- _Causality: Affects: ValueError::parse_config --CORRELATES--> KeyError::validate_request (strength 1.00)_
```

Without margin installed, all health fields are `None` and the existing exponential decay path runs unchanged. No breakage, no new dependencies unless you opt in.

**MCP tool**: the `cairn_health` tool exposes the full health analysis to any MCP-compatible agent — per-cluster health state, drift trajectory, anomaly classification, escalation level, and causal links.

---

## Multi-project and federated modes

**Local** (default): store lives in `.cairn/` inside the repo. Travels with the code. Private.

**Org-shared**: point multiple projects at a shared store. One project's failure pattern becomes a lint rule for every project in the org. Requires a sync layer — Redis or a shared SQLite on a mounted volume.

**Federated** (roadmap): anonymized cross-org pattern aggregation. The structural shape of a failure — not the code, not the project — gets contributed to a shared pattern index. "This class of async error appears in 40% of projects using this library version" becomes a default warning. The store is public infrastructure, not a vendor moat.

---

## Python version support

| Feature | Python 3.12+ | Python 3.10–3.11 |
|---|---|---|
| `sys.monitoring` capture | ✓ native, near-zero overhead | ✗ |
| `@cairn.watch` decorator | ✓ | ✓ fallback |
| pytest plugin | ✓ | ✓ |
| AST scanner | ✓ | ✓ |
| Git linker | ✓ | ✓ |

Python 3.12+ is recommended. The `sys.monitoring` API — introduced in PEP 669 — provides selective bytecode instrumentation that makes capture genuinely low-overhead. On earlier versions, Cairn falls back to decorator-based capture with no `sys.monitoring` dependency.

---

## Relationship to existing projects

Cairn is not a replacement for Sentry, pytest-cov, or your observability stack. It is a dev-time layer that sits between your runtime and your AI agent. In production, keep using whatever you're using. In development, Cairn captures the signal that production tools weren't designed to expose to an agent context injector.

Cairn is also not a memory system for agents in the CodeFire / fcontext sense. Those tools help you persist notes across sessions. Cairn generates the notes from evidence so you don't have to write them.

---

## Runbook

Common operations from first install through CI and agent integration.

### Zero-config first run

```bash
pip install cairn[pytest]
pytest
cairn status
```

Cairn registers its pytest plugin automatically via the `pytest11` entry point. No `conftest.py` changes needed. Your first test run generates signal.

---

### Bootstrap an existing codebase

```bash
pip install cairn[pytest,coverage]
coverage run --branch -m pytest
cairn scan . --with-coverage .coverage --with-git HEAD~20
cairn distill
cairn context --output .cairn/context.md
```

Reference the output in your agent instructions file:

```markdown
<!-- CLAUDE.md or AGENTS.md -->
@.cairn/context.md
```

---

### Auto-distillation via git hook

```bash
cairn install-hook
```

Installs a `post-commit` hook. After every commit, `cairn distill` runs automatically and keeps `.cairn/context.md` current. Your next session inherits the latest patterns with no manual intervention.

---

### CI integration (GitHub Actions)

```yaml
# .github/workflows/cairn.yml
name: cairn signal capture

on: [push, pull_request]

jobs:
  capture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 50          # needed for --with-git

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install
        run: pip install -e ".[dev,pytest,coverage]"

      - name: Capture
        run: |
          coverage run --branch -m pytest
          cairn scan . --with-coverage .coverage --with-git HEAD~20
          cairn distill

      - name: Upload store
        uses: actions/upload-artifact@v4
        with:
          name: cairn-store
          path: .cairn/store.db
```

Merge CI-produced stores into your local store:

```bash
# After downloading the artifact
cairn sync ci-artifacts/store.db
```

---

### Scaffold AI agent integration

```bash
cairn install
```

Writes `.github/copilot-instructions.md` referencing `.cairn/context.md`. For Claude Code or Cline, add the reference manually:

```markdown
<!-- CLAUDE.md -->
Read @.cairn/context.md before writing any code. It contains this codebase's
recent failure patterns, distilled from runtime evidence.
```

---

### MCP server (Claude Code, Cline, Cursor)

```bash
# stdio mode — works with Cline and Claude Code
cairn serve

# HTTP+SSE mode — for remote agents or multi-session setups
cairn serve --transport http --host 0.0.0.0 --port 8765

# Org-shared store — protect with a bearer token, bind to a public address
cairn serve --transport http --host 0.0.0.0 --port 8765 --token "$CAIRN_TOKEN"
# or: export CAIRN_TOKEN=mysecret && cairn serve --transport http ...
```

The HTTP server exposes both the MCP SSE/message endpoints **and** REST endpoints
for pushing records from any language or CI system:

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET`  | `/health` | none | Liveness check |
| `POST` | `/ingest` | `Bearer <token>` (if `--token` set) | Push JSONL truth records |
| `POST` | `/query`  | `Bearer <token>` (if `--token` set) | Semantic/keyword search |
| `GET`  | `/sse`    | none | MCP SSE stream |
| `POST` | `/message`| none | MCP JSON-RPC messages |

**Ingest records from any machine:**
```bash
curl -s -X POST http://your-server:8765/ingest \
  -H 'Authorization: Bearer $CAIRN_TOKEN' \
  -H 'Content-Type: application/x-ndjson' \
  --data-raw '{"content":"KeyError in config loader","source":"ci","domain":"myapp"}'
# → {"ingested": 1, "errors": []}
```

**Query from any machine:**
```bash
curl -s -X POST http://your-server:8765/query \
  -H 'Authorization: Bearer $CAIRN_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"q": "config error", "top_k": 5}'
# → {"results": [...]}
```

Add to your MCP client config:

```json
{
  "servers": {
    "cairn": {
      "command": "cairn",
      "args": ["serve"],
      "cwd": "/path/to/your/project"
    }
  }
}
```

The server exposes these tools: `cairn_status`, `cairn_search`, `cairn_context` (Markdown context blob), `cairn_recent_failures` (JSON records, filtered by decayed confidence), `cairn_health` (margin-powered drift/anomaly/causality analysis, requires `cairn[health]`), and `cairn_synthesis` (ranked intervention plan).

---

### Query and inspect the store

```bash
# Semantic / keyword search
cairn query "async timeout"
cairn query "parse config ValueError" --source pytest --top-k 5

# Source and confidence breakdown
cairn stats

# Full status with stale count
cairn status
```

---

### Prune stale records

```bash
# Preview without deleting
cairn prune --older-than 60d --dry-run

# Delete records older than 60 days
cairn prune --older-than 60d

# Delete below a confidence threshold
cairn prune --min-confidence 0.1
```

---

### Export for external tooling

```bash
cairn export --format json > records.json
cairn export --format csv  > records.csv

# Filter by source before exporting
cairn export --source ast --format json > ast-findings.json
```

---

### Multi-machine and team setup

Sync stores from CI, team members, or microservices into a single authoritative store:

```bash
cairn sync /mnt/shared/.cairn/store.db
cairn sync ./teammate-store.db
cairn sync ./ci-store.db
```

Records are upserted — repeated syncs from the same source never create duplicates. `observation_count` accumulates across syncs for shared records.

---

## Development

```bash
git clone https://github.com/yourusername/cairn
cd cairn
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
```

The backtest suite (`tests/test_backtest.py`) is a machine-readable specification of Cairn's core contracts: AST detection precision and recall, exact decay formula, writer idempotency, clustering accuracy, context quality, and pipeline end-to-end. If you change an algorithm, the backtest immediately tells you what changed. Run it in isolation:

```bash
pytest tests/test_backtest.py -v
```

---

## Status

Early development. Phase 1 (signal capture) is the current focus:

- [x] `TruthRecord` schema and SQLite writer (with observation counting)
- [x] pytest plugin (`cairn[pytest]`)
- [x] `sys.monitoring` capture module — with call depth + duration tracking + periodic flush
- [x] `@cairn.watch` decorator
- [x] AST scanner (9 checks: broad_except, mutable_default, deep_nesting, missing_annotation, unreachable_code, magic_number, print_statement, assert_in_library, fstring_in_logging)
- [x] Git linker (`cairn scan --with-git`)
- [x] Distillation pass (`cairn distill --use-llm`)
- [x] Context injection CLI (multi-domain aware, cross-domain summary)
- [x] MCP server
- [x] `cairn annotate` — inline source annotations (`--clear` to remove)
- [x] `cairn install-hook` — git hook installer for auto-distillation
- [x] Untested branch capture (`cairn scan --with-coverage`, `cairn[coverage]`)
- [x] Test template generator (`cairn templates`)
- [x] Auto-embedding on write (when sentence-transformers installed)
- [x] Confidence bumping on re-observation
- [x] `cairn export` — JSON/CSV export for external tooling
- [x] `cairn scan --exclude` — configurable exclusion patterns
- [x] Git blame integration (`cairn scan --with-git --blame`)
- [x] `cairn prune` — remove stale/decayed records from the store
- [x] Decay-adjusted `cairn status` (active vs stale breakdowns)
- [x] MCP `cairn_recent_failures` filters by decayed confidence
- [x] MCP `cairn_health` — margin-powered drift/anomaly/causality analysis (`cairn[health]`)
- [x] `cairn query` — keyword/semantic search CLI with source + age filters
- [x] `cairn config show` — print resolved config (defaults + pyproject.toml)
- [x] `cairn distill` tags source records with `pattern_cluster` after each run
- [x] `cairn status` cluster breakdown (distilled pattern names + confidence)
- [x] Org-shared store — `POST /ingest` + `POST /query` REST endpoints with optional bearer auth
- [x] Margin-powered health analysis — drift tracking, anomaly detection, causal discovery, escalation policy (`cairn[health]`)
- [ ] Federated mode

---

## Name

A cairn is a stack of stones left on a trail by previous travelers. Found by the next one. That's the mechanic.

---

## License

GNU Affero General Public License v3.0 or later (`AGPL-3.0-or-later`).

Commercial licensing is available for organizations that need to use Cairn in
closed-source or proprietary products and cannot comply with AGPL terms.
Open an issue in this repository to discuss a commercial license.
