cairn-ai 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,13 @@
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { lstatSync, readFileSync, readlinkSync, realpathSync, readdirSync } from "node:fs";
5
- import { readFile, rename, rm } from "node:fs/promises";
6
- import { join, relative, resolve, sep } from "node:path";
5
+ import { lstat, readFile, rename, rm } from "node:fs/promises";
6
+ import { dirname, join, relative, resolve, sep } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import { assertNoSymlinkComponents, safeMkdir, safeWriteFile, withStateLock } from "./cairn-safe-fs.mjs";
9
+ import { cairnHome, goalStateDirectory, worktreeId } from "./cairn-paths.mjs";
10
+
11
+ export { worktreeId } from "./cairn-paths.mjs";
9
12
 
10
13
  export const GOAL_STATE_VERSION = 2;
11
14
  export const GOAL_STATUSES = new Set(["active", "paused", "blocked", "cancelled", "completed"]);
@@ -33,17 +36,17 @@ const taskTransitions = {
33
36
  };
34
37
 
35
38
  export function goalStatePath(root = process.cwd()) {
36
- return join(resolve(root), ".cairn", "state.json");
39
+ return join(goalStateDirectory(root), "state.json");
37
40
  }
38
41
 
39
42
  export async function readGoalState({ root = process.cwd() } = {}) {
40
43
  try {
41
44
  const path = goalStatePath(root);
42
- await assertNoSymlinkComponents(root, path);
45
+ await assertNoSymlinkComponents(cairnHome(), path);
43
46
  const text = await readFile(path, "utf8");
44
47
  return validateState(migrateState(JSON.parse(text)));
45
48
  } catch (error) {
46
- if (error?.code === "ENOENT") return null;
49
+ if (error?.code === "ENOENT") return migrateLegacyGoalState(root);
47
50
  if (error instanceof SyntaxError) throw new Error(`Cairn goal state is invalid JSON: ${error.message}`);
48
51
  throw error;
49
52
  }
@@ -54,8 +57,15 @@ export async function startGoal({ root = process.cwd(), goal, planId, tasks, com
54
57
  const normalizedPlanId = boundedRequiredText(planId, "planId", PLAN_ID_MAX_LENGTH);
55
58
  if (!Array.isArray(tasks) || tasks.length === 0) throw new Error("tasks must contain at least one task");
56
59
 
57
- return withStateLock(root, async () => {
58
- const existing = await readGoalState({ root });
60
+ return withGoalStateLock(root, async () => {
61
+ let existing = await readGoalState({ root });
62
+ const currentWorktreeId = worktreeId(root);
63
+ if (existing && existing.goal.worktreeId !== currentWorktreeId) {
64
+ const path = goalStatePath(root);
65
+ await assertNoSymlinkComponents(cairnHome(), path, { allowMissing: false });
66
+ await rm(path);
67
+ existing = null;
68
+ }
59
69
  if (existing && !terminalGoalStatuses.has(existing.goal.status)) {
60
70
  throw new Error(`An active Cairn goal already exists (${existing.goal.id}); pause, block, cancel, or complete it first`);
61
71
  }
@@ -75,6 +85,7 @@ export async function startGoal({ root = process.cwd(), goal, planId, tasks, com
75
85
  completionCriteria: normalizeCriteria(completionCriteria),
76
86
  requiredEvidence: normalizeCriteria(requiredEvidence),
77
87
  evidencePolicy: validEvidencePolicy(evidencePolicy),
88
+ worktreeId: currentWorktreeId,
78
89
  ownerSessionId: optionalText(ownerSessionId, "ownerSessionId"),
79
90
  blocker: null,
80
91
  createdAt: now,
@@ -88,18 +99,36 @@ export async function startGoal({ root = process.cwd(), goal, planId, tasks, com
88
99
  });
89
100
  }
90
101
 
102
+ export async function reconcileWorktreeState({ root = process.cwd() } = {}) {
103
+ const initial = await readGoalState({ root });
104
+ if (!initial || initial.goal.worktreeId === worktreeId(root)) {
105
+ return { state: initial, removed: false };
106
+ }
107
+ return withGoalStateLock(root, async () => {
108
+ const state = await readGoalState({ root });
109
+ if (!state || state.goal.worktreeId === worktreeId(root)) {
110
+ return { state, removed: false };
111
+ }
112
+ const staleWorktreeId = state.goal.worktreeId;
113
+ const path = goalStatePath(root);
114
+ await assertNoSymlinkComponents(cairnHome(), path, { allowMissing: false });
115
+ await rm(path);
116
+ return { state: null, removed: true, staleWorktreeId };
117
+ });
118
+ }
119
+
91
120
  export async function writeGoalState({ root = process.cwd(), state } = {}) {
92
- return withStateLock(root, () => writeGoalStateUnlocked({ root, state }));
121
+ return withGoalStateLock(root, () => writeGoalStateUnlocked({ root, state }));
93
122
  }
94
123
 
95
124
  async function writeGoalStateUnlocked({ root = process.cwd(), state } = {}) {
96
125
  const validated = validateState(state);
97
126
  const path = goalStatePath(root);
98
- await safeMkdir(root, ".cairn");
99
- await assertNoSymlinkComponents(root, path);
127
+ await ensureGoalStateDirectory(root);
128
+ await assertNoSymlinkComponents(cairnHome(), path);
100
129
  const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
101
130
  try {
102
- await safeWriteFile(root, temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
131
+ await safeWriteFile(cairnHome(), temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
103
132
  await rename(temporaryPath, path);
104
133
  } catch (error) {
105
134
  await rm(temporaryPath, { force: true }).catch(() => {});
@@ -109,21 +138,38 @@ async function writeGoalStateUnlocked({ root = process.cwd(), state } = {}) {
109
138
 
110
139
  export async function transitionGoal({ root = process.cwd(), status, blocker } = {}) {
111
140
  const nextStatus = validGoalStatus(status);
141
+ if (terminalGoalStatuses.has(nextStatus)) {
142
+ return withGoalStateLock(root, async () => {
143
+ const state = await readGoalState({ root });
144
+ if (!state) throw new Error("No Cairn goal state exists; start a goal first");
145
+ const next = applyGoalTransition(structuredClone(state), nextStatus, blocker, root);
146
+ next.revision += 1;
147
+ validateState(next);
148
+ const path = goalStatePath(root);
149
+ await assertNoSymlinkComponents(cairnHome(), path, { allowMissing: false });
150
+ await rm(path);
151
+ return next;
152
+ });
153
+ }
112
154
  return mutateGoal(root, (state) => {
113
- const currentStatus = state.goal.status;
114
- if (currentStatus === nextStatus) return state;
115
- if (!goalTransitions[currentStatus].has(nextStatus)) {
116
- throw new Error(`Cannot transition goal from ${currentStatus} to ${nextStatus}`);
117
- }
118
- if (nextStatus === "completed") ensureGoalCanComplete(state, root);
119
- if (nextStatus === "blocked") state.goal.blocker = requiredText(blocker, "blocker");
120
- if (nextStatus === "active") state.goal.blocker = null;
121
- state.goal.status = nextStatus;
122
- state.goal.updatedAt = timestamp();
123
- return state;
155
+ return applyGoalTransition(state, nextStatus, blocker, root);
124
156
  });
125
157
  }
126
158
 
159
+ function applyGoalTransition(state, nextStatus, blocker, root) {
160
+ const currentStatus = state.goal.status;
161
+ if (currentStatus === nextStatus) return state;
162
+ if (!goalTransitions[currentStatus].has(nextStatus)) {
163
+ throw new Error(`Cannot transition goal from ${currentStatus} to ${nextStatus}`);
164
+ }
165
+ if (nextStatus === "completed") ensureGoalCanComplete(state, root);
166
+ if (nextStatus === "blocked") state.goal.blocker = requiredText(blocker, "blocker");
167
+ if (nextStatus === "active") state.goal.blocker = null;
168
+ state.goal.status = nextStatus;
169
+ state.goal.updatedAt = timestamp();
170
+ return state;
171
+ }
172
+
127
173
  export async function setTaskStatus({ root = process.cwd(), taskId, status, blocker } = {}) {
128
174
  const nextStatus = validTaskStatus(status);
129
175
  const normalizedTaskId = requiredText(taskId, "taskId");
@@ -153,6 +199,34 @@ export async function setTaskStatus({ root = process.cwd(), taskId, status, bloc
153
199
  });
154
200
  }
155
201
 
202
+ export async function replanGoal({ root = process.cwd(), tasks } = {}) {
203
+ if (!Array.isArray(tasks) || tasks.length === 0) {
204
+ throw new Error("replan tasks must contain at least one incomplete task");
205
+ }
206
+
207
+ return mutateGoal(root, (state) => {
208
+ assertGoalMutable(state);
209
+ if (state.goal.status !== "active") throw new Error("Only an active goal can be replanned");
210
+
211
+ const completedTasks = state.tasks.filter((task) => task.status === "completed");
212
+ const completedIds = new Set(completedTasks.map((task) => task.id));
213
+ const replacementTasks = tasks.map((task, index) => {
214
+ const source = typeof task === "string" ? task : { ...task, status: undefined, blocker: undefined };
215
+ const normalized = normalizeTask(source, index, index === 0 ? "active" : "pending");
216
+ if (completedIds.has(normalized.id)) {
217
+ throw new Error(`Replanned task conflicts with completed task id: ${normalized.id}`);
218
+ }
219
+ return normalized;
220
+ });
221
+
222
+ state.tasks = [...completedTasks, ...replacementTasks];
223
+ state.receipts = state.receipts.filter((receipt) => receipt.scope === "task" && completedIds.has(receipt.taskId));
224
+ state.goal.blocker = null;
225
+ state.goal.updatedAt = timestamp();
226
+ return state;
227
+ });
228
+ }
229
+
156
230
  export async function assignTask({ root = process.cwd(), taskId, agentId = null } = {}) {
157
231
  const normalizedTaskId = requiredText(taskId, "taskId");
158
232
  const normalizedAgentId = agentId === null ? null : requiredText(agentId, "agentId");
@@ -403,6 +477,11 @@ export async function handleGoalCli(args = process.argv.slice(2), { stdout = con
403
477
  emit(JSON.stringify(state));
404
478
  return state;
405
479
  }
480
+ if (command === "replan") {
481
+ const state = await replanGoal({ root, tasks: parseTasks(options.tasks ?? positional.slice(0)) });
482
+ emit(JSON.stringify(state));
483
+ return state;
484
+ }
406
485
  if (command === "assign") {
407
486
  const state = await assignTask({ root, taskId: options.task ?? positional[0], agentId: options.agent ?? positional[1] ?? null });
408
487
  emit(JSON.stringify(state));
@@ -452,11 +531,11 @@ export async function handleGoalCli(args = process.argv.slice(2), { stdout = con
452
531
  emit(JSON.stringify(state));
453
532
  return state;
454
533
  }
455
- throw new Error("Usage: cairn-goal start|status|task|assign|receipt|verify|pause|resume|block|cancel|complete [--root PATH]");
534
+ throw new Error("Usage: cairn-goal start|status|task|replan|assign|receipt|verify|pause|resume|block|cancel|complete [--root PATH]");
456
535
  }
457
536
 
458
537
  async function mutateGoal(root, mutate) {
459
- return withStateLock(root, async () => {
538
+ return withGoalStateLock(root, async () => {
460
539
  const state = await readGoalState({ root });
461
540
  if (!state) throw new Error("No Cairn goal state exists; start a goal first");
462
541
  const next = await mutate(structuredClone(state));
@@ -466,6 +545,44 @@ async function mutateGoal(root, mutate) {
466
545
  });
467
546
  }
468
547
 
548
+ async function withGoalStateLock(root, operation) {
549
+ await ensureGoalStateDirectory(root);
550
+ return withStateLock(goalStateDirectory(root), operation);
551
+ }
552
+
553
+ async function ensureGoalStateDirectory(root) {
554
+ const home = cairnHome();
555
+ let existingAncestor = home;
556
+ while (true) {
557
+ try {
558
+ const stat = await lstat(existingAncestor);
559
+ if (!stat.isDirectory()) throw new Error(`Cairn home ancestor is not a directory: ${existingAncestor}`);
560
+ break;
561
+ } catch (error) {
562
+ if (error?.code !== "ENOENT") throw error;
563
+ const parent = dirname(existingAncestor);
564
+ if (parent === existingAncestor) throw error;
565
+ existingAncestor = parent;
566
+ }
567
+ }
568
+ await safeMkdir(existingAncestor, relative(existingAncestor, goalStateDirectory(root)));
569
+ }
570
+
571
+ async function migrateLegacyGoalState(root) {
572
+ const legacyPath = join(resolve(root), ".cairn", "state.json");
573
+ try {
574
+ await assertNoSymlinkComponents(root, legacyPath, { allowMissing: false });
575
+ const state = validateState(migrateState(JSON.parse(await readFile(legacyPath, "utf8"))));
576
+ await writeGoalStateUnlocked({ root, state });
577
+ await rm(legacyPath);
578
+ return state;
579
+ } catch (error) {
580
+ if (error?.code === "ENOENT") return null;
581
+ if (error instanceof SyntaxError) throw new Error(`Cairn goal state is invalid JSON: ${error.message}`);
582
+ throw error;
583
+ }
584
+ }
585
+
469
586
  function normalizeTask(task, index, defaultStatus) {
470
587
  const source = typeof task === "string" ? { title: task } : task;
471
588
  if (!source || typeof source !== "object") throw new Error(`tasks[${index}] must be a string or object`);
@@ -516,6 +633,7 @@ function validateState(value) {
516
633
  if (!Array.isArray(goal.requiredEvidence) || goal.requiredEvidence.length === 0) throw new Error("goal.requiredEvidence must be a non-empty array");
517
634
  goal.requiredEvidence.forEach((kind, index) => requiredText(kind, `goal.requiredEvidence[${index}]`));
518
635
  if (goal.ownerSessionId !== null && goal.ownerSessionId !== undefined) requiredText(goal.ownerSessionId, "goal.ownerSessionId");
636
+ if (goal.worktreeId !== null && goal.worktreeId !== undefined) requiredText(goal.worktreeId, "goal.worktreeId");
519
637
  if (goal.blocker !== null && goal.blocker !== undefined) requiredText(goal.blocker, "goal.blocker");
520
638
  if (!Array.isArray(value.tasks) || value.tasks.length === 0) throw new Error("Cairn goal state must contain tasks");
521
639
  if (!Array.isArray(value.receipts)) throw new Error("Cairn goal state evidence records (receipts) must be an array");
@@ -916,6 +1034,7 @@ function allowedOptions(command) {
916
1034
  start: ["goal", "plan", "tasks", "criteria", "requiredEvidence", "session", "evidencePolicy", "evidence-policy"],
917
1035
  status: [],
918
1036
  task: ["task", "status", "reason"],
1037
+ replan: ["tasks"],
919
1038
  assign: ["task", "agent"],
920
1039
  receipt: ["task", "kind", "scope", "command", "exitCode", "exit-code", "exit", "timestamp", "goalId", "goal-id", "plan"],
921
1040
  verify: ["task", "kind", "scope", "watch", "timeout-ms"],
@@ -1,9 +1,55 @@
1
- import { existsSync } from "node:fs";
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
2
4
  import { dirname, join, resolve } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
 
5
7
  export const RUNTIME_LOCATOR_SCHEMA_VERSION = 1;
6
8
 
9
+ export function cairnHome(env = process.env) {
10
+ const userHome = env.HOME ?? env.USERPROFILE ?? homedir();
11
+ return resolve(env.CAIRN_HOME ?? join(userHome, ".cairn"));
12
+ }
13
+
14
+ export function repositoryId(root = process.cwd()) {
15
+ return `project-${hashPath(gitLayout(root).commonDir)}`;
16
+ }
17
+
18
+ export function worktreeId(root = process.cwd()) {
19
+ return `worktree-${hashPath(gitLayout(root).gitDir)}`;
20
+ }
21
+
22
+ export function goalStateDirectory(root = process.cwd(), env = process.env) {
23
+ return join(cairnHome(env), "projects", repositoryId(root), "worktrees", worktreeId(root));
24
+ }
25
+
26
+ function gitLayout(root) {
27
+ const workspaceRoot = realpathSync(resolve(root));
28
+ const gitEntry = join(workspaceRoot, ".git");
29
+ try {
30
+ const stat = lstatSync(gitEntry);
31
+ const gitDir = stat.isDirectory() ? realpathSync(gitEntry) : resolveGitFile(workspaceRoot, gitEntry);
32
+ const commonFile = join(gitDir, "commondir");
33
+ const commonDir = existsSync(commonFile)
34
+ ? realpathSync(resolve(gitDir, readFileSync(commonFile, "utf8").trim()))
35
+ : gitDir;
36
+ return { gitDir, commonDir };
37
+ } catch (error) {
38
+ if (error?.code !== "ENOENT") throw error;
39
+ return { gitDir: workspaceRoot, commonDir: workspaceRoot };
40
+ }
41
+ }
42
+
43
+ function resolveGitFile(workspaceRoot, gitEntry) {
44
+ const match = readFileSync(gitEntry, "utf8").match(/^gitdir:\s*(.+)\s*$/m);
45
+ if (!match) throw new Error(`Invalid Git worktree file: ${gitEntry}`);
46
+ return realpathSync(resolve(workspaceRoot, match[1]));
47
+ }
48
+
49
+ function hashPath(value) {
50
+ return createHash("sha256").update(value).digest("hex");
51
+ }
52
+
7
53
  export function resolvePluginRoot(moduleUrl = import.meta.url) {
8
54
  return resolve(dirname(fileURLToPath(moduleUrl)), "..");
9
55
  }
@@ -3,7 +3,7 @@ import { realpathSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { evaluateStop, isGoalOwnedBySession, readGoalState } from "./cairn-goal.mjs";
6
+ import { evaluateStop, isGoalOwnedBySession, readGoalState, reconcileWorktreeState } from "./cairn-goal.mjs";
7
7
  import { resolveRepoRoot } from "./cairn-paths.mjs";
8
8
  import { assertNoSymlinkComponents, safeMkdir, safeWriteFile } from "./cairn-safe-fs.mjs";
9
9
 
@@ -65,11 +65,13 @@ This file is a short index of active and completed work plans.
65
65
 
66
66
  ## Planning Rules
67
67
 
68
- - Every agent must start assigned work by reading the project-root \`MEMORY.md\` for domain knowledge and repository policy.
69
- - For implementation or continued execution, first write an initial plan with \`triage-plan\` active, synchronize it to native UI plan/goal tools and the repository goal before exploration, then update it after triage.
68
+ - Read project-root \`MEMORY.md\` first when it exists; if it is absent, continue without repository memory.
69
+ - For non-trivial implementation or continuation of planned work, first write or restore a plan with \`triage-plan\` active and synchronize it before exploration. Known-target Git/GitHub operations stay plan/goal-free unless they require code edits, conflict resolution, destructive recovery, release/deploy, or design.
70
70
  - Plans must be decision-complete before implementation.
71
71
  - Run complexity triage before applying agent, plugin, or delegated workflow guidance.
72
72
  - Record the selected Light Path or Heavy Path and the checked Heavy Path signals in \`docs/plan/<topic>.md\`.
73
+ - Record request, planning, and code checkpoints. When the route changes before editing, synchronize the plan artifact, repository goal task roadmap through \`goal replan\`, and native UI plan. After editing starts, a new Heavy Path signal promotes Light Path to Heavy Path; stop further edits, mark affected evidence stale, synchronize all three roadmaps, and repeat the code checkpoint.
74
+ - Keep models inherited. Record requested/effective reasoning effort per task: Light planning/implementation/verification=\`medium\`; Heavy planning/review/implementation=\`high\`; final verification/review=\`xhigh\`. A route change also synchronizes the reasoning effort profile, preserving completed profiles and recalculating incomplete profiles. Unsupported host/value means effective=\`inherited\` with no model/global config change.
73
75
  - Split implementation into small module tasks.
74
76
  - Detect repository stack and required LSP/check tools before implementation.
75
77
  - Record missing required tools and suggested commands. Run only pinned, supported installation steps after explicit user approval.
@@ -97,11 +99,13 @@ const planTemplateKo = `# PLAN
97
99
 
98
100
  ## Planning Rules
99
101
 
100
- - 모든 에이전트는 배정된 작업을 시작할 때 도메인 지식과 저장소 정책을 위해 프로젝트 루트 \`MEMORY.md\`를 먼저 읽어야 합니다.
101
- - 구현 또는 계속 실행 요청은 먼저 \`triage-plan\`이 active인 초기 계획을 쓰고, 탐색 전에 UI plan/goal과 저장소 goal을 동기화한 뒤 트리아지 결과로 계획을 갱신합니다.
102
+ - 프로젝트 루트 \`MEMORY.md\`가 있으면 먼저 읽고, 없으면 저장소 메모리 없이 계속 진행합니다.
103
+ - 비단순 구현 또는 계획된 작업 재개는 먼저 \`triage-plan\`이 active인 계획을 쓰거나 복원하고 탐색 전에 동기화합니다. 대상이 확정된 Git/GitHub 운영은 코드 수정·충돌 해결·파괴적 복구·릴리스/배포·설계가 필요하지 않으면 plan/goal 없이 실행합니다.
102
104
  - 계획은 구현 전에 의사결정이 완료된 상태여야 합니다.
103
105
  - 에이전트, 플러그인, 위임 워크플로 지침을 적용하기 전에 복잡도 트리아지를 실행합니다.
104
106
  - 선택한 Light Path 또는 Heavy Path와 확인한 Heavy Path 신호를 \`docs/plan/<topic>.md\`에 기록합니다.
107
+ - 요청, 계획, 코드 체크포인트를 기록합니다. 편집 전에 경로가 바뀌면 plan artifact, 저장소 goal task roadmap, native UI plan을 동기화합니다. 편집 뒤 새 Heavy Path 신호가 나오면 추가 편집을 중단하고 관련 증거를 stale로 표시한 뒤 세 roadmap과 코드 체크포인트를 다시 맞춥니다.
108
+ - 모델은 상속하고 task별 requested/effective reasoning effort를 기록합니다. Light 계획/구현/검증은 \`medium\`, Heavy 계획/검토/구현은 \`high\`, 최종 검증/검토는 \`xhigh\`입니다. 경로 변경 시 reasoning effort profile도 동기화하고 완료 profile은 보존하며 미완료 profile은 재계산합니다. 미지원 host/value는 model/global config 변경 없이 effective=\`inherited\`입니다.
105
109
  - 구현은 작은 모듈 작업으로 나눕니다.
106
110
  - 구현 전에 저장소 스택과 필요한 LSP/check 도구를 감지합니다.
107
111
  - 필요한 도구가 없으면 누락 상태와 제안 명령을 기록합니다. 명시적 사용자 승인 뒤에만 고정된 지원 설치 단계를 실행합니다.
@@ -157,11 +161,17 @@ export async function runStateResult(event = "manual", {
157
161
  }
158
162
 
159
163
  if (event === "session-start" || event === "user-prompt-submit") {
160
- const state = await readGoalState({ root: resolvedRoot });
164
+ const reconciled = await reconcileWorktreeState({ root: resolvedRoot });
165
+ const state = reconciled.state;
161
166
  if (state?.goal?.status === "active" && !isGoalOwnedBySession(state, payload?.session_id)) {
162
167
  return foreignSessionContextResult({ ko, event });
163
168
  }
164
- return contextResult({ ko, state, event });
169
+ const result = contextResult({ ko, state, event });
170
+ if (!reconciled.removed) return result;
171
+ const notice = ko
172
+ ? "Cairn worktree 점검: 현재 worktree에 속하지 않는 stale Cairn 상태를 제거했습니다."
173
+ : "Cairn worktree check: removed stale Cairn state that did not belong to this worktree.";
174
+ return prependContext(result, notice);
165
175
  }
166
176
 
167
177
  if (event === "post-tool-use") {
@@ -181,7 +191,7 @@ export async function runStateResult(event = "manual", {
181
191
  }
182
192
 
183
193
  if (event === "stop" || event === "subagent-stop") {
184
- const state = await readGoalState({ root: resolvedRoot });
194
+ const { state } = await reconcileWorktreeState({ root: resolvedRoot });
185
195
  if (!isGoalOwnedBySession(state, payload?.session_id)) return { status: 0, message: "", hookOutput: {} };
186
196
  const gate = evaluateStop(state, {
187
197
  subagent: event === "subagent-stop",
@@ -223,18 +233,34 @@ async function writeIfMissing(root, path, content) {
223
233
  }
224
234
  }
225
235
 
236
+ function prependContext(result, notice) {
237
+ const specific = result.hookOutput?.hookSpecificOutput;
238
+ if (!specific) return { ...result, message: `${notice}\n${result.message}` };
239
+ return {
240
+ ...result,
241
+ message: `${notice}\n${result.message}`,
242
+ hookOutput: {
243
+ ...result.hookOutput,
244
+ hookSpecificOutput: {
245
+ ...specific,
246
+ additionalContext: `${notice}\n${specific.additionalContext}`,
247
+ },
248
+ },
249
+ };
250
+ }
251
+
226
252
  function contextResult({ ko, state, event }) {
227
- const base = ko ? "Cairn kernel: 루트 MEMORY.md를 먼저 읽으세요." : "Cairn kernel: read root MEMORY.md first.";
253
+ const base = ko ? "Cairn kernel: 루트 MEMORY.md는 선택 사항이며, 있으면 읽으세요." : "Cairn kernel: root MEMORY.md is optional; read it when present.";
228
254
  const idlePolicy = event === "user-prompt-submit"
229
255
  ? (ko
230
- ? " 구현/계속 실행이면 cairn-plan을 로드하고 active plan과 current task를 복원하거나 생성하세요. 상담·설명·계획 전용은 goal 없이 처리하세요."
231
- : " For implementation/continue, load cairn-plan and restore or create the active plan and current task. Keep consultation, explanation, and plan-only requests goal-free.")
256
+ ? " 비단순 구현이나 계획된 작업 재개는 cairn-plan과 plan/task를 복원·생성하세요. 대상이 확정된 Git/GitHub 상태·fetch·checkout·merge·push·PR 작업은 코드 수정·충돌 해결·파괴적 복구·릴리스/배포·설계가 필요하지 않으면 plan/goal 없이 실행하세요. 상담·설명·계획 전용도 goal 없이 처리하세요."
257
+ : " For non-trivial implementation or planned-work continuation, load cairn-plan and restore/create its plan and task. Known-target Git/GitHub operations stay plan/goal-free unless they require code edits, conflict resolution, destructive recovery, release/deploy, or design. Consultation, explanation, and plan-only requests are also goal-free.")
232
258
  : (ko
233
259
  ? " 중요 작업 전에 cairn-plan을 로드하고 active plan과 current task를 복원하세요."
234
260
  : " Before non-trivial work, load cairn-plan and restore the active plan and current task.");
235
261
  const failClosed = ko
236
- ? " state, skill, plan 또는 required reference가 없거나 읽을 수 없거나 일치하지 않으면 수정·위임·완료하지 말고 보고하거나 차단하세요."
237
- : " If state, skill, plan, or a required ref is missing, unreadable, or inconsistent, do not edit, delegate, or complete; report/block.";
262
+ ? " active state와 그 skill·plan·required task reference가 없거나 읽을 수 없거나 일치하지 않을 때만 중단하세요."
263
+ : " Stop only if active state and its skill, plan, or required task ref is missing, unreadable, or inconsistent.";
238
264
  if (!state || state.goal.status !== "active") return contextHookResult({ event, message: `${base}${idlePolicy}${failClosed}` });
239
265
  const task = state.tasks.find((item) => item.status === "active")
240
266
  ?? state.tasks.find((item) => item.status === "pending")
@@ -273,8 +299,8 @@ function contextResult({ ko, state, event }) {
273
299
 
274
300
  function foreignSessionContextResult({ ko, event }) {
275
301
  const message = ko
276
- ? "Cairn kernel: 루트 MEMORY.md를 먼저 읽으세요. 저장소 goal을 다른 session이 소유합니다. cairn goal status로 상태를 확인하고 소유권이 해결될 때까지 Cairn 작업을 시작·수정·위임·완료하지 마세요. 이 capsule은 goal, plan, task 상세를 노출하지 않습니다."
277
- : "Cairn kernel: read root MEMORY.md first. A repository goal is owned by another session. Inspect cairn goal status; until ownership is resolved, do not start, edit, delegate, or complete Cairn work. This capsule does not expose goal, plan, or task details.";
302
+ ? "Cairn kernel: 루트 MEMORY.md는 선택 사항이며, 있으면 읽으세요. 저장소 goal을 다른 session이 소유합니다. cairn goal status로 상태를 확인하고 소유권이 해결될 때까지 Cairn 작업을 시작·수정·위임·완료하지 마세요. 이 capsule은 goal, plan, task 상세를 노출하지 않습니다."
303
+ : "Cairn kernel: root MEMORY.md is optional; read it when present. A repository goal is owned by another session. Inspect cairn goal status; until ownership is resolved, do not start, edit, delegate, or complete Cairn work. This capsule does not expose goal, plan, or task details.";
278
304
  return contextHookResult({ event, message });
279
305
  }
280
306
 
@@ -84,8 +84,8 @@ if [ ! -f "$plan" ]; then
84
84
 
85
85
  ## Planning Rules
86
86
 
87
- - 모든 에이전트는 배정된 작업을 시작할 때 도메인 지식과 저장소 정책을 위해 프로젝트 루트 `MEMORY.md`를 먼저 읽어야 합니다.
88
- - 구현 또는 계속 실행 요청은 먼저 `triage-plan`이 active인 초기 계획을 쓰고, 탐색 전에 UI plan/goal과 저장소 goal을 동기화한 뒤 트리아지 결과로 계획을 갱신합니다.
87
+ - 프로젝트 루트 `MEMORY.md`가 있으면 먼저 읽고, 없으면 저장소 메모리 없이 계속 진행합니다.
88
+ - 비단순 구현 또는 계획된 작업 재개는 먼저 `triage-plan`이 active인 계획을 쓰거나 복원하고 탐색 전에 동기화합니다. 대상이 확정된 Git/GitHub 운영은 코드 수정·충돌 해결·파괴적 복구·릴리스/배포·설계가 필요하지 않으면 plan/goal 없이 실행합니다.
89
89
  - 계획은 구현 전에 의사결정이 완료된 상태여야 합니다.
90
90
  - 에이전트, 플러그인, 위임 워크플로 지침을 적용하기 전에 복잡도 트리아지를 실행합니다.
91
91
  - 선택한 Light Path 또는 Heavy Path와 확인한 Heavy Path 신호를 `docs/plan/<topic>.md`에 기록합니다.
@@ -117,8 +117,8 @@ This file is a short index of active and completed work plans.
117
117
 
118
118
  ## Planning Rules
119
119
 
120
- - Every agent must start assigned work by reading the project-root `MEMORY.md` for domain knowledge and repository policy.
121
- - For implementation or continued execution, first write an initial plan with `triage-plan` active, synchronize it to native UI plan/goal tools and the repository goal before exploration, then update it after triage.
120
+ - Read project-root `MEMORY.md` first when it exists; if it is absent, continue without repository memory.
121
+ - For non-trivial implementation or continuation of planned work, first write or restore a plan with `triage-plan` active and synchronize it before exploration. Known-target Git/GitHub operations stay plan/goal-free unless they require code edits, conflict resolution, destructive recovery, release/deploy, or design.
122
122
  - Plans must be decision-complete before implementation.
123
123
  - Split implementation into small module tasks.
124
124
  - Detect repository stack and required LSP/check tools before implementation.
@@ -138,9 +138,9 @@ fi
138
138
  case "$event" in
139
139
  session-start|user-prompt-submit)
140
140
  if is_ko; then
141
- printf '%s\n' "Cairn 컨텍스트: 모든 에이전트는 작업 시작 시 도메인 지식과 정책 색인인 프로젝트 루트 MEMORY.md를 먼저 읽어야 합니다."
141
+ printf '%s\n' "Cairn 컨텍스트: 프로젝트 루트 MEMORY.md가 있으면 읽고, 없으면 저장소 메모리 없이 계속 진행합니다."
142
142
  else
143
- printf '%s\n' "Cairn context: every agent must start by reading the project-root MEMORY.md for domain knowledge and repository policy."
143
+ printf '%s\n' "Cairn context: read project-root MEMORY.md first when present; if absent, continue without repository memory."
144
144
  fi
145
145
  ;;
146
146
  post-tool-use)
package/scripts/cairn.mjs CHANGED
@@ -31,6 +31,11 @@ export async function main(command = "help", args = [], { runner = spawnSync } =
31
31
  process.exitCode = result.status ?? 1;
32
32
  return;
33
33
  }
34
+ if (command === "cleanup") {
35
+ const result = runner(process.execPath, [join(scriptDir, "cairn-cleanup.mjs"), ...args], { stdio: "inherit" });
36
+ process.exitCode = result.status ?? 1;
37
+ return;
38
+ }
34
39
  if (["memory", "plan", "work", "review"].includes(command)) {
35
40
  console.log(message(command));
36
41
  return;
@@ -68,65 +73,68 @@ function isCliEntry() {
68
73
  const planExecutionPolicy = "The user-called/main agent is the orchestrator; implementation edits go to worker subagents whenever subagent tools are available. When the subagent tool provides a progress-reporting channel, subagents report status to the orchestrator when starting work, when deciding or confirming direction, during periodic progress, and when finishing; the orchestrator must immediately relay received status events to the user. If no mid-run reporting channel exists, the orchestrator relays observable events such as assignment, waiting, and final completion. When a delegated subagent finishes, it must provide its final report before leaving; after the orchestrator captures the final report and evidence, the orchestrator must close or release the completed subagent, then review the final report and evidence before marking the work complete. If subagent tools are unavailable, the main agent takes over implementation directly and records that takeover in evidence.";
69
74
  const workExecutionPolicy = "The user-called/main agent orchestrates, while worker subagents perform implementation edits whenever subagent tools are available. When the subagent tool provides a progress-reporting channel, subagents report status to the orchestrator when starting work, when deciding or confirming direction, during periodic progress, and when finishing; the orchestrator must immediately relay received status events to the user. If no mid-run reporting channel exists, the orchestrator relays observable events such as assignment, waiting, and final completion. When a delegated subagent finishes, it must provide its final report before leaving; after the orchestrator captures the final report and evidence, the orchestrator must close or release the completed subagent, then review the final report and evidence before marking the work complete. If subagent tools are unavailable, the main agent takes over implementation directly and records that takeover in evidence.";
70
75
  const tokenEfficientWorkPolicy = "Use a test contract for requirements, invariants, boundaries, and failure modes before the minimum implementation. The tool exit code is authoritative. Run goal verify -- <argv> and expand only failures. Inspect package lifecycle scripts: content-producing or unknown scripts must never use --ignore-scripts; only absent or proven content-neutral scripts may use it while prior full-check evidence remains fresh.";
76
+ const stagedTriagePolicy = "Use request, planning, and code checkpoints. Before the first edit, evidence may change either route; synchronize the plan artifact, repository goal task roadmap through goal replan, and native UI plan. After editing starts, a new Heavy Path signal promotes Light Path to Heavy Path: stop further edits, mark affected evidence stale, synchronize all three roadmaps, and repeat the code checkpoint.";
77
+ const stagedTriagePolicyKo = "요청, 계획, 코드 체크포인트를 사용합니다. 첫 편집 전에는 증거에 따라 어느 경로로든 바꿀 수 있으며 plan artifact, 저장소 goal task roadmap, native UI plan을 동기화합니다. 편집 뒤 새 Heavy Path 신호가 나오면 Light Path를 Heavy Path로 승격하고 추가 편집을 중단하며 관련 증거를 stale로 표시한 뒤 세 roadmap과 코드 체크포인트를 다시 맞춥니다.";
78
+ const reasoningEffortPolicy = "Inherit models; route reasoning effort by task: Light planning/implementation/verification=medium; Heavy planning/review/implementation=high; final verification/review=xhigh. Only new task dispatch may use a supported host option. Unsupported host/value means effective=inherited; never change model/global config.";
71
79
  const artifactLocalePolicy = "Write user-visible responses and generated or updated documentation, plans, and memory artifacts in the OS locale unless the user asks for another language.";
72
80
  const artifactLocalePolicyKo = "사용자가 다른 언어를 요청하지 않는 한, 사용자에게 보이는 응답과 생성 또는 갱신하는 문서, 계획, 메모리 산출물은 OS locale 언어로 작성합니다.";
73
81
 
74
82
  const messages = {
75
83
  en: {
76
84
  memory: `Use the cairn-memory skill. Keep MEMORY.md as a short index and write details to docs/memory/<domain>.md. ${artifactLocalePolicy}`,
77
- plan: `Use the cairn-plan skill before non-trivial implementation. Every agent must first read the project-root MEMORY.md for domain knowledge and repository policy. Understand the whole work first, then classify it into executable tasks and sub-tasks. Keep PLAN.md as an index, write detailed plans to docs/plan/<topic>.md, and record Light/Heavy Path triage before any mutation. ${artifactLocalePolicy} ${planExecutionPolicy}`,
78
- work: `Use cairn-work only after a plan exists and every agent reads root MEMORY.md. ${tokenEfficientWorkPolicy} Execute one task through its recorded Light/Heavy Path: ${workExecutionPolicy} ${artifactLocalePolicy} Resume active work after side questions unless asked to pause, stop, or switch.`,
85
+ plan: `Use the cairn-plan skill before non-trivial implementation. Read project-root MEMORY.md first when present; if absent, continue without repository memory. Understand the whole work first, then classify it into executable tasks and sub-tasks. Keep PLAN.md as an index, write detailed plans to docs/plan/<topic>.md, and record Light/Heavy Path triage before any mutation. ${stagedTriagePolicy} ${reasoningEffortPolicy} ${artifactLocalePolicy} ${planExecutionPolicy}`,
86
+ work: `Use cairn-work only after a plan exists. Read root MEMORY.md when present and continue without it when absent. ${tokenEfficientWorkPolicy} Execute one task through its recorded Light/Heavy Path. ${stagedTriagePolicy} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicy} Resume active work after side questions unless asked to pause, stop, or switch.`,
79
87
  review: `Use the cairn-review skill. Compare evidence against PLAN.md and MEMORY.md. ${artifactLocalePolicy}`,
80
- usage: "Usage: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
88
+ usage: "Usage: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
81
89
  },
82
90
  ko: {
83
91
  memory: `cairn-memory 스킬을 사용하세요. MEMORY.md는 짧은 색인으로, 상세 내용은 docs/memory/<domain>.md에 기록합니다. ${artifactLocalePolicyKo}`,
84
- plan: `중요 구현 전에는 cairn-plan 스킬을 사용하세요. 모든 에이전트는 도메인 지식과 저장소 정책을 위해 프로젝트 루트 MEMORY.md를 먼저 읽어야 합니다. 먼저 전체 작업을 파악한 뒤 실행 가능한 task와 필요한 sub-task로 분류합니다. PLAN.md는 색인으로, 상세 계획은 docs/plan/<topic>.md에 기록하며 파일 변경 전 Light/Heavy Path 트리아지를 남깁니다. ${artifactLocalePolicyKo} ${planExecutionPolicy}`,
85
- work: `계획이 있고 모든 에이전트가 루트 MEMORY.md를 읽은 뒤에만 cairn-work를 사용하세요. ${tokenEfficientWorkPolicy} 기록된 Light/Heavy Path로 한 task를 수행합니다. ${workExecutionPolicy} ${artifactLocalePolicyKo} 곁가지 질문 뒤에는 일시중지·중단·전환 요청이 없으면 active work를 재개합니다.`,
92
+ plan: `중요 구현 전에는 cairn-plan 스킬을 사용하세요. 프로젝트 루트 MEMORY.md가 있으면 먼저 읽고, 없으면 저장소 메모리 없이 계속 진행합니다. 먼저 전체 작업을 파악한 뒤 실행 가능한 task와 필요한 sub-task로 분류합니다. PLAN.md는 색인으로, 상세 계획은 docs/plan/<topic>.md에 기록하며 파일 변경 전 Light/Heavy Path 트리아지를 남깁니다. ${stagedTriagePolicyKo} ${reasoningEffortPolicy} ${artifactLocalePolicyKo} ${planExecutionPolicy}`,
93
+ work: `계획이 있으면 cairn-work를 사용하세요. 루트 MEMORY.md가 있으면 읽고 없으면 무시하고 진행합니다. ${tokenEfficientWorkPolicy} 기록된 Light/Heavy Path로 한 task를 수행합니다. ${stagedTriagePolicyKo} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicyKo} 곁가지 질문 뒤에는 일시중지·중단·전환 요청이 없으면 active work를 재개합니다.`,
86
94
  review: `cairn-review 스킬을 사용하세요. PLAN.md와 MEMORY.md 기준으로 증거를 비교합니다. ${artifactLocalePolicyKo}`,
87
- usage: "사용법: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
95
+ usage: "사용법: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
88
96
  },
89
97
  ja: {
90
98
  memory: `cairn-memory スキルを使用してください。MEMORY.md は短い索引にし、詳細は docs/memory/<domain>.md に記録します。${artifactLocalePolicy}`,
91
- plan: `cairn-plan スキルを使用してください。PLAN.md は索引にし、詳細計画は docs/plan/<topic>.md に記録します。${artifactLocalePolicy} ${planExecutionPolicy}`,
92
- work: `cairn-work スキルを使用してください。1つのモジュールスライスを実行し、モジュール証拠と表面証拠を取得します。${tokenEfficientWorkPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
99
+ plan: `cairn-plan スキルを使用してください。PLAN.md は索引にし、詳細計画は docs/plan/<topic>.md に記録します。${reasoningEffortPolicy} ${artifactLocalePolicy} ${planExecutionPolicy}`,
100
+ work: `cairn-work スキルを使用してください。1つのモジュールスライスを実行し、モジュール証拠と表面証拠を取得します。${tokenEfficientWorkPolicy} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
93
101
  review: `cairn-review スキルを使用してください。PLAN.md と MEMORY.md に照らして証拠を比較します。${artifactLocalePolicy}`,
94
- usage: "使い方: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
102
+ usage: "使い方: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
95
103
  },
96
104
  zh: {
97
105
  memory: `使用 cairn-memory skill。将 MEMORY.md 保持为简短索引,并将详细内容写入 docs/memory/<domain>.md。${artifactLocalePolicy}`,
98
- plan: `使用 cairn-plan skill。将 PLAN.md 保持为索引,并将详细计划写入 docs/plan/<topic>.md。${artifactLocalePolicy} ${planExecutionPolicy}`,
99
- work: `使用 cairn-work skill。执行一个模块切片,并收集模块证据。${tokenEfficientWorkPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
106
+ plan: `使用 cairn-plan skill。将 PLAN.md 保持为索引,并将详细计划写入 docs/plan/<topic>.md。${reasoningEffortPolicy} ${artifactLocalePolicy} ${planExecutionPolicy}`,
107
+ work: `使用 cairn-work skill。执行一个模块切片,并收集模块证据。${tokenEfficientWorkPolicy} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
100
108
  review: `使用 cairn-review skill。根据 PLAN.md 和 MEMORY.md 对比证据。${artifactLocalePolicy}`,
101
- usage: "用法: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
109
+ usage: "用法: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
102
110
  },
103
111
  es: {
104
112
  memory: `Usa la skill cairn-memory. Mantén MEMORY.md como un índice breve y escribe los detalles en docs/memory/<domain>.md. ${artifactLocalePolicy}`,
105
- plan: `Usa la skill cairn-plan. Mantén PLAN.md como índice y escribe los planes detallados en docs/plan/<topic>.md. ${artifactLocalePolicy} ${planExecutionPolicy}`,
106
- work: `Usa la skill cairn-work. Ejecuta un solo segmento de módulo y captura evidencia de módulo y de superficie. ${tokenEfficientWorkPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
113
+ plan: `Usa la skill cairn-plan. Mantén PLAN.md como índice y escribe los planes detallados en docs/plan/<topic>.md. ${reasoningEffortPolicy} ${artifactLocalePolicy} ${planExecutionPolicy}`,
114
+ work: `Usa la skill cairn-work. Ejecuta un solo segmento de módulo y captura evidencia de módulo y de superficie. ${tokenEfficientWorkPolicy} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
107
115
  review: `Usa la skill cairn-review. Compara la evidencia con PLAN.md y MEMORY.md. ${artifactLocalePolicy}`,
108
- usage: "Uso: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
116
+ usage: "Uso: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
109
117
  },
110
118
  fr: {
111
119
  memory: `Utilisez la skill cairn-memory. Gardez MEMORY.md comme index court et écrivez les détails dans docs/memory/<domain>.md. ${artifactLocalePolicy}`,
112
- plan: `Utilisez la skill cairn-plan. Gardez MEMORY.md comme index et écrivez les plans détaillés dans docs/plan/<topic>.md. ${artifactLocalePolicy} ${planExecutionPolicy}`,
113
- work: `Utilisez la skill cairn-work. Exécutez une tranche de module et capturez les preuves de module et de surface. ${tokenEfficientWorkPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
120
+ plan: `Utilisez la skill cairn-plan. Gardez MEMORY.md comme index et écrivez les plans détaillés dans docs/plan/<topic>.md. ${reasoningEffortPolicy} ${artifactLocalePolicy} ${planExecutionPolicy}`,
121
+ work: `Utilisez la skill cairn-work. Exécutez une tranche de module et capturez les preuves de module et de surface. ${tokenEfficientWorkPolicy} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
114
122
  review: `Utilisez la skill cairn-review. Comparez les preuves avec PLAN.md et MEMORY.md. ${artifactLocalePolicy}`,
115
- usage: "Utilisation: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
123
+ usage: "Utilisation: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
116
124
  },
117
125
  de: {
118
126
  memory: `Verwenden Sie die Skill cairn-memory. Halten Sie MEMORY.md als kurzen Index und schreiben Sie Details nach docs/memory/<domain>.md. ${artifactLocalePolicy}`,
119
- plan: `Verwenden Sie die Skill cairn-plan. Halten Sie MEMORY.md als Index und schreiben Sie Detailpläne nach docs/plan/<topic>.md. ${artifactLocalePolicy} ${planExecutionPolicy}`,
120
- work: `Verwenden Sie die Skill cairn-work. Führen Sie einen Modulabschnitt aus und erfassen Sie Modul- und Oberflächenbelege. ${tokenEfficientWorkPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
127
+ plan: `Verwenden Sie die Skill cairn-plan. Halten Sie MEMORY.md als Index und schreiben Sie Detailpläne nach docs/plan/<topic>.md. ${reasoningEffortPolicy} ${artifactLocalePolicy} ${planExecutionPolicy}`,
128
+ work: `Verwenden Sie die Skill cairn-work. Führen Sie einen Modulabschnitt aus und erfassen Sie Modul- und Oberflächenbelege. ${tokenEfficientWorkPolicy} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
121
129
  review: `Verwenden Sie die Skill cairn-review. Vergleichen Sie Belege mit PLAN.md und MEMORY.md. ${artifactLocalePolicy}`,
122
- usage: "Verwendung: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
130
+ usage: "Verwendung: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
123
131
  },
124
132
  pt: {
125
133
  memory: `Use a skill cairn-memory. Mantenha MEMORY.md como um índice curto e escreva detalhes em docs/memory/<domain>.md. ${artifactLocalePolicy}`,
126
- plan: `Use a skill cairn-plan. Mantenha MEMORY.md como índice e escreva planos detalhados em docs/plan/<topic>.md. ${artifactLocalePolicy} ${planExecutionPolicy}`,
127
- work: `Use a skill cairn-work. Execute uma fatia de módulo e capture evidências de módulo e de superfície. ${tokenEfficientWorkPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
134
+ plan: `Use a skill cairn-plan. Mantenha MEMORY.md como índice e escreva planos detalhados em docs/plan/<topic>.md. ${reasoningEffortPolicy} ${artifactLocalePolicy} ${planExecutionPolicy}`,
135
+ work: `Use a skill cairn-work. Execute uma fatia de módulo e capture evidências de módulo e de superfície. ${tokenEfficientWorkPolicy} ${reasoningEffortPolicy} ${workExecutionPolicy} ${artifactLocalePolicy}`,
128
136
  review: `Use a skill cairn-review. Compare evidências com PLAN.md e MEMORY.md. ${artifactLocalePolicy}`,
129
- usage: "Uso: cairn install|upgrade|doctor|uninstall|init|memory|plan|work|review|toolcheck|goal|task",
137
+ usage: "Uso: cairn install|upgrade|doctor|uninstall|cleanup|init|memory|plan|work|review|toolcheck|goal|task",
130
138
  },
131
139
  };
132
140