# cairn — Full Documentation ## Learn ### Quickstart #### Install cairn cairn ships as an extension for kli, the runtime it was built for, and kli serves it to any client that speaks the Model Context Protocol. In this tutorial you install cairn one of two ways, point an agent at it, and confirm the tool surface is live. By the end you have a working cairn that an agent can plan in and resume into. You do not have to adopt kli as your main agent to use cairn; any MCP client works. You need kli built and a project directory to work in. The directory you launch from selects the project whose task graph cairn opens, so run these steps inside the repository you want the graph to track. ## Install with kli There are two ways to install. Bake cairn into the kli image declaratively, or install it at runtime from a running session. Pick one. ### Declaratively Take cairn as a flake input and name it in your kli configuration. kli's Home Manager and NixOS modules compile the listed extensions into the image. cairn needs SQLite on its library path, because its observation store is built on SQLite FTS5. ```nix { inputs.cairn.url = "github:kleisli-io/cairn"; } ``` ```nix programs.kli = { enable = true; extensions = [ inputs.cairn.packages.${system}.default ]; blessedNativeLibs = [ pkgs.sqlite ]; }; ``` Rebuild, and cairn is in the image. Anything baked in this way stays put until you edit your configuration and rebuild again. ### At runtime Each cairn [release page](https://github.com/kleisli-io/cairn/releases) carries the exact install command for that version: the url of the published bundle and the git tree hash it is pinned to. For v0.1.1: ```text kli install https://github.com/kleisli-io/cairn/releases/download/v0.1.1/cairn.bundle 1b92b2928de8de87711c299d7353f1e0b602068e ``` kli fetches the bundle, recomputes the tree hash over its contents, and refuses to install on a mismatch, so what loads is exactly the bytes the release names. It shows what it is about to add and asks first; pass `--yes` in scripts and other non-interactive runs. The install is durable: the next `kli` or `kli mcp-serve` launch finds cairn without installing again. The same install runs from inside a kli session as `/install `, loading cairn live with no rebuild. The companion commands manage what you have installed: `/extensions` lists them, `/enable` and `/disable` toggle one, and `/uninstall` removes a runtime-installed extension. cairn releases are also signed. To require the signature rather than the pin alone, add the Kleisli.IO release key to [`trustRoots`](/kli/config/settings#trustroots); the key is published on each release page and committed at [`release/trust/cairn-release.pub`](https://github.com/kleisli-io/cairn/blob/main/release/trust/cairn-release.pub). [Sharing extensions](/kli/extend/sharing-extensions) walks the full trust flow. To load a local build for a single run instead of installing it, start kli with `--extension`: ```text kli --extension ``` That extension lives only for the life of that run. ## Serve to any MCP client The same kli, built with cairn, serves the extension to any other client over stdio. One command does it: ```text kli mcp-serve cairn ``` Wire a client to that command. Claude Code, Claude Desktop, and Cursor all read the same `mcpServers` block, so a single server entry connects any of them: ```json { "mcpServers": { "cairn": { "command": "kli", "args": ["mcp-serve", "cairn"] } } } ``` The client launches kli in your project directory, and that working directory picks the project whose task graph cairn opens. Once connected, the client lists all fourteen of cairn's tools, with the bundled workflow prompts and the `cairn-method` skill exposed as MCP prompts and resources. The slash commands, the per-turn context injection, and the compaction folding stay behind, because kli provides those as the host rather than over the tool protocol. That is why kli is the fuller home for cairn. See [the mcp-serve reference](/cairn/cli/mcp-serve) for the full client matrix, exactly what travels, and the exposed surface in full. ## Confirm it is live Connect your agent and have it call one read-only tool to confirm cairn answers. `task_bootstrap` orients on the current task in a single call; in a fresh project with no current task set, it answers with a clear message instead of crashing: ```text No task to bootstrap; pass task_id or select a task first. ``` That message comes back as a tool result, not a protocol error, and it means the tool surface is wired and the graph is empty, which is exactly the state a first session starts from. From here, run [your first cairn session](/cairn/get-started/your-first-cairn-session) to create a task, record an observation, write a handoff, and bootstrap back into the work. #### Your first cairn session This tutorial walks cairn's continuity loop once so you can see how your agent keeps work alive across a reset. The agent creates a task, records an observation on it, scaffolds a handoff, then resumes that task in a fresh session with one call. Those are the three writes that make up the heartbeat: `task_create`, `observe`, and `handoff`, all read back with `task_bootstrap`. The returned text after each call is explained, so you can tell what the agent reads back. cairn has to be installed and reachable over MCP first; if it is not, see [Install cairn](/cairn/get-started/install-cairn). Each call below is shown the way the agent issues it over MCP, and the text after it is what cairn sends back. ## Create a task A task is a node in the graph: a slug-addressed unit of work that carries a status, a description, edges, metadata, and its observations. The agent creates one by calling `task_create` with a `name`: ```text task_create(name="wire up the export endpoint") ``` cairn mints a slug from the name, stamps today's UTC date as its namespace, and returns: ```text Created 2026-06-22-wire-up-the-export-endpoint. ``` The date prefix is system-owned, so the agent passes a plain name and cairn handles the namespacing. `task_create` also adopts the new task as the [current task](/cairn/concepts/the-current-task-pointer), but only when none is set yet. Nothing was selected before this call, so the agent is now pointed at `2026-06-22-wire-up-the-export-endpoint`, and the writes that follow act on it without naming it again. ## Record an observation An observation is the cheapest write cairn has, and the heartbeat of the loop: a freeform note the agent appends to a task as it works. It records one with `observe`: ```text observe(text="export route returns 500 when the date filter is absent; needs a default range") ``` The note writes against the current task and returns: ```text Observed on 2026-06-22-wire-up-the-export-endpoint. ``` The agent leans on `observe`. It does not move the current pointer and it never blocks, so leaving a trail of what the agent found, decided, and ruled out costs almost nothing, and that trail is what a later session reads back. To write against a different task without switching, the agent passes `task_id`. For the discipline behind the heartbeat, see [The cairn-method](/cairn/concepts/the-cairn-method). ## Write a handoff A handoff is a resumable summary of where a task stands, and the `summary` is its load-bearing field. The agent scaffolds one with `handoff`: ```text handoff(summary="export endpoint half-built; 500 on missing date filter, default range still TODO") ``` cairn writes a skeleton note (frontmatter, a state snapshot, and empty sections to overwrite with the rich body), records the event, and returns the path it wrote to: ```text Handoff scaffolded for 2026-06-22-wire-up-the-export-endpoint at /…/2026-06-22_14-03-09_export-endpoint-half-built.md ``` There is no trailing period after the path. The tool is deterministic: it lays down a skeleton and returns the path, nothing more. It never drives an authoring turn; that is what the `/handoff` slash command does, and a person types that one. See [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) for the tool-versus-command split. ## Resume Close the MCP client and reopen it. The graph survives the reset; the conversation does not. That is the whole point: the plan lives outside the chat, so a fresh session reloads it. In the new session the current pointer is empty, so the agent names the task explicitly and orients on it with `task_bootstrap`: ```text task_bootstrap(task_id="2026-06-22-wire-up-the-export-endpoint") ``` One call returns the task's computed state and its open handoffs: ```text 2026-06-22-wire-up-the-export-endpoint [open] recent: - export route returns 500 when the date filter is absent; needs a default range handoffs: - export endpoint half-built; 500 on missing date filter, default range still TODO (/…/2026-06-22_14-03-09_export-endpoint-half-built.md) ``` The observation and the handoff summary are both back, reloaded into a session that knew nothing a moment ago. `task_bootstrap` records no event; it is a read. Because the agent passed `task_id`, the call switched the current pointer to that task, so the writes it makes next land there. For the no-id and unset-pointer cases, see [The current-task pointer](/cairn/concepts/the-current-task-pointer). That is the full heartbeat: the agent created a task, left an observation, scaffolded a handoff, and resumed the whole thing from a clean session with one call. From here, [Plan and resume](/cairn/get-started/plan-and-resume) builds a small plan as a graph of phases and runs the same loop across it, and [The cairn-method](/cairn/concepts/the-cairn-method) explains the discipline this heartbeat is part of. #### Plan and resume This tutorial turns a single task into a small plan, works one phase, and resumes the plan from cold, so you can see how your agent holds a multi-step plan together across a reset. The agent forks three phases off a root task, orders them with `depends-on` edges, asks cairn which phase is ready, marks a phase `completed`, watches the ready set move, then bootstraps back into the plan as a fresh session would. By the end the plan lives in the graph, not in the conversation, so a context reset reloads it instead of losing it. You need cairn installed and reachable over MCP, and your agent should have run the [bootstrap-observe-handoff loop](/cairn/get-started/your-first-cairn-session) at least once. This tutorial assumes you know what a task, an observation, and a handoff are; if not, read [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) first. Each call below is shown the way the agent issues it over MCP, and the text after it is what cairn returns. ## Create the root task A plan in cairn is a root task with phases hanging off it, not a markdown file. The agent creates the root first: ```text task_create(name="ship the export feature") ``` ```text Created 2026-06-22-ship-the-export-feature. ``` The slug is date-prefixed and minted from the name. `task_create` adopts the new task as the [current task](/cairn/concepts/the-current-task-pointer) because none was set, so every following call that omits `task_id` acts on it. ## Fork the phases A phase is a child task joined to its parent by a `phase-of` edge. `task_fork` mints the child, records both `task.create` and `task.fork`, and, unlike `task_create`, always switches the current pointer to the new child. The parent argument is `from`. When the agent omits it, `task_fork` forks from the current task. Here it forks three phases, passing `from` so each hangs off the root rather than off the previous child: ```text task_fork(name="design the export schema", from="2026-06-22-ship-the-export-feature") ``` ```text Forked 2026-06-22-design-the-export-schema from 2026-06-22-ship-the-export-feature (phase-of). ``` ```text task_fork(name="implement the writer", from="2026-06-22-ship-the-export-feature") ``` ```text Forked 2026-06-22-implement-the-writer from 2026-06-22-ship-the-export-feature (phase-of). ``` ```text task_fork(name="add the download button", from="2026-06-22-ship-the-export-feature") ``` ```text Forked 2026-06-22-add-the-download-button from 2026-06-22-ship-the-export-feature (phase-of). ``` The default `edge_type` is `phase-of`, so it was not passed. **A fork with no `from` and no current task fails** with `No parent task; pass from or select a task first.` The current pointer now sits on the last child forked. That is fine; the next steps address tasks by slug, which never disturbs the pointer. ## Order the phases The phases are siblings under one parent. Nothing yet says the writer waits on the schema, or that the button waits on the writer. A `depends-on` edge says exactly that. `task_link` draws a typed edge from a source task to a target; the agent passes `task_id` to set the source explicitly rather than rely on wherever the pointer happens to be. The writer depends on the schema: ```text task_link(task_id="2026-06-22-implement-the-writer", target_id="2026-06-22-design-the-export-schema", edge_type="depends-on") ``` ```text Linked 2026-06-22-implement-the-writer -> 2026-06-22-design-the-export-schema (depends-on). ``` The download button depends on the writer: ```text task_link(task_id="2026-06-22-add-the-download-button", target_id="2026-06-22-implement-the-writer", edge_type="depends-on") ``` ```text Linked 2026-06-22-add-the-download-button -> 2026-06-22-implement-the-writer (depends-on). ``` `edge_type` is required for `task_link` and must be one of `phase-of`, `depends-on`, or `related`. `phase-of` is the structural backbone the forks already built; `depends-on` is lateral ordering between siblings. The plan is now described: three phases, with a chain of dependencies running schema → writer → button. ## Ask what is ready The frontier is the ready subset of a plan: the phases that are not done and whose dependencies are all settled. cairn computes it; the agent does not work it out by hand. The agent sets the pointer back to the root first, because `plan-frontier` is current-scoped: it reads the phases of the current task and keeps the ones with no unsettled `depends-on` target. ```text task_bootstrap(task_id="2026-06-22-ship-the-export-feature") ``` That orients on the root and makes it current, which is what the `plan` and `plan-frontier` views read from. Now the agent queries the frontier. The query argument is a single TQ form, and a named view is run by wrapping its name as `(query "plan-frontier")`: ```text task_query(query="(query \"plan-frontier\")") ``` ```text 1 task: - 2026-06-22-design-the-export-schema (active) obs=0 edges=2 ``` Only the schema phase comes back. The writer is blocked behind the schema and the button behind the writer, so neither is ready yet. The `obs=0 edges=2` suffix is enrichment the `plan-frontier` view carries: an observation count and an edge count. Here that edge count is the one `phase-of` link up to the root plus the one incoming `depends-on` from the writer. ## Complete a phase and re-query Once the schema work is done, the agent marks the phase complete. `task_update_status` takes a `status` from the closed set `open`, `active`, `completed`, `abandoned`, `blocked`, and is idempotent. ```text task_update_status(task_id="2026-06-22-design-the-export-schema", status="completed") ``` ```text 2026-06-22-design-the-export-schema is now completed. ``` The agent asks the frontier again. The root is still current, so no re-orientation is needed: ```text task_query(query="(query \"plan-frontier\")") ``` ```text 1 task: - 2026-06-22-implement-the-writer (active) obs=0 edges=3 ``` The frontier moved. The schema dropped out because it is settled, and the writer surfaced because its one dependency is now `completed`. The button is still held back behind the writer. The writer's `edges=3` counts its `phase-of` link to the root, its outgoing `depends-on` to the schema, and the incoming `depends-on` from the button. No plan document was edited; one status changed and the ready set recomputed from the graph. This is how the agent finds the next thing to do without re-reading the whole plan: [Views](/cairn/reference/views) documents the other built-in views, `plan`, `leaf-tasks`, and `stale-phases`, that answer related questions. ## Resume the plan in a new session Now simulate what happens after a context reset, a `/clear`, or a colleague's agent picking up the work tomorrow. The plan is in the graph; one call reloads it. `task_bootstrap` returns computed state, neighbors, open handoffs, and recent observations, and records no event, so orienting never mutates the log. ```text task_bootstrap(task_id="2026-06-22-ship-the-export-feature") ``` ```text 2026-06-22-ship-the-export-feature [active] children: 2026-06-22-add-the-download-button, 2026-06-22-design-the-export-schema, 2026-06-22-implement-the-writer ``` The whole plan came back from the slug alone: the root, its status, and the three phases hanging off it as children. The `phase-of` backbone is what the `children:` line reports; the lateral `depends-on` edges live between the phases, so they surface when the agent queries the frontier rather than in the root's neighbor readout. From here the agent re-runs `(query "plan-frontier")` to find the writer waiting, and the work resumes with no memory of the previous session required. That is the cairn method end to end: one status change moved the frontier from the schema to the writer, and a single `task_bootstrap` reloaded the whole plan from the slug. To go deeper, read [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) for the readiness rule, and [The cairn-method](/cairn/concepts/the-cairn-method) for the working discipline a real plan runs inside. ### Concepts #### What cairn is cairn is continuity for agents. It is a durable task graph an agent plans into and resumes from: tasks joined by typed edges, freeform observations attached as the work happens, and handoffs that carry a session forward. An agent is sharp for the length of one session, and then the context resets. The plan, the half-finished step, the reason the last decision went the way it did — all of it lived in the conversation, so when the conversation ends, it is gone. cairn keeps that plan outside the conversation. After a reset, the agent reloads it from the graph instead of reconstructing it from a transcript. ## What "continuity" means here Continuity is the property that work survives the boundary between sessions. Without it, an agent that loses its context redoes finished steps, or stalls to ask you where things stand. cairn gives the agent somewhere durable to put the plan, and a way to read it back. The plan is a graph the agent queries, not a markdown file it re-reads top to bottom. A single `task_bootstrap` call returns the current task's state, its neighbors, its open handoffs, and recent observations. From there the agent asks the graph real questions — which phases are ready to start, what depends on a given task, what has gone stale — rather than scrolling old chat. The plan lives where a context reset cannot reach it, and the graph answers in the same terms the agent used to write it. Three nouns carry the continuity. A **task** is a node, addressed by a date-prefixed slug like `2026-06-20-ship-the-thing`, holding status, description, edges, and metadata. An **observation** is the cheapest write — a freeform note appended to a task as the work happens, the heartbeat of a session. A **handoff** is a resumable summary written for whoever picks up next, its summary load-bearing. See [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) for how each one behaves. ## Why a cairn A cairn is a pile of stones raised to mark a route across ground that holds no path of its own — a ridge, a snowfield, a stretch of moor where one direction looks like the next. A walker adds a stone and goes on. The pile stays, and shows whoever comes after which way the route ran. The word is Scottish Gaelic, *càrn*, a heap of stones. The tool keeps that habit. Each session leaves a little more on the graph and moves on, and the next one reads the stones instead of guessing the way. The stone outlasts the walker who set it, and the graph outlasts the session that wrote it. ## A continuity subsystem, shipped as an extension cairn began as the continuity subsystem inside kli, the agent runtime it was built for, and it ships as exactly the kind of extension kli exists to host. kli is a host for agent extensions; cairn is the one that handles memory across sessions. Run inside kli, cairn is at its fullest. The agent gets the tools, and the host adds three things on top: slash commands for steering by hand, per-turn folding of the current task's live state into the model's view, and folding of your observations and handoffs into the summary when a long conversation is compacted. That is why kli is the fuller home for cairn. See [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities) for how the host enforces the boundary between what an agent can read and what it can change. ## Works with any MCP client cairn does not require you to adopt kli as your main agent. Pointed at a serving process, an MCP client — Claude Code, Claude Desktop, Cursor — sees cairn's fourteen tools as MCP tools, with its workflow prompts and the `cairn-method` skill exposed as MCP prompts and readable resources. Capability gating carries over the wire, so the read / write / observe split still holds. What does not travel is the part kli provides as the host: the slash commands, context injection, and compaction folding named above stay behind. A plain MCP client still gets a complete, useful surface. One command — `kli mcp-serve cairn` — serves the extension to anything that speaks the Model Context Protocol. The command, its client configuration, and the exposed surface are documented in [mcp-serve](/cairn/cli/mcp-serve). ## The recursive loop These docs are written to be read by a machine mid-task as readily as by a person at a desk. So the pages lead with what a thing *is*, keep their anchors stable, and quote tool names and return strings verbatim — an agent cannot paraphrase a wrong string into a working call. These pages were themselves produced by an agent dogfooding the [cairn-method](/cairn/concepts/the-cairn-method), the same bootstrap, observe, and handoff loop that keeps any plan across resets. ## Where cairn earns its weight, and where it does not cairn pays off when work spans more sessions than a single context holds, when a plan has phases and dependencies worth tracking, or when more than one session might touch the same work and needs to see the others. It is a poor fit for a one-shot task you will finish in the session you start it, or a single-agent flow short enough to keep entirely in context. For those, the graph is overhead — a plain note to yourself wins. cairn tracks what you are doing without prescribing how you do it, which means the judgment of when to reach for it stays yours. ## Related - [The task graph](/cairn/concepts/the-task-graph) — the two stores and the typed-edge model underneath everything here - [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) — the three nouns and how each write behaves - [The cairn-method](/cairn/concepts/the-cairn-method) — the working loop and the disciplines that wrap it - [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) — why a write survives a rebuild - [Install cairn](/cairn/get-started/install-cairn) — bake it into kli or serve it to another client - [Your first cairn session](/cairn/get-started/your-first-cairn-session) — the bootstrap, observe, handoff loop made concrete #### The task graph A new agent session knows nothing about the work it inherited. It has a fresh context window, a goal, and no memory of the decisions that led here. The task graph is what it reads instead of remembering: a durable, queryable structure that holds the shape of the work — which task is which, which phase blocks which, what was tried and learned — outside any one conversation. cairn keeps that structure in two stores, and they exist because the work poses two different questions. ## Two stores, two questions cairn answers two different questions, so it keeps two different stores. **The task store** answers *what is the structure of the work?* It holds tasks as nodes and the typed relationships between them as edges. A task is a node addressed by a slug, a date-prefixed identifier minted at creation like `2026-06-22-add-fts-index`, carrying a status, a description, edges to other tasks, and open metadata. Edges are typed: a phase belongs to a plan, one task depends on another, two tasks are related. This is a directed graph, and the query language ([TQ](/cairn/reference/tq-language)) walks it. **The observation store** answers *what happened, and what was learned?* It holds observations, the freeform notes an agent appends as it works, indexed for full-text search. Where the task store is structured and typed, the observation store is prose. An observation is the cheapest write cairn offers, and over a session there are many of them. You do not navigate them by edge; you [search](/cairn/reference/tools#task-search) them by content. The split is the point. Structure is small, slow-changing, and worth typing strictly. Notes are voluminous, fast-flowing, and worth indexing loosely. Forcing both into one shape would make the structure noisy or the notes rigid. Keeping them apart lets each store do one job well. An agent can ask a structural question (`what is ready to work on?`) without wading through prose, or a textual question (`where did we hit the FTS5 tokenizer issue?`) without walking edges. ## Tasks are slug-addressed nodes Every task has a slug, and the slug is its address everywhere: in edges, in queries, in handoffs, in the current-task pointer. Slugs are date-prefixed, so `2026-06-22-` orders the day's work and the suffix names it. Because the address is a stable string rather than a numeric id, an agent can refer to a task by name across sessions without holding a handle, and a handoff written today still points at the right node tomorrow. A task carries a small fixed set of fields the graph queries against, plus open metadata read as text. The fixed fields a [TQ](/cairn/reference/tq-language) query can name are these eleven — six textual, two numeric, and three timestamps (universal-time, numeric-comparable and date-queryable): | Type | Fields | | --- | --- | | text | `slug`, `status`, `description`, `depot`, `parent`, `display-name` | | number | `obs-count`, `edge-count` | | timestamp | `created-ts`, `updated-ts`, `status-ts` | Anything else you set with [`task_set_metadata`](/cairn/reference/tools#task-set-metadata) is open metadata. It rides along on the node and reads as text, but it is not a typed field the query language type-checks. Status is a closed enum (`open`, `active`, `completed`, `abandoned`, `blocked`) split by the query layer into an active half (`open`, `active`, `blocked`) and a dormant half (`completed`, `abandoned`), which is how a source like `(active)` knows what to return. ## Edges come in two classes The graph has two kinds of edge, and the difference is not cosmetic: they are stored in different places and traversed differently. **Structural edges carry the fibration.** A `phase-of` edge says one task is a phase of another, and it is folded into the child's parent pointer rather than stored as a separate edge row. [`task_fork`](/cairn/reference/tools#task-fork) writes exactly such a `phase-of` edge, its default `edge_type`, when it splits a child off a parent. The store recognizes a second structural type, `forked-from`, as an internal alias of that same parent pointer. Once folded, `phase-of` and `forked-from` are indistinguishable, which is why a task has exactly one structural parent. `forked-from` is not a value any tool accepts as `edge_type`; it exists only as a classification the graph reports. The backbone these types carry is the parent pointer, the plan-to-phase tree, while the order among sibling phases is tracked on a separate link. So "one structural parent" is a claim about ownership, not ordering. **Lateral edges are relations the backbone does not carry.** `depends-on` and `related` live in a separate edge table, each a real row from a source task to a destination task. `depends-on` orders work: a phase that depends on another is not ready until the other settles. `related` is the catch-all link between two tasks that belong together without one owning the other. A task can have many lateral edges in either direction, and they are the cross-links over the structural tree. The store does not enforce acyclicity on them: a row from src to dst is rejected only when the two are the same task, so two tasks may each depend on the other. Traversal is built to tolerate that, which is why the `(:closure)` step is cycle-safe. The closed edge vocabulary a tool accepts is exactly `phase-of`, `depends-on`, `related`. That is the set [`task_link`](/cairn/reference/tools#task-link) and [`task_fork`](/cairn/reference/tools#task-fork) validate against, and an `edge_type` outside it is refused. The graph self-describes this split: the `(edges)` source returns each edge type tagged with its class, `structural` or `lateral`, so an agent can read the taxonomy off the graph rather than memorizing it. ```lisp (-> (edges) (:select :class)) ``` ```text 4 tasks: - depends-on class=lateral - forked-from class=structural - phase-of class=structural - related class=lateral ``` A query that traverses an edge validates the edge type before it touches any data; an unknown edge type is a structured error, not a silent empty result. That distinction matters to a model. An empty result means *nothing matched*; an error means *you asked the wrong question*. cairn never lets one look like the other. ## Observations index the prose Observations are stored in their own table and mirrored into a full-text index built on SQLite's FTS5. The tokenizer keeps identifiers and `file:line` spans whole, treating underscores, colons, hyphens, and dots as token characters rather than separators, so a search for `obs_fts` or `store.lisp:89` finds the note that mentions it. There is no stemming and there are no stopwords; what you wrote is what is indexed. This is why the observation store is searched, not walked. An agent re-entering a task does not traverse to its observations; it queries them by content with [`task_search`](/cairn/reference/tools#task-search), which compiles its input into an FTS5 `MATCH`. The structured graph and the searched index meet at the task: each observation belongs to a task node, so `obs-count` is a queryable field on the node, and an agent can move from *which task is busiest* (a graph query) to *what was said on it* (a search) in two calls. ## How the two stores stay one truth Both stores are projections of a single append-only event log, not independent databases that could disagree. Every write, whether a task created, an edge linked, or an observation recorded, appends one event, and the task store and the observation store are folds of that same log. The graph you query is materialized state; the log is the truth it is derived from, and the projection can be rebuilt from the log at any time. That is the subject of [events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile), and it is why the two-store split never becomes a consistency problem: there is one source, and two views of it. ## Related - [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) — the three nouns the graph is built from - [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — how `phase-of` and `depends-on` edges make a plan you can query - [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — the closed enums and the eleven queryable fields, in full - [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) — why both stores are folds of one durable log - [The TQ language](/cairn/reference/tq-language) — how a query walks the typed-edge graph #### Tasks, observations, and handoffs A cairn graph is built from three nouns. A **task** is a node: a date-prefixed slug with a status, a description, edges to other tasks, open metadata, and a stream of observations. An **observation** is the cheapest write you can make, one line of freeform text appended to a task. A **handoff** is a resumable summary: a scaffolded document whose one-line summary is the first thing the next session reads. These three are all you write. Everything else cairn shows you, from the frontier to the timeline, is computed from them. Each noun maps to a verb you call and an event the call records. That is the shape of the whole system: a small, closed vocabulary of writes, each one durable, each one replayable. Understanding what each noun is, and what it is *not*, tells you when to reach for which. ## The task: a node with a date-prefixed slug A task is identified by a slug minted from the name you give it. `task_create` over the name `wire up auth` yields a slug of the form `-wire-up-auth` (the UTC date stamp followed by the slugified name) and returns `Created .`. The date namespace is system-owned. If you hand `task_create` a name that already carries a leading `YYYY-MM-DD-` prefix, it strips that before stamping today's, so the prefix never doubles and minting the same name twice in a day is idempotent. A name that recovers nothing descriptive is rejected before any event is recorded. The slug is the address. Every other write names a task by slug or operates on the current one. A task carries: - **a status** — one of `open`, `active`, `completed`, `abandoned`, `blocked`, moved with `task_update_status` (` is now .`); - **a description** — the freeform line set at creation; - **edges** — typed links to other tasks (`phase-of`, `depends-on`, `related`), added with `task_link` and removed with `task_sever`; - **metadata** — open key/value pairs set with `task_set_metadata` (`Set on .`); - **observations** — the append-only text stream described below. `task_get` renders all of this for one slug: status, description, parent, children, edges, metadata, and the five most recent observations. Past five, it appends a one-line pointer to the rest — read a task's full observation history with `timeline(full=true, types=observation)`. None of it is a file you edit. The task is a projection of the events recorded against its slug, so the rendered node is always whatever the log replays to. See [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile). Creating the first task adopts it as current when no current task is set; after that, `task_create` leaves the pointer alone. Forking with `task_fork` always switches the pointer to the child. The difference matters when you are building a plan, and it is covered in [The current task pointer](/cairn/concepts/the-current-task-pointer). ## The observation: the cheapest write, the heartbeat An observation is one line of freeform text appended to a task. You call `observe` with `text`, it records an `observation` event, and it returns `Observed on .`. That is the entire transaction. `observe` changes nothing but the observation stream and leaves your focus where it was. This cheapness is the point. Because an observation costs nothing, the design assumes you record one whenever you learn something: a dead end you ruled out, the file and line where the real bug lives, a decision and the reason behind it. They are the running record of why the task moved the way it did. The store indexes them for full-text search, so an observation written today is what a future session retrieves with `task_search` when it asks "did anyone already look at this?" See [The cairn-method](/cairn/concepts/the-cairn-method) for the discipline and [`task_search`](/cairn/reference/tools#task-search) for the retrieval side. An observation resolves to the current task unless you pass `task_id` to scope it elsewhere. Because it does not move the pointer, you can annotate a neighboring task mid-flow and stay on your own. ## The handoff: a resumable summary, summary first A handoff is a document that lets a fresh session pick up where you stopped. You call `handoff` with a one-line `summary`; it mints a path under the task scratchpad, writes a skeleton document, records a `handoff.create` event, and returns `Handoff scaffolded for at `. The skeleton is deterministic: frontmatter, a `State` snapshot computed from the task, and empty `Recent work` and `Next steps` sections. The tool never drives an authoring turn. It hands you a valid file and a path; you read the scaffold and overwrite it with the rich body. The `summary` is load-bearing: a resuming session reads it first and can often act on it without opening the file, so its precision matters more than the body's. It is also what `task_bootstrap` surfaces in its `handoffs:` block when you orient on the task. Two surfaces write handoffs, and they are not the same: - **The `handoff` tool** scaffolds the skeleton and records the event. It is deterministic and never authors prose. - **The `/handoff` command**, under a kli host, is an authoring turn: it composes the one-line summary, calls the tool to scaffold and record, reads the scaffold, then overwrites it with a thorough document — task status, critical references, recent changes in `file:line` form, learnings, artifacts, graph state, and numbered next steps. The tool gives you the durable record and the file; the command gives you the writing. Either way the summary comes first. See [Slash commands](/cairn/cli/slash-commands) for the `/handoff` authoring command and [the `handoff` tool](/cairn/reference/tools#handoff) for the scaffold it builds on. ## Three nouns, three events Every write you make against the graph reduces to one of these nouns appending one event: | Noun | Verb | Event recorded | Returns | | --- | --- | --- | --- | | Task | `task_create` | `task.create` | `Created .` | | Task | `task_fork` | `task.create` + `task.fork` | `Forked from ().` | | Task | `task_link` | `task.link` | `Linked -> ().` | | Task | `task_sever` | `task.sever` | `Severed -> ().` | | Task | `task_set_metadata` | `task.set-metadata` | `Set on .` | | Task | `task_update_status` | `task.update-status` | ` is now .` | | Observation | `observe` | `observation` | `Observed on .` | | Handoff | `handoff` | `handoff.create` | `Handoff scaffolded for at ` | Reads — `task_get`, `timeline`, `task_search`, `task_query`, and `task_bootstrap` — record no event. They project the log; they do not extend it. `task_bootstrap` in particular looks like a write because it can move the current pointer, but it records nothing: it switches focus and reads back state. The boundary between what extends the log and what merely reads it is enforced by capability, not by which tools are visible. See [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities). ## Why this shape The three nouns are deliberately uneven in cost. A task is a commitment: it has a slug, a status, a place in the graph. An observation is nearly free, one line with no ceremony, because the friction of recording what you learned is the thing that loses knowledge between sessions. A handoff sits in between: more than a note, less than a meeting, a single document whose summary a tired or truncated reader can act on without opening it. Keeping the write vocabulary this small is what makes a cairn graph durable and replayable. There is no rich task object to migrate, no handoff schema to version. There is a log of `task.create`, `observation`, `handoff.create`, and a handful of edge and status events, and the task you see is what that log folds to. ## Related - [The task graph](/cairn/concepts/the-task-graph) — how tasks and their typed edges form the durable DAG these nouns live in - [The current task pointer](/cairn/concepts/the-current-task-pointer) — which task an unscoped `observe` or `handoff` resolves to, and what moves the pointer - [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) — how each recorded event folds into the task you read back - [The cairn method](/cairn/concepts/the-cairn-method) — the bootstrap → observe → handoff loop these nouns were built for - [Slash commands](/cairn/cli/slash-commands) — the `/handoff` authoring command and the `handoff` tool it scaffolds with #### Plans, phases, and the frontier A plan in cairn is a graph of tasks, not a document. There is no `plan.md` that an agent reads, edits, and re-reads until it drifts from the work. The plan is the root task, the phases are its children, and the ordering between phases is an edge type the query language understands. Because the plan is structure rather than prose, cairn can answer a question a markdown file never could: of everything left to do, which tasks are ready *right now*. That answer is the frontier, and it is the single question an agent asks at the top of every working session. This matters because the reader resuming a plan is usually a fresh context. A flat checklist forces that fresh context to re-derive what blocks what; a graph carries the dependencies as data, so the next task is computed, not remembered. ## A plan is a root task with phase children The plan is a [task](/cairn/concepts/tasks-observations-handoffs#the-task-a-node-with-a-date-prefixed-slug) — a node in the [task graph](/cairn/concepts/the-task-graph) — that you grow phases beneath. You create the root with `task_create`, then break it into phases with `task_fork`. Each fork records a `phase-of` edge from the child up to the parent, so the plan's shape lives in the graph the moment you build it. `task_create` takes one required argument, `name`, and mints the slug from it; the slug carries today's date as a prefix. ```text task_create(name="Migrate the auth service to the new token format") -> Created 2026-06-22-migrate-auth-service. ``` `task_fork` carves a phase off whatever task it forks `from` (the parent slug; it defaults to the current task), and its `edge_type` defaults to `phase-of`. The parent argument is `from` — there is no `parent` or `parent_id`. ```text task_fork(name="Add the new token codec", from="2026-06-22-migrate-auth-service") -> Forked 2026-06-22-add-the-new-token-codec from 2026-06-22-migrate-auth-service (phase-of). ``` A phase is a phase because of its `phase-of` edge, not because of anything in its description. `phase-of` is a *structural* edge: in the store it is folded into the parent foreign key, so every task has at most one parent and the phase backbone is always a tree. That single-parent rule is what lets the plan view start from one task and walk straight down to its phases. See [The task graph](/cairn/concepts/the-task-graph#edges-come-in-two-classes) for why structural and lateral edges are stored differently. A phase is meant to be an independently verifiable unit of work — something that can move from `open` to `completed` on its own, with its own acceptance, while the rest of the plan stays put. Phases that bundle three unrelated outcomes are hard to mark done and harder to resume into. Forking one phase per checkable result keeps the frontier honest. ## depends-on edges order the phases Phase-of says *what belongs to the plan*. It says nothing about *what comes first*. Ordering is a separate, lateral edge: `depends-on`, added with `task_link`. ```text task_link(target_id="2026-06-22-add-the-new-token-codec", edge_type="depends-on", task_id="2026-06-22-cut-over-the-verifier") -> Linked 2026-06-22-cut-over-the-verifier -> 2026-06-22-add-the-new-token-codec (depends-on). ``` Read `A depends-on B` as "A cannot proceed until B is settled." A `depends-on` edge is lateral — it lives in the edges table, not the parent key — so a phase can depend on any number of others without disturbing the phase-of tree. The two edge types compose: phase-of builds the plan's spine, depends-on threads ordering across it. The closed edge vocabulary accepted by `task_link` is `phase-of`, `depends-on`, and `related`; see [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields#edge-types). Keeping the backbone and the ordering on different edge types is deliberate. You can restructure the plan (re-parent a phase) without touching the dependency order, and you can re-order work without re-parenting anything. Neither operation forces a rewrite of a prose plan, because there is no prose plan to rewrite. ## The frontier is the ready subset Two named [views](/cairn/reference/views) answer the two questions a plan poses. `plan` returns the whole plan scoped to where you are standing. `plan-frontier` returns the subset of that plan that is ready to work. The `plan` view is current-scoped: it reads from the [current task pointer](/cairn/concepts/the-current-task-pointer) and walks one hop down the phase-of backbone to the phases, falling back to your sibling phases when the current task is itself a phase. ```lisp (-> (current) (:follow :phase-of) (:or-else (-> (current) (:back :phase-of) (:follow :phase-of))) (:enrich)) ``` `plan-frontier` filters `plan` down to the tasks that are both unfinished and unblocked: ```lisp (-> (query "plan") (:where (and (not (or (= :status "completed") (= :status "abandoned"))) (all (:follow :depends-on) (or (= :status "completed") (= :status "abandoned")))))) ``` Read that predicate as the frontier rule, in two clauses: - **Not yet settled.** The task's own status is not `completed` or `abandoned`. A settled task is behind you, not ahead. (`completed` and `abandoned` are the *dormant* half of the status enum; `open`, `active`, and `blocked` are the *active* half — see [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields#statuses).) - **Every dependency settled.** Walking each `depends-on` edge forward, *all* the tasks it points to are `completed` or `abandoned`. A phase with one unfinished dependency is not on the frontier yet — its turn comes when that dependency settles. A task with no `depends-on` edges trivially satisfies the second clause: `all` over an empty set is true. So the frontier of a fresh plan is exactly its unblocked entry points. As you complete phases, the frontier advances to the phases they unblocked. You never compute this by hand; you ask for it. `frontier` is an alias — the view `frontier` is defined as `(query "plan-frontier")`, so either name returns the same ready subset. ## The shape of working a plan With the plan in the graph, an agent's session has a fixed shape, and the shape is the point. The loop is a frontier query, a completion, and a re-query: ask which tasks are ready, settle one of them, and ask again. Nothing in between edits a list. The loop runs over [task_query](/cairn/reference/tools#task-query). The frontier query inherits `plan`'s enrichment, so each line carries its inline counts — `obs=N edges=N` — after the status: ```text task_query(query="(query \"plan-frontier\")") -> 2 tasks: - 2026-06-22-add-the-new-token-codec (open) obs=0 edges=1 - 2026-06-22-write-the-migration-guide (open) obs=2 edges=0 ``` A completion is a single `task_update_status` against any task on the frontier: ```text task_update_status(status="completed", task_id="2026-06-22-add-the-new-token-codec") -> 2026-06-22-add-the-new-token-codec is now completed. ``` The re-query is the same call as before, and the phase that depended on the one just settled now appears — the frontier moved without anyone editing a list. This is the same heartbeat the [cairn-method](/cairn/concepts/the-cairn-method) wraps in research, plan, implement, and validate discipline. For the step-by-step version on a toy plan, see [Plan and resume](/cairn/get-started/plan-and-resume). ## When a plan graph is overkill A plan graph earns its keep when work outlives one context: many phases, real ordering between them, and a resume that has to recompute what is ready. For a single task you will finish in one sitting, that machinery is friction. Create the task, observe against it, and skip the phases — a one-node "plan" has a frontier of one, which is just the task itself. Reach for `task_fork` and `depends-on` when the ordering is something a future context would otherwise have to reconstruct from memory. ## Related - [Plan and resume](/cairn/get-started/plan-and-resume) — build the root, fork phases, order them with `depends-on`, and resume the plan - [The task graph](/cairn/concepts/the-task-graph) — the two stores and the structural-vs-lateral edge split - [The current task pointer](/cairn/concepts/the-current-task-pointer) — why `plan` and `plan-frontier` are current-scoped - [Views](/cairn/reference/views) — the built-in views `plan`, `plan-frontier`, `leaf-tasks`, and `stale-phases` with their TQ source text - [The TQ language](/cairn/reference/tq-language) — the query language the frontier rule is written in #### The current-task pointer The current-task pointer is a single task slug cairn holds for your session and resolves against whenever a tool omits an explicit task. You call `observe` with a line of text and no slug, and cairn knows where to file it; you call `handoff` with a summary and no slug, and it scaffolds under the right task. Nothing in those calls named a task, yet every write landed on one — the pointer supplied the target. It is the difference between an agent that has to repeat its own slug on every call and one that can plan, observe, and hand off in a flow because cairn already knows what it is working on. ## What the pointer is The current-task pointer is one task slug, stored per session, that names the task in focus right now. It is not a property of the [task graph](/cairn/concepts/the-task-graph) — the graph does not have a "current" node. The pointer lives next to the session, alongside the open database handle, and it is what makes a slugless tool call meaningful. Without a pointer, "this task" has no referent and a current-scoped write fails rather than guessing. Two sessions never share a pointer. The pointer is keyed in the active protocol's own storage, so a serving cairn over MCP keeps a separate current task for each connected client. One agent forking into a new phase does not move another agent's focus. Concurrency is real, and each session carries its own answer to "what am I on." ## How a tool finds its task The write tools that target a task — `observe`, `task_link`, `task_sever`, `task_set_metadata`, `task_update_status`, `handoff` — resolve their target the same way. The tool takes the `task_id` argument when you pass one; otherwise it falls back to the current pointer; and if neither names a task, it fails with: ```text No current task; create or select one first. ``` That fallback is the whole point. In the common case you set the pointer once and then write against it without repeating the slug. When you need to touch a different task without leaving the one you are on, you pass `task_id` — and that is the second half of the rule, the part that surprises people. ## Acting on a task does not make it current A `task_id` argument scopes one call. It does not move the pointer. You can drop an observation on a sibling task, read another task's state, or flip a blocker to `completed`, all while the pointer stays exactly where it was. ```text observe(text: "the parser handles the empty case", task_id: "2026-06-22-parser-edge-cases") → Observed on 2026-06-22-parser-edge-cases. ``` After that call, the current task is still whatever it was before. The observation went where you aimed it; your focus did not follow. This separation lets an agent record a quick note on a related task and return to its own work without an explicit switch-back step. The slug you passed is a one-shot address, not a new home. Two surfaces deliberately break this rule, because their job is to move you. ## What moves the pointer Three operations set the current task. Each is explicit about it. **Creating the first task adopts it.** `task_create` mints a date-prefixed slug and records the task. If — and only if — no task is current, it adopts the new one as current, so a fresh session that creates a task is immediately working on it. Create a second task and the pointer does not move; you are still on the first until you say otherwise. This is the gentle case: adoption fills an empty pointer, never overwrites a full one. ```text task_create(name: "rework the search backend") → Created 2026-06-22-rework-the-search-backend. ``` **Forking always switches.** `task_fork` spawns a child task and unconditionally makes the child current. Forking is how you descend into a [phase of a plan](/cairn/concepts/plans-phases-and-the-frontier), and the assumption is that you want to work in the phase you just opened. The parent comes from the `from` argument — a parent slug — and defaults to the current task when you omit it. Pass neither a `from` nor have a current task, and the fork fails: ```text No parent task; pass from or select a task first. ``` So `task_fork` reads the pointer (as the default parent) and then overwrites it (with the new child) in a single call. After a fork you are one level deeper, on the phase, with the parent recorded as the edge source. ```text task_fork(name: "migrate the index schema", from: "2026-06-22-rework-the-search-backend") → Forked 2026-06-22-migrate-the-index-schema from 2026-06-22-rework-the-search-backend (phase-of). ``` **Bootstrapping orients and may switch.** `task_bootstrap` is the set-current surface for resumption — the one call you make to re-enter work. Its pointer behavior splits on whether you name a task: - With an explicit `task_id`, it switches the pointer to that task. Orienting on a task makes it current, so any session you spawn and the [per-turn context cairn injects](/cairn/concepts/events-projection-and-reconcile) agree on which task is in focus. - With no `task_id`, it orients on the current task and leaves the pointer untouched; if no task is current, there is nothing to orient on and the call fails. Either way, `task_bootstrap` records no event and switches the pointer only after it confirms the task exists. If you name a task that is not there, the pointer does not move; you get a not-found result and a list of recent slugs to aim at instead. With nothing to orient on at all: ```text No task to bootstrap; pass task_id or select a task first. ``` | Operation | Effect on the pointer | | --- | --- | | `task_create` | Adopts the new task only when none is current | | `task_fork` | Always switches to the child | | `task_bootstrap` with `task_id` | Switches to the named task (after it is found) | | `task_bootstrap` without `task_id` | Orients on the current task; never moves the pointer (fails if none is current) | | Any tool's `task_id` argument | None — scopes one call, leaves the pointer alone | ## Why the pointer feeds every turn The pointer does more than route writes. Each model turn, cairn reads the current pointer live, renders that task's computed state and its open handoffs, and splices the block in as ephemeral context — present for that turn, never written to the durable log. The agent sees what it is working on without being told, and the block always reflects live state because it is rebuilt from the pointer and the store every turn rather than cached. This is the recursive payoff of a per-session pointer. Move it with `task_fork` and the very next turn's injected context is the new phase. Resume a task with `task_bootstrap` and the context block follows your focus. The pointer is the one piece of session state that decides both where writes land and what the model is reminded of. That is why moving it is always explicit, never a side effect of touching another task. ## When the pointer is more than you need A single-task, single-session run barely exercises the pointer: you create one task, it is adopted, and every slugless call lands on it for the life of the session. The rules above earn their keep when work fans out — multiple phases, multiple sessions, a `task_id` aimed sideways while focus stays put. For one linear thread of observations, walk through [your first session](/cairn/get-started/your-first-cairn-session) and let adoption do the work; the switch-versus-scope distinction can wait until you fork. ## Related - [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — how `task_fork` descends into the phase the pointer then follows - [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) — the writes that resolve against the pointer - [The cairn-method](/cairn/concepts/the-cairn-method) — where bootstrap sits in the orient, record, hand off loop - [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) — the per-turn context block the pointer drives - [Tools reference](/cairn/reference/tools#task-bootstrap) — the exact signatures of `task_create`, `task_fork`, and `task_bootstrap` #### Events, projection, and reconcile cairn stores no state you cannot reconstruct. Every write is one event appended to a durable, append-only log; everything you query — the task graph, observations, handoffs, metadata, even the views you define — is a projection folded from those events. The log is the truth. The database is a cache of the log, and it can be thrown away and rebuilt. This is why a cairn store survives a crash mid-write, moves between machines as plain text, and resumes the same way every time: the answer to "what happened?" is always the events, replayed in the same order. You do not call any of this. There is no commit, no flush, no migration step in the tool surface. Understanding the model tells you why cairn's writes are cheap, why two agents writing the same store do not corrupt it, and why the durable artifact on disk is a file you can read. ## The event log is the source of truth Each task owns a log: a single `events.ndjson` file, one JSON event per line. That file is canonical. Every observation, status change, link, fork, and handoff is appended to it as an event before anything else happens, and an event, once written, is never edited or deleted. To know the full history of a task, you read its log top to bottom. The line format is deterministic. cairn encodes each event with its keys sorted and its `data` payload nested as a canonical object, so the same event always serializes to the same bytes — the encoder and decoder are exact inverses, and a line decoded on one machine folds into the same state on another. An event carries its identity and provenance: `event_id` is a portable ULID, the stable id that travels with the event; `event_key` is the de-duplication key; `task_id`, `type`, `ts`, and the session that wrote it ride along too. The local row number is the only field the line does not carry, because it is local. Because the log is append-only NDJSON, it is durable and portable for free. There is no binary format to corrupt, no schema to migrate in place. Copy the directory and you have copied the history. ## The projection is a fold over events You never query the log directly. cairn folds the events into a SQLite projection — the materialized view your reads run against — and that projection holds the shapes you actually ask about: the `tasks` table, the `edges` between them, the full-text `observations` index, `handoffs`, `task_metadata`, and `named_views`. Every one of these is derived. Apply the events in order and the projection appears; the same events always produce the same projection. Two consequences matter. First, the projection is disposable: it is a cache, and cairn can rebuild it from the logs at any time. Second, replay is **portable**, not local. cairn folds the log by `(ts, event_id)` — timestamp, then ULID as tiebreaker — not by the local row number a given database happened to assign. The local `seq` orders one database's storage; the portable order reproduces the *same* state on any store, regardless of the order rows landed locally. That distinction is what lets a store rebuilt on a fresh machine match the original exactly. User-defined views are not special. When you call `define!` to name a query (see [Built-in and user-defined views](/cairn/reference/views)), cairn records a view-definition event and folds it into the `named_views` table. `undefine!` records the inverse. Replay reproduces your view vocabulary along with everything else, because a view is just more events in the log — there is no separate place your customizations live and could be lost. ## Reconcile unions the log and the cache The two stores can drift. A database row gets committed but the matching append to the log never lands — the crash gap between a write and its mirror. Or you copy fresh logs into a store whose cache predates them. cairn closes both gaps on store-open with **reconcile**, which unions the per-task logs (the truth) with the SQLite cache (rebuildable) and brings them back into agreement. Reconcile runs in three moves: **Ingest log lines the cache lacks.** cairn replays every log into the database, skipping events it already holds — de-duplication is keyed on `event_key`, so re-ingesting the same event is a no-op. If any new event enters the cache, cairn re-folds the whole projection in portable order, so the materialized state reflects the full log rather than a partial splice. **Export cache rows the logs lack.** For the reverse gap — a committed row whose append never reached the file — cairn appends those events back to the right task's log. This is the only path that writes the log during open, and it writes only what is missing, comparing by `event_key`. After this step, every event in the cache is also on disk. **Refresh the watermarks.** cairn stamps each log's current watermark so the next open sees an unchanged store and skips it. That last move is the efficiency story. A watermark is a content hash and byte length of a log file, stored in the database's `schema_meta`. On open, cairn compares each log's current watermark against the stored one; a file that has not moved is skipped in both directions. An unchanged store reconciles to a no-op — reconcile is cheap precisely because most logs do not change between opens. A store with no backing file (an in-memory store) reconciles to a no-op as well. ## Why the order is append-then-project The durable append happens first, and the cache update rides only after it. cairn appends the event to the log, then folds it into the projection — never the other way around. During ingest, cairn deliberately suppresses the mirror-write step, because the events it is replaying are already on disk; mirroring them again would be redundant work against the file it is reading from. This ordering is what makes the crash gap recoverable rather than fatal. If cairn crashes after the log append but before the cache catches up, the next open's ingest step replays the missing line and the projection heals. If it crashes after a cache write but before the mirror append — the rarer gap — the export step writes the line back. Whichever side is ahead, reconcile pulls the other forward, and because both directions key on `event_key`, neither can double-apply. ## Where this model earns its keep, and where it does not Event sourcing is not free. The projection is a second copy of state that must be kept in step, replay has a cost proportional to history, and reasoning about a fold is more work than reasoning about a row you overwrote. cairn takes that cost on purpose, because its whole job is continuity: an agent must be able to resume into the exact state a prior session left, on a machine that may not be the one that wrote it, after a context reset or a crash. A durable, portable, replayable log is the mechanism that delivers it, and the [current-task pointer](/cairn/concepts/the-current-task-pointer) is the one piece of state deliberately kept *outside* this log, because it is per-session, not part of the shared history. If your work is a single throwaway task in one session, you will never see reconcile do anything and the projection will never need rebuilding — the model is overkill, and that is fine; it stays out of your way. The model pays off the moment work outlives a session or is shared across more than one agent. That is the case cairn is built for. ## Related - [The task graph](/cairn/concepts/the-task-graph) — the typed-edge graph and observation index that the projection materializes - [Tasks, observations, handoffs](/cairn/concepts/tasks-observations-handoffs) — the three nouns that become events - [The current-task pointer](/cairn/concepts/the-current-task-pointer) — the per-session state kept outside the event log - [Views](/cairn/reference/views) — how `define!` records a view event that replay reproduces #### Reads, writes, and capabilities A capability is a tag on a tool that says what kind of effect the tool can have on the store. cairn stamps every tool with one of three: `:cairn/read`, `:cairn/write`, or `:cairn/observe`. The boundary between reading and writing is drawn by those tags, in the tool's `:metadata`, not by which tools a client happens to expose. A host that grants a session `:cairn/read` but withholds `:cairn/write` gets a session that can query, inspect, and orient, and cannot change a single edge — no matter what it calls. This matters because cairn is one durable graph that many sessions touch at once, and an agent that can plan freely should not be able to overwrite another session's plan by accident. The split is coarse on purpose: three capabilities, not fourteen, so a host reasons about a session by the class it granted rather than by the tool list it exposed. ## Three capabilities, one per effect class Every tool declares its capability in its registration. The classes partition the fourteen tools cleanly. `:cairn/observe` covers exactly one tool: `observe`. An observation is the cheapest write cairn has — it appends one freeform note and moves nothing else — so it gets its own capability. A host can hand a session the right to record observations without the right to restructure the graph. `:cairn/write` covers the eight tools that change graph structure or state: `task_create`, `task_fork`, `task_link`, `task_sever`, `task_set_metadata`, `task_update_status`, `handoff`, and `task_query_write`. These create tasks, draw and cut edges, move status, set metadata, scaffold handoffs, and run the mutating query forms. Each appends an event through the same durable boundary; see [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile). `:cairn/read` covers the five tools that compute over the store and return text without touching it: `task_search`, `task_get`, `timeline`, `task_query`, and `task_bootstrap`. A read tool resolves a target, hydrates state, renders it, and returns. `task_bootstrap` switches the current-task pointer as a side effect of orienting, but it records no event and writes nothing to the graph — the [pointer is per-session](/cairn/concepts/the-current-task-pointer), not part of the durable task graph, so moving it is a read-class act. The full per-tool capability column lives in [the tools reference](/cairn/reference/tools). ## The query language carries the gate as one dynamic variable The two query tools are the interesting case, because both run the same language — TQ — over the same store, and the only difference between them is whether writes are allowed. cairn does not give the read tool a smaller language and the write tool a bigger one. It gives them the identical interpreter and flips one dynamic variable. `task_query` runs TQ with mutation refused. `task_query_write` runs TQ with mutation allowed. When the gate is closed, any form whose head ends in `!` — a mutating step like `(:set-status! "completed")` or a write source like `(define! ...)` — is refused before it can record anything. The refusal is a structured error, not a crash: ```text Query error: Mutations need the task_query_write surface; task_query is read-only. ``` One variable being the entire gate has a useful consequence: the read surface is *provably* non-mutating. You do not audit fourteen runners to trust that `task_query` cannot write; you audit one branch. Two guards back that branch. The append is gated by the flag the read tool binds shut, so a mutating step is refused before it reaches the durable boundary. And the read tool withholds the write context entirely — it binds the event sink to nothing — so even a step that somehow slipped the flag would have no boundary to append through. The check fires on a step's internal `:mutation` kind tag, the one `(-> (schema) (:select :kind))` projects as `kind=mutation`, not on registry membership, so a new mutating step is gated the moment it is tagged, with no second list to keep in sync. ## Set-operation operands stay read-only on both surfaces There is a subtler rule inside `task_query_write`. The set-algebra steps — `(:union Q)`, `(:intersect Q)`, `(:minus Q)`, `(:or-else Q)` — and the operand of `(define! "name" Q)` evaluate their sub-query `Q` with the write gate shut, even when the outer query runs on the write surface. You combine and define *over* selections; you do not mutate *inside* them. So any `!`-form inside an operand is refused — it is an error, not a silently skipped step. This is a legal write-surface query: the operand `(dormant)` is read-only, and the outer `(:set-status! "completed")` runs against everything `(active)` keeps after the minus: ```lisp (-> (active) (:minus (dormant)) (:set-status! "completed")) ``` The mutation step returns the set it touched — each node now carrying the written status — so the call renders that set, and a trailing `(:count)` would report how many tasks it moved: ```text 2 tasks: - 2026-06-20-wire-the-cache (completed) - 2026-06-21-link-check-gate (completed) ``` Now move the mutation *into* the operand. The operand is evaluated with the gate bound shut, so the `(:set-status! "open")` inside `(:minus ...)` hits the same refusal as a write on the read surface: ```lisp (-> (active) (:minus (-> (dormant) (:set-status! "open"))) (:set-status! "completed")) ``` Because the refusal is a structured `cairn-query-error` and the only handler is at the top of the runner, the *whole call* fails: it returns `isError:true`, the outer `(:set-status! "completed")` never executes, and nothing is touched. There is no per-step recovery — one refused operand mutation aborts the entire query. "Operands are read-only" means a write inside an operand aborts the call, not that the inner write is dropped while the outer write proceeds. A query that builds a target set by composing other queries can never have one of those building-block queries quietly change the data it selects from, and it can never half-apply either. ## A capability is not a promise that the tool was exposed The capability tag describes what a tool *would* do if called; it does not by itself decide whether a client can see the tool. Exposure is the host's job. A kli host reads the `:capabilities` metadata and decides, per session, which tools to register and which to deny. Each read tool, for instance, carries the same shape the host inspects: ```lisp :metadata '(:capabilities (:cairn/read)) ``` The host never executes the tool to learn its class; it reads that tag and routes on it. Over plain MCP — [`kli mcp-serve cairn`](/cairn/cli/mcp-serve) — the same metadata travels with each tool so any MCP client can reason about it the same way. This is why the boundary is enforced by capability and not by tool-name exposure. Hiding `task_query_write` from a client's tool list is a presentation choice; refusing `!`-forms at the interpreter is the enforcement. The first can be reconfigured per host; the second holds wherever TQ runs. The second is the load-bearing one: an agent holding only `:cairn/read` that sends a mutation through `task_query` meets the refusal above, because the gate lives at the interpreter rather than in whatever tool list the host chose to expose. The write surface is `task_query_write`, reachable only when the host has granted it. ## When the split is more than you need For a single agent on a one-off task, the read/write/observe distinction is invisible — you hold all three and never think about which class a tool falls in. The capability model earns its keep when more than one session shares a store, or when a host wants to grant a constrained session (a reviewer, a watcher, a search-only helper) the ability to look without the ability to touch. If that is not your situation, treat this page as background: the tools behave the same whether or not you ever think about the tag they carry. ## Related - [The tools reference](/cairn/reference/tools) — every tool with its capability gate in the parameter header - [The TQ language](/cairn/reference/tq-language) — the read-versus-write gate inside the query grammar, step by step - [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) — the durable boundary every write passes through - [The current task pointer](/cairn/concepts/the-current-task-pointer) — why moving it is a read-class act - [Serve cairn over MCP](/cairn/cli/mcp-serve) — how capability metadata travels over MCP #### The cairn-method The cairn-method is a working discipline, not a feature. cairn gives an agent a durable place to keep work; the cairn-method is how an agent uses that place so a fresh session can pick the work back up. It is a loop with four phases: research, plan, implement, validate. Two heartbeat writes hold the phases together — `observe` and `handoff` — and one orienting read, `task_bootstrap`, re-enters the loop after a reset. The method ships as a skill (`cairn-method`) and six bundled prompts, so the agent reading this page can load the same discipline that authored it. ## Why a method and not just tools A model can call [the fourteen tools](/cairn/reference/tools) without any method and get nothing durable. The graph fills with tasks that have no observations, plans whose phases never settle, handoffs that summarize nothing a next session can act on. The tools are necessary but insufficient on their own. What makes the work resumable is the order the agent writes in — what it records, when, and against which task. The cairn-method names that order. It says: orient before writing, record findings as they surface, structure the plan as a graph the frontier can read, advance only on a real gate, and leave a summary the next session reads first. Each rule maps onto a tool or a query that already exists. The method is the contract between the agent and its future self. ## The loop The method runs as a loop across sessions, not a script inside one. A session enters by orienting, does one kind of work, and leaves a trail. The next session — the same agent after a context reset, or a different one — enters the same way and continues. This section is the cross-session heartbeat — orient, record, hand off; the four work phases (next section) run inside it. **Orient.** A session begins with [`task_bootstrap`](/cairn/reference/tools#task-bootstrap). One call returns the task's state, its neighbors, any open handoffs, and the recent observations. An explicit `task_id` switches the [current-task pointer](/cairn/concepts/the-current-task-pointer) to that task; with no `task_id`, it orients on the current task and adopts it only when one is not yet set. Called with neither a `task_id` nor a current task, it returns `No task to bootstrap; pass task_id or select a task first.` `task_bootstrap` records no event, so re-entering costs nothing — the call repeats freely whenever the thread is lost. **Record.** Throughout the work, [`observe`](/cairn/reference/tools#observe) is the heartbeat — the cheapest write cairn has. It appends one freeform observation to the current task and returns `Observed on .` It does not move the pointer and it does not need a status change to justify it. The cairn-method favors liberal observation over a mandated cadence: a research finding, a red-then-green test, a constraint hit, a decision made. These observations are what [`task_search`](/cairn/reference/tools#task-search) later retrieves, so a finding written once need never be re-derived. **Hand off.** When the work spans sessions, [`handoff`](/cairn/reference/tools#handoff) scaffolds a resumable note. The one-line summary comes first and is the load-bearing field a resuming session reads before anything else; the tool returns `Handoff scaffolded for at ` and writes a skeleton, which a rich body then overwrites. The summary is the contract; the file supplements the live timeline and observations, and never contradicts them. When the two disagree, the live stream wins. This loop is recursive: the page you are reading was produced by an agent running it — see [What cairn is](/cairn/concepts/what-cairn-is) for that story in full. ## The four phases Inside the loop, the work itself moves through four phases. Each ships as a prompt — `research`, `plan`, `implement`, `validate` — and each leans on the `cairn-method` skill for the discipline rather than restating it. Two more prompts, `handoff` and `resume`, wrap the loop's heartbeat and re-entry. Six prompts in total. **Research documents what is.** Before anything changes, research documents the codebase as it stands — findings, not prescriptions. Every claim carries a `file:line` or a source URL; hypotheses stay phrased as hypotheses until a root cause is reproduced. The method requires a current task before any current-scoped write, because `observe` and the `knowledge` query both resolve against it, and every finding then sinks through `observe`. It also expects a `task_search` before re-investigation, since the answer may already be in the graph. **Plan structures the work as a graph.** A plan in cairn is a task DAG, not a markdown file — [the graph is the source of truth](/cairn/concepts/plans-phases-and-the-frontier). Phases are child tasks created with [`task_fork`](/cairn/reference/tools#task-fork) (default edge `phase-of`); real prerequisite ordering is a `depends-on` edge added with [`task_link`](/cairn/reference/tools#task-link). Because `task_fork` makes the new child current unconditionally, the method always names the parent with `from=`. Each phase carries its own acceptance and is independently verifiable. There is no scaffold tool; the edges are built with the tools above. **Implement advances on real gates.** Work moves through the DAG one ready phase at a time, asking `(query "plan-frontier")` for the next phase whose every `depends-on` predecessor is settled. A phase can be advanced without moving the pointer: passing `task_id=` to `observe` and [`task_update_status`](/cairn/reference/tools#task-update-status) targets it while the plan stays current. The gate is irreducible — no phase reaches `completed` until its build, tests, and typecheck actually pass. The passing output advances the DAG, not vibes. **Validate is independent and skeptical.** A separate pass runs the gates itself rather than trusting a self-reported result, reviews the code against the DAG, and reports honestly. It reads `(query "plan")` for completion status, `(query "stale-phases")` for phases active under a finished parent, and `timeline` to reconstruct what was actually done. It classifies findings as matches, deviations, or potential issues, each with a `file:line`, and refuses to green-light advancement while build or test failures remain. These phases are documentarian and test-first by design. The method documents what is, not what should be; it writes the failing test first, for the right reason; it leaves zero `TODO`/`FIXME`/`HACK` in shipped code. It treats honest deviation as content — when reality differs from the plan, the agent pauses, surfaces the gap, gets a decision, and records it with `observe`. ## Where the method is overkill The cairn-method earns its weight when work outlives a single context window — multi-session efforts, plans with real phase ordering, or several agents touching the same tree. It is overkill for a one-off task finished in one sitting and never resumed. For a single quick change, a `task_create` and one `observe` is the whole method; forking phases and writing a handoff for work that ends in the same turn is ceremony, not continuity. The full loop is for the case where a reset would otherwise cost the context — that is what cairn exists for. When it would not, a plainer note wins. ## Related - [What cairn is](/cairn/concepts/what-cairn-is) — continuity for agents, and the recursive loop in depth - [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — why a plan is a DAG and how the frontier picks the ready work - [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) — the three nouns the loop writes - [The current-task pointer](/cairn/concepts/the-current-task-pointer) — what `task_bootstrap` and `task_fork` move, and what `task_id` leaves alone - [Serve cairn over MCP](/cairn/cli/mcp-serve) — exposing the tools, the six prompts, and the cairn-method skill over MCP ## Reference ### Tools & Query Language #### MCP tools cairn provides fourteen MCP tools. They are the wire surface an agent calls to plan in the task graph and resume into it: one observe tool, eight writes, and five reads. Each entry below mirrors the tool's registered schema, so the page tracks the served surface and does not drift from it. Every tool carries a capability gate in its metadata: `:cairn/observe`, `:cairn/write`, or `:cairn/read`. The gate is the boundary, not the tool name — read and write are decided by capability, not by which tools are exposed (see [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities)). A call whose gate is not granted runs nothing. Errors travel on two channels. Protocol faults — malformed JSON-RPC, an unknown tool, a missing required argument the schema rejects — surface as transport-level errors. Tool-execution failures surface as a normal tool result with `isError: true`, carrying the verbatim text the model reads and self-corrects from: `name is required.`, `No current task; create or select one first.`, and the rest quoted in the entries below. Every returned and error string on this page is copied from source; treat the punctuation as load-bearing. Most tools resolve a target task the same way: the `task_id` argument when given, otherwise the [current task pointer](/cairn/concepts/the-current-task-pointer). When neither names a task, a current-scoped call fails with `No current task; create or select one first.` Task arguments accept a bare slug or a depot-prefixed one; the depot prefix is stripped before lookup. ## Tools by capability | Tool | Capability | One line | | --- | --- | --- | | [`observe`](#observe) | `:cairn/observe` | Record a freeform observation on the current task. | | [`task_create`](#task-create) | `:cairn/write` | Create a top-level task. | | [`task_fork`](#task-fork) | `:cairn/write` | Create a child task and make it current. | | [`task_link`](#task-link) | `:cairn/write` | Add a typed edge to a target task. | | [`task_sever`](#task-sever) | `:cairn/write` | Remove a typed edge to a target task. | | [`task_set_metadata`](#task-set-metadata) | `:cairn/write` | Set a free key/value on a task. | | [`task_update_status`](#task-update-status) | `:cairn/write` | Set a task's status. | | [`handoff`](#handoff) | `:cairn/write` | Scaffold a resumable handoff. | | [`task_query_write`](#task-query-write) | `:cairn/write` | Run a TQ query that may mutate or define views. | | [`task_search`](#task-search) | `:cairn/read` | Full-text search over observations. | | [`task_get`](#task-get) | `:cairn/read` | Read computed task state. | | [`timeline`](#timeline) | `:cairn/read` | Recent events for a task. | | [`task_query`](#task-query) | `:cairn/read` | Run a read-only TQ query. | | [`task_bootstrap`](#task-bootstrap) | `:cairn/read` | Orient on a task in one call. | ## Tool reference The entries below run observe, then the write tools, then the reads. Each one uses the same field order: summary, capability gate, signature, parameters, returns, semantics, example, see also. ### observe Record a freeform observation on the current task. This is the cheapest call and the work heartbeat. Capability gate: `:cairn/observe`. Signature: `observe(text, task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `text` | string | yes | The observation. Blank text fails with `text is required.` | | `task_id` | string | no | Task to observe on. Defaults to the current task. | Returns: `Observed on .` Records one `observation` event. Semantics: an observation never moves the current pointer; it appends to the task you are already on (or to `task_id`). Observations are the corpus [`task_search`](#task-search) ranks over. Example: `observe(text="bm25 ranking confirmed against t/cairn-search.lisp")` on task `2026-06-22-tune-search` → `Observed on 2026-06-22-tune-search.` See also: [The cairn-method](/cairn/concepts/the-cairn-method) — [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs). ### task_create Create a top-level task. Adopts the new task as current only when no current task is set. Capability gate: `:cairn/write`. Signature: `task_create(name, description?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Descriptive task name. Blank fails with `name is required.` Slugified and stamped with today's UTC date; a name too thin to slugify is rejected. | | `description` | string | no | Longer description stored on the task. | Returns: `Created .` Records one `task.create` event. Semantics: the slug is `YYYY-MM-DD-`. Minting is idempotent over the date prefix — a name you already date-prefixed is stripped back to its core before today's prefix is stamped, so the prefix stays singular. The task becomes current only if none was set; an existing pointer is left undisturbed. Example: `task_create(name="tune search ranking")` with no current task → `Created 2026-06-22-tune-search-ranking.` See also: [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs). ### task_fork Create a child task linked to a parent and make the child current. The default edge is `phase-of`, so forking builds a plan's phase backbone. Capability gate: `:cairn/write`. Signature: `task_fork(name, from?, edge_type?, description?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Child task name. Blank fails with `name is required.` | | `from` | string | no | Parent slug. Defaults to the current task; with neither, fails with `No parent task; pass from or select a task first.` The parent argument is `from`. | | `edge_type` | string | no | One of `phase-of`, `depends-on`, `related`. Defaults to `phase-of`. An out-of-enum value fails with `"" is not a valid edge type; expected one of phase-of, depends-on, related.` | | `description` | string | no | Longer description for the child. | Returns: `Forked from ().` Records `task.create` for the child and `task.fork` on the parent. Semantics: `task_fork` always switches the current pointer to the new child. A task cannot fork from itself; that fails with `A task cannot fork from itself.` Example: `task_fork(name="add prefix query support")` while current on `2026-06-22-tune-search` → `Forked 2026-06-22-add-prefix-query-support from 2026-06-22-tune-search (phase-of).` See also: [Plan and resume](/cairn/get-started/plan-and-resume) — [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields). ### task_link Create a typed edge from the current task to a target task. Capability gate: `:cairn/write`. Signature: `task_link(target_id, edge_type, task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `target_id` | string | yes | Edge destination slug. Blank fails with `target_id is required.` | | `edge_type` | string | yes | One of `phase-of`, `depends-on`, `related`. An out-of-enum value fails with `"" is not a valid edge type; expected one of phase-of, depends-on, related.` | | `task_id` | string | no | Edge source. Defaults to the current task. | Returns: `Linked -> ().` Records one `task.link` event. Semantics: links the resolved source to `target_id`. A task cannot link to itself; that fails with `A task cannot link to itself.` Use `depends-on` for lateral ordering inside a plan and `related` for cross-references; `phase-of` names structural parentage. See [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) for the closed enum. Example: `task_link(target_id="2026-06-22-tokenizer-fix", edge_type="depends-on")` on `2026-06-22-add-prefix-query-support` → `Linked 2026-06-22-add-prefix-query-support -> 2026-06-22-tokenizer-fix (depends-on).` See also: [`task_sever`](#task-sever) — [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — [The task graph](/cairn/concepts/the-task-graph). ### task_sever Remove a typed edge from the current task to a target task. Capability gate: `:cairn/write`. Signature: `task_sever(target_id, edge_type, task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `target_id` | string | yes | Edge destination slug. Blank fails with `target_id is required.` | | `edge_type` | string | yes | One of `phase-of`, `depends-on`, `related`. An out-of-enum value fails with `"" is not a valid edge type; expected one of phase-of, depends-on, related.` | | `task_id` | string | no | Edge source. Defaults to the current task. | Returns: `Severed -> ().` Records one `task.sever` event. Semantics: the inverse of [`task_link`](#task-link) — it removes the named edge. Severing is recorded as its own event, so the graph's history shows when an edge was dropped. Example: `task_sever(target_id="2026-06-22-tokenizer-fix", edge_type="depends-on")` on `2026-06-22-add-prefix-query-support` → `Severed 2026-06-22-add-prefix-query-support -> 2026-06-22-tokenizer-fix (depends-on).` See also: [`task_link`](#task-link) — [The task graph](/cairn/concepts/the-task-graph). ### task_set_metadata Set a free key/value on a task — for example `display-name`, `phase`, `objective`, `acceptance`, or `tags`. Capability gate: `:cairn/write`. Signature: `task_set_metadata(key, value, task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `key` | string | yes | Metadata key. Blank fails with `key is required.` | | `value` | string | yes | Value to store. Required by the schema; a null value is coerced to the empty string rather than rejected. | | `task_id` | string | no | Task to annotate. Defaults to the current task. | Returns: `Set on .` Records one `task.set-metadata` event. Semantics: metadata is open — any key is accepted, and setting an existing key overwrites it. Most keys are read back as open text. A handful map to typed [queryable fields](/cairn/reference/edges-statuses-and-fields) (for instance `display-name`); the rest stay free-form. Example: `task_set_metadata(key="acceptance", value="prefix queries return ranked snippets")` on `2026-06-22-add-prefix-query-support` → `Set acceptance on 2026-06-22-add-prefix-query-support.` See also: [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — [Tasks, observations, handoffs](/cairn/concepts/tasks-observations-handoffs). ### task_update_status Set a task's status. Idempotent. Capability gate: `:cairn/write`. Signature: `task_update_status(status, task_id?, reopen?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | One of `open`, `active`, `completed`, `abandoned`, `blocked`. Missing fails with `status is required.`; out of enum fails with `"" is not a valid status; expected one of open, active, completed, abandoned, blocked.` | | `task_id` | string | no | Task to update. Defaults to the current task. | | `reopen` | boolean | no | When true and `status` is omitted, sets the status to `active` to revive a completed task. | Returns: ` is now .` Records one `task.update-status` event. Semantics: status is lower-cased before validation. Re-applying the same status is a no-op event-wise but still returns the line. Marking a phase `completed` is what advances the plan frontier (see [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier)). Example: `task_update_status(status="completed")` on `2026-06-22-tokenizer-fix` → `2026-06-22-tokenizer-fix is now completed.` See also: [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier). ### handoff Scaffold a resumable handoff for the current task. The summary is the load-bearing field; an optional path points at a written note. Capability gate: `:cairn/write`. Signature: `handoff(summary, path?, task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string | yes | One-line summary of where the work stands. Blank fails with `summary is required.` | | `path` | string | no | File path for the note. Defaults to a timestamped file under the task scratchpad. | | `task_id` | string | no | Task to hand off. Defaults to the current task. | Returns: `Handoff scaffolded for at ` — no trailing period. Records one `handoff.create` event. Semantics: `handoff` is deterministic. It mints a path, writes a skeleton (frontmatter, a computed state snapshot, and empty `## Recent work` / `## Next steps` sections), records the event, and returns the path. It never drives an authoring turn — the caller overwrites the skeleton with the rich body. An existing file at the path is left in place. The richer, interactive authoring flow is the [`/handoff` slash command](/cairn/cli/slash-commands), which is separate from this tool. Example: `handoff(summary="prefix queries land; tokenizer fix is next")` on `2026-06-22-add-prefix-query-support` → `Handoff scaffolded for 2026-06-22-add-prefix-query-support at /2026-06-22_14-30-05_prefix-queries-land-tokenizer-fix-is-next.md` See also: [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) — [Slash commands](/cairn/cli/slash-commands). ### task_query_write The write surface for the TQ query language, accepting the `!`-forms that [`task_query`](#task-query) refuses. Capability gate: `:cairn/write`. Signature: `task_query_write(query)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | yes | A single TQ s-expression. Accepts mutating steps (`:set-status!`, `:set!`, `:link!`, `:unlink!`) and the view forms `define!` / `undefine!`. | Returns: the rendered query result as text. A mutation step records one append-only event per task it touches and returns the set, so a trailing `(:count)` reports how many tasks were affected. `define!` records the named view in the durable log. Semantics: mutations converge on re-run — the same query applied twice reaches the same state. Sub-query operands inside set operations always evaluate read-only, even on this surface. `(define! "name" Q)` records `Q` as a reusable named view resolvable as `(query "name")`; a user view shadows a built-in of the same name. Read-only queries also run here, but prefer [`task_query`](#task-query) for them. Example: `task_query_write(query="(-> (query \"plan-frontier\") (:set-status! \"active\"))")` → marks every ready phase `active` and returns the set. See also: [The TQ language](/cairn/reference/tq-language) — [Views](/cairn/reference/views) — [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities). ### task_search Full-text ranked search over observations. Returns matching tasks with a snippet. Capability gate: `:cairn/read`. Signature: `task_search(query, limit?, task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | yes | Free-text search. Blank fails with `query is required.` Sanitized into an FTS5 MATCH expression. | | `limit` | integer | no | Maximum results. Defaults to 10, clamped to a maximum of 50. | | `task_id` | string | no | Restrict the search to one task's observations. | Returns: ranked rows (task slug plus a snippet), or `No matching observations for "".` when nothing matches. A query the engine cannot parse fails with `Could not parse the search query "".` Records no event. Semantics: results are BM25-ranked. The sanitizer is forgiving so identifiers and paths search literally: a token carrying any of the special characters `" ( ) : - . / ^ * +` is phrase-quoted (so `tools.lisp:45-78` and `depends-on` match as written), a trailing `*` stays a prefix query, and tokens join with implicit AND. A query that sanitizes to nothing usable returns the no-match line rather than erroring. Example: `task_search(query="prefix query*")` → ranked observations across tasks whose text contains a token starting with `prefix` and the token `query`. **Deferred scope.** A `scope` parameter exists in the runner (defaulting to observations) but is not in the registered wire schema, so tool-call search is deferred. Treat observation search as the advertised contract. See also: [The task graph](/cairn/concepts/the-task-graph) — [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs). ### task_get Read computed task state: status, description, parent, children, edges, metadata, and recent observations. Capability gate: `:cairn/read`. Signature: `task_get(task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | string | no | Task to read. Defaults to the current task. | Returns: a rendered state block, or `No task .` when the slug is unknown. Records no event. Semantics: the state is computed from the event log, not stored as a row of flags. The block carries the slug and status, then any description, parent, children, outgoing edges, metadata pairs, and up to five most recent observations. When more than five observations exist, a final pointer line — `… N earlier observations — call timeline with full=true, types=observation to read them all` — names how to read the rest. Each edge line prints the edge type first, then the destination slug. It is the same snapshot embedded at the top of [`task_bootstrap`](#task-bootstrap). Example: `task_get(task_id="2026-06-22-add-prefix-query-support")` → ```text 2026-06-22-add-prefix-query-support [active] add prefix query support parent: 2026-06-22-tune-search edges: depends-on 2026-06-22-tokenizer-fix recent: - prefix queries land; tokenizer fix is next ``` See also: [`timeline`](#timeline) — [`task_bootstrap`](#task-bootstrap) — [The task graph](/cairn/concepts/the-task-graph). ### timeline Recent events for a task, most recent first. Capability gate: `:cairn/read`. Signature: `timeline(task_id?, types?, full?, before_seq?, after_seq?, limit?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | string | no | Task to read. Defaults to the current task. | | `types` | string | no | Comma-separated event-type filter, validated against the live vocabulary. Omitted or blank means every type. | | `full` | boolean | no | When true, render each event verbatim — a header line plus the untruncated multi-line body — instead of the one-line digest. Defaults to false. | | `before_seq` | integer | no | Exclusive upper seq bound; also the page-back cursor printed when output is truncated. | | `after_seq` | integer | no | Exclusive lower seq bound. | | `limit` | integer | no | Number of events. Defaults to 20. | Returns: a header line ` — last events` followed by one line per event (sequence number, event type, and a one-line digest of its payload). With `full=true`, each event instead renders as a header ` ( UTC)` and its verbatim body, block-indented when multi-line. Output is capped at a soft character ceiling: when the next event would exceed it, emission stops and prints `… truncated at chars; events remaining; pass before_seq= to continue`. Records no event. Semantics: where [`task_get`](#task-get) shows computed state, `timeline` shows the raw event stream that produced it — `observation`, `task.create`, `task.fork`, `task.link`, `task.sever`, `task.update-status`, `task.set-metadata`, `handoff.create`, and so on. By default each digest is collapsed to a single line and truncated; `full=true` renders bodies verbatim, making `timeline` the way to read a task's complete observation history. Filter to one or more event types with `types` — an unknown type errors with a nearest-match suggestion rather than silently returning nothing. Window or page with `before_seq` (exclusive) and `after_seq` (exclusive): feeding the `before_seq` cursor from a truncated page back into the next call returns the older events with no gap and no overlap. Read it to reconstruct how a task got where it is. Example: `timeline(task_id="2026-06-22-add-prefix-query-support", limit=3)` → ```text 2026-06-22-add-prefix-query-support — last 3 events 7 handoff.create prefix queries land; tokenizer fix is next → ... 6 task.update-status → active 5 observation bm25 ranking confirmed against t/cairn-search.lisp ``` Example: `timeline(types="observation", full=true)` renders every observation on the current task verbatim — full multi-line bodies, no 140-character digest cut — newest first. See also: [`task_get`](#task-get) — [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile). ### task_query Run a read-only query over the task graph and return text. The query is a single s-expression in cairn's query language (TQ). Capability gate: `:cairn/read`. Signature: `task_query(query)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | yes | One TQ s-expression. A Lisp form, not a bare word — names and probes are wrapped in parens. | Returns: the rendered query result as text. Every form yields a value or a structured error, never a crash or a silent no-op. Records no event. Semantics: a query is a `SOURCE` optionally threaded through `STEP`s with `->`, for example `(-> (current) (:follow :phase-of) (:ids))`. Transformer steps compose node-set to node-set; shaper steps such as `(:select :slug :status)`, `(:count)`, and `(:ids)` terminate into a value. Pass a reflective form on its own to learn the surface: `(schema)` lists the sources, steps, predicates, and write forms, with steps and predicates carrying their kinds and argument signatures — call it first; `(views)` lists named views, `(fields)` the queryable fields, `(edges)` the edge types. These are sources that list names, so project with `(:select …)`. An unknown field, predicate, step, source, or edge errors with a nearest-match suggestion rather than a silent miss. Timestamps (`created-ts`, `updated-ts`, `status-ts`) are universal-time, so a date intent is one predicate — `(on :updated-ts "2026-06-24")`, `(since …)`, `(before …)` — and an integer operand to a numeric comparison against a timestamp is read as Unix seconds. Run a named view by wrapping it as `(query "plan")` or `(query "plan-frontier")`; a bare word like `plan` is a query error, since it is not a source form. The `!`-forms — mutations and view definitions — are refused here; use [`task_query_write`](#task-query-write). Example: `task_query(query="(-> (current) (:follow :phase-of) (:select :slug :status))")` → the current task's phase-of children with their statuses. See also: [The TQ language](/cairn/reference/tq-language) — [Views](/cairn/reference/views). ### task_bootstrap Orient on a task in one call: its computed state, neighbors, open handoffs, and recent observations. Capability gate: `:cairn/read`. Signature: `task_bootstrap(task_id?)` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | string | no | Task to orient on. Defaults to the current task; with neither, fails with `No task to bootstrap; pass task_id or select a task first.` An unknown slug fails with `No task .` and lists recent slugs. | Returns: the [`task_get`](#task-get) state block, then any open handoffs. Records no event. Semantics: `task_bootstrap` is the single-call session-start read. An explicit `task_id` switches the current pointer to that task — orienting on a task makes it current, so spawned sessions and the injected context agree on what is in focus. With no `task_id`, it orients on the current task and adopts it only when none is set; the pointer switches only after the task is found. Example: `task_bootstrap(task_id="2026-06-22-add-prefix-query-support")` → the task's state block and its open handoffs. See also: [The current task pointer](/cairn/concepts/the-current-task-pointer) — [The cairn-method](/cairn/concepts/the-cairn-method). #### The TQ query language TQ is the S-expression query language cairn evaluates over the [task graph](/cairn/concepts/the-task-graph). A query is one source form, or a source threaded through pipeline steps with `->`. It is read with `*read-eval*` disabled and walked, never evaluated, then lowered onto SQL over the task projection plus a few in-memory folds. Every result renders to text before it leaves the runner, so nothing but a string crosses the tool boundary, and every failure is a structured `Query error:` or `Parse error:` rather than a backtrace. Two tools run TQ: [`task_query`](/cairn/reference/tools#task-query) reads, and [`task_query_write`](/cairn/reference/tools#task-query-write) reads and writes. The language is the same on both; one dynamic gate decides whether a mutation form is allowed. ## Synopsis ```text QUERY := SOURCE | (-> SOURCE STEP...) SOURCE := (all) | (active) | (dormant) | (current) | (node "") | (query "") | (schema) | (views) | (fields) | (edges) | (define! "" QUERY) | (undefine! "") # write surface only STEP := ( ARG...) ``` A query is exactly one S-expression. The bare `->` head opens a pipeline whose first element is a source and whose rest are steps applied left to right. A step is a keyword form like `(:follow :phase-of)`; a bare keyword such as `:ids` is rejected — every operator is a form. A source carrying the wrong arity or an empty string argument errors rather than returning an empty set. ## Read versus write The `!`-suffix marks every mutation. `task_query` binds the gate off, so any `!`-form — a `(:set-status! ...)` step or a `(define! ...)` source — is refused: ```text Query error: Mutations need the task_query_write surface; task_query is read-only. ``` `task_query_write` binds the gate on and accepts them. The gate keys on the `!`-suffix and the step's `:mutation` kind, not on which tool is registered, so a write can never slip through the read surface. Inside set-operation operands and inside `(define! "name" Q)` the gate is forced off regardless of surface: you combine and define over selections, you do not mutate inside them. A mutation form within an operand is refused even under `task_query_write`. See [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities). ## Sources A source is a nullary producer of a node-set. A node is a slug paired with its hydrated properties. The four broad sources exclude the reserved `@`-prefixed namespace (the `@cairn` view node); real task slugs are date-prefixed, so nothing a user created is dropped, and `(node ...)` and `(current)` still address reserved nodes. | Form | Yields | Notes | | --- | --- | --- | | `(all)` | Every task. | Excludes the reserved namespace. | | `(active)` | Tasks with status `open`, `active`, or `blocked`. | The live half of the status enum. | | `(dormant)` | Tasks with status `completed` or `abandoned`. | The settled half. | | `(current)` | The current task. | Errors when no task is selected: `This query needs a current task; select one first.` | | `(node "substr")` | Tasks whose slug contains the substring. | Case-insensitive; a non-empty pattern is required. | | `(query "name")` | The named view resolved to its node-set. | An unknown name errors and enumerates the available views. | | `(schema)` | The source/step/write grammar as a node-set. | Reflective; see below. | | `(views)` | The named views, built-in and user, as a node-set. | Reflective. | | `(fields)` | The queryable fields and their value types. | Reflective. | | `(edges)` | The edge vocabulary and each type's class. | Reflective. | | `(define! "name" Q)` | Records `Q` as a named view; returns `Q`'s tasks. | Write surface only. | | `(undefine! "name")` | Removes a user view, restoring any built-in; returns nothing. | Write surface only. | `recent` is a **view**, not a source: reach it with `(query "recent")`. There is no `(recent)` source form. The list of resolvable view names lives in [Views](/cairn/reference/views). ## Steps A step transforms or shapes the running node-set. Transformers map a node-set to a node-set and compose; shapers terminate the pipeline into a final value (a projection, a group, a slug list, or a count); mutations write and return their input so they keep composing. A shaper must be last — a transformer after a terminal value errors with `This step expects a set of tasks, but the previous step produced a final value.` **Transformers** map a node-set to a node-set, so they chain. ### `(:follow EDGE)` The tasks one `EDGE` hop forward. Structural edges (`phase-of`, `forked-from`) traverse the parent foreign key; lateral edges (`depends-on`, `related`) traverse the edges table. The edge is validated first, so an unknown type errors even over an empty input — the edge is a property of the query, not of the data. ```lisp (-> (node "auth") (:follow :phase-of) (:ids)) ``` ### `(:back EDGE)` The tasks one `EDGE` hop backward — the inverse of `:follow`. From a forked child, `(:back :phase-of)` reaches its parent. ```lisp (-> (node "phase-3") (:back :phase-of) (:ids)) ``` ### `(:where PRED)` The tasks satisfying predicate `PRED`. A predicate that references a non-local field auto-enriches the set on demand; a predicate over a base field reads it directly without enrichment. See [Predicates](#predicates). ```lisp (-> (active) (:where (= :status "blocked")) (:ids)) ``` ### `(:sort FIELD [:asc|:desc])` The node-set ordered by `FIELD` — numeric when the field holds numbers, lexical otherwise. The direction is optional and defaults to `:desc`; pass `:asc` to ascend. A direction that is neither `:asc` nor `:desc` is a structured error with a suggestion, not a silently ignored argument. ```lisp (-> (active) (:sort :updated-ts)) ; most-recent first — the default (-> (active) (:sort :slug :asc) (:ids)) ; A→Z by slug ``` ```text Query error: :sort needs :asc or :desc; did you mean :asc? ``` ### `(:take N)` The first `N` tasks. `N` must be an integer; a negative `N` clamps to zero. ```lisp (-> (active) (:sort :obs-count) (:take 10)) ``` ### `(:enrich)` Add counts and promoted metadata (`:obs-count`, `:edge-count`, and any metadata keys) to each node, so fields beyond the hydrated base become readable and printable. ```lisp (-> (current) (:enrich)) ``` ### `(:union Q)` Tasks in the pipeline or in sub-query `Q`, deduplicated by slug. `Q` must itself yield a node-set, evaluated read-only. ```lisp (-> (active) (:union (dormant)) (:count)) ``` ### `(:intersect Q)` Tasks in both the pipeline and sub-query `Q`. ```lisp (-> (all) (:intersect (active)) (:count)) ``` ### `(:minus Q)` Pipeline tasks not in sub-query `Q`. ```lisp (-> (all) (:minus (active)) (:ids)) ``` ### `(:or-else Q)` The pipeline if it holds any task, else sub-query `Q`. Used by the `plan` view to fall back from a root's children to its siblings. ```lisp (-> (current) (:follow :phase-of) (:or-else (active))) ``` ### `(:closure EDGE...)` The forward transitive closure over one or more edge types, cycle-safe and bounded to depth 5. The starting nodes are excluded unless a cycle leads back to one. Each edge is validated eagerly. The `knowledge` view is `(:closure :phase-of :depends-on :related)`. ```lisp (-> (current) (:closure :phase-of :depends-on :related) (:enrich)) ``` **Shapers** end the pipeline in a final value and must come last. The text below each shaper is exactly what the runner returns. ### `(:select FIELD...)` Project the named fields; at least one field is required. Every selected field is validated against the [field vocabulary](/cairn/reference/edges-statuses-and-fields), so an unknown field is a structured error with a nearest-match suggestion rather than a silently dropped column: ```lisp (-> (all) (:select :updated-at)) ``` ```text Query error: Unknown field :updated-at; did you mean :updated-ts? ``` A projected field that is not already on the node — a count or a promoted metadata key — is enriched on demand, so it shows its value instead of nothing. The result is rectangular: every selected field prints on every row, an absent value as the empty-set glyph `∅`, and a [timestamp field](/cairn/reference/edges-statuses-and-fields#fields) as its raw universal-time integer followed by the decoded UTC date. ```lisp (-> (node "auth") (:select :display-name :status)) ``` ```text 1 task: - 2026-04-02-auth-rework display-name=Auth rework status=active ``` ```lisp (-> (node "rate-limit") (:select :slug :obs-count :updated-ts :parent)) ``` ```text 1 task: - 2026-04-05-rate-limit slug=2026-04-05-rate-limit obs-count=4 updated-ts=3984388200 (2026-04-05) parent=∅ ``` ### `(:group-by FIELD)` Bucket the node-set by `FIELD` value, largest bucket first. ```lisp (-> (active) (:group-by :status)) ``` ```text 2 groups: ## active (2) - 2026-04-02-auth-rework [Auth rework] (active) - 2026-04-05-rate-limit (active) ## blocked (1) - 2026-03-30-token-store (blocked) ``` ### `(:ids)` The slugs only, one per line, under a count line. ```lisp (-> (active) (:ids)) ``` ```text 3 tasks: - 2026-03-30-token-store - 2026-04-02-auth-rework - 2026-04-05-rate-limit ``` The count line pluralizes: a one-task result reads `1 task:`, and an empty result is the single line `No matching tasks.` ### `(:count)` The cardinality, as a bare number with no surrounding text. ```lisp (-> (all) (:count)) ``` ```text 2 ``` **Mutations** are transformers that also write. A mutation records one event per real task through the durable [event log](/cairn/concepts/events-projection-and-reconcile) and returns its input set, so `(:count)` after it reports the size of the set it ran over — equal to the number of real tasks touched when no synthesized nodes are present. A synthesized node — a grammar node from a reflective source — carries no task id and is passed over, never minting a phantom, yet stays in the returned set. Events are append-only and their projection effects converge, so re-running a mutation is safe. Every mutation requires the `task_query_write` surface. ### `(:set-status! STATUS)` Set each task's status. `STATUS` must be one of `open`, `active`, `completed`, `abandoned`, `blocked`. ```lisp (-> (active) (:set-status! "completed") (:count)) ``` ```text 3 ``` ### `(:set! :key "value")` Set a metadata field on each task. The key is a field keyword; it refuses `:slug` and `:status` (status has its own step). ```lisp (-> (current) (:set! :objective "ship it")) ``` ### `(:link! :edge "target")` Add an edge from each task to the constant `target` slug. `:edge` is one of `phase-of`, `depends-on`, or `related` — the same closed enum the [`task_link`](/cairn/reference/tools) tool takes. A `depends-on` or `related` link lands in the edges table; a `phase-of` link is structural and folds into the parent foreign key rather than the lateral table, exactly as the [edges reference](/cairn/reference/edges-statuses-and-fields) describes. A task equal to the target is skipped — a task does not edge to itself. ```lisp (-> (node "phase-2") (:link! :depends-on "phase-1")) ``` ### `(:unlink! :edge "target")` Remove the edge from each task to `target`. `:edge` is one of `phase-of`, `depends-on`, or `related`, matching `(:link!)`. ```lisp (-> (node "phase-2") (:unlink! :depends-on "phase-1")) ``` ## Predicates A predicate is the argument to `(:where ...)` and to quantifiers. It is a form of `(slug props)` and composes with boolean combinators. | Form | Meaning | | --- | --- | | `(= LHS v)` | `LHS` equals literal `v`. `LHS` is a field keyword or `(count TRAV)`. | | `(has :field)` | `:field` is present and non-null on the node. | | `(matches :field "substr")` | `:field`'s value contains the case-insensitive substring. | | `(> LHS n)` | `LHS` is greater than real number `n`. | | `(< LHS n)` | `LHS` is less than `n`. | | `(>= LHS n)` | `LHS` is at least `n`. | | `(on :ts "YYYY-MM-DD")` | timestamp field `:ts` falls on the given UTC day. | | `(since :ts "YYYY-MM-DD")` | `:ts` is on or after the given UTC day. | | `(before :ts "YYYY-MM-DD")` | `:ts` is before the given UTC day. | | `(and P...)` | Every sub-predicate holds. | | `(or P...)` | Some sub-predicate holds. | | `(not P)` | `P` does not hold. | | `(all TRAV P)` | `P` holds for every task one `TRAV` hop away. | | `(any TRAV P)` | `P` holds for some task one `TRAV` hop away. | | `(none TRAV P)` | `P` holds for no task one `TRAV` hop away. | In a quantifier, `TRAV` is `(:follow EDGE)` or `(:back EDGE)`. The traversal's edge is validated eagerly, so an ill-typed quantifier errors even over an empty focus set. Over a focus node with no neighbours, `all` and `none` are vacuously true and `any` is false. Numeric comparisons are type-checked. The left side of `>`, `<`, or `>=` must be a numeric field (one of `created-ts`, `updated-ts`, `status-ts`, `obs-count`, `edge-count`) or `(count TRAV)`, and the operand a real number; comparing a text field numerically is an error, not a silent empty match. `(count TRAV)` is the cardinality of a relative traversal, usable wherever a numeric left side is. ```lisp (-> (node "release") (:where (> (count (:follow :depends-on)) 1)) (:ids)) ``` The full field vocabulary and which fields are textual versus numeric is in [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields). ### Dates and timestamps `created-ts`, `updated-ts`, and `status-ts` are `:timestamp` fields: CL universal-time, an integer count of seconds since `1900-01-01 UTC`. They order as integers and read as dates. The `on` / `since` / `before` predicates window the clock by a calendar day so a date intent is one predicate, not a reverse-engineered range. Each parses a strict `YYYY-MM-DD` operand at UTC midnight; `on` matches the 24-hour day `[day, day+1)`, `since` matches `≥ day`, and `before` matches `< day`. ```lisp (-> (active) (:where (on :updated-ts "2026-06-24")) (:ids)) (-> (active) (:where (since :created-ts "2026-06-01")) (:count)) ``` A date predicate is gated to timestamp fields, and a malformed date is a structured error rather than a silent empty result: ```text Query error: on needs a timestamp field; :status is text. Query error: "not-a-date" is not a YYYY-MM-DD date. ``` Because a timestamp orders as an integer, the numeric predicates compare it too. An **integer** operand to `>`, `<`, or `>=` against a timestamp field is read as Unix seconds and lifted onto the same universal-time clock the write path uses, so a Unix-epoch literal lands where you mean it instead of below every value. `(< :updated-ts 2500000000)` compares against the lifted instant, not the raw integer. ### Unknown names suggest a fix Every dispatched name — a field, a predicate, a step, a source, an edge type — is validated against its vocabulary, and an unknown one is a structured error carrying the nearest match within a small edit distance, or the full list when nothing is close. The query never silently misfires on a typo. ```text Query error: Unknown predicate matchez; did you mean matches? Query error: Unknown step :zelect; did you mean :select? Query error: Unknown source recent; known: active, all, current, dormant, edges, fields, node, query, schema, views. ``` ## Reflective forms `(schema)`, `(fields)`, `(edges)`, and `(views)` are sources that render the language's own vocabularies as node-sets in the same carrier task queries produce, so every step composes over them. The slugs are bare names, never date-prefixed, so they never collide with a task, and a task traversal over a grammar node is an honest empty set rather than an error. Each list is generated from the registry the interpreter dispatches on, so the description cannot drift from behaviour. | Source | Each node carries | | --- | --- | | `(schema)` | `:category` (`source`/`step`/`predicate`/`write`); `:kind` on steps (`transformer`/`shaper`/`mutation`) and predicates (`leaf`/`combinator`/`quantifier`); `:signature` (the machine-readable argument contract) on steps and predicates; and `:doc`. | | `(fields)` | `:origin` (`declared` or `metadata`), `:type` (`text`, `number`, or `timestamp`), and on a timestamp field `:unit` and `:epoch`. | | `(edges)` | `:class` (`structural` or `lateral`). | | `(views)` | `:origin` (`builtin` or `user`) and `:source` (the view's TQ text). | `(schema)` lists predicates alongside sources and steps, so the predicate vocabulary — including the date predicates and their signatures — is discoverable from the one call. There is no `(help)` source; `(schema)` is the canonical entry point to the grammar. Probe the grammar with the algebra: ```lisp (-> (schema) (:where (= :kind "shaper")) (:ids)) (-> (schema) (:where (= :category "predicate")) (:select :kind :signature)) (-> (fields) (:where (= :type "timestamp")) (:ids)) (-> (edges) (:where (= :class "structural")) (:ids)) (-> (views) (:where (= :origin "builtin")) (:ids)) ``` The first lists the shapers by their bare names, sorted with the rest of the carrier: ```text 4 tasks: - count - group-by - ids - select ``` The second reads the predicate vocabulary with its kinds and signatures — for example the three date predicates share the signature `field-ts lit-date`, so `(-> (schema) (:where (= :signature "field-ts lit-date")) (:ids))` recovers `before`, `on`, and `since`. Reflective sources run on the read surface — they describe, never mutate — and stay total. Use `(:select ...)` to project a field, for example `(-> (schema) (:where (= :category "step")) (:select :doc))`. ## Worked examples Each program is one full query. Read-surface examples run under either tool; write-surface examples require [`task_query_write`](/cairn/reference/tools#task-query-write). The text fence under each query is the runner's actual output. ```lisp (-> (active) (:where (not (has :parent)))) ``` ```text 2 tasks: - 2026-04-02-auth-rework [Auth rework] (active) - 2026-04-05-rate-limit (active) ``` Read. Active tasks with no parent — the `active-roots` view: every live root in the graph. With no `(:ids)` or `(:select ...)` shaper the result is the node-set itself, one `- slug [display] (status)` line per task; `:parent` is a base field, so the predicate reads it without enrichment and the lines carry no `obs=`/`edges=` counts. Add `(:enrich)` before the implicit render to print them. ```lisp (-> (current) (:follow :phase-of) (:ids)) ``` ```text 3 tasks: - 2026-04-02-auth-rework-phase-1 - 2026-04-02-auth-rework-phase-2 - 2026-04-02-auth-rework-phase-3 ``` Read. The phases directly under the current task. ```lisp (query "plan-frontier") ``` Read. The phases of the current plan that are not done and whose every `depends-on` dependency is settled — the ready work. `(query "frontier")` is an alias. ```lisp (-> (node "auth") (:where (matches :description "oauth")) (:ids)) ``` Read. Tasks whose slug contains `auth` and whose description mentions `oauth`. ```lisp (-> (active) (:union (dormant)) (:count)) ``` ```text 2 ``` Read. The size of the whole task set, by the status partition. ```lisp (-> (current) (:closure :phase-of :depends-on :related) (:enrich)) ``` Read. The transitive neighbourhood of the current task across every edge — the `knowledge` view. ```lisp (-> (active) (:set-status! "completed") (:count)) ``` ```text 3 ``` Write. Complete every active task and report how many were touched; re-running over the now-empty active set touches nothing and returns `0`. ```lisp (-> (node "phase-2") (:link! :depends-on "phase-1")) ``` Write. Add a `depends-on` edge from each `phase-2` task to `phase-1`. The link step returns its input set, so the rendered output is the touched `phase-2` nodes. ```lisp (define! "blocked-work" (-> (active) (:where (= :status "blocked")))) ``` Write. Record a user view named `blocked-work`; it then resolves on the read surface via `(query "blocked-work")` and shadows any built-in of the same name. `(undefine! "blocked-work")` removes it. ## Related - [Tools](/cairn/reference/tools#task-query) — the `task_query` and `task_query_write` tool surface that runs TQ. - [Views](/cairn/reference/views) — the built-in named views, each with its TQ source text. - [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — the closed enums TQ predicates and steps validate against. - [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities) — why the `!`-gate is one dynamic boundary. #### Built-in and user-defined views A view is a named query: a label bound to a piece of [TQ source text](/cairn/reference/tq-language) that resolves to a set of tasks when you call `(query "name")`. cairn ships ten built-in views, expressed in TQ itself. They are the language's shipped vocabulary — `plan-frontier`, `leaf-tasks`, `stale-phases`, and the rest — and you read them on the read surface through [`task_query`](/cairn/reference/tools#task-query) or define your own through [`task_query_write`](/cairn/reference/tools#task-query-write). Each built-in is data, not tool surface. The view's body is ordinary TQ, so you can read the definition with `(-> (views) (:where (= :origin "builtin")) (:select :source))`, copy it, and adapt it. A view defined under your own name shadows the built-in of the same name; remove the user view and the built-in reappears. ## Reading a view Resolve any view by name with the `(query "name")` source: ```text task_query (query "plan-frontier") ``` The result renders as a task list — one line per task, slug first, then display name, status, and any counts the view enriched: ```text 2 tasks: - 2026-06-20-write-the-parser (open) obs=3 edges=2 - 2026-06-20-wire-the-cli (open) obs=1 edges=1 ``` `(query "name")` is a read. A view defined on the write surface still resolves on the read surface, because reading a view is a read regardless of how the view was created. Naming a view that does not exist is a structured error that enumerates every resolvable name — both built-in and user-defined: ```text Query error: Unknown named query plan-fronteir. Available: active-roots, orphans, leaf-tasks, stale-phases, plan, plan-frontier, frontier, recent, busy, hub-tasks, knowledge. ``` ## The ten built-in views The table lists every shipped view with its TQ source text exactly as cairn defines it. `frontier` is an alias — its body is `(query "plan-frontier")` — so the eleven accepted names resolve to ten distinct queries. | View | TQ source | Yields | | --- | --- | --- | | `active-roots` | `(-> (active) (:where (not (has :parent))))` | Live tasks with no parent — the roots of every open plan. | | `orphans` | `(-> (all) (:where (= :edge-count 0)))` | Tasks with no edges in any direction: unparented, unlinked, unforked. | | `leaf-tasks` | `(-> (active) (:where (= (count (:follow :phase-of)) 0)))` | Live tasks with no phase children — the bottom of the plan tree. | | `stale-phases` | `(-> (active) (:where (any (:back :phase-of) (or (= :status "completed") (= :status "abandoned")))))` | Live phases whose parent has already settled, a sign the plan moved on without them. | | `plan` | `(-> (current) (:follow :phase-of) (:or-else (-> (current) (:back :phase-of) (:follow :phase-of))) (:enrich))` | The phases of the current task's plan, enriched with counts. Current-scoped. | | `plan-frontier` | `(-> (query "plan") (:where (and (not (or (= :status "completed") (= :status "abandoned"))) (all (:follow :depends-on) (or (= :status "completed") (= :status "abandoned"))))))` | The ready subset of `plan`: unsettled phases whose every `depends-on` dependency is settled. | | `frontier` | `(query "plan-frontier")` | Alias for `plan-frontier`. | | `recent` | `(-> (active) (:sort :updated-ts) (:take 20))` | The twenty most recently touched live tasks. | | `busy` | `(-> (active) (:sort :obs-count) (:take 20))` | The twenty live tasks with the most observations. | | `hub-tasks` | `(-> (active) (:sort :edge-count) (:take 20))` | The twenty live tasks with the most edges — the connectors in the graph. | | `knowledge` | `(-> (current) (:closure :phase-of :depends-on :related) (:enrich))` | The transitive neighborhood of the current task across all three edge types, cycle-safe and depth-bounded. | Three of these views read the `(current)` source — `plan`, `frontier`/`plan-frontier`, and `knowledge` — so they resolve against [the current task pointer](/cairn/concepts/the-current-task-pointer) and error when no task is selected. The rest range over `(active)` or `(all)` and resolve the same way in every session. The [TQ sources](/cairn/reference/tq-language#sources) table defines each producer. ### Built-in source forms in TQ Each built-in is a complete TQ program, and every piece it draws on is documented in the [TQ language reference](/cairn/reference/tq-language). The [sources](/cairn/reference/tq-language#sources) are `(active)`, `(all)`, and `(current)`. The [steps](/cairn/reference/tq-language#steps) are `(:where)`, `(:follow)`, `(:back)`, `(:sort)`, `(:take)`, `(:or-else)`, `(:closure)`, and `(:enrich)`. Inside a predicate they reach across edges with the `(any TRAV P)` and `(all TRAV P)` quantifiers and count with the `(count TRAV)` field-expression. `plan-frontier` composes over `plan` by name, and `frontier` composes over `plan-frontier` by name, so a view defined in terms of another view resolves transitively. A view defined in terms of itself is a structured error, not an unbounded loop. Define a view named `self` whose body is `(query "self")` and resolving it reports the cycle rather than recursing — `self` in the error text is the offending view name: ```text Query error: View self is defined in terms of itself. ``` ## `recent` is a view, not a source `recent` is a view name, so you read it with `(query "recent")`. `(recent)` is not a source form. The query language has exactly ten sources — `(all)`, `(active)`, `(dormant)`, `(current)`, `(node "substr")`, `(query "name")`, `(schema)`, `(views)`, `(fields)`, `(edges)` — and `recent` is not among them. Calling `(recent)` as if it were a source is an unknown-source error that lists the ten known sources: ```text Query error: Unknown source recent; known: active, all, current, dormant, edges, fields, node, query, schema, views. ``` The same holds for every other view name: `(plan)`, `(orphans)`, and `(busy)` are not sources. Reach a view only through `(query "name")`. ## Defining your own views `define!` and `undefine!` are write forms — they record an event and are accepted only on the [`task_query_write`](/cairn/reference/tools#task-query-write) surface. The read surface refuses them: ```text Query error: Mutations need the task_query_write surface; task_query is read-only. ``` ### `define!` ```text task_query_write (define! "blocked-phases" (-> (active) (:where (= :status "blocked")))) ``` `(define! "name" Q)` validates `Q` read-only, records its source text as a `view.define` event, and returns `Q`'s tasks — so `define!` is interchangeable with the query it names. The view name must be a non-empty string. The query `Q` is evaluated read-only even on the write surface: you define over a selection, you do not mutate inside the definition. After the call, `(query "blocked-phases")` resolves on either surface. ### `undefine!` ```text task_query_write (undefine! "blocked-phases") ``` `(undefine! "name")` records a `view.undefine` event and returns no tasks. It removes the user view of that name. If a built-in of the same name exists, that built-in becomes visible again. ### Shadowing a built-in A user view of a built-in's name shadows it in the one resolver. Define `orphans` over `(dormant)`, and `(query "orphans")` now returns settled tasks instead of edgeless ones; `undefine!` it and the built-in `(-> (all) (:where (= :edge-count 0)))` resolves again: ```text task_query_write (define! "orphans" (dormant)) task_query (query "orphans") # now resolves to your definition task_query_write (undefine! "orphans") task_query (query "orphans") # the built-in is back ``` A view is a fold of `view.define`/`view.undefine` events, so a rebuild reconstructs your views exactly. See [events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) for how the view vocabulary replays from the durable log. ## Listing every view The `(views)` [source](/cairn/reference/tq-language#sources) produces every resolvable view as a queryable node-set. Each view node carries an `:origin` of `builtin` or `user` and a `:source` field holding its TQ text, so you can filter and project the catalogue with the algebra itself: ```text task_query (-> (views) (:where (= :origin "builtin")) (:ids)) ``` ```text task_query (-> (views) (:where (= :origin "user")) (:select :source)) ``` A built-in shadowed by a user view of the same name reports once, as `user` — `(views)` reflects what resolves, not what ships. ## Related - [TQ language](/cairn/reference/tq-language) — the sources, steps, and predicates every view is written in. - [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — why the frontier is the ready subset of a plan. - [Tools](/cairn/reference/tools#task-query) — the `task_query` and `task_query_write` wire surface. #### Edges, statuses, and fields cairn's task graph is built from three closed vocabularies: the edge types that connect tasks, the statuses a task moves through, and the fields a query reads. Each is a small fixed set defined in one place in the source, so a tool, a query predicate, and a stored `CHECK` constraint can never disagree about what is legal. This page is the catalogue of those sets. It also covers the reflective sources — `(edges)`, `(fields)`, `(schema)`, `(views)` — that let a query read the catalogue back from the running store. A field absent from the typed set is not an error: it is open metadata, set with [`task_set_metadata`](/cairn/reference/tools#task-set-metadata) and read as text. The closed sets below are the ones the language understands well enough to validate, classify, and type-check. ## Edge types An edge connects two tasks. cairn stores edges two ways, and which way an edge is stored decides how a query traverses it. A **lateral** edge lives in the `edges` table as an explicit row from a source task to a target task. A **structural** edge names the task fibration — a child's single parent — and folds into the child's `parent_task_id`, never the `edges` table. A query traverses a lateral edge by joining the `edges` table and a structural edge by walking the parent foreign key. The surface verb is the same ([`:follow`](/cairn/reference/tq-language#steps) / `:back`); the storage and the cardinality differ. The [task graph](/cairn/concepts/the-task-graph) page covers why the two are stored apart. | Edge type | Class | Accepted by tools | Notes | | --- | --- | --- | --- | | `phase-of` | structural | yes | A phase belongs to its parent plan. Folded into the child's `parent_task_id`; a task has at most one parent. Default `edge_type` for [`task_fork`](/cairn/reference/tools#task-fork). | | `depends-on` | lateral | yes | This task waits on its target. The ordering edge the frontier reads to decide readiness. | | `related` | lateral | yes | A loose association with no ordering or ownership meaning. | | `forked-from` | structural | no | A structural alias of the parent pointer that the traversal layer recognizes; in the store it is indistinguishable from `phase-of` (both fold into `parent_task_id`). It is never the edge a fork writes — a fork records `phase-of` by default — and never a value you pass as `edge_type`. | ### The tool-accepted vocabulary [`task_link`](/cairn/reference/tools#task-link), [`task_sever`](/cairn/reference/tools#task-sever), and [`task_fork`](/cairn/reference/tools#task-fork) accept exactly three `edge_type` values: `phase-of`, `depends-on`, `related`. The check is case-insensitive — `Depends-On` is accepted and lowercased — and anything outside the set is refused before the write: ```text "blocks" is not a valid edge type; expected one of phase-of, depends-on, related. ``` `blocks` and `blocked-by` look like they should work, and they read cleanly, but they are **not** part of the tool-accepted vocabulary. Internally cairn projects both onto `depends-on` while preserving the original name in a tag; that projection is an implementation detail of how a raw edge is normalized, not a value the tools advertise. Pass `depends-on` directly. ### Traversal classes A query traversal classifies its edge keyword the same way before it runs, and an unrecognized edge is a structured error rather than a silently empty result — the edge is a property of the query, not of the data. The error carries the nearest known type, or the full list when nothing is close: ```text Unknown edge type spawned-by; known: :depends-on, :forked-from, :phase-of, :related. ``` Note the traversal vocabulary is wider than the tool vocabulary: a query may traverse `forked-from` (and `phase-of`) because both name the structural fibration, even though only `phase-of` is a tool-accepted `edge_type`. The mutation steps [`:link!`](/cairn/reference/tq-language#steps) / `:unlink!` narrow back to the three tool-accepted edge_type values (`phase-of`, `depends-on`, `related`), mirroring the write tools. ## Statuses Every task carries exactly one of five statuses. The set is closed and the SQLite `CHECK` constraint mirrors it, so an invalid status cannot reach the store. | Status | Half | Meaning | | --- | --- | --- | | `open` | active | Created, not yet started. | | `active` | active | In progress. | | `blocked` | active | Stalled on an external condition; still live work. | | `completed` | dormant | Finished and settled. | | `abandoned` | dormant | Settled without completion; will not be resumed. | The status enum partitions into an **active** half (`open`, `active`, `blocked`) and a **dormant** half (`completed`, `abandoned`). This is the partition the query sources read: [`(active)`](/cairn/reference/tq-language#sources) yields the live half and `(dormant)` yields the settled half. A plan's [frontier](/cairn/concepts/plans-phases-and-the-frontier) is then ready precisely when a phase is not dormant and all of its `depends-on` targets are. The partition is checked at load time against the full enum, so a status can never be live and settled at once, nor fall outside both halves. [`task_update_status`](/cairn/reference/tools#task-update-status) lowercases its `status` argument and validates it against the set; anything else is refused: ```text "done" is not a valid status; expected one of open, active, completed, abandoned, blocked. ``` `completed` and `abandoned` are not terminal in the data — a task is reopened by setting it `active` again, and the `reopen` flag on `task_update_status` is shorthand for exactly that. They are terminal only in the sense that the dormant half drops out of the active sources until you move it back. ## Fields A field is a named, typed value a query reads off a task. Eleven fields are declared with a type; the type is what lets a numeric predicate refuse a textual field instead of comparing garbage, and a date predicate refuse anything but a clock. | Field | Type | Source | | --- | --- | --- | | `slug` | text | The task's date-prefixed identifier; the node's own name. | | `status` | text | The current status (see above). | | `description` | text | The freeform description set at creation. | | `depot` | text | The depot the task belongs to. | | `parent` | text | The slug of the structural parent, or absent for a root. | | `display-name` | text | A human-readable label; defaults to the slug. | | `created-ts` | timestamp | Creation time, universal-time seconds. | | `updated-ts` | timestamp | Last-touched time, universal-time seconds. | | `status-ts` | timestamp | Time of the last status change. | | `obs-count` | number | Number of observations recorded on the task. | | `edge-count` | number | Number of edges incident to the task (lateral plus structural). | The six **text** fields are `slug`, `status`, `description`, `depot`, `parent`, `display-name`. The two **number** fields are `obs-count` and `edge-count`. The three **timestamp** fields are `created-ts`, `updated-ts`, and `status-ts`. The type drives predicate type-checking: `(> :obs-count 3)` compiles because `obs-count` is numeric, while `(> :status 1)` is rejected as ill-typed before it touches a row: ```lisp (-> (active) (:where (> :status 1))) ``` ```text Query error: > needs a numeric field; :status is text. ``` A **timestamp** is CL universal-time — an integer count of seconds since the epoch `1900-01-01 UTC`. It is numeric-comparable, so it orders and compares as an integer (`(> :updated-ts 0)` is well-typed), and it is the only type the [date predicates](/cairn/reference/tq-language#dates-and-timestamps) `on` / `since` / `before` accept, letting `(on :updated-ts "2026-06-24")` express a calendar-day intent directly. When projected with [`:select`](/cairn/reference/tq-language#steps), a timestamp renders as its raw integer and its decoded UTC date — `updated-ts=3984388200 (2026-04-05)` — so the integer clock stays legible. The unit and epoch are not buried in this page: `(fields)` carries them on every timestamp field, below. Any field not in this table is **open metadata** — a key written with `task_set_metadata` or the [`:set!`](/cairn/reference/tq-language#steps) step. It is read on demand and treated as text, so `(matches :owner "mika")` works against a metadata key cairn has never heard of; only the declared eleven get a numeric or timestamp type. The counts `obs-count` and `edge-count`, and any promoted metadata, are added by enrichment ([`:enrich`](/cairn/reference/tq-language#steps)), which a query inserts automatically when a step references a field that is not already local. ## Reflective sources The three vocabularies above are queryable from the running store, not only documented here. Four sources synthesize the catalogue into the same node-set carrier that task queries produce, so every [TQ](/cairn/reference/tq-language) step composes over them for free. The names they yield are bare (never date-prefixed), so they never collide with a real task. Each source is generated from the registry the interpreter dispatches on, so the readout cannot drift from behavior. | Source | Yields | Per-node props | | --- | --- | --- | | `(edges)` | The edge vocabulary, one node per type | `:class` — `structural` or `lateral` | | `(fields)` | The declared fields plus any live metadata keys | `:origin` (`declared` / `metadata`), `:type` (`text` / `number` / `timestamp`), and `:unit` + `:epoch` on a timestamp | | `(views)` | The named [views](/cairn/reference/views), built-in and user | `:origin` (`builtin` / `user`), `:source` (TQ text) | | `(schema)` | The combinator grammar: every source, step, predicate, and write form | `:category` (`source` / `step` / `predicate` / `write`), `:kind` (on steps and predicates), `:signature` (on steps and predicates), `:doc` | `(edges)` lists all four known edge types — the three tool-accepted plus `forked-from` — because the traversal surface understands all four. To recover only the tool-accepted lateral set, filter on the class: ```lisp (-> (edges) (:where (= :class "lateral")) (:ids)) ``` ```text 2 tasks: - depends-on - related ``` The same algebra reads the fields. List every numeric field, or every timestamp field: ```lisp (-> (fields) (:where (= :type "number")) (:ids)) ``` ```text 2 tasks: - edge-count - obs-count ``` ```lisp (-> (fields) (:where (= :type "timestamp")) (:ids)) ``` ```text 3 tasks: - created-ts - status-ts - updated-ts ``` The clock is self-describing: project a timestamp field's `:unit` and `:epoch` instead of reverse-engineering them from raw values. ```lisp (-> (fields) (:where (= :type "timestamp")) (:select :type :unit :epoch)) ``` ```text 3 tasks: - created-ts type=timestamp unit=seconds epoch=1900-01-01 UTC (CL universal-time) - status-ts type=timestamp unit=seconds epoch=1900-01-01 UTC (CL universal-time) - updated-ts type=timestamp unit=seconds epoch=1900-01-01 UTC (CL universal-time) ``` `(fields)` also lists the **open metadata** keys live in the store, each tagged `:origin "metadata"` (declared fields are `:origin "declared"`), so a query discovers the metadata vocabulary it can filter and project without reading any code — `(-> (fields) (:where (= :origin "metadata")) (:ids))`. `(schema)` describes the grammar itself, including the reflective sources — it lists `edges`, `fields`, `views`, and `schema` among its source nodes, and it lists the predicate vocabulary (each predicate tagged `:category "predicate"` with its `:kind` and `:signature`) alongside the sources and steps. That makes the language self-documenting: an agent that has the `(schema)` source can discover the rest of the surface without leaving the query, which is the same loop these docs serve for a human reader. There is no `(statuses)` source; the status partition is documented above and surfaces through `(active)` / `(dormant)` rather than a reflective list. ## Related - [Tools](/cairn/reference/tools) — the writers that consume these enums: `task_link`, `task_fork`, `task_update_status`, `task_set_metadata`. - [The TQ language](/cairn/reference/tq-language) — the sources, steps, and predicates that read fields and traverse edges. - [Views](/cairn/reference/views) — the built-in named queries phrased over these vocabularies. - [The task graph](/cairn/concepts/the-task-graph) — why structural and lateral edges are stored differently. - [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — how `phase-of`, `depends-on`, and the status partition combine into readiness. ### Commands & Serving #### Slash commands cairn registers six slash commands when it loads under a kli host: `/observe`, `/handoff`, `/task`, `/tasks`, `/workon`, and `/where`. They are the hand-typed surface over the same task graph the [MCP tools](/cairn/reference/tools) reach over the wire: a line you type at the prompt instead of a tool the model calls. Two of them, `/observe` and `/handoff`, do something an agent can also do for itself. The other four are operator conveniences the model never sees. The commands depend on a host that offers a command surface. cairn registers them only against a `:commands/v1` provider. A headless run with no such provider registers nothing, so there are no slash commands when cairn is served straight over [`kli mcp-serve cairn`](/cairn/cli/mcp-serve). For that path, use the tools directly. A command's first word names it; everything typed after the first word is one free-text tail passed to the runner. `/where` takes no tail. ## The six commands | Command | Syntax | Effect | Model-visible | MCP analogue | | --- | --- | --- | --- | --- | | `/observe` | `/observe ` | Record an observation on the current task. | yes | [`observe`](/cairn/reference/tools#observe) | | `/handoff` | `/handoff [guidance]` | Compose and record a resumable handoff for the current task. | yes | [`handoff`](/cairn/reference/tools#handoff) (the turn authors the body; the tool only scaffolds) | | `/task` | `/task [id]` | Show the current task, or select one by id. | no | partial — pointer read/set, like [`task_bootstrap`](/cairn/reference/tools#task-bootstrap) without the open-handoffs readout | | `/tasks` | `/tasks [query]` | List recent tasks, or run a task-graph query. | no | [`task_query`](/cairn/reference/tools#task-query) | | `/workon` | `/workon [id]` | Select a task and seed a re-entry turn. | no | none | | `/where` | `/where` | Show the resolved database path, project, and how it was resolved. | no | none | **Model visibility** is the `:model-visible` metadata on each command. `/observe` and `/handoff` carry no such metadata, so their effect rides in the conversation the model reads. `/task`, `/tasks`, `/workon`, and `/where` are registered with `:model-visible nil`: they run and print to you, but their echo is hidden from the model. That split lets an operator inspect or steer the graph without spending the readout as context. See [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities) for the boundary the operator surfaces sit behind. A command and its MCP analogue are not the same call. `/observe` invokes the `observe` tool unchanged. `/handoff` and `/workon` do work the tools cannot: `/handoff` drives an authoring turn and `/workon` injects a deterministic block, and both patch the transcript. The other operator surfaces — `/task`, `/tasks`, and `/where` — have their own output shapes. The sections below give each command's exact behavior and the verbatim text it prints. In the example blocks, a leading `>` marks the line you type; every other line is what cairn prints back. ## /observe Record an observation on the current task. Syntax: `/observe ` `/observe` is a thin wrapper over the [`observe`](/cairn/reference/tools#observe) tool: it passes your tail as the `text` argument, invokes the tool, and returns the tool's result unchanged. The observation lands on the current task; it does not move the current-task pointer. On success the result is the tool's own line: ```text > /observe rotation test still red at the 30-day boundary Observed on 2026-06-22-auth-rotation. ``` With no tail, the command fails before reaching the tool: ```text > /observe Usage: /observe ``` `/observe` is model-visible, so the observation and its confirmation enter the conversation. From a non-kli MCP client, call the [`observe`](/cairn/reference/tools#observe) tool directly — same effect, same return text. ## /handoff Compose and record a resumable handoff for the current task. Syntax: `/handoff [guidance]` `/handoff` is richer than the [`handoff`](/cairn/reference/tools#handoff) tool, but the command itself does little. Its only deterministic act is to start one interactive authoring turn and reply `Composing a handoff...`. The session is then handed an instruction to compose a one-line summary, call the `handoff` tool to scaffold and record the document, read the scaffold back, and overwrite it with a rich body. Those steps are what the turn is told to do, not guarantees the command performs. The tool, by contrast, only scaffolds a deterministic skeleton and records the event; it never writes prose. Any tail you pass is guidance threaded into that authoring instruction: ```text > /handoff Composing a handoff... > /handoff focus on the rotation regression; the refresh path is done Composing a handoff... ``` Because it drives an authoring turn, `/handoff` needs an interactive session. Headless, with no agent-session service, it declines and writes nothing: ```text Handoff authoring needs an interactive session. ``` From a non-kli MCP client, the [`handoff`](/cairn/reference/tools#handoff) tool is the right call: it scaffolds the skeleton and you supply the body yourself. ## /task Show the current task, or select one by id. Syntax: `/task [id]` With no tail, `/task` prints the current task's computed state: its status, description, parent, children, edges, metadata, and recent observations. With no current task set, it says so and offers recent slugs: ```text > /task No current task. Recent: 2026-06-22-auth-rotation, 2026-06-21-search-index ``` With a tail, `/task ` sets the current-task pointer to that slug and prints the task's state. An unknown id leaves the pointer untouched and lists recent slugs instead: ```text > /task 2026-06-22-auth-rotation Current task set to 2026-06-22-auth-rotation. 2026-06-22-auth-rotation [active] …computed task state… > /task no-such-task No task no-such-task. Recent: 2026-06-22-auth-rotation, 2026-06-21-search-index ``` `/task` is hidden from the model: it reads or moves the pointer without entering the conversation. The model's own way to orient and adopt a task is [`task_bootstrap`](/cairn/reference/tools#task-bootstrap), which adds open handoffs; `/task` is the bare pointer surface. See [The current task pointer](/cairn/concepts/the-current-task-pointer). ## /tasks List recent tasks, or run a task-graph query. Syntax: `/tasks [query]` `/tasks` runs the [TQ query language](/cairn/reference/tq-language) against the live graph and renders the result. With no tail, it runs the built-in `recent` view. With a tail, it interprets your tail as a TQ program, read with `*read-eval*` disabled so the form is walked and never evaluated. The query runs scoped to the current task, so current-relative forms like the `(current)` source and the current-scoped views `(query "plan")` and `(query "plan-frontier")` resolve against the pointer. ```text > /tasks …the recent view… > /tasks (-> (query "plan-frontier") (:ids)) …ready phases… ``` Three failure channels carry the underlying message verbatim, which is what lets you correct the query in place: ```text Parse error: Query error: The query could not be executed. ``` `/tasks` is hidden from the model. It is read-only: it runs the read query engine, so it does not accept TQ's `!`-forms — the write source `define!` and the mutation steps such as `:set-status!`. To define a view or mutate the graph through TQ, use the [`task_query_write`](/cairn/reference/tools#task-query-write) tool. ## /workon Select a task and seed a re-entry turn. Syntax: `/workon [id]` `/workon` has no MCP analogue. It does something only a host with a transcript can. It selects the target task — your tail, or the single most recent task when you pass none — sets the current-task pointer, records a `resume` event on the task, and injects a resume block straight into the durable transcript so the model re-enters the work with state in hand. The injected block opens with a fixed line, then the task's computed state and its open handoffs: ```text Following the cairns - resuming 2026-06-22-auth-rotation. 2026-06-22-auth-rotation [active] …computed task state… …open handoffs, newest first… ``` The command itself replies with a short confirmation, hidden from the model: ```text Working on 2026-06-22-auth-rotation. ``` When there is no task to work on — no tail and no recent task — it says so and writes nothing: ```text No task to work on. ``` An unknown id lists recent slugs and leaves the pointer untouched, the same shape `/task` uses for a miss. The transcript injection is a one-shot context patch. Headless, or when no agent context is bound, the selection and the event still happen but no block is written. Under a kli host, `/workon` is the operator counterpart to the model calling [`task_bootstrap`](/cairn/reference/tools#task-bootstrap) on re-entry. ## /where Show the resolved database path, project, and how it was resolved. Syntax: `/where` `/where` takes no tail and has no MCP analogue. It reports which store the session is talking to and how that store was chosen. The output is a fixed template: ```text Database: /…/cairn.db Resolved by: project Project: proj-1a2b3c4d5e6f (auth-service) Source: kli-dir ``` `Resolved by` is the precedence step that won — `override`, `db-path`, `data-dir`, `project`, or `global`, highest first. `Project` is the content-addressed project id and its display name. `Source` is how the project root was found: `setting`, `kli-dir`, `repo`, or `scratch`. When the session falls back to the reserved scratch project, a final line is appended: ```text Scratch: yes ``` `/where` is hidden from the model. See [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) for how the store behind that path is built and rebuilt. ## Related - [Tools](/cairn/reference/tools) — the fourteen MCP tools these commands sit over, with full parameter tables and return text. - [Serve cairn over MCP](/cairn/cli/mcp-serve) — the headless surface, where the tools travel but the slash commands do not. - [The current task pointer](/cairn/concepts/the-current-task-pointer) — what `/task` and `/workon` move, and the adopt-when-unset rule. - [TQ language](/cairn/reference/tq-language) — the query language `/tasks` runs, including why it refuses `!`-forms. - [`handoff` tool](/cairn/reference/tools#handoff) — the deterministic scaffold `/handoff` authors on top of. #### Serving over MCP `kli mcp-serve cairn` runs a kli built with the cairn extension as a Model Context Protocol server over stdio. The process exposes one extension's surface to a single client: cairn's fourteen tools, seven prompts, and seven readable resources. A client connected this way plans, observes, queries, and resumes against the same durable task graph it would reach inside kli, without adopting kli as its agent. This page documents the command, its client configuration, and the exact surface it carries. For the install paths behind the `kli` binary, see [Install cairn](/cairn/get-started/install-cairn). ## The command ```text kli mcp-serve cairn ``` The invocation is `kli mcp-serve `, where `` is the name of an extension built into the kli image — here, `cairn`. The process speaks MCP over stdio, reading requests on stdin and writing responses on stdout — the transport every MCP client uses to launch a server. On launch it prints no banner to stdout; stdout carries JSON-RPC only, every diagnostic goes to stderr, and the process blocks waiting for the first request — a clean start goes quiet rather than printing anything. It serves until the client closes the connection. | Element | Value | Notes | | --- | --- | --- | | Binary | `kli` | The same `kli` you install the extension into. See [Install cairn](/cairn/get-started/install-cairn). | | Subcommand | `mcp-serve` | Runs the named extensions as an MCP server. | | Argument | `cairn` | The extension to expose. One process serves only the extensions you name. | | Transport | stdio | Requests on stdin, responses on stdout; diagnostics on stderr. | | Scope | one client | One `mcp-serve` process serves a single client. | The working directory the client launches `kli` in selects the project whose task graph cairn opens. There is no separate database flag; the project is the directory. ## Client configuration Claude Code, Claude Desktop, and Cursor read the same `mcpServers` block. The canonical entry is: ```json { "mcpServers": { "cairn": { "command": "kli", "args": ["mcp-serve", "cairn"] } } } ``` | Field | Value | Meaning | | --- | --- | --- | | `command` | `kli` | The binary the client spawns. | | `args` | `["mcp-serve", "cairn"]` | The subcommand and the extension to expose. | The client spawns this process, lists its tools, and routes tool calls to it, picking up a newly added entry on its next restart. ## Exposed surface `mcp-serve` exposes one extension's surface and nothing else. Naming `cairn` gives the client cairn's surface alone; no other extension's tools, prompts, or resources travel on the same process. The surface is the same regardless of which client connects. ### Tools The fourteen cairn tools appear as MCP tools, each gated by the capability it declares. The full per-tool reference — parameters, return text, and semantics — is in [Tools](/cairn/reference/tools). | Capability | Tools | | --- | --- | | `:cairn/observe` | `observe` | | `:cairn/write` | `task_create`, `task_fork`, `task_link`, `task_sever`, `task_set_metadata`, `task_update_status`, `handoff`, `task_query_write` | | `:cairn/read` | `task_search`, `task_get`, `timeline`, `task_query`, `task_bootstrap` | The serve subject holds exactly the capabilities its exposed tools declare, so the `:cairn/read` / `:cairn/write` / `:cairn/observe` split applies over MCP just as it does in kli. The capability each tool declares is the gate; the tool name is not. A read-only client never reaches `task_query_write` or any other write tool. See [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities). ### Prompts Seven MCP prompts travel on the process: the six cairn-method workflow prompts plus the `cairn-method` skill, which the serve loop lists as a prompt as well as a resource. They carry the documentarian-and-TDD discipline of the cairn-method into the client's prompt picker. | Prompt | Stage of the loop | | --- | --- | | `research` | Read the ground before changing it. | | `plan` | Structure the work as a graph of phases. | | `implement` | Do one phase with the discipline it asks for. | | `validate` | Check the phase against its acceptance. | | `handoff` | Write the resumable summary. | | `resume` | Read the stones and pick the work back up. | | `cairn-method` | The whole method, expanded as a prompt. | The six workflow prompts are the named loop; see [The cairn-method](/cairn/concepts/the-cairn-method) for how they compose. The seventh, `cairn-method`, is the bundled skill surfaced in the prompt list. ### Resources Every prompt and the skill are also served as readable MCP resources — seven in all, each a markdown body the model reads straight off the prebuilt prompt or skill. The workflow prompts are advertised at `cairn://prompts/` (for example `cairn://prompts/research`), and the skill at `cairn://skills/cairn-method`; the scheme is the extension name. A client that reads the skill resource gets the method in full — the bootstrap-observe-handoff heartbeat and the discipline wrapped around it — as text the model can consult mid-task. ## What stays kli-only Three things kli provides do not travel over the tool protocol, because kli supplies them as the host rather than as MCP surface. A plain MCP client does not get them. - **Slash commands.** `/observe`, `/handoff`, `/task`, `/tasks`, `/workon`, and `/where` are registered under a kli command provider, not as MCP tools, so an MCP client cannot invoke them. See [Slash commands](/cairn/cli/slash-commands). - **Per-turn context injection.** Inside kli, the current task's live state — its status and its open handoffs — is folded into the model's view on every turn. Over MCP the client calls `task_bootstrap` itself to stay oriented. - **Compaction folding.** When kli compacts a long conversation, the observations and handoffs from the dropped span are folded into the summary so the durable markers survive the cut. An MCP client manages its own context window, and its compaction does not know about those markers. The data surface is identical either way: writes append events and fold into the same queryable projection no matter which client made them, so a graph built over MCP reads back exactly as one built inside kli. ## What a connected server reports A connected server advertises the fourteen tools listed above, among them `task_bootstrap`, `task_create`, `observe`, `task_query`, and `task_search`. `task_bootstrap` reads the live graph. In a fresh project with no current task set, it reports that there is nothing to orient on rather than failing: ```text No task to bootstrap; pass task_id or select a task first. ``` That message means the tool surface is wired and the project's graph is empty — the state a first session starts from. In a project that already has tasks, `task_bootstrap` returns state, neighbors, open handoffs, and recent observations. Either result means the server is connected and reading the right project. ## Related - [Install cairn](/cairn/get-started/install-cairn) — the declarative and runtime install ledgers behind the `kli` binary. - [Tools](/cairn/reference/tools) — the full per-tool reference for the fourteen exposed tools. - [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities) — the capability boundary that carries over MCP. - [Slash commands](/cairn/cli/slash-commands) — the kli-host steering surface that does not travel over the protocol. ## Additional pages (including unstable internal surfaces) ### Commands & Serving cairn reaches the world outside the [MCP tools](/cairn/reference/tools) through two surfaces, one for each reader: the slash commands a person types at a kli prompt, and the `kli mcp-serve cairn` server that carries the tools to any MCP client an agent runs in. ## Pages - [Slash commands](/cairn/cli/slash-commands) — the six commands cairn registers under a kli host (`/observe`, `/handoff`, `/task`, `/tasks`, `/workon`, `/where`), with syntax, model visibility, and how each relates to the MCP tools (`/workon` and `/where` have none). - [Serving over MCP](/cairn/cli/mcp-serve) — `kli mcp-serve cairn`, the stdio invocation, the `mcpServers` block clients read, and the tools, prompts, and resources it carries. The two surfaces share one task graph and read back identically; they differ only in the conveniences the kli host adds on top. Both sit behind one capability boundary; see [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities). ### Concepts Start with the one-paragraph definition in [What cairn is](/cairn/concepts/what-cairn-is); from there the section moves out to the graph and the three nouns you write, then to how plans and the current pointer move over that graph, and finally to the event log and capability boundary underneath it all. These pages are for the person who wants to understand how cairn works before trusting it with a plan; they explain, they do not instruct. When you want to run the loop yourself, go to the [Quickstart](/cairn/get-started); for exact values, go to [Reference](/cairn/reference). - [What cairn is](/cairn/concepts/what-cairn-is) — continuity for agents, the stones that name a route, and why the plan lives outside the conversation. - [The task graph](/cairn/concepts/the-task-graph) — the two stores: a typed-edge graph of slug-addressed tasks, and a full-text index of observations. - [Tasks, observations, and handoffs](/cairn/concepts/tasks-observations-handoffs) — the three nouns you write, and the events each one records. - [Plans, phases, and the frontier](/cairn/concepts/plans-phases-and-the-frontier) — why a plan is a task DAG, not a markdown file, and how the frontier finds ready work. - [The current-task pointer](/cairn/concepts/the-current-task-pointer) — the per-session slug cairn resolves slugless writes against, and what moves it. - [Events, projection, and reconcile](/cairn/concepts/events-projection-and-reconcile) — why every write is an event in a durable log, and the database a rebuildable cache. - [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities) — how the read/write/observe boundary is drawn by capability, not by tool name. - [The cairn-method](/cairn/concepts/the-cairn-method) — the loop, the heartbeat writes, and the discipline an agent uses to leave work a next session can resume. The order is dependency order: each page names a thing the pages below it lean on, so reading top to bottom never asks you to take a term on faith. ### Quickstart This is the tutorial track for a person setting cairn up and running it through an agent: the linear path from an empty install to a plan you can resume from a clean session. cairn keeps that plan in a durable task graph outside the conversation, so a context reset reloads it instead of losing it. Read these in order. Each page builds on the one before it. - [Install cairn](/cairn/get-started/install-cairn) — add cairn to kli declaratively or at runtime, serve it to any MCP client, and confirm the tool surface is live. - [Your first cairn session](/cairn/get-started/your-first-cairn-session) — run the bootstrap-observe-handoff loop once, end to end, and learn what each returned string means. - [Plan and resume](/cairn/get-started/plan-and-resume) — fork phases, order them with `depends-on`, ask the frontier what is ready, complete a phase, and bootstrap back into the plan. When you have run the loop and want to understand why the graph behaves as it does, [Concepts](/cairn/concepts) explains the task graph, the current pointer, the event log, and the capability boundary. The full surface lives in the [Reference](/cairn/reference): the fourteen MCP tools, the TQ query language, the built-in views, and the closed edge, status, and field enums. ### Reference This section is the precise spec an agent executes against — the catalogue of cairn's wire surface. cairn provides fourteen MCP tools and one query language, and everything an agent can ask or change goes through them. Because an agent consumes it directly, the site serves these pages as Markdown and an llms.txt index as well as HTML; each tool, operator, view, and enum has a stable anchor, every returned and error string is copied from source, and the pages are written to be read out of order and deep-linked. Every call carries a capability gate in its metadata — `:cairn/observe`, `:cairn/write`, or `:cairn/read` — and this section tells you which gate each tool sits behind. [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities) explains why the boundary is drawn at the capability rather than the tool name. ## Pages - [MCP tools](/cairn/reference/tools) — the fourteen tools (one observe, eight writes, five reads), each with its parameters, capability gate, returned text, and verbatim errors. - [The TQ query language](/cairn/reference/tq-language) — TQ's sources, pipeline steps, predicates, set algebra, reflection, and the one dynamic gate that separates [`task_query`](/cairn/reference/tools#task-query) from [`task_query_write`](/cairn/reference/tools#task-query-write). - [Built-in and user-defined views](/cairn/reference/views) — the ten named views cairn ships, each with its exact TQ source text, and how a user view shadows a built-in until you `(undefine!)` it. - [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — the closed vocabularies: three edge types, five statuses, eleven typed fields, and the reflective sources that read them back from the store. For the command-line surface — the six slash commands under a kli host and serving cairn over [`kli mcp-serve cairn`](/cairn/cli/mcp-serve) — see [Commands & Serving](/cairn/cli). For what the nouns mean before you call them, start at [Concepts](/cairn/concepts).