cairn-attention · git:20260727.490969f · 2026-07-27 · sha256 bb1ebaad8edcd941

cairn-attention git:20260727.490969fA

Immutable. This exact content never changes and is served at /api/v1/blob/bb1ebaad8edcd941.

---
name: cairn-attention
description: Resolve Cairn's pending-attention queue inline (DEC drafts, baseline findings, drift events).
when_to_use: |
  Use when SessionStart flagged `attention_count > 0` — pending DEC
  drafts in `_inbox/`, baseline sensor findings, or drift from last
  GC sweep. Also chained from `cairn-adopt` Step 5 to drain fresh
  DEC drafts. Skip when operator in-flight on task or recent turn
  already deferred this surface.
---

# Skill: cairn-attention

## Host portability

This skill is shared by Claude Code, Cursor, and Codex. Resolve Cairn MCP
tools with the host's native discovery mechanism. `AskUserQuestion` means the
host's structured question UI; when that UI is unavailable, ask the identical
concise A/B/C question in chat and pause. References to restarting a client
mean restarting the active host.

You are surfacing Cairn's pending-attention queue inline so the
operator can resolve drafts and findings without leaving the chat.
Spec: `docs/PLUGIN_ARCHITECTURE.md` §11.

## Step 0 — bootstrap preflight

Before surfacing any DEC choices, verify the clone is bootstrapped.
SessionStart auto-runs `cairn join` when `core.hooksPath` is unset,
so by the time this skill engages bootstrap should be wired. If
`cairn_resolve_attention` still refuses with `BOOTSTRAP_REQUIRED`,
SessionStart's auto-bootstrap failed — call `cairn_bootstrap_retry`
once to retry inline. On `ok: true`, fall through to Step 0.7. On
`ok: false`, surface the `failed_steps` list to the operator and
end the turn (the `remediation` field of the error envelope cites
this same tool plus an agent-client restart as the recovery paths).
Never reference `cli.mjs` or `cairn join` directly in the chat
surface — Plugin spec §11 forbids exposing CLI subcommands to the
operator.

## Step 0.2 — mission attention (phase_ready_to_exit + drift)

Before the regular attention queue, handle mission-specific surfaces.
These don't go through `cairn_resolve_attention` — missions have
their own resolver tool (`cairn_mission_advance`).

Preload the mission tools alongside the existing attention tools:

```
ToolSearch(select:mcp__plugin_cairn_cairn__cairn_mission_get,mcp__plugin_cairn_cairn__cairn_mission_advance,AskUserQuestion)
```

Then call:

```
cairn_mission_get({})
```

If `active: false`, skip this step.

### 0.2a — phase ready to exit

When the active phase's tasks all graduate under `exit_gate=prompt`,
Cairn surfaces a phase-exit decision via one of two channels — both
arrive as plain context, no red "Stop hook error" frame:

1. **MCP response (primary).** When the model itself calls
   `cairn_task_complete`, the response carries a structured
   `phase_ready_to_exit` block (mission_id, mission_title, phase_id,
   phase_title, exit_criteria). The model decides whether to surface
   an `AskUserQuestion` in the same turn — do so at a natural
   stopping point unless the operator is running autonomously.
2. **UPS additionalContext (auto-graduator fallback).** When the
   Stop-hook auto-graduator completes a task (attestation written
   without an explicit MCP call), it writes the pending hint to
   `.cairn/sessions/<id>/phase-ready-pending.json` and emits a
   `systemMessage` operator notice. The UserPromptSubmit hook reads
   the file on the next prompt and injects via `additionalContext`.

The hint fires exactly once per phase (the `ready_emitted` flag on
`phase_progress` suppresses re-emission until the cursor advances or
reopens). Surface a single `AskUserQuestion`:

> Phase `<active_phase_title>` looks done. Move on?
>
> Exit criteria: `<active_phase_exit_criteria>`.
>
> - `[a]` Mark phase done, advance to next phase
> - `[b]` Keep working on this phase

Dispatch:

```
cairn_mission_advance({phase_id: "<active_phase>", choice: "exit" | "not_yet"})
```

When the mission's `exit_gate` is `auto`, the cursor already
advanced silently (no prompt fires). When `manual`, the prompt
is suppressed entirely — operator must invoke advance directly.

Render a one-line outcome after the call:

```
✓ Phase advanced (next: <next_phase>) · M/N done.
```

If `closed: true`, the mission auto-closed on last phase complete:

```
✓ Mission MIS-… complete. Archived.
```

