Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/isaacriehm/cairn --skill cairn-adopt명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | cairn-adopt |
| description | One-time Cairn adoption pipeline for a new project. |
| when_to_use | Use when operator opens a supported coding agent in a project without `.cairn/` AND Cairn not declined. Drives one-time adoption inline via cairn_init_run MCP tool as state machine — each phase returns complete (advance) or needs_input (AskUserQuestion, thread answer, re-invoke). Skip when `.cairn/` exists or operator picked "never". |
| allowed-tools | Skill(cairn:cairn-attention), Task(curator-map), Task(curator-reduce), Task(component-annotator), Task(component-registrar) |
This skill is shared by Claude Code, Cursor, and Codex. Tool names in this file describe capabilities, not a required vendor API:
ToolSearch(...) is the Claude Code spelling; Codex and Cursor use their
equivalent discovery surface.AskUserQuestion means the host's structured question UI. If unavailable,
ask the same concise A/B/C question in chat and pause for the answer.../../agents/. Claude Code may dispatch them
by name; other hosts read the matching brief and pass it to their native
subagent tool. If the host has no subagent tool, execute the brief inline.You are guiding the operator through one-time Cairn adoption for the
current project. Adoption is visual, comprehensive, and one-time —
once finished, Cairn runs invisibly forever. Refer to
docs/PLUGIN_ARCHITECTURE.md §6 for the canonical phase sequence.
Open the skill with one ToolSearch call that batch-loads every
deferred tool the loop needs. This avoids one round-trip per phase.
Use the fully-qualified MCP tool names (the bare cairn_… form
silently no-ops in select:). AskUserQuestion is built-in and stays
unprefixed.
ToolSearch(select:mcp__plugin_cairn_cairn__cairn_init_resume,mcp__plugin_cairn_cairn__cairn_init_run,mcp__plugin_cairn_cairn__cairn_decision_get,mcp__plugin_cairn_cairn__cairn_resolve_attention,mcp__plugin_cairn_cairn__cairn_attention_dedup,AskUserQuestion)
After this single call all phase tools + the question tool + the attention resolver are loaded for the rest of the skill.
Before doing anything else, classify the project's adoption state. There
are three buckets, NOT two — fresh, mid-adoption, and fully adopted —
because Phase 4-seed writes .cairn/config.yaml very early. A simple
ls .cairn check can't distinguish "operator quit during Phase 7" from
"adoption finished cleanly N sessions ago."
Run this single shell probe to classify. It is ghost-aware: a
ghost-adopted repo has no in-repo .cairn/ — its state lives out-of-repo
at ~/.cairn/state/<root-commit>/. The probe resolves the effective state
home (in-repo when present, else the out-of-repo ghost dir keyed on the
repo's root-commit) before classifying, so a previously-adopted ghost repo
is recognized as adopted / mid-adoption instead of re-triggering fresh
and re-prompting consent:
node -e '
const fs=require("node:fs");
const os=require("node:os");
const path=require("node:path");
const cp=require("node:child_process");
const root=process.cwd();
let home=path.join(root,".cairn");
if(!fs.existsSync(home)){
// No in-repo .cairn — this repo may be ghost-adopted. Ghost state lives
// at ~/.cairn/state/<repo-id>; repo-id is the move-stable root-commit SHA
// (matches registerGhostRepo). Resolve it and probe there instead.
let rc="";
try{rc=cp.execFileSync("git",["-C",root,"rev-list","--max-parents=0","HEAD"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim().split(/\s+/)[0]||"";}catch{}
if(rc){const g=path.join(os.homedir(),".cairn","state",rc);if(fs.existsSync(g))home=g;}
}
const initState=path.join(home,"init-state.json");
const config=path.join(home,"config.yaml");
if(!fs.existsSync(home)){console.log("fresh");process.exit(0);}
if(fs.existsSync(initState)){
try{
const s=JSON.parse(fs.readFileSync(initState,"utf8"));
console.log("mid-adoption:"+(s.currentPhase||"unknown"));
}catch{console.log("mid-adoption:unparseable");}
process.exit(0);
}
if(fs.existsSync(config)){console.log("adopted");process.exit(0);}
console.log("fresh");'
Branch on the output:
fresh → check operator decline-state, then continue to Step 1
(consent prompt). Decline check: ${CLAUDE_PLUGIN_DATA}/projects.json
→ abort if decline-never is recorded for the current absolute repo
path.mid-adoption:<phase> → adoption is in progress and was
interrupted (operator /exit, crash, rate-limit bail, etc.). Consent
was already granted. Skip Step 1 + Step 1.5, jump straight to Step 2
(cairn_init_resume). Surface a plain one-line note like "Picking up
Cairn setup where it left off." — do NOT print the raw <phase> id to
the user; the per-phase banner already narrates the current step.adopted → fully adopted. Surface a one-line note ("Project
already adopted — /cairn:cairn-resume or /cairn:cairn-attention
for daily flow.") and exit.If the probe errors entirely, fail closed by exiting with no output.
Call AskUserQuestion directly. The user may have never heard of Cairn,
so the question text itself must say, in plain words, what it is and what
it does for them — no jargon, no "ground state" / "DEC" / "ingest".
Question text (use this, or very close to it):
Set up Cairn for this project? Cairn reads your code, docs, and commit history once to learn your project — the decisions you've made, the rules to follow, and the components you've built — then keeps that knowledge handy so your AI assistant stays consistent instead of re-guessing. One-time, ~1-2 minutes; you can stop and resume anytime.
Options (set each option's description to the plain line shown):
yes — "Set it up now — I'll watch it go."not now — "Ask me again next time I open this project."never for this project — "Don't set up Cairn here."Do not add a separate preamble line. Do not render the question as inline
markdown — the AskUserQuestion UI is the canonical render path; the
question text + descriptions above ARE the explanation.
yes → continue to Step 1.5.not now → record decline-temp in projects.json (re-prompt
after 7 days) and end the turn.never for this project → record decline-never in projects.json
and end the turn.The statusline is the only mid-turn render channel during the long ingestion phases. Without it the operator stares at a frozen turn for minutes during 7b-source-comments. Detect whether the user-level config is already wired before asking; if it is, skip this step silently.
Detect — wired iff the command contains the runtime-glob marker:
node -e '
const fs=require("node:fs");
const os=require("node:os");
const p=os.homedir()+"/.claude/settings.json";
if(!fs.existsSync(p)){console.log("missing");process.exit(0);}
try{
const s=JSON.parse(fs.readFileSync(p,"utf8"));
const c=(s.statusLine&&s.statusLine.command)||"";
console.log(c.includes("plugins/cache/*/.active-version-path")?"wired":"unwired");
}catch{console.log("unreadable");}'
wired → skip to Step 2.
missing / unwired / unreadable → render AskUserQuestion:
Cairn's statusline shows live progress during the long adoption phases (especially 7b-source-comments, which is several minutes on busy monorepos). Wire it into your user-level
~/.claude/settings.jsonnow?
a) wire and reopen — patch settings now, ask the operator to
/exit and reopen so this adoption has live progressb) wire and continue — patch settings now, this adoption runs
without live progress (next session sees it)c) skip — leave settings alone; operator can run
/cairn:cairn-statusline-setup laterOn a or b, run the patch (same logic as /cairn:cairn-statusline-setup
Step 3 — re-implemented inline so the adopt loop doesn't depend on a
sibling slash command):
Verify the SessionStart shim exists at any plugin cache slug.
The hook writes to ~/.claude/plugins/cache/<slug>/.active-version-path
where <slug> is whatever marketplace name Claude Code installed
the plugin under — locate via a shell-free Node glob (works on
Windows cmd/PowerShell too, where ls/head are absent):
node -e "const f=require('node:fs'),{homedir}=require('node:os'),{join}=require('node:path');const C=join(homedir(),'.claude','plugins','cache');let s=null,t=-1;try{for(const x of f.readdirSync(C)){const p=join(C,x,'.active-version-path');try{const m=f.statSync(p).mtimeMs;if(m>t){t=m;s=p}}catch{}}}catch{}console.log(s||'')"
On empty output, surface a one-line note ("statusline patch needs the plugin's SessionStart shim — re-run after the first session completes") and continue to Step 2 anyway.
Read ~/.claude/settings.json (create with {} if missing). Use
the Edit tool with the current file as old_string so other
top-level fields stay intact. Set statusLine to the shell-free Node
launcher below — one command for every platform (macOS, Linux, native
Windows cmd/PowerShell), because Node performs the cache glob, mtime
sort, file read, and re-spawn with no shell. It resolves the freshest
.active-version-path shim, validates the path still exists, and
falls back to globbing the newest dist/cli.mjs across cache version
dirs if the shim dangles — so a slug rename or a deleted version dir
self-heals. User-level statusLine cannot use ${CLAUDE_PLUGIN_ROOT}
(plugin-only var), so the launcher discovers the bundle itself; never
hardcode a slug or version path:
{
"type": "command",
"command": "node -e \"const f=require('node:fs'),{homedir}=require('node:os'),{join}=require('node:path'),{spawnSync}=require('node:child_process');const C=join(homedir(),'.claude','plugins','cache');const newest=a=>{let b=null,t=-1;for(const p of a){try{const m=f.statSync(p).mtimeMs;if(m>t){t=m;b=p}}catch{}}return b};const slugs=()=>{try{return f.readdirSync(C).filter(s=>f.existsSync(join(C,s,'.active-version-path')))}catch{return[]}};let cli=null;const sh=newest(slugs().map(s=>join(C,s,'.active-version-path')));if(sh){try{const c=f.readFileSync(sh,'utf8').trim();if(c&&f.existsSync(c))cli=c}catch{}}if(!cli){const z=[];for(const s of slugs()){const pd=join(C,s,'cairn');let vs;try{vs=f.readdirSync(pd)}catch{continue}for(const v of vs){const p=join(pd,v,'dist','cli.mjs');if(f.existsSync(p))z.push(p)}}cli=newest(z)}if(cli)process.exit(spawnSync(process.execPath,[cli,'status-line'],{stdio:'inherit'}).status??0)\"",
"refreshInterval": 10
}
On a) wire and reopen, also surface:
Statusline wired.
/exitand reopen Claude Code in this project to activate it for the live progress indicator during adoption. Adoption resumes from.cairn/init-state.jsonafter reopen.
End the turn — the operator restarts and the next session resumes adoption with the statusline live.
On b) wire and continue or c) skip, fall through to Step 1.6.
Fresh adoptions only. This step runs once, on the fresh branch,
before the pipeline writes its first byte. Skip it entirely on
mid-adoption / adopted — the mode was already chosen and recorded in
the global registry; re-asking would imply a mid-life mode flip, which is
not supported.
The choice is never auto-defaulted and never inferred from the
path. Ask it explicitly via AskUserQuestion. Use plain labels — the
user shouldn't have to know the words "committed" or "ghost":
committed. Description:
"Cairn's notes live in your repo and get committed with your code, so
teammates who clone it share what Cairn learned. Adds a .cairn/
folder and a few small markers."ghost. Description:
"Cairn keeps everything outside your repo — your code and git history
are never touched, not one byte. Just for you, on this machine. Best
for client or consulting work."Keep those one-liners as each option's description. Do not preamble;
the AskUserQuestion widget is the canonical render path. Map the picked
label back to committed / ghost below.
Record the answer for Step 3:
committed → the implicit default. The pipeline writes in-repo as
today. Pass no ghost argument to cairn_init_resume.ghost → on the first cairn_init_resume call in Step 3, pass
{ ghost: true }. That registers the repo as ghost in the global
registry before any state is written, so the fresh init-state, the
Phase 4 seed, and every later writer resolve to the out-of-repo home.
Pass ghost: true only on that first call; subsequent cairn_init_run
calls and any resume re-entry need no flag (the registry entry persists).Fall through to Step 2.
Run the deterministic preflight check:
git rev-parse --is-inside-work-tree 2>/dev/null || true
If the directory is not a git working tree, surface a one-line note +
AskUserQuestion (init git repo / abort). On init git repo,
run:
git init
git config --local safe.directory "$(pwd)"
git config --local core.fileMode false
The two git config --local calls are idempotent and silent if
already set. They prevent the WSL-from-Windows dubious ownership
error and avoid spurious mode-only diffs across cross-platform
clones. Cairn's Phase 1 detect re-applies them automatically when
WSL is detected (process.platform === "linux" AND /proc/version
matches Microsoft|WSL), so an operator who skips this step still
ends up with safe.directory + core.fileMode set. On abort, end
the turn.
The Claude binary is no longer required for adoption — the bundled
plugin includes everything cairn needs. Do not check for claude
on PATH.
This is a state-machine loop against the cairn_init_* MCP tools.
Contract (v0.3.5): the phase tools persist state to
.cairn/init-state.json after every successful return and read it
back on the next call. You do not thread state through tool
arguments — phase tools take only an optional answer field for
needs_input phases. Tool responses are skinny: {status, nextPhase},
{status, question}, or {status, error}. Never try to read the
on-disk state until the loop has terminated; until then the phase
tools own all writes to it.
Init the pipeline by calling cairn_init_resume. Pass no args
for committed mode (the default); pass { ghost: true } on this first
call only when the operator chose ghost in Step 1.6 — that registers
the repo as ghost before any state is written, so the whole pipeline
resolves to the out-of-repo home. It returns { status: "ready" | "done", nextPhase: <PhaseId> | null, repoRoot }. If status === "done" the
project is already mid-init or fully adopted — abort and tell the operator
to check .cairn/init-state.json.
Loop until done:
while nextPhase != null:
if nextPhase == "9b-curate":
# Skill-driven pseudo-phase. Dispatch curator-map + curator-reduce
# subagents to write .cairn/init/curator/final.jsonl, THEN call
# cairn_init_run (which advances state once it sees the file).
# See Step 3.5.
run curator orchestration (Step 3.5)
args = { "phase": nextPhase }
result = call cairn_init_run(args) # tool reads state from disk
# v0.9.0: phases 8-docs-ingest and 10-rules-merge are no-op markers.
# The unified curator pipeline (9a-walker → skill orchestration →
# 9c-emit) replaces them. Both no-op runners stamp `skipped:
# "merged-into-9-curator"` and advance the state machine.
switch (result.status):
case "needs_input":
answer = AskUserQuestion(result.question.prompt, result.question.options)
# Pass result.question.options.map(o => o.detail) as the
# AskUserQuestion description field so the operator sees the
# secondary hint inline with each choice.
result = call cairn_init_run({ ...args, answer: answer.id })
# second call returns "complete" | "error"; fall through.
case "complete":
nextPhase = result.nextPhase
continue
case "error":
surface result.error.message + result.error.detail to operator
ask via AskUserQuestion: `retry phase` / `abort`
if "retry phase": continue (state on disk is intact — error path does NOT clobber)
else: end turn
The phase tools persist state to .cairn/init-state.json after every
successful return so a mid-init /exit resumes cleanly on the next
session — the top of this loop just calls cairn_init_resume again.
During each phase, render a plain-English status banner BEFORE invoking the tool — so the user (who is NOT a Cairn developer and does not know terms like "curator", "shard", "DEC/INV", or which AI model runs) always knows, in their own words, what's happening and roughly how long it'll take. NEVER show the raw phase id, the internal phase name, or a provider or model name in the banner.
Format:
---
**⬡ Cairn — <title>** · <eta>
<one plain line: what this step does for them>
Use this exact registry — match the runtime phase id to its row and
render the title, line, and eta verbatim. Never leak the id.
ETAs are ranges; tip toward the high end when
outputs["2-walker"].total_files > 300.
| phase id | title | line | eta |
|---|---|---|---|
1-detect | Checking your setup | Looking at your toolchain and how the project is organized. | <1s |
2-walker | Taking inventory | A quick pass over your files. | <1s / ~2s |
3-mapper | Mapping your project | Working out what each part of your code does. | ~30-60s / 2-4min |
4-seed | Setting up | Creating the small workspace where Cairn keeps its notes. | <1s |
5-preflight | Sizing the job | Estimating how long the deeper steps will take. | <1s / ~3s |
6-brand | Your project's voice | — | your choice |
7-topic-index | Tidying your notes | Spotting overlapping notes across your docs so nothing's counted twice. | ~30s / 2-10min |
9a-walker | Reading your decisions & rules | Gathering the decisions and rules already written in your code, docs, and commit history. | <5s |
9c-emit | Saving what it learned | Writing those decisions and rules into Cairn's memory. | <5s |
9d-comp-walk | Finding your components | Listing the UI pieces that aren't catalogued yet. | <5s |
9f-comp-emit | Building your component catalog | Indexing your components so they get reused, not rebuilt. | <5s |
11-baseline | First health check | A first scan for anything worth flagging. | <1s / ~5s |
13-multidev | Team setup tips | Noting a couple of setup hints for your teammates. | <1s |
8-docs-ingest, 9b-curate, 9e-comp-annotate, and 10-rules-merge
are not listed. The docs/rules markers are v0.9.0 no-op markers (curator
pipeline subsumed both). 9b-curate and 9e-comp-annotate are
skill-driven pseudo-phases — Step 3.5 and Step 3.6 below handle their
surfaces (parallel subagent dispatch); skip the banner for both since
the dispatch is the surface.
The line in the table above IS the under-banner note — one plain
sentence is enough for every step, including the slow ones (3-mapper,
7-topic-index, 9a-walker). For 7-topic-index you MAY add a second
short line: "Watch the ⏳ on your statusline for live progress." Never add
a model name, a token count, or an internal term to make a step sound
busier — plain and honest beats impressive-sounding jargon.
ETA banner — phase 5-preflight: when this phase completes, render
its bannerLines verbatim as a single block before invoking phase
6-brand. The pre-flight scan walks the source/doc/rule trees,
counts the units each long model-assisted phase will process, and computes
totalSeconds/totalSecondsHigh against the per-machine calibration
cache at ~/.cairn/cache/eta-calibration.json. Read the banner from
.outputs["5-preflight"].bannerLines and surface it so the operator
sees an honest pre-commit estimate before the long phases start.
After every long phase completes, the runtime folds the measured
seconds / units rate back into the cache via EWMA so subsequent
adoptions on this machine get a tighter estimate (self-corrects in
3-4 runs).
Live progress: phases 3-mapper, 7-topic-index, and 9a-walker
write .cairn/init/progress.json after every batch / pair / module
processed. The Cairn statusline reads it and renders
⬡ cairn ⏳ adopt <phase> X/Y (P%) ~Nm in real time so the operator
isn't staring at a frozen turn for minutes. Step 1.5 wires this if it
isn't already. The 9b-curate subagent dispatch in Step 3.5 surfaces
its own status — operator sees parallel subagent output in chat.
When the phase is operator-driven (<eta> = operator) the
AskUserQuestion widget appears immediately after the banner — do NOT
add a third "what would you like to do" line; the widget is the prompt.
If the operator interrupts (/exit, Ctrl-C mid-phase, or kills
the session): adoption is safe to resume. Phase state persists
to .cairn/init-state.json after every successful phase return.
The next session's SessionStart banner re-prompts via cairn-adopt;
the loop picks up at the same currentPhase via cairn_init_resume.
Surface this rule to the operator if they ask whether they can bail
on a long-running phase — they can.
Do not render the phase's question inline when a phase returns
needs_input — AskUserQuestion is the only render path;
double-rendering produces the question as scrollback text AND as an
interactive widget.
Never spawn a subagent to drive the pipeline. The skill itself is
the orchestrator. Spawning a generic-purpose Agent to run the loop
loses the operator-facing banner channel and burns tokens on a
nested ToolSearch + state re-discovery — adoption stays in this turn.
(Step 3.5 dispatches typed curator-map / curator-reduce
subagents for the 9b-curate pseudo-phase only; that is not the
pipeline driver, just one phase's parallel work.)
When the loop hits nextPhase === "9b-curate", run this orchestration
before invoking cairn_init_run for that phase. The MCP runner
for 9b-curate just confirms final.jsonl exists under the curator dir
Path resolution (ghost-aware). The
9a-walkerresult returns an absolutecurator_dir. In committed mode it equals.cairn/init/curator, so the repo-relative paths in the steps below work as-is. In ghost mode the repo has no in-repo.cairn/—curator_dirpoints out-of-repo under the state home. There, substitutecurator_dirfor every.cairn/init/curatorpath below, and give thecurator-map/curator-reducesubagents ABSOLUTE paths under it.9b-curatevalidatesfinal.jsonlat that absolutecurator_dir; a repo-relative write lands in the client tree and fails9b-curate-missing-final.
Render a status banner before dispatch:
---
**⬡ Cairn — Pulling out your decisions & rules** · ~1-3 min
Reading your code and docs in parallel to find the decisions and rules
worth remembering. Runs on your Claude plan — no extra API charge.
(Internally this dispatches the curator-map / curator-reduce
subagents below — but none of those words go to the user.)
Read shards.json with node (portable — never cat/jq, which the
Bash tool can't rely on across hosts). In committed mode the path is
.cairn/init/curator/shards.json; in ghost mode read
<curator_dir>/shards.json:
node -e "console.log(require('node:fs').readFileSync(process.argv[1],'utf8'))" .cairn/init/curator/shards.json
The file contains { shards: Shard[], total_input_tokens_estimate, cap_per_shard }. Each Shard carries shard_id, module,
comment_ids, and shard_file — the basename of a ready-to-read
per-shard corpus slice that 9a-walker already wrote under
<curator_dir>/shards/. If shards is empty (small repo or aggressive
pre-filter), write an empty final.jsonl and jump to 3.5.4 (advance via
cairn_init_run).
9a-walker writes each shard's records to
<curator_dir>/shards/<shard_file> as part of the phase — there is no
manual slicing step. Do NOT read corpus.jsonl and filter it yourself;
pass each subagent its shard_file directly. (Earlier revisions hand-sliced
the corpus in Bash; that was fragile — one run filtered on the wrong field
name and had to retry.)
curator-map subagents in parallel rounds of 4For each shard:
key_modules row to source
module_summary and module_flags.shard_id, the absolute
shard_path (resolve as <curator_dir>/shards/<shard_file>), absolute
candidates_path (target:
.cairn/init/curator/candidates/<shard_id>.jsonl), module,
module_summary, module_flags, and project_domain.curator-map subagent via the Task tool. Send up to
four briefs in a single assistant message so they execute in
parallel; await all four before dispatching the next round.Each subagent writes its candidates JSONL to disk and returns a short summary. The skill reads disk; do not parse subagent return text as the canonical output.
curator-reduce subagentOnce every shard's candidates/<shard_id>.jsonl exists, spawn one
curator-reduce subagent. Its brief includes the
candidates_glob, the absolute final_path
(.cairn/init/curator/final.jsonl), project_domain, and the full
key_modules array.
The reducer is a single Sonnet call by default. If the aggregated candidates exceed ~150k tokens (rare; usually only on >1k-shard monorepos), the reducer's own brief tells it to run a domain-bucket pre-reduce internally and produce one final output.
Call cairn_init_run({ phase: "9b-curate" }). The MCP runner reads
final.jsonl, counts entries, stamps final_entries into outputs,
advances to 9c-emit. If the runner errors with
9b-curate-missing-final, the curator orchestration silently failed
to write final.jsonl — surface the error to the operator and ask
whether to retry or abort. Retries restart Step 3.5 from the top.
The next loop iteration runs 9c-emit, which validates each entry
and writes ground state.
When the loop hits nextPhase === "9e-comp-annotate", run this
orchestration before invoking cairn_init_run for that phase. The
prior phase (9d-comp-walk) wrote the corpus of un-headered component
files; this step dispatches subagents that write @cairn headers into
those files. The MCP runner for 9e-comp-annotate just counts how many
files now carry a header and advances — it never blocks, so annotation
is opportunistic (anything not headered here surfaces as missing-header
debt in the attention queue after adoption).
Ghost mode — register, do NOT annotate. If this adoption is ghost
(the operator picked ghost in Step 1.6, or the repo is registered
ghost), you MUST NOT dispatch the component-annotator — it writes
@cairn headers into client source, which ghost forbids (constraint 2).
Instead dispatch the component-registrar subagent: same
classification, but it calls cairn_component_register (out-of-repo, no
source edit) for each unit. Use the same single-consent + rounds-of-4
dispatch as Step 3.6.2, with these ghost differences:
component-registrar (not component-annotator). Its brief
inlines the same file / export_name / workspace / categories
fields; it registers via the MCP tool and returns a one-line receipt.unregistered-unit offer in the post-adoption attention queue.9e-comp-annotate runner then counts registered /
still_unregistered across the corpus (it never reads a header in
ghost). Always advances.This whole step is OPTIONAL and gated on operator consent. Writing headers mutates source files. If the operator declines, skip straight to Step 3.6.4 — the deterministic emit phase still indexes whatever headers already exist and queues the rest as debt.
Read missing.jsonl with node (portable — never cat):
node -e "try{process.stdout.write(require('node:fs').readFileSync(process.argv[1],'utf8'))}catch{}" .cairn/init/components/missing.jsonl
Each line is { file, workspace, export_name, categories }. If the file
is absent or empty (no missing headers — e.g. a re-adoption), skip to
Step 3.6.4. Otherwise render a banner:
---
**⬡ Cairn — Cataloguing your components** · ~1-2 min
N components aren't catalogued yet. With your OK, Cairn adds a short
catalogue note to each so they get reused, not rebuilt — your code's
behavior doesn't change. Runs on your Claude plan — no extra API charge.
Surface one AskUserQuestion consent gate covering the WHOLE set —
not one per batch. Writing headers mutates source, so consent is
required, but ask exactly once (a 50-file repo previously cost the
operator a dozen identical yes-clicks, each one stalling the phase for
minutes):
[a] Catalogue all N now · [b] Skip for now (I'll show them later)On [a], dispatch component-annotator subagents in parallel rounds of
~4 across the entire corpus — no further prompts between rounds. Send up
to four briefs per assistant message, await each round, then continue to
the next automatically until every file is annotated. On [b], skip
straight to Step 3.6.4 (the deterministic emit still indexes existing
headers and queues the rest as missing-header debt). Each brief MUST
inline:
file — absolute path to annotate.export_name — the detected export; the @cairn value MUST be the
exact exported name (rename neither).categories — the workspace taxonomy; @category MUST be one of these.@aliases ≥2
concrete searchable nouns. @purpose one line. Add @singleton ONLY
for app-shell parts the project intends to exist exactly once.The subagent edits the file and returns a one-line receipt. Read disk, not the return text, as the source of truth.
component-annotator agentThe agent definition lives at agents/component-annotator.md
(Task(component-annotator) is pre-approved in this skill's
frontmatter). It carries the full @cairn grammar + the
write-once-correct rules (port invariant 8) inline so the requirements
ride the brief, not buried prose.
Call cairn_init_run({ phase: "9e-comp-annotate" }). The runner counts
annotated / still_missing (committed) or registered /
still_unregistered (ghost) across the corpus and advances to
9f-comp-emit, which builds the index, drafts singleton §INVs, and
queues any still-missing headers / unregistered units + audit findings to
the attention baseline. No error path — annotation/registration is
best-effort by design.
When the loop exits with nextPhase === null, the on-disk .cairn/
state is complete but core.hooksPath is still unset on this clone.
Run bootstrap silently — the operator just consented to adoption,
so there is no separate consent gate for the per-clone wiring:
node "${CLAUDE_PLUGIN_ROOT}/dist/cli.mjs" join
If ${CLAUDE_PLUGIN_ROOT} does not expand in the Bash tool (some hosts
don't inject it into the skill's shell), resolve the bundle the same
portable way the statusline launcher does — glob the freshest cli.mjs
under the plugin cache — rather than hand-writing an absolute path:
node -e "const f=require('node:fs'),{homedir}=require('node:os'),{join}=require('node:path'),{spawnSync}=require('node:child_process');const C=join(homedir(),'.claude','plugins','cache');let cli=null,t=-1;try{for(const s of f.readdirSync(C)){const pd=join(C,s,'cairn');let vs;try{vs=f.readdirSync(pd)}catch{continue}for(const v of vs){const p=join(pd,v,'dist','cli.mjs');try{const m=f.statSync(p).mtimeMs;if(m>t){t=m;cli=p}}catch{}}}}catch{}if(!cli){console.error('cairn bundle not found');process.exit(1)}process.exit(spawnSync(process.execPath,[cli,'join'],{stdio:'inherit'}).status??0)"
NEVER hardcode a versioned absolute path like
.../cache/<slug>/cairn/<version>/dist/cli.mjs — the slug and version
drift, and on this public repo an operator-home path must not be written
into a command. The cairn join step is idempotent; expected output is
two-three lines confirming hooks-path + chmod + .cli-path. Surface
nothing if it succeeds; on failure, surface the stderr +
AskUserQuestion (retry bootstrap / skip).
This step is mandatory and produces a single assistant turn that
contains BOTH a summary text block AND a Skill tool call. Do NOT
end the turn with text only — the operator has not seen the pending
DEC drafts yet, and ending here orphans them in _inbox/.
The phase tools persist final state to .cairn/init-state.json and
do not clear it on terminal completion. Read the persisted state
to source the summary fields. The read is wrapped so a missing/relocated
state file (ghost mode keeps it out-of-repo; some builds clear it on
completion) degrades to an all-zero summary instead of crashing the
mandatory final turn — the chained attention Skill call below must still
fire either way:
node -e '
const fs=require("node:fs");
let o={};
try{ o=(JSON.parse(fs.readFileSync(".cairn/init-state.json","utf8")).outputs)||{}; }catch{}
const g=(k)=>o[k]||{};
console.log(JSON.stringify({
curator_records:(g("9a-walker").records_total)||0,
curator_shards:(g("9a-walker").shards)||0,
curator_final:(g("9b-curate").final_entries)||0,
decs_emitted:(g("9c-emit").decsWritten||[]).length,
invs_emitted:(g("9c-emit").invsWritten||[]).length,
curator_dropped:(g("9c-emit").dropped)||0,
components_indexed:(g("9f-comp-emit").indexed)||0,
components_missing:(g("9f-comp-emit").missing)||0,
components_annotated:(g("9e-comp-annotate").annotated)||0,
singletons_drafted:(g("9f-comp-emit").singletons_drafted)||0,
component_audit:(g("9f-comp-emit").audit_findings)||0,
baseline_findings:(g("11-baseline").findingsCount)||0,
multidev_hosts:(g("13-multidev").hostKinds)||[]
}));'
baseline_findings is findingsCount (soft + hard) — do NOT read
totalFindings, which does not exist on the audit result and silently
reports 0 ("clean sweep" when the sweep actually found dozens).
In the same assistant message, do both:
Emit a short, friendly recap — what Cairn learned + that it now works
on its own. Plain words only; the user does not know "ground state",
"curator", "§INV", "baseline", or "multidev". Open with a clear "Cairn
is set up" line and the ⬡ mark, then a few bullets from the values
above:
decs_emitted + invs_emitted → "N decisions and M rules
learned from your code, docs, and history." (Call them decisions and
rules — never "DEC/INV", "ground state", or "invariants seeded".)curator_dropped (only when > 0) → "(skipped K low-confidence ones)"
— a brief aside, so they know the bar held.components_indexed + singletons_drafted (only when
components_indexed > 0; non-UI repos skip this) → "N components
catalogued", adding ", K marked one-of-a-kind" when
singletons_drafted > 0. When components_missing > 0 or
component_audit > 0, add one plain line: "a few components still
need cataloguing — I'll show you next."baseline_findings → "First health check: N items flagged for
your review." (Omit the word "sensor".)multidev_hosts (only when non-empty) → one plain line that teammate
setup hints were noted.Close with one line that sets expectations: from here Cairn works quietly in the background and the user's AI assistant uses this automatically — nothing else to install or run.
Immediately call the Skill tool with skill: "cairn:cairn-attention"
to drain pending DEC drafts. The allowed-tools line in this skill's
frontmatter pre-approves that single chained call. The cairn-attention
skill renders DEC-a3f7b2c directly via AskUserQuestion; do not surface
"Now reviewing the N pending DEC drafts…" prose — the next skill's
prompt is the operator's next surface.
If you emit only the summary text and end the turn, adoption is incomplete — the operator never gets the chance to accept/reject drafts. The Skill call is the contract that adoption finished.
If a phase returned error and the operator chose abort, the state
file persists at .cairn/init-state.json; the next session's
SessionStart banner re-prompts to resume.
.cairn/ground/ from this skill. The phase tools
own those writes (under the per-write flock).npx ..., cairn <subcommand>, or any CLI from
the operator-facing chat output. Surface only AskUserQuestion
prompts and one-line status updates.[a]/[b]/[c] blockquote for a question that
also goes through AskUserQuestion. Pick one render path.state through phase tool arguments. Phase tools read
state from .cairn/init-state.json; the only argument that flows
back in is answer for needs_input phases..cairn/ground/brand/voice.md when it appears in your context.
Default to plain English when it's absent. Any code or document the
skill writes is always full English regardless of voice.