Cairnline is a coordination server, not an agent runtime. It stores durable project state and exposes it over MCP so an operator, human worker, or AI agent host can coordinate work without Cairnline knowing how that host launches, prompts, sandboxes, or supervises agents.
Core rule:
Assignment is coordination. Execution is capability-dependent.
An integrating host remains responsible for:
- selecting providers, models, local runtimes, and human workers;
- mapping project roles to host-specific agents, subagents, assistants, chats, tasks, or manual queues;
- enforcing tools, writes, network, filesystem, approval, and sandbox policy;
- protecting credentials, cookies, login sessions, secrets, and private host-memory;
- creating workspaces, branches, and worktrees when code work needs them;
- deciding whether it can launch/supervise an assignment or should leave it queued for manual/MCP pull.
Cairnline records intent, context metadata, evidence, reviews, handoffs, and memory candidates. It never grants runtime authority.
| Cairnline concept | What it means | Host mapping |
|---|---|---|
| Project | Durable identity for a body of work. | Workspace, plan, repo, issue set, launch, research thread, or design effort. |
| Root | Optional filesystem/workspace metadata. | Local folder, checkout, worktree, mounted volume, or no mapping for rootless projects. |
| Project role | Responsibility needed by the project. | Host agent persona, subagent, custom agent, human role, review lane, or queue label. |
| Skill metadata | Capability/instruction metadata and provenance. | Optional hint to load host-native instructions; not a permission grant. |
| Work item | Reviewable unit of work. | Task, ticket, todo, issue, plan step, review item, or human checklist row. |
| Assignment | Work item + role + desired execution record. | Claimable job, task run, chat launch, external process, or manual work card. |
| Desired agent | Portable hint for who/what should claim. | Host-specific agent kind, preset, model family, human worker, or "any". |
| Evidence/review/handoff | Collaboration artifacts. | Run output, PR link, notes, screenshots, review verdicts, or next-agent packet. |
| Memory candidate | Proposed durable memory awaiting approval. | Host-specific memory review queue; explicit promotion stays operator-controlled. |
Cairnline assignments can request one of four execution modes:
manual
→ mcp_pull
→ external_adapter
→ orchestrated
manual: a human or external process works from the assignment record.mcp_pull: a compatible MCP client polls, claims, reads context, records evidence, and completes work.external_adapter: an orchestrator may launch a non-MCP agent adapter.orchestrated: an orchestrator may supervise execution, retries, approvals, and runtime evidence.
The mode is a request, not authorization. A host may downgrade, ignore, or reject a mode if its policy does not allow it.
Embedding hosts that let operators change a queued assignment's role, root,
execution mode, or desired-agent metadata should use
Service.UpdateQueuedAssignment with the exact coordination snapshot and
updated_at value that were read:
replacement := assignment.Coordination()
replacement.RoleID = newRoleID
updated, err := service.UpdateQueuedAssignment(ctx, projectID, assignment.ID,
cairnline.QueuedAssignmentUpdate{
Expected: assignment.Coordination(),
ExpectedUpdatedAt: assignment.UpdatedAt,
Replacement: replacement,
})The update is compare-and-set: it preserves lifecycle/execution fields and
returns ErrConflict if another writer changed or claimed the assignment.
This prevents a stale editor from releasing a claim that may already have
started execution. Snapshot import remains an administrative/offline workflow
and is not exposed as a live whole-assignment mutation.
Portable MCP workers use a fenced pre-start claim rather than relying on the
caller-chosen claimed_by label:
queued ── claim ──> claimed (lease active) ── start ──> running (fence retained)
│
└── expiry + explicit recover ──> queued
Service.ClaimAssignmentWithLeasereturns an assignment whoseclaimcontains a server-generatedid,acquired_at, andexpires_at.- While status is
claimed, callService.RenewAssignmentClaimbefore the expiry when provisioning or context preparation may take longer than the configured TTL.WithAssignmentClaimLeaseTTLchanges the default five minutes for an embedding server;coordination.capabilitiesreports the effective value. Positive custom values are rounded up to whole seconds with a one-second minimum. - Pass the exact claim id to
PrepareAssignmentWithClaim,ReleaseAssignmentWithClaim,UpdateAssignmentStatusWithClaim, andCompleteAssignmentWithClaim. Missing ids are invalid; expired or superseded ids conflict. - When work advances out of
claimed, Cairnline retires the reservation expiry but retains its id as a fencing generation for later worker writes.
Claim ids are concurrency values, not authentication credentials. They may be
stored in portable snapshots and returned in assignment reads. Hosts still
decide which principals may invoke these methods or their MCP tools. For
content-only MCP clients, assignments.get and assignments.list include
claim_id plus claim_expires_at while the reservation is active, or
claim_fence after work starts.
If a pre-start claim expires, it stays claimed until an authorized host or
operator calls RecoverAssignmentClaim with the exact expired id. Recovery
requeues it and clears prepared execution/context references. This operation is
explicit so the host can reconcile any resources it created during preparation.
projects.operations_brief and projects.health expose the stable
recover_assignment_claim action hint for this state.
It is never valid for running, awaiting_approval, or awaiting_review work:
Cairnline cannot determine that host execution is dead and never cancels or
requeues it automatically.
The original ClaimAssignment, PrepareAssignment, ReleaseAssignment,
UpdateAssignmentStatus, and CompleteAssignment methods remain
embedding-host authority surfaces for trusted reconciliation and existing
Hecate integration. They bypass worker fencing by design and must not be
exposed directly as agent tools. Embedders with custom Store
implementations must implement the new claim-lease methods before upgrading.
Breaking (alpha): the portable worker claim contract is now fenced.
assignments.prepare and assignments.release take claim_id instead of
claimed_by; assignments.update_status and assignments.complete also
require the current claim_id. assignments.claim returns that id in both its
text and structured result. The public Store interface adds the seven leased
claim/renew/recover and fenced mutation methods, so custom stores must implement
them. Snapshot version 2 persists claim generations; version 1 imports remain
supported only as unleased host-authoritative history.
SQLite migration and v1 snapshot import preserve existing claimed rows as unleased host-authoritative state; they do not invent an expiry or silently make old work stealable. Before handing one of those rows to an MCP worker, a trusted embedding host must reconcile/release it and let the worker claim it again under the leased contract.
Handoff editors follow the same rule. Use Service.PatchHandoff,
Service.UpdateHandoffStatus, or Service.DeleteHandoff with the exact
handoff UpdatedAt value that was read. Cairnline advances the revision
monotonically and returns ErrConflict rather than overwriting a newer edit.
Snapshot restore uses a separate store-only path.
This is an intentional alpha contract break: the public Store/Service handoff
mutation signatures and the corresponding MCP tools now require a revision
token. Embedders with custom Store implementations must add the CAS, snapshot
restore, and atomic follow-up methods before upgrading.
Live assistant update_handoff proposal actions must likewise carry the exact
handoff.updated_at revision; historical proposal ledgers remain importable,
but a tokenless historical action cannot be applied.
When an operator explicitly accepts a handoff and asks for follow-up work, use
Service.AcceptHandoffWithFollowUp with the current handoff revision, the
accept_and_ensure_follow_up intent, and one stable idempotency key for that
operator action. Cairnline atomically ensures one linked portable assignment
and marks the handoff accepted. When no assignment is linked, Cairnline creates
one queued using defaults from the target work item and role; rootless work
remains rootless. An existing linked assignment is returned in its current
authoritative state, including a state that has already progressed or finished.
The command records coordination only: it never launches newly created work,
and hosts must make a separate policy-controlled launch or claim decision.
An exact-key retry preserves the original outcome and assignment identity while
returning current authoritative records. It conflicts if a later operator
decision closed or relinked the handoff.
Progress and completion transitions are also atomic, and terminal completion is
first-writer-wins. Once an assignment is completed, failed, or cancelled, later
progress or terminal writes return ErrConflict. Completion preserves an
existing started_at. A direct queued completion or failure records the
transition as both start and finish for compatibility; cancelling queued work
records only completed_at, so portable readers do not report that unstarted
work began.
Hosts that want agent-neutral interoperability should start here:
coordination.capabilities
assignments.next
assignments.claim
assignments.renew_claim (only while still claimed and nearing expiry)
assignments.context
assignments.launch_packet
evidence.record
assignments.complete
- Call
coordination.capabilitiesonce during setup or health checks to learn the server contract and boundaries. - Poll
assignments.nextwith the host's available kind and skill ids. - Claim one assignment with
assignments.claimand retain the returnedstructuredContent.claim.idfencing value. - Read
assignments.contextand/orassignments.launch_packet. Both include the project's enabled durable memory entries. - Renew with
assignments.renew_claimif the assignment remainsclaimedas expiry approaches. Pass the claim id to prepare, progress, and completion mutations. - Build the host-native prompt/run packet from the structured metadata.
- Record evidence as the run produces useful proof.
- Complete, fail, or cancel the assignment explicitly.
If the host crashes before work starts, another authorized host may explicitly
call assignments.recover_claim once the reservation expires, then claim the
queued assignment again. If it crashes after work starts, the executing host
must reconcile or resume through its execution_ref; claim expiry never makes
runtime work stealable.
execution_ref is a structured, host-neutral record of the execution a host
attached to an assignment: kind, task_id, run_id, session_id,
trace_id, and pending_approvals. Cairnline never dereferences these values;
they exist so a coordination row survives migration between hosts without
losing which execution it referred to. A host maps its own identifiers onto
these slots — a task orchestrator fills task_id/run_id, a chat-first host
fills session_id, and any host can attach trace_id for observability.
Breaking (alpha): the pre-structured bare-string execution_ref is not
accepted anywhere — MCP arguments fail with an invalid-arguments error, and
stored rows or snapshots that still hold string refs fail decode with an error
telling the operator to rebuild or re-seed the store from the host's
authoritative data. Cairnline contracts are unstable alpha; no compatibility
shim is kept.
When execution pauses on a host-side human approval gate, set the assignment
status to awaiting_approval (optionally with pending_approvals in the ref)
and return it to running once the gate resolves. The status is portable
coordination state; the approval object, its policy, and its resolution stay
host-owned. Portable readers treat awaiting_approval as blocked-on-operator,
not as active execution.
Every MCP tool failure carries a stable, machine-readable code so a host can map
a coordination failure onto its own error taxonomy (for example an HTTP status)
without parsing human prose. On failure the tool result sets isError: true,
keeps the human-readable message in content, and adds a structuredContent
error envelope:
{
"error": {
"code": "not_found",
"message": "root \"x\" not found"
}
}error.message repeats the human-readable text; error.code is one of the
fixed values below. The catalog derives from Cairnline's typed store sentinels
plus one default. Hosts should branch on error.code, treat unknown codes as
internal, and never parse error.message.
| Code | Meaning | Suggested host HTTP status |
|---|---|---|
not_found |
Referenced entity does not exist | 404 |
invalid |
Bad or missing input, including argument-decode and validation failures | 400 |
already_exists |
Id or uniqueness collision | 409 |
conflict |
Invalid state transition or claim race | 409 |
internal |
Unexpected, unclassified server-side error | 500 |
A Go host embedding Cairnline can classify errors it raises itself and reuse the
same code constants from the public package
(github.com/hecatehq/cairnline): ErrorCodeNotFound, ErrorCodeInvalid,
ErrorCodeAlreadyExists, ErrorCodeConflict, ErrorCodeInternal, and the
ClassifyErrorCode(err error) string helper (which returns "" for a nil
error and internal for anything unclassified).
Cairnline roles answer "what responsibility does this project need?" They are not agent presets or permission bundles.
Good role names:
- Architect
- Implementer
- Reviewer
- Researcher
- Designer
- Release Manager
- Operator
The host maps role plus desired-agent metadata to its own runtime:
Cairnline role + desired_agent + skill metadata
↓
host policy and operator settings
↓
host-native agent/task/chat/manual work
Examples:
- A coding agent host may map
role=Reviewer,skill_ids=["review"]to its review subagent. - A human-first host may show the same assignment in a "Review" queue.
- An orchestrator may map
desired_agent.kind="external_adapter"to one of its installed adapters only after its own approvals pass. - A rootless planning host may ignore roots entirely and use the work item brief plus memory/evidence metadata.
Skill ids are hints. Cairnline discovery stores metadata such as title, description, path, source refs, suggested tool names, and nullable permission preferences. Cairnline does not read skill bodies into prompts, execute skill code, install remote skills, or convert suggested permissions into authority.
Host behavior should be explicit:
- Load host-native skill bodies only if the host already trusts that skill source and its own policy allows it.
- Treat unknown skill ids as warnings or routing misses, not hard failures.
- Treat suggested tool/write/network permissions as review metadata.
- Keep local host instruction formats host-owned.
assignments.launch_packet is the best portable packet for a host preparing a
run. It should include:
- project identity and optional roots;
- work item and assignment metadata;
- role instructions;
- desired-agent and skill metadata;
- relevant evidence, reviews, handoffs, accepted memory, and memory candidates;
- warnings for missing, disabled, or conflicting coordination references.
Hosts should translate that packet into their own prompt format and enforce their own token, privacy, and egress policy before sending anything to a model or external process.
A host that embeds Cairnline in-process does not have to shell out to the stdio binary to speak MCP. The root package builds the fully-registered server and exposes a single per-message entry point, so the host can mount Cairnline's tool and resource surface on whatever transport it already runs.
service := cairnline.NewMemoryService() // or NewSQLiteService(ctx, path)
server := cairnline.NewMCPServer(service, version)
// Custom transport: feed each inbound JSON-RPC message through HandleMessage.
resp, ok := server.HandleMessage(ctx, msg) // ok == false for notifications (no response)
// Or run the built-in stdio loop:
// server.Serve(ctx, os.Stdin, os.Stdout)
// Optionally advertise a protocol extension during initialize:
// server.DeclareExtension("io.modelcontextprotocol/ui", nil)HandleMessage processes one JSON-RPC message and returns the encoded response;
ok is false for notifications, which produce no reply. Serve owns the stdio
read/write loop and dispatches through the same path, so stdio behavior is
identical whichever entry point the host uses. DeclareExtension advertises an
optional protocol extension in the initialize capabilities under its extension
id; Cairnline declares none by default, so a host opts in explicitly.
Cairnline should still be useful without an AI agent. A simple UI or script can:
- create rootless projects;
- create roles and work items;
- queue assignments for humans;
- record evidence and reviews;
- inspect closeout readiness;
- approve or reject memory candidates in a separate operator flow.
The same coordination state can later be claimed by an MCP-capable agent host without changing the project model.
Cairnline ships optional interactive views using the MCP Apps extension
(io.modelcontextprotocol/ui, SEP-1865). A view is a self-contained HTML
document served as a ui:// resource. A host that negotiates the extension can
render the view for a tool result; a host that does not simply uses the tool's
existing text and structuredContent. Views are additive and never change tool
call results.
- A resource served over standard
resources/readwith mime typetext/html;profile=mcp-app(note: no space after;). Itstextis a single HTML document with all CSS and JS inlined — no external requests. - It also appears in
resources/list, so a host can prefetch it at connection setup.
Cairnline's first app is Project Status (ui://cairnline/project-status), a
read-only view that renders projects.health, projects.operations_brief, and
projects.activity results.
A tool references its view through the tool descriptor's _meta:
{
"name": "projects.health",
"_meta": { "ui": { "resourceUri": "ui://cairnline/project-status" } }
}The nested _meta.ui.resourceUri path is the MCP Apps linkage. When the host
calls the tool, it may render the referenced view and deliver the tool result to
it. A host that ignores _meta still gets the unchanged text/structuredContent
response.
The server is reactive: it advertises the extension in initialize capabilities
only when at least one app is registered, declaring the supported content type:
{ "capabilities": { "extensions": {
"io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] }
} } }The host renders the view in a sandboxed iframe and drives it over JSON-RPC on
window.postMessage:
- The view posts
ui/initializeand thenui/notifications/initializedon load. - The host delivers the tool result as a
ui/notifications/tool-resultnotification whose params are a standardCallToolResult; the view readsparams.structuredContent.
Cairnline's views declare a strict, default-deny CSP
(default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline') and
grant no external origins; connect-src stays denied because postMessage needs
no network. Hosts should still render every view in a sandboxed iframe and must
not loosen the declared origins.
A single view backs all three projection tools. Per the MCP Apps maturity
guidance the view holds no durable cross-turn state; it keeps only light
per-project state within a session so the health, operations-brief, and activity
results for one project accumulate into a single combined view. That state is
keyed on project_id: the first result carrying a different project_id resets
every section, so one project's results can never bleed into another's. The view
renders whatever tool result the host delivers and never persists beyond the
session.
Views live in internal/app/views and are bundled — together with the official
@modelcontextprotocol/ext-apps SDK they use for the host bridge — to a single
self-contained HTML file (bun install then bun run build, ESM inlined into a
<script type="module">) that is committed under dist/ and embedded with
//go:embed; the runtime and go test need no JS toolchain. Register a built
view with app.RegisterApps and tag its tools with the _meta.ui.resourceUri
helper. See internal/app/views/README.md for the build (including why the
bundle targets ESM rather than IIFE) and the headless render check.
Before exposing Cairnline tools to an agent, a host should decide:
- Which mutating tools the agent may call.
- Whether assignments can be claimed automatically or require operator review.
- Which component renews pre-start claims, how it retains the non-secret claim id, and how it stops stale workers after a conflict.
- Whether evidence locators are only stored, rendered as text, or opened as links after scheme validation.
- Whether local roots are readable by the host, and under what path boundary.
- Whether skill metadata may trigger host-native instruction loading.
- How to reconcile prepared host resources before recovering an expired claim, and how to handle running work separately through host supervision.
- How to show operator confirmation for memory promotion and destructive project changes.
- Whether to render
ui://app views, and if so, only inside a sandboxed iframe under the view's declared default-deny CSP without loosening origins.
A minimal useful integration does not need orchestration. It only needs:
coordination.capabilitiesprojects.listassignments.nextassignments.claimassignments.renew_claimwhen pre-start work approaches expiryassignments.recover_claimfor explicit expired-claim recoveryassignments.contextassignments.launch_packetevidence.recordassignments.complete
That is enough for an MCP-capable host to pull one queued assignment, do work under its own policy, record proof, and hand control back to the operator.