### 0.2a.5 — pending mission resync

If `.cairn/missions/<id>/_resync.json` exists for the active mission,
the operator amended the source spec doc. Read the marker file
(`Read` tool — it's a small JSON), surface the diff via
`AskUserQuestion`:

> Mission `<mission_id>` resync pending — spec at `<spec_path>`
> proposes:
>
> - +<N> phase(s) added: `<id1>`, `<id2>`
> - −<M> phase(s) removed: `<id3>`
> - ↻<K> phase(s) renamed
> - <P> phase(s) with new exit_criteria
>
> Pick:
>
> - `[a]` accept — rewrite roadmap.md, refresh spec.md, reconcile
>   phase_progress (added → pending, removed → dropped)
> - `[b]` reject — delete the marker, keep roadmap.md unchanged

Dispatch via:

```
cairn_mission_resync_accept({outcome: "accept" | "reject"})
```

After the call, render a one-line outcome:

```
✓ Resync applied (+N −M ↻K). Cursor: <next_phase>.
```

If the marker file is missing, skip this sub-step.

### 0.2b — mission drift

If `mission_get` returned a non-empty `drift_phase_ids`, the operator
edited `roadmap.md` mid-mission and removed phases that still have
graduated task records in `state.json`. Surface a single block per
drift id:

> Mission drift detected — phase `<id>` no longer in roadmap.md but
> has graduated tasks linked.
>
> - `[a]` accept drift — drop phase from `phase_progress` (orphans
>   the linked task records; the tasks themselves remain in
>   `tasks/done/`).
> - `[b]` restore phase to roadmap.md (operator edits the file by
>   hand; this option defers the prompt for 24h while they work).
> - `[c]` defer 24h

Dispatch via `cairn_mission_advance({phase_id: "<drift_id>", choice: "drop"})`
on `[a]` — `drop` removes the drifted entry from `phase_progress`
and journals the resolution. The tool refuses `drop` when the phase
is still present in roadmap.md (operator restored it). For `[b]`
and `[c]`, write the defer file the same way as 0.2a (`choice:
"defer"` is reserved for cursor-phase exits; for drift defers the
skill writes `.cairn/.mission-phase-deferred-until` directly with
the drifted id).

If both 0.2a and 0.2b have items, batch them under one `AskUserQuestion`
call (max 4 questions per batch — same rule as the main queue).

After mission attention resolves, fall through to the regular
attention queue below.

## Step 0.7 — flag duplicate DEC drafts

Phase 7b emits one DEC draft per essay-class comment, so the same
idea appearing in N files produces N near-duplicate drafts. Cluster
them deterministically (no LLM, no quota) before the operator sees
the per-item triage:

```
cairn_attention_dedup({})
```

Returns:

```
{ draftsScanned, clusters, draftsInClusters, reducible,
  thresholdFloor, thresholdDefinite }
```

Each entry in `clusters` is a `{ tier, averageSimilarity, drafts[] }`.
Tiers:

- `definite` (Jaccard >= 0.5) — render under `## Definite duplicates`.
  Default action: keep the first-listed draft (lowest DEC id) as the
  survivor; surface a single `AskUserQuestion` per cluster:
  - `[a]` keep DEC-NNNN, reject the rest (default)
  - `[b]` keep them all (treat as distinct)
  - `[c]` reject the whole cluster
- `potential` (0.4 to 0.5) — render under `## Potential duplicates`.
  Operator triages each member normally in Step 3; surface the
  cluster as context only, do not auto-merge.

Render the summary as one block right before the per-item prompts:

```
Found N duplicate clusters across M drafts (P reducible).
  • Definite (≥0.5): X clusters, Y drafts
  • Potential (0.4–0.5): Z clusters, W drafts
```

For each definite cluster the operator picked `keep ... reject the rest`,
loop over the non-survivor ids and call:

```
cairn_resolve_attention({ kind: "decision_draft", choice: "b",
  item_id: "DEC-NNNN" })
```

`choice: "b"` renames the draft to `.rejected.md` so the id stays
reserved (never recycled). The survivor stays in `_inbox/` and flows
into Step 3 normally.

If `clusters.length === 0`, skip rendering — the dedup section is
empty noise when there's nothing to cluster.

## Step 1 — read attention sources

Run these in parallel. Use the MCP tools exclusively for DEC
content; **never** `cat`, `Read`, or otherwise inline-read draft
files.

1. List draft paths only (no contents): `Bash: ls .cairn/ground/decisions/_inbox/*.draft.md .cairn/ground/invariants/_inbox/*.draft.md 2>/dev/null`
   Both DEC drafts (init-curator + manual record_decision) and INV drafts
   (resync re-curation) live in parallel `_inbox/` dirs; the id prefix
   (`DEC-`/`INV-`) tells them apart.
2. For each draft id (parsed from the filename), call the matching getter —
   `cairn_decision_get({id: "DEC-NNNN"})` for a DEC, `cairn_invariant_get({id:
   "INV-NNNN"})` for an INV. The response carries `id`, `title`, `status`,
   plus the body markdown.
3. **Conflicts** — list pending contradictions written by phase 7c:
   `Bash: ls .cairn/ground/conflicts/*.md 2>/dev/null`. Each
   filename has the shape `<a-id>__<b-id>.md` where both ids match
   `(DEC|INV)-<hash7>`. Read each conflict file directly via the
   `Read` tool — these files are NOT in the ledger and have no MCP
   getter. Parse the YAML frontmatter to capture `a_id`, `b_id`,
   `a_source`, `b_sot_path`, `reasoning`. Treat the rendered prose
   blocks (`## DEC-<a> ...`, `## DEC-<b> ...`) as the verbatim
   sides to surface.
4. Latest baseline audits (path only) — read the newest of the sensor
   sweep, the component store sweep, AND the config-drift sweep:
   `Bash: ls -1t .cairn/baseline/sensor-audit-*.yaml | head -1`
   `Bash: ls -1t .cairn/baseline/components-*.yaml | head -1`
   `Bash: ls -1t .cairn/baseline/config-drift-*.yaml | head -1`
   The `components-*.yaml` and `config-drift-*.yaml` files share the
   sensor-audit payload shape
   (`sensors: [{sensor_id, findings: [{path, line, severity, message}]}]`),
   so parse them identically. `components-*`'s `component-missing-header`
   findings are components still needing a `@cairn` header; `component-audit`
   findings are advisory inline-rebuild / name-collision EXTEND hints. The
   `config-drift-*` file's `config-drift` findings are the "your project grew"
   nudge — a grown dir / new file type outside declared component scope, a
   declared componentDir that vanished, or a `.gitignore` entry missing from
   `off_limits`; each `message` names the one-line config edit that resolves
   it. Surface them all as `baseline_finding` items under one group.
5. Drift events: `cairn_search({query: "drift"})` against the
   staleness log if any.
6. Recent invalidation events: read the per-session events marker,
   then list `.cairn/events/*.json` newer than `last_polled_ts`.

**Rejected DECs are not in the queue.** Reject is a final operator
decision; surfacing rejected ids every session would force re-triage
of already-resolved items. When the operator explicitly asks to
reconsider a specific id ("restore DEC-1234567", "un-reject DEC-NNNN"),
call `cairn_resolve_attention` with that id directly — the tool
auto-restores from `.rejected.md` (or already-accepted `<id>.md`)
transparently and applies the chosen `a/b/c`. Response carries
`auto_restored_from: "rejected" | "accepted"` so the skill can
surface the rollback in its summary.

For each item, build a tuple `{kind, id, title, source, severity}`
from the MCP responses.

## Step 2 — sort and cap

Sort by:

1. Hard inconsistencies first (kind=conflict)
2. DEC drafts (kind=decision_draft) + INV drafts (kind=invariant_draft) —
   oldest-first by ID so the summary surfaced earlier in the session matches
   the order the operator sees here
3. Baseline findings (kind=sensor_finding) — by sensor severity
4. Drift events (kind=drift)
5. Cross-session invalidation events (kind=invalidation)

Surface up to **4 items in a single `AskUserQuestion` call** — Claude
Code's question tool accepts a `questions` array of length ≤ 4 and
renders all of them in one inline panel. Batching keeps the operator
out of repeated round-trips. If the queue has more than 4 pending,
emit the first 4 in batch 1, then a separate single-question prompt:

- `continue` — show the next batch (up to 4 more)
- `defer all remaining` — defer every remaining same-kind item in ONE
  `cairn_resolve_attention` call via `item_ids` (see Step 3's bulk
  path). Use this for a large post-adoption baseline backlog rather
  than walking batch by batch — pre-existing debt is silenced until the
  matching file next changes.
- `later` — defer until next session

A fresh adoption commonly surfaces dozens of soft baseline findings
(pre-existing debt). Offer `defer all remaining` up front in that case
so the operator clears the backlog with a single pick + single call.

## Step 3 — surface up to 4 items per AskUserQuestion call

Build the `questions` array — one entry per item, all in the same
tool call. For each entry, pass the option's `detail` field as the
question `description` so the operator sees the secondary context
(source path, severity) inline with each choice. **Do not also
render any question as inline markdown** — the `AskUserQuestion` UI
is the canonical render path.

Per-kind option labels (≤ 30 chars each so mobile mode doesn't
truncate):

**DEC draft:** `accept` / `reject` / `edit first`
**INV draft:** `accept` / `reject` / `edit first`
**Baseline finding:** `triage now` / `suppress` / `defer`
**Invalidation event:** `refresh in scope` / `continue under old` / `abort`
**Drift event:** `refresh source` / `defer` / `dismiss`
**Conflict:** `keep A side` / `keep B side` / `merge both` / `archive both`
  — see Step 3b for the side-by-side prose render that MUST appear
    on the same turn before the question lands.

The tool returns answers as a parallel array (one answer per
question, in the order they were sent). For items that need DISTINCT
kind/choice, call `cairn_resolve_attention` once each:

```
cairn_resolve_attention({kind: "decision_draft", item_id: "DEC-deadbee", choice: "a"})
```

**Bulk path — same kind + same choice → ONE call with `item_ids`.**
When the operator applies the same decision to many items (most
commonly defer or suppress across a batch of baseline findings), pass
the id array instead of looping. Do NOT emit one call per finding —
a backlog of dozens otherwise costs dozens of sequential
`resolve_attention` round-trips that one call clears:

```
cairn_resolve_attention({ kind: "baseline_finding", choice: "c",
  item_ids: ["stub-catalog::a.ts:1", "stub-catalog::b.ts:9", …] })
```

It returns `{ ok, batch:true, count, resolved, failed, results }`.
Distinct-choice calls can still run in parallel (separate tool_use
blocks in the same turn) — the MCP write lock serializes them on disk.

The tool dispatches by kind: `decision_draft` for accept/reject/edit,
`invariant_draft` for accept/reject/edit (accept graduates the draft to an
active `§INV`; reject archives a `.rejected.md` tombstone; INV drafts come from
resync re-curation), `baseline_finding` for triage/suppress/defer,
`invalidation_event` for refresh/continue/abort, `drift` for
refresh/defer/dismiss, `bypass` and `review` for Stop-hook surfaces. On
`decision_draft + a`, the tool also strips the
originating source comment and replaces it with a bare `§DEC-NNNN`
symbol (matching the `§INV-NNNN` invariant convention; Cairn Lens
resolves title + body from the ledger) when the DEC came from
`init-source-comments`.

## Step 3b — `kind: conflict` side-by-side render (REQUIRED)

Plan §5.4.1 — when a conflict item is in the batch, the operator
must see both verbatim sides AND the model judge's reasoning before
they pick. AskUserQuestion's option labels alone don't carry that
prose. Render a chat message on the SAME turn as the
`AskUserQuestion`, BEFORE the tool call lands:

````markdown
**Conflict** — `<a-id>` (`<a_source>`) vs `<b-id>` (`<b_sot_path>`)

**`<a-id>` says:**

```
<verbatim prose A from the conflict file>
```

**`<b-id>` says:**

```
<verbatim prose B from the conflict file>
```

**Difference:** <one-line excerpt from `reasoning` frontmatter>
````

Then the AskUserQuestion `[a]` / `[b]` / `[c]` / `[d]` lands as
described in Step 3. The tool dispatches to:

```
cairn_resolve_attention({
  kind: "conflict",
  item_id: "<a-id>__<b-id>",     // filename minus .md
  choice: "a" | "b" | "c" | "d",
  rationale: "<optional operator note>"
})
```

Resolution outcomes (plan §5.4.1 — **never rewrite source files**):

- `[a]` keep A → B gets `status: superseded`, `superseded_by: <a-id>`;
  A gets `supersedes: <b-id>`. Conflict file deleted.
- `[b]` keep B → mirror of [a].
- `[c]` merge → fresh DEC carries both sides + operator's `rationale`
  as a third "Merge rationale" section. Both old DECs/INVs get
  `status: superseded`, `superseded_by: <merged-id>`. Conflict file
  deleted. (Mixed DEC/INV merges produce a DEC; pure INV/INV merges
  stay INV.)
- `[d]` archive both → both sides flip `status: archived`. Conflict
  file moves to `.cairn/ground/conflicts/_archived/`. Reopen later
  by restoring it manually.

After the resolve call, render a one-line outcome to chat:

```
✓ Conflict resolved — <verb> (<winner-id> stands · <loser-id> superseded).
```

**Hard rule** — losing-side prose stays in the source file. CLAUDE.md /
AGENTS.md / `.claude/rules/*` are operator-curated narrative; cairn
never silently rewrites them. The next phase 5b pass will re-cite
or surface the orphan; the operator can manually clean up the doc.

## Step 3a — `edit first` inline edit flow

When the operator picks `edit first` on a DEC draft, the resolve
call returns `{ resolved_kind: "decision_edit_pending", body }` with
the full draft markdown. **Do not point the operator at the file**
and **do not assume they can read the Read-tool output** — emit the
body as a regular chat message so it lands in the conversation.

### Step 3a.1 — render the draft as chat output (REQUIRED)

Before any tool call in this turn, write a chat message containing:

````markdown
**Editing DEC-NNNN.**

```markdown
<paste the full body returned by cairn_resolve_attention here —
title line, frontmatter, every section, no truncation>
```
````

This is a **plain assistant message** — text content, NOT an
AskUserQuestion `description` field, NOT a Read tool result. The
operator must see the actual draft text in chat before they pick.
Skipping this step is a bug — never ask "what to change?" without
first showing what's there.

### Step 3a.2 — menu

After the rendered body, on the SAME turn, call `AskUserQuestion`
with:

- `[a]` Rewrite the title
- `[b]` Rewrite the rationale (body)
- `[c]` Rewrite both
- `[d]` Cancel — keep the draft as-is and re-prompt accept/reject

### Step 3a.3 — capture replacement text

On `[a]` / `[b]` / `[c]`: ask the operator for the replacement
text inline using a single follow-up `AskUserQuestion` per field
with a freeform-style prompt — e.g.
`question: "Replacement title for DEC-NNNN?"`. Operator types the
new value in chat; capture it verbatim.

### Step 3a.4 — apply the edit

Use the `Edit` tool against the draft file at
`.cairn/ground/decisions/_inbox/DEC-NNNN.draft.md`:

- title rewrite → replace the `# DEC-NNNN — <old title>` line AND
  the `title:` frontmatter field
- rationale rewrite → replace the body content under
  `## Proposed rationale` (or whatever the section is named in
  this draft) up to the next `## ` heading or end-of-file

### Step 3a.5 — re-render and re-prompt

After the file is updated, re-render the NEW body as a chat message
(same `markdown` block format as Step 3a.1) and re-prompt with a
fresh `AskUserQuestion`: `[a] accept` / `[b] reject` /
`[c] edit again`. Loop Step 3a.3–3a.5 until the operator picks
accept or reject. Then call `cairn_resolve_attention` with that
choice on the SAME `DEC-NNNN` to finalize.

Never tell the operator "open the file in your editor." The whole
point of inline attention resolution is keeping them in the chat.

## Step 4 — close the turn

Do not write a separate "events poll cursor" stamp. The Stop hook
runs at the end of every assistant turn and advances the cursor as
part of its normal cross-session event drain (see
`cairn-core/src/hooks/runners/stop.ts`). Manual advancement is
redundant.

## Hard rules

- Surface ≤ 4 items per turn. Do not flood the chat.
- Every option must cite the underlying source (file path, sensor id,
  session id) so the operator has full context.
- Never auto-resolve. Even soft conflicts route through
  AskUserQuestion.
- Hard inconsistencies (kind=conflict) block the next cairn-direction
  invocation until resolved — make that visible in the surface text.
- Never render an inline `[a]/[b]/[c]` blockquote for a question that
  also goes through `AskUserQuestion`. Pick one render path.
- Do not mirror Stop-hook surfaces. The Stop hook owns stalled-task
  triage, bypass-commit triage, and phase-exit prompts; do not
  pre-render those questions yourself — the hook will fire the
  surface at end-of-turn. Double-asking the operator is friction.
- Honor operator autonomy intent. If the operator has signalled
  hands-off in this session (`advance autonomously`, `do not stop`,
  `ignore stop hooks`, or any phrase that says "keep going"),
  suppress non-blocking AskUserQuestion calls. The only allowed
  pause is a kind=conflict surface (hard inconsistency).
- Match the project's chat-reply voice from
  `.cairn/ground/brand/voice.md` when it appears in your context.
  Default to plain English when it's absent. Any DEC body the skill
  writes is always full English regardless of voice.