Resolve Cairn's pending-attention queue inline (DEC drafts, baseline findings, drift events).
Scanned 6/10/2026
Install via CLI
openskills install isaacriehm/cairn---
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
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 a Claude Code 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 2>/dev/null`
2. For each draft id (parsed from the filename), call
`cairn_decision_get({id: "DEC-NNNN"})` — the tool resolves both
accepted decisions and `_inbox/` drafts. 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 BOTH the
sensor sweep and the component store sweep:
`Bash: ls -1t .cairn/baseline/sensor-audit-*.yaml | head -1`
`Bash: ls -1t .cairn/baseline/components-*.yaml | head -1`
The `components-*.yaml` file shares the sensor-audit payload shape
(`sensors: [{sensor_id, findings: [{path, line, severity, message}]}]`),
so parse it identically. Its `component-missing-header` findings are
the components still needing a `@cairn` header; `component-audit`
findings are advisory inline-rebuild / name-collision EXTEND hints.
Surface both as `baseline_finding` items.
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) — 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)
- `later` — defer until next session
## 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`
**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). Loop through the answers
and call `cairn_resolve_attention` once per item:
```
cairn_resolve_attention({kind: "decision_draft", item_id: "DEC-deadbee", choice: "a"})
```
Resolve calls can run in parallel (separate tool_use blocks in the
same assistant turn) — the MCP write lock serializes them on disk.
The tool dispatches by kind: `decision_draft` for accept/reject/edit,
`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 Haiku 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 present (Cairn's spec-delta
scan injects it into SessionStart context). Default to plain
English when the file is absent or empty. Any DEC body the skill
writes is always full English regardless of voice.
No comments yet. Be the first to comment!