cairn-ai 0.2.2 → 0.2.4

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.
@@ -58,11 +58,12 @@ export async function createReport({
58
58
  runner = run,
59
59
  timeoutMs = DEFAULT_TIMEOUT_MS,
60
60
  localChecker = localExecutableAvailable,
61
+ platform = process.platform,
61
62
  } = {}) {
62
63
  const resolvedRoot = resolve(root);
63
64
  const entries = await collectEntries(resolvedRoot);
64
65
  const detected = detectStacks(entries);
65
- const requirements = buildRequirements(detected, entries);
66
+ const requirements = buildRequirements(detected, entries, commandOk, platform);
66
67
  const results = [];
67
68
  const installRefused = install && !yes;
68
69
 
@@ -72,6 +73,7 @@ export async function createReport({
72
73
  runner,
73
74
  timeoutMs,
74
75
  localChecker,
76
+ platform,
75
77
  });
76
78
  let installed = false;
77
79
  let installDiagnostic = null;
@@ -95,6 +97,7 @@ export async function createReport({
95
97
  runner,
96
98
  timeoutMs,
97
99
  localChecker,
100
+ platform,
98
101
  });
99
102
  }
100
103
  }
@@ -157,22 +160,24 @@ export async function collectEntries(base) {
157
160
 
158
161
  export function detectStacks(paths) {
159
162
  const stacks = new Set();
160
- if (paths.some((path) => path === "package.json")) stacks.add("javascript");
161
- if (paths.some((path) => path === "tsconfig.json" || path.endsWith(".ts") || path.endsWith(".tsx"))) stacks.add("typescript");
162
- if (paths.some((path) => path === "pyproject.toml" || path.endsWith(".py"))) stacks.add("python");
163
- if (paths.some((path) => path === "composer.json" || path.endsWith(".php"))) stacks.add("php");
164
- if (paths.some((path) => path === "pom.xml" || path === "build.gradle" || path.endsWith(".java"))) stacks.add("java");
163
+ if (hasBasename(paths, "package.json")) stacks.add("javascript");
164
+ if (hasBasename(paths, "tsconfig.json") || paths.some((path) => path.endsWith(".ts") || path.endsWith(".tsx"))) stacks.add("typescript");
165
+ if (hasBasename(paths, "pyproject.toml") || paths.some((path) => path.endsWith(".py"))) stacks.add("python");
166
+ if (hasBasename(paths, "composer.json") || paths.some((path) => path.endsWith(".php"))) stacks.add("php");
167
+ if (hasBasename(paths, "pom.xml", "build.gradle", "build.gradle.kts") || paths.some((path) => path.endsWith(".java"))) stacks.add("java");
165
168
  if (paths.some((path) => path.endsWith(".kt") || path.endsWith(".kts"))) stacks.add("kotlin");
166
- if (paths.some((path) => path === "Package.swift" || path.endsWith(".xcodeproj") || path.endsWith(".xcworkspace") || path.endsWith(".swift"))) stacks.add("swift");
167
- if (paths.some((path) => path === "go.mod" || path.endsWith(".go"))) stacks.add("go");
168
- if (paths.some((path) => path === "Cargo.toml" || path.endsWith(".rs"))) stacks.add("rust");
169
+ if (hasBasename(paths, "Package.swift") || paths.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace") || path.endsWith(".swift"))) stacks.add("swift");
170
+ if (hasBasename(paths, "go.mod") || paths.some((path) => path.endsWith(".go"))) stacks.add("go");
171
+ if (hasBasename(paths, "Cargo.toml") || paths.some((path) => path.endsWith(".rs"))) stacks.add("rust");
169
172
  return [...stacks];
170
173
  }
171
174
 
172
175
  export function buildRequirements(stacks, entries = [], _commandAvailable = commandOk, platform = process.platform) {
173
176
  const requirements = [];
174
177
  if (stacks.includes("javascript")) {
175
- requirements.push(req("node", ["--version"], "JavaScript runtime for package scripts and CLI checks"));
178
+ requirements.push(unsafeReq("node", ["--version"], "JavaScript runtime for package scripts and CLI checks", "automatic runtime installation is unavailable"));
179
+ const manager = packageManager(entries);
180
+ requirements.push(unsafeReq(manager, ["--version"], `JavaScript package manager selected from ${packageManagerSource(entries)}`, "automatic package-manager installation is unavailable"));
176
181
  }
177
182
  if (stacks.includes("typescript")) {
178
183
  requirements.push(unsafeReq("typescript-language-server", ["--version"], "TypeScript LSP server", "automatic package installation is disabled without pinned package versions"));
@@ -183,7 +188,7 @@ export function buildRequirements(stacks, entries = [], _commandAvailable = comm
183
188
  requirements.push(unsafeReq("ruff", ["--version"], "Python lint and format checker", "automatic Python package installation is disabled without pinned package versions"));
184
189
  }
185
190
  if (stacks.includes("php")) {
186
- const composerProject = entries.includes("composer.json");
191
+ const composerProject = hasBasename(entries, "composer.json");
187
192
  requirements.push(req("php", ["--version"], "PHP runtime for Composer scripts and CLI checks"));
188
193
  if (composerProject) requirements.push(req("composer", ["--version"], "PHP package manager for project-local tool installation"));
189
194
  requirements.push(unsafeReq("phpactor", ["--version"], "PHP LSP server", "automatic Composer package installation is disabled without pinned package versions"));
@@ -204,7 +209,7 @@ export function buildRequirements(stacks, entries = [], _commandAvailable = comm
204
209
  if (stacks.includes("swift")) {
205
210
  requirements.push(req("swift", ["--version"], "Swift toolchain for build and test commands"));
206
211
  requirements.push(req("sourcekit-lsp", ["--version"], "Swift LSP server"));
207
- if (entries.some((path) => path.endsWith(".xcodeproj") || path.endsWith(".xcworkspace"))) {
212
+ if (entries.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace"))) {
208
213
  requirements.push(req("xcodebuild", ["-version"], "Xcode build verification"));
209
214
  }
210
215
  }
@@ -229,13 +234,16 @@ export function addJvmRequirements(requirements) {
229
234
  }
230
235
 
231
236
  export function addJvmBuildToolRequirements(requirements, entries = []) {
232
- if (entries.includes("gradlew.bat")) pushUnique(requirements, req("gradlew.bat", ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
233
- else if (entries.includes("gradlew")) pushUnique(requirements, req("./gradlew", ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
234
- else if (entries.includes("build.gradle") || entries.includes("build.gradle.kts")) pushUnique(requirements, req("gradle", ["--version"], "Gradle availability"));
235
- if (entries.includes("mvnw.cmd")) pushUnique(requirements, req("mvnw.cmd", ["--version"], "Maven wrapper availability", null, { localOnly: true }));
236
- else if (entries.includes("mvnw.bat")) pushUnique(requirements, req("mvnw.bat", ["--version"], "Maven wrapper availability", null, { localOnly: true }));
237
- else if (entries.includes("mvnw")) pushUnique(requirements, req("./mvnw", ["--version"], "Maven wrapper availability", null, { localOnly: true }));
238
- else if (entries.includes("pom.xml")) pushUnique(requirements, req("mvn", ["--version"], "Maven availability"));
237
+ const gradleWindows = findBasename(entries, "gradlew.bat");
238
+ const gradleUnix = findBasename(entries, "gradlew");
239
+ if (gradleWindows) pushUnique(requirements, req(repositoryCommand(gradleWindows), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
240
+ else if (gradleUnix) pushUnique(requirements, req(repositoryCommand(gradleUnix), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
241
+ else if (hasBasename(entries, "build.gradle", "build.gradle.kts")) pushUnique(requirements, req("gradle", ["--version"], "Gradle availability"));
242
+ const mavenWindows = findBasename(entries, "mvnw.cmd", "mvnw.bat");
243
+ const mavenUnix = findBasename(entries, "mvnw");
244
+ if (mavenWindows) pushUnique(requirements, req(repositoryCommand(mavenWindows), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
245
+ else if (mavenUnix) pushUnique(requirements, req(repositoryCommand(mavenUnix), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
246
+ else if (hasBasename(entries, "pom.xml")) pushUnique(requirements, req("mvn", ["--version"], "Maven availability"));
239
247
  }
240
248
 
241
249
  export function pushUnique(requirements, requirement) {
@@ -276,10 +284,12 @@ export function unsafeReq(command, args, reason, installUnavailableReason) {
276
284
  }
277
285
 
278
286
  export function packageManager(entries = []) {
279
- if (entries.includes("bun.lockb") || entries.includes("bun.lock")) return "bun";
280
- if (entries.includes("pnpm-lock.yaml")) return "pnpm";
281
- if (entries.includes("yarn.lock")) return "yarn";
282
- return "npm";
287
+ return selectPackageManager(entries).manager;
288
+ }
289
+
290
+ export function packageManagerSource(entries = []) {
291
+ const selected = selectPackageManager(entries);
292
+ return selected.path ?? "the npm default (no lockfile found)";
283
293
  }
284
294
 
285
295
  export function installDev(pm, packages) {
@@ -294,20 +304,24 @@ export function inspectCommand(requirement, {
294
304
  runner = run,
295
305
  timeoutMs = DEFAULT_TIMEOUT_MS,
296
306
  localChecker = localExecutableAvailable,
307
+ platform = process.platform,
297
308
  } = {}) {
298
- const candidates = commandCandidates(requirement.command, root);
309
+ const candidates = commandCandidates(requirement.command, root, platform);
299
310
  const localOnly = requirement.localOnly || isRepositoryCommand(requirement.command, root);
300
311
 
301
312
  if (!localOnly) {
302
- const diagnostic = execute(requirement.command, requirement.args, { root, runner, timeoutMs });
303
- if (diagnostic.status === 0) {
304
- return {
305
- ok: true,
306
- availability: "verified",
307
- source: "system",
308
- candidate: requirement.command,
309
- diagnostic,
310
- };
313
+ let diagnostic = null;
314
+ for (const candidate of systemCommandCandidates(requirement.command, platform)) {
315
+ diagnostic = execute(candidate, requirement.args, { root, runner, timeoutMs });
316
+ if (diagnostic.status === 0) {
317
+ return {
318
+ ok: true,
319
+ availability: "verified",
320
+ source: "system",
321
+ candidate,
322
+ diagnostic,
323
+ };
324
+ }
311
325
  }
312
326
 
313
327
  for (const candidate of candidates.slice(1)) {
@@ -324,7 +338,7 @@ export function inspectCommand(requirement, {
324
338
 
325
339
  return {
326
340
  ok: false,
327
- availability: diagnostic.timedOut ? "timeout" : "missing",
341
+ availability: diagnostic?.timedOut ? "timeout" : "missing",
328
342
  source: null,
329
343
  candidate: null,
330
344
  diagnostic,
@@ -370,6 +384,11 @@ export function commandCandidates(command, root = process.cwd(), platform = proc
370
384
  return [...new Set(candidates.flatMap((candidate) => hasWindowsExecutableExtension(candidate) ? [candidate] : [candidate, ...extensions.map((extension) => `${candidate}${extension}`)]))];
371
385
  }
372
386
 
387
+ export function systemCommandCandidates(command, platform = process.platform) {
388
+ if (platform !== "win32" || hasWindowsExecutableExtension(command) || isAbsolute(command)) return [command];
389
+ return [command, `${command}.cmd`, `${command}.bat`, `${command}.exe`];
390
+ }
391
+
373
392
  export function localExecutableAvailable(command, platform = process.platform) {
374
393
  try {
375
394
  const info = statSync(command);
@@ -479,6 +498,69 @@ function normalizePath(path) {
479
498
  return path.split(sep).join("/");
480
499
  }
481
500
 
501
+ function pathBasename(path) {
502
+ const normalized = normalizePath(path);
503
+ return normalized.slice(normalized.lastIndexOf("/") + 1);
504
+ }
505
+
506
+ function hasBasename(paths, ...names) {
507
+ const expected = new Set(names);
508
+ return paths.some((path) => expected.has(pathBasename(path)));
509
+ }
510
+
511
+ function findBasename(paths, ...names) {
512
+ const expected = new Set(names);
513
+ return [...paths]
514
+ .filter((path) => expected.has(pathBasename(path)))
515
+ .sort(compareRepositoryPaths)[0] ?? null;
516
+ }
517
+
518
+ function hasPathSegmentWithSuffix(path, ...suffixes) {
519
+ return normalizePath(path)
520
+ .split("/")
521
+ .some((segment) => suffixes.some((suffix) => segment.endsWith(suffix)));
522
+ }
523
+
524
+ function repositoryCommand(path) {
525
+ const normalized = normalizePath(path);
526
+ return normalized.startsWith("./") ? normalized : `./${normalized}`;
527
+ }
528
+
529
+ function selectPackageManager(entries) {
530
+ const lockfiles = [
531
+ { manager: "bun", names: new Set(["bun.lock", "bun.lockb"]), priority: 0 },
532
+ { manager: "pnpm", names: new Set(["pnpm-lock.yaml"]), priority: 1 },
533
+ { manager: "yarn", names: new Set(["yarn.lock"]), priority: 2 },
534
+ { manager: "npm", names: new Set(["package-lock.json", "npm-shrinkwrap.json"]), priority: 3 },
535
+ ];
536
+ const candidates = [];
537
+
538
+ for (const path of entries) {
539
+ const normalized = normalizePath(path);
540
+ const name = pathBasename(normalized);
541
+ const match = lockfiles.find(({ names }) => names.has(name));
542
+ if (!match) continue;
543
+ candidates.push({
544
+ manager: match.manager,
545
+ path: normalized,
546
+ depth: normalized.split("/").length - 1,
547
+ priority: match.priority,
548
+ });
549
+ }
550
+
551
+ candidates.sort((left, right) => left.depth - right.depth
552
+ || left.priority - right.priority
553
+ || left.path.localeCompare(right.path));
554
+ return candidates[0] ?? { manager: "npm", path: null };
555
+ }
556
+
557
+ function compareRepositoryPaths(left, right) {
558
+ const leftPath = normalizePath(left);
559
+ const rightPath = normalizePath(right);
560
+ const depth = leftPath.split("/").length - rightPath.split("/").length;
561
+ return depth || leftPath.localeCompare(rightPath);
562
+ }
563
+
482
564
  function hasWindowsExecutableExtension(command) {
483
565
  return /\.(cmd|bat|exe)$/i.test(command);
484
566
  }
@@ -0,0 +1,68 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "release": "0.2.2",
4
+ "files": {
5
+ ".agents/workflows/cairn-doctor.md": "cb5525f795ec1b1b3155396b6e175362af0f88ef376f9b92271ef5f7924f9da8",
6
+ ".agents/workflows/cairn-install.md": "036a74fae6df7c7b7b4dc567a3fdb4986fae4ef41c273ad2950a54e57a5e3cfd",
7
+ ".agents/workflows/cairn-memory.md": "af7c94799d0d0878d5386610ba95be9c22a673f75d81e5449053ef4af6bb8c2c",
8
+ ".agents/workflows/cairn-plan.md": "ad6791e3c34cefff161bf9d0d985dd3fe277808c6d3d3e43ffedd132fda756bd",
9
+ ".agents/workflows/cairn-review.md": "4a8698b2680cab3220a2b318eb053ae9b69378f745c0d9084eabd912a65832f5",
10
+ ".agents/workflows/cairn-toolcheck.md": "530051de82b89ba97a5f8bc8951e0c80482849551cb81c6c5dc5dd975c2f0845",
11
+ ".agents/workflows/cairn-uninstall.md": "81f2e1551eff44b97128da0d715fe1b43f14305eb0dc1b06aad4743fae8d2d67",
12
+ ".agents/workflows/cairn-upgrade.md": "0a646846edb989aef7cbce3963721906860f0edc1af55811cc3b4b3ad3ff9f15",
13
+ ".agents/workflows/cairn-work.md": "c508db4dd2f8c00c4439c10153cce5a63a0fcb60f9e3ffa94412f3e64ab1e84b",
14
+ ".claude/agents/explorer.md": "bb7807ebc8a39d1e63b342e0d4e44d24b93ee110d15e322ec60d43c49b9a9d45",
15
+ ".claude/agents/worker.md": "2619eb04fce8dd637cf506ac604f7d41cb8fb13934a2cd4e9ed2df05b95ab30e",
16
+ ".claude/commands/cairn-doctor.md": "1c6b39fe9f7fdbd285018d39677b0e35b9cd5ddb99eb343f85bb5f735efbb733",
17
+ ".claude/commands/cairn-install.md": "52b2b09c0a756112b13da9dd6653fbc84a86b026b24c123b248c80c797dbd77e",
18
+ ".claude/commands/cairn-memory.md": "3da4f95c24b2b12eddf09ef365023a2d1fd79467f8c50dee86e5a860c62bd9fd",
19
+ ".claude/commands/cairn-plan.md": "dfb9d5fb4643158323f3b4e116dfda518384a74ca300b6571929e806a5cdae1f",
20
+ ".claude/commands/cairn-review.md": "c5a8aa682e871c824f07830ef2cc8d6644df12e256393ed1a54739cbc3b0f817",
21
+ ".claude/commands/cairn-toolcheck.md": "169e52144875b79b230fbe9f20343c820b81c4f6a87a5328d9db7155253d819d",
22
+ ".claude/commands/cairn-uninstall.md": "86c46dc88f1f7d236046d996768b91145deacdbc8b5a456044d9696ee1769038",
23
+ ".claude/commands/cairn-upgrade.md": "8b7b82a3d235e0f6a0dbedc393a3ebe5773e88d593bcaa8468ac7bbc653b038d",
24
+ ".claude/commands/cairn-work.md": "6b434795411ffeac53625f3daceefee2edb47030dd9b93d764a539de5c79562b",
25
+ ".codex-plugin/plugin.json": "5d75b04c4240da2235c5489a328ac94450f7d9c49078fba33273d1ca0eb96a30",
26
+ "LICENSE": "1284a0177aecbc8ed521c2c99480f0f6767ab1ca4b832f30d513ae1aa1368f92",
27
+ "README.de.md": "3d73f892136bd1beb15f1e95071b85b74b9e18ef38d8cebde02a09266beadb28",
28
+ "README.es.md": "7bc44d65b47b1c8ee9a7a6a65360bef89a0ed611c1e808d0ed1a4a95108c9271",
29
+ "README.fr.md": "65eb784d27249c50e317708adf75aafb2495e4f552cb0ec3b123999a920abef0",
30
+ "README.ja.md": "241fd360d6ea430494b099872fbe9f64480e22ef26409e7fc43a4eee1fe183ee",
31
+ "README.ko.md": "f39289045e26834ce518ffded3af3f6ac45ca09ec72c86eef6aa1c265c5d7aad",
32
+ "README.md": "2ccb2cab43bad082ccc566cf9330d577e66df283746e37c4d8ab25a935e57473",
33
+ "README.pt.md": "306ccf468c59d2cea50e385c99308aa4d7a952dbfd65c5586cf06faab13e7b5b",
34
+ "README.zh.md": "d788cdccb4fc327b8242085eed45a3e8ac59a0f3c4bf53f51dcae49e0996f578",
35
+ "agents/explorer.md": "21e0306e1184bd0adc9d5339f86f8e8e0125892054142047446c705581cf50d1",
36
+ "agents/worker.md": "6564dc9676687bc8f5a2b62691f4f3a776c0f5580bfa9f9bf3fe1c4831acd377",
37
+ "commands/cairn-doctor.md": "96af34769f64796d1b75837f4124b75f04da4363d80b521d387489d79a0a5ae3",
38
+ "commands/cairn-install.md": "0a6208ad7a3057bf7ee3af49654e009ae96154d25335f78706c9cca5d9482be9",
39
+ "commands/cairn-memory.md": "396e3d3d0863267aef2e9d450f6c664268aa53f3e61cfd759fd9a6d568b98aa5",
40
+ "commands/cairn-plan.md": "278da82240c8d19f11cf83d8da084f8b309443a8736f7e75503250a0a9b16e1e",
41
+ "commands/cairn-review.md": "1ce071e653ed06cfc915cae134aed8bc3391b3563af5bff5531289e292d90596",
42
+ "commands/cairn-toolcheck.md": "b0ce6737f69bb8e138b49b9b412c0cf0e9dc50534c31657eb0936f1513d5572c",
43
+ "commands/cairn-uninstall.md": "bf7005ef29817c19b39a21c4ffc440da0f0e694a0a09355c6e23777a8a291ab5",
44
+ "commands/cairn-upgrade.md": "593d1ceb3af93c94242ccb8de0a2601519072303f463be4961fd803e71e5d880",
45
+ "commands/cairn-work.md": "475de81bbf54c4deccccd7a63969efb20db6654ff34734f93daf95d4a8799e40",
46
+ "docs/model-guidance/README.md": "9291a6ca8a7717aefb39facf0dd085672e30fed2ba02461bd1e27efb0c6d1826",
47
+ "docs/model-guidance/claude.md": "7c8a3ff0e9d639735088d765794d168c43b87a6611dc89a6776a5e840ea2df19",
48
+ "docs/model-guidance/codex.md": "45eb20fcf35b9ecfc0ea7159235081c5afdce983d73174505079d3db8198ab5c",
49
+ "hooks/hooks.json": "5603c6533a461b53b332273ba2172a625fd6ae21d24e1ceb3a1c16862737b098",
50
+ "package.json": "d8e558f53ad4cd7d24a19d119aed27811a9ce88bde70d6423a7b6bd55e4e8802",
51
+ "scripts/cairn": "0df05a9c2480617eebd11d4e6299c3ff56fe3e243801c1fb55ab6a80a9006fe6",
52
+ "scripts/cairn-goal.mjs": "05f7277ba0ffa26ce5674259a6a4d40de01ffdb746645e7e32ff7c680ca214cd",
53
+ "scripts/cairn-lifecycle.mjs": "74b543bbb201514f4120f6de35b5d27643644b6a01f2ebf347c1f767c57f4109",
54
+ "scripts/cairn-paths.mjs": "815a33bc2ad6b29a3cd1c1db867f7c3e36e2913772e64dc53e563a2f9c30d674",
55
+ "scripts/cairn-state.mjs": "199b28e5ba3e9a296279866ba73f9769150537ceb90722b2958a1a0303511379",
56
+ "scripts/cairn-state.sh": "26f817a50a70b79aade3212ba89dbe0ac507998a4ce1565510624623b3899186",
57
+ "scripts/cairn-toolcheck.mjs": "141ec6572f81f9bf82b3e69b4b074e877ffa6166722faf3dd5b0a003a44e431d",
58
+ "scripts/cairn.mjs": "935048771c7393a0f5900ebc1a0501b0cfe2b63703febe9135c1fba541ca5169",
59
+ "skills/cairn-memory/SKILL.md": "0dcf32ceee7133df2889ea33e1b0856a0d5eedfd6d66ec43835c6b68846b66d1",
60
+ "skills/cairn-plan/SKILL.md": "a6997738a0311d0b9f40cf150e768f879af2577cbc4194595ed267fd42a91ac4",
61
+ "skills/cairn-review/SKILL.md": "52d2d2c3b96d6341f15350255acffa83e54ca3669a88f1c927811accb06f6a6a",
62
+ "skills/cairn-work/SKILL.md": "a20e47bb7cee345c526d619787e6ce054ea96806a51e8815a4329440237592be",
63
+ "templates/MEMORY.md": "a7791c4d400d3acec0dd813709c1dd8362ffc2ed2f9c407730b807b9370bdd75",
64
+ "templates/PLAN.md": "66a53a8f347949761cadfb95677f2e2ffca4e9dcc7b7129cdd70693cc558e00b",
65
+ "templates/domain-memory.md": "4a22556739a6068694c23eef723a6c40e41855884d420ed92623c3a5af42de49",
66
+ "templates/work-plan.md": "40458e17fbd1f8c307ba07638612131f996f711fd100d31e3fca06eeb8a8edb9"
67
+ }
68
+ }
@@ -17,6 +17,10 @@ Do not treat planning as optional for implementation work that is more than a na
17
17
 
18
18
  Plan from the whole work down. First understand the full requested outcome and affected surfaces, then classify the work into small executable tasks. If one task is still too broad to verify cleanly, split that task into sub-tasks.
19
19
 
20
+ ## Re-entry and Required References
21
+
22
+ At the start and after compaction, restart, or handoff, restore context in this order: root `MEMORY.md` → `cairn-plan` → the active plan → current-task references → model guidance recorded by the plan. A new implementation request with no persisted goal may create its initial plan through the procedure below. If persisted state names a plan or task and that reference is missing, unreadable, or inconsistent, do not explore, edit, delegate, or complete work; report or record a blocker first. Do not add a read-receipt field that claims the model read a file: filesystem/state consistency and bound tool evidence remain the fail-closed authority.
23
+
20
24
  ## Runtime Location
21
25
 
22
26
  Resolve Cairn's read-only runtime from `references/cairn-runtime.json` next to this `SKILL.md`. Its `pluginRoot` contains Cairn's `scripts/`, `templates/`, commands, agents, and model guidance. In a source checkout where the locator does not exist, resolve the plugin root from this `SKILL.md` location (`../..`). Never search the target repository for Cairn scripts, templates, or model guidance, and never derive the plugin root from the current working directory.
@@ -13,6 +13,8 @@ Review is not another implementation loop. Confirm that the completed task satis
13
13
 
14
14
  Every review run and every delegated review agent must read the project-root `MEMORY.md` before doing assigned work.
15
15
 
16
+ At the start and after compaction, restart, delegation, or handoff, restore context in this order: root `MEMORY.md` → `cairn-review` → the active plan → completed/current-task references → model guidance recorded by the plan. Confirm those references agree with persisted goal state before reviewing. If any required reference is missing, unreadable, or inconsistent, do not approve, delegate, record final review, or complete the goal; report a blocker. Do not use a read-receipt field as evidence that a model understood a reference.
17
+
16
18
  When subagent tools are available, each agent may recursively delegate bounded review sub-tasks to subagents. Every child subagent must read the project-root `MEMORY.md`, keep the assigned scope, and preserve others' edits.
17
19
 
18
20
  Resolve any `cairn://...` resource through `references/cairn-runtime.json` next to this `SKILL.md` (or this skill's `../..` source root during development). Never look for Cairn runtime resources in the target repository.
@@ -21,6 +21,10 @@ Delegated subagents must provide a final report before leaving. After the orches
21
21
 
22
22
  If the user asks a side question, status question, or narrow clarification while the selected task is still active, use the active goal's ordered task roadmap to preserve position, answer briefly, and then resume the current task unless the user explicitly asks to pause, stop, or switch tasks.
23
23
 
24
+ ## Re-entry and Required References
25
+
26
+ At the start and after compaction, restart, delegation, or handoff, restore context in this order: root `MEMORY.md` → `cairn-work` → the active plan → current-task references → model guidance recorded by the plan. Confirm that the plan task, persisted current task, and worker assignment agree before acting. If state, skill, plan, assignment, or a required reference is missing, unreadable, or inconsistent, do not edit, delegate, record completion evidence, or complete work; report a blocker to the orchestrator. Do not introduce read receipts as proof of model attention: current state, readable references, and fresh tool-bound evidence are the authority.
27
+
24
28
  When subagent tools are available, each agent may recursively delegate bounded sub-tasks to subagents. Every child subagent must read the project-root `MEMORY.md`, keep the assigned scope, and preserve others' edits.
25
29
 
26
30
  ## Runtime Location
@@ -41,6 +45,8 @@ node "<pluginRoot>/scripts/cairn.mjs" goal complete --quiet --root "<repoRoot>"
41
45
 
42
46
  `goal verify` executes the argv after `--` without a shell and records its exit code, bounded tool output, output digest, and watched-workspace fingerprint. The legacy `goal receipt` command imports declared evidence for compatibility; declared evidence does not satisfy a default tool-bound goal.
43
47
 
48
+ Verification defaults to 600,000 ms (10 minutes), accepts a shorter `--timeout-ms`, and is capped at 3,600,000 ms (1 hour). The external command never holds the state lock. A receipt is committed only when the starting goal/task identity and the pre/post watched fingerprint still match. Choose the watch set and verification command at the semantic boundary that proves the claimed behavior.
49
+
44
50
  Use `goal pause`, `goal block --reason "<concrete blocker>"`, or `goal cancel` when that is the truthful state. A task can be blocked with `goal task --task "<task-id>" --status blocked --reason "<concrete blocker>"`.
45
51
 
46
52
  ## Token-Efficient Test Contract
@@ -84,6 +84,8 @@ Describe the observable result.
84
84
  - Token budget: high reasoning for plan/triage/test design; bounded minimum implementation; concise tool-derived verification summaries.
85
85
  - Verification ladder: focused tests, one final full check, then package dry-run if no files changed. Inspect lifecycle scripts first; run normal `npm pack --dry-run` by default. Content-producing or unknown scripts must never use `--ignore-scripts`; only absent or proven content-neutral scripts may use it while evidence remains fresh.
86
86
  - Evidence freshness: relevant changes after verification make affected evidence stale.
87
+ - Verification execution: `goal verify` defaults to 600,000 ms (10 minutes), is capped at 3,600,000 ms (1 hour), and records only when the starting goal/task identity and pre/post watched fingerprint match.
88
+ - Evidence boundary: choose the semantic boundary whose watch set and command prove the claimed behavior.
87
89
  - Dry-run or check mode before external-state mutation:
88
90
  - No dry-run available, if applicable:
89
91
  - Verification loop budget: two passes per task by default.