cairn-ai 0.2.6 → 0.2.8
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.
- package/.agents/workflows/cairn-memory.md +2 -2
- package/.agents/workflows/cairn-plan.md +3 -3
- package/.agents/workflows/cairn-review.md +2 -2
- package/.agents/workflows/cairn-work.md +4 -4
- package/.claude/agents/explorer.md +1 -1
- package/.claude/agents/worker.md +1 -1
- package/.claude/commands/cairn-memory.md +1 -1
- package/.claude/commands/cairn-plan.md +1 -1
- package/.claude/commands/cairn-review.md +1 -1
- package/.claude/commands/cairn-work.md +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.de.md +3 -2
- package/README.es.md +3 -2
- package/README.fr.md +3 -2
- package/README.ja.md +3 -2
- package/README.ko.md +16 -8
- package/README.md +14 -8
- package/README.pt.md +3 -2
- package/README.zh.md +3 -2
- package/agents/worker.md +3 -1
- package/commands/cairn-install.md +2 -1
- package/commands/cairn-memory.md +2 -2
- package/commands/cairn-plan.md +3 -3
- package/commands/cairn-review.md +2 -2
- package/commands/cairn-work.md +4 -4
- package/docs/model-guidance/README.md +5 -5
- package/docs/model-guidance/claude.md +1 -1
- package/docs/model-guidance/codex.md +3 -3
- package/package.json +4 -2
- package/scripts/cairn-cleanup.mjs +12 -6
- package/scripts/cairn-goal-cli.mjs +239 -0
- package/scripts/cairn-goal-evidence.mjs +186 -0
- package/scripts/cairn-goal-operations-evidence.mjs +124 -0
- package/scripts/cairn-goal-operations-mutations.mjs +63 -0
- package/scripts/cairn-goal-operations.mjs +187 -0
- package/scripts/cairn-goal-policy.mjs +36 -0
- package/scripts/cairn-goal-state-validation.mjs +199 -0
- package/scripts/cairn-goal-state.mjs +124 -0
- package/scripts/cairn-goal.mjs +33 -1078
- package/scripts/cairn-lifecycle-commands.mjs +200 -0
- package/scripts/cairn-lifecycle-config.mjs +153 -0
- package/scripts/cairn-lifecycle-integrity.mjs +218 -0
- package/scripts/cairn-lifecycle-legacy.mjs +142 -0
- package/scripts/cairn-lifecycle-lock.mjs +205 -0
- package/scripts/cairn-lifecycle-mirror.mjs +91 -0
- package/scripts/cairn-lifecycle-staging.mjs +152 -0
- package/scripts/cairn-lifecycle-transaction-journal.mjs +210 -0
- package/scripts/cairn-lifecycle-transaction-targets.mjs +119 -0
- package/scripts/cairn-lifecycle-transaction.mjs +12 -0
- package/scripts/cairn-lifecycle.mjs +103 -1185
- package/scripts/cairn-paths.mjs +35 -5
- package/scripts/cairn-safe-fs.mjs +61 -35
- package/scripts/cairn-state-context.mjs +156 -0
- package/scripts/cairn-state-hook.mjs +26 -0
- package/scripts/cairn-state-init.mjs +137 -0
- package/scripts/cairn-state.mjs +32 -286
- package/scripts/cairn-toolcheck-detect.mjs +229 -0
- package/scripts/cairn-toolcheck-runtime.mjs +178 -0
- package/scripts/cairn-toolcheck.mjs +79 -424
- package/scripts/cairn.mjs +17 -3
- package/skills/cairn-memory/SKILL.md +3 -2
- package/skills/cairn-plan/SKILL.md +7 -4
- package/skills/cairn-review/SKILL.md +4 -4
- package/skills/cairn-work/SKILL.md +23 -7
- package/scripts/cairn-state.sh +0 -167
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { delimiter, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
6
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_TIMEOUT_MS,
|
|
7
|
+
commandOk,
|
|
8
|
+
execute,
|
|
9
|
+
inspectCommand,
|
|
10
|
+
localExecutableAvailable,
|
|
11
|
+
run,
|
|
12
|
+
} from "./cairn-toolcheck-runtime.mjs";
|
|
13
|
+
import { buildRequirements, collectEntries, detectStacks } from "./cairn-toolcheck-detect.mjs";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
commandCandidates,
|
|
17
|
+
commandOk,
|
|
18
|
+
DEFAULT_TIMEOUT_MS,
|
|
19
|
+
inspectCommand,
|
|
20
|
+
isWithin,
|
|
21
|
+
localExecutableAvailable,
|
|
22
|
+
run,
|
|
23
|
+
shouldUseShell,
|
|
24
|
+
systemCommandCandidates,
|
|
25
|
+
} from "./cairn-toolcheck-runtime.mjs";
|
|
26
|
+
export {
|
|
27
|
+
addJvmBuildToolRequirements,
|
|
28
|
+
addJvmRequirements,
|
|
29
|
+
buildRequirements,
|
|
30
|
+
collectEntries,
|
|
31
|
+
composerInstall,
|
|
32
|
+
detectStacks,
|
|
33
|
+
installDev,
|
|
34
|
+
jdtlsInstall,
|
|
35
|
+
packageManager,
|
|
36
|
+
packageManagerSource,
|
|
37
|
+
pushUnique,
|
|
38
|
+
pythonInstall,
|
|
39
|
+
req,
|
|
40
|
+
unsafeReq,
|
|
41
|
+
} from "./cairn-toolcheck-detect.mjs";
|
|
42
|
+
|
|
43
|
+
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
44
|
+
const usage = "Usage: cairn toolcheck [--root PATH] [--json] [--install --yes]";
|
|
9
45
|
|
|
10
46
|
if (isCliEntry()) {
|
|
47
|
+
const args = process.argv.slice(2);
|
|
11
48
|
try {
|
|
12
|
-
const options = parseCliArgs(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (options.
|
|
16
|
-
console.log(
|
|
49
|
+
const options = parseCliArgs(args);
|
|
50
|
+
if (options.action === "help") {
|
|
51
|
+
console.log(usage);
|
|
52
|
+
} else if (options.action === "version") {
|
|
53
|
+
console.log(`cairn ${version}`);
|
|
17
54
|
} else {
|
|
18
|
-
|
|
55
|
+
const report = await createReport(options);
|
|
56
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
57
|
+
else printReport(report);
|
|
58
|
+
if (report.install.refused || report.results.some((result) => !result.ok)) process.exitCode = 1;
|
|
19
59
|
}
|
|
20
|
-
|
|
21
|
-
if (report.install.refused || report.results.some((result) => !result.ok)) process.exitCode = 1;
|
|
22
60
|
} catch (error) {
|
|
23
|
-
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
if (args.includes("--json")) {
|
|
63
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: { code: "CLI_USAGE", message } }, null, 2));
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`Cairn toolcheck error: ${message}`);
|
|
66
|
+
}
|
|
24
67
|
process.exitCode = 2;
|
|
25
68
|
}
|
|
26
69
|
}
|
|
@@ -30,25 +73,35 @@ export function parseCliArgs(args, cwd = process.cwd()) {
|
|
|
30
73
|
let install = false;
|
|
31
74
|
let yes = false;
|
|
32
75
|
let json = false;
|
|
76
|
+
let action = "check";
|
|
77
|
+
const provided = new Set();
|
|
33
78
|
|
|
34
79
|
for (let index = 0; index < args.length; index += 1) {
|
|
35
80
|
const argument = args[index];
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
81
|
+
if (["--help", "--version", "--install", "--yes", "--json"].includes(argument)) {
|
|
82
|
+
if (provided.has(argument)) throw new Error(`${argument} may only be provided once`);
|
|
83
|
+
provided.add(argument);
|
|
84
|
+
if (argument === "--help") action = "help";
|
|
85
|
+
else if (argument === "--version") action = "version";
|
|
86
|
+
else if (argument === "--install") install = true;
|
|
87
|
+
else if (argument === "--yes") yes = true;
|
|
88
|
+
else json = true;
|
|
89
|
+
} else if (argument === "--root" || argument.startsWith("--root=")) {
|
|
90
|
+
if (provided.has("--root")) throw new Error("--root may only be provided once");
|
|
91
|
+
provided.add("--root");
|
|
92
|
+
const value = argument === "--root" ? args[index + 1] : argument.slice("--root=".length);
|
|
41
93
|
if (!value || value.startsWith("--")) throw new Error("--root requires a path");
|
|
42
94
|
root = value;
|
|
43
|
-
index += 1;
|
|
44
|
-
} else
|
|
45
|
-
|
|
46
|
-
if (!value) throw new Error("--root requires a path");
|
|
47
|
-
root = value;
|
|
95
|
+
if (argument === "--root") index += 1;
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`unknown option: ${argument}`);
|
|
48
98
|
}
|
|
49
99
|
}
|
|
50
100
|
|
|
51
|
-
|
|
101
|
+
if (provided.has("--help") && provided.has("--version")) {
|
|
102
|
+
throw new Error("--help and --version cannot be used together");
|
|
103
|
+
}
|
|
104
|
+
return { root: resolve(cwd, root), install, yes, json, action };
|
|
52
105
|
}
|
|
53
106
|
|
|
54
107
|
export async function createReport({
|
|
@@ -140,333 +193,6 @@ export async function createReport({
|
|
|
140
193
|
};
|
|
141
194
|
}
|
|
142
195
|
|
|
143
|
-
export async function collectEntries(base) {
|
|
144
|
-
const output = [];
|
|
145
|
-
async function walk(dir) {
|
|
146
|
-
for (const entry of await readdir(dir)) {
|
|
147
|
-
if ([".git", ".cairn", "node_modules", "vendor", "target", ".venv", "__pycache__"].includes(entry)) continue;
|
|
148
|
-
const path = join(dir, entry);
|
|
149
|
-
const relativePath = normalizePath(relative(base, path));
|
|
150
|
-
if (relativePath === "test/fixtures" || relativePath.startsWith("test/fixtures/")) continue;
|
|
151
|
-
const info = await lstat(path);
|
|
152
|
-
if (info.isSymbolicLink()) continue;
|
|
153
|
-
if (info.isDirectory()) await walk(path);
|
|
154
|
-
else output.push(relativePath);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
await walk(base);
|
|
158
|
-
return output;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function detectStacks(paths) {
|
|
162
|
-
const stacks = new Set();
|
|
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");
|
|
168
|
-
if (paths.some((path) => path.endsWith(".kt") || path.endsWith(".kts"))) stacks.add("kotlin");
|
|
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");
|
|
172
|
-
return [...stacks];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function buildRequirements(stacks, entries = [], _commandAvailable = commandOk, platform = process.platform) {
|
|
176
|
-
const requirements = [];
|
|
177
|
-
if (stacks.includes("javascript")) {
|
|
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"));
|
|
181
|
-
}
|
|
182
|
-
if (stacks.includes("typescript")) {
|
|
183
|
-
requirements.push(unsafeReq("typescript-language-server", ["--version"], "TypeScript LSP server", "automatic package installation is disabled without pinned package versions"));
|
|
184
|
-
requirements.push(unsafeReq("tsc", ["--version"], "TypeScript compiler verification", "automatic package installation is disabled without pinned package versions"));
|
|
185
|
-
}
|
|
186
|
-
if (stacks.includes("python")) {
|
|
187
|
-
requirements.push(unsafeReq("basedpyright", ["--version"], "Python LSP/type checker", "automatic Python package installation is disabled without pinned package versions"));
|
|
188
|
-
requirements.push(unsafeReq("ruff", ["--version"], "Python lint and format checker", "automatic Python package installation is disabled without pinned package versions"));
|
|
189
|
-
}
|
|
190
|
-
if (stacks.includes("php")) {
|
|
191
|
-
const composerProject = hasBasename(entries, "composer.json");
|
|
192
|
-
requirements.push(req("php", ["--version"], "PHP runtime for Composer scripts and CLI checks"));
|
|
193
|
-
if (composerProject) requirements.push(req("composer", ["--version"], "PHP package manager for project-local tool installation"));
|
|
194
|
-
requirements.push(unsafeReq("phpactor", ["--version"], "PHP LSP server", "automatic Composer package installation is disabled without pinned package versions"));
|
|
195
|
-
requirements.push(unsafeReq("phpstan", ["--version"], "PHP static analysis verification", "automatic Composer package installation is disabled without pinned package versions"));
|
|
196
|
-
requirements.push(unsafeReq("php-cs-fixer", ["--version"], "PHP formatting verification", "automatic Composer package installation is disabled without pinned package versions"));
|
|
197
|
-
}
|
|
198
|
-
if (stacks.includes("java")) {
|
|
199
|
-
addJvmRequirements(requirements);
|
|
200
|
-
requirements.push(unsafeReq("jdtls", ["--version"], "Java LSP server", "checksum-free JDTLS latest downloads are disabled"));
|
|
201
|
-
addJvmBuildToolRequirements(requirements, entries);
|
|
202
|
-
}
|
|
203
|
-
if (stacks.includes("kotlin")) {
|
|
204
|
-
addJvmRequirements(requirements);
|
|
205
|
-
requirements.push(req("kotlin-lsp.sh", ["--help"], "Kotlin LSP server"));
|
|
206
|
-
requirements.push(req("kotlinc", ["-version"], "Kotlin compiler verification"));
|
|
207
|
-
addJvmBuildToolRequirements(requirements, entries);
|
|
208
|
-
}
|
|
209
|
-
if (stacks.includes("swift")) {
|
|
210
|
-
requirements.push(req("swift", ["--version"], "Swift toolchain for build and test commands"));
|
|
211
|
-
requirements.push(req("sourcekit-lsp", ["--version"], "Swift LSP server"));
|
|
212
|
-
if (entries.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace"))) {
|
|
213
|
-
requirements.push(req("xcodebuild", ["-version"], "Xcode build verification"));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (stacks.includes("go")) {
|
|
217
|
-
requirements.push(unsafeReq("gopls", ["version"], "Go LSP server", "unpinned go install targets are disabled"));
|
|
218
|
-
requirements.push(unsafeReq("golangci-lint", ["--version"], "Go lint verification", "unpinned go install targets are disabled"));
|
|
219
|
-
}
|
|
220
|
-
if (stacks.includes("rust")) {
|
|
221
|
-
requirements.push(unsafeReq("rust-analyzer", ["--version"], "Rust LSP server", "automatic rustup changes are disabled without a pinned toolchain manifest"));
|
|
222
|
-
requirements.push(unsafeReq("cargo", ["clippy", "--version"], "Rust clippy verification", "automatic rustup changes are disabled without a pinned toolchain manifest"));
|
|
223
|
-
}
|
|
224
|
-
return requirements;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function addJvmRequirements(requirements) {
|
|
228
|
-
if (!requirements.some((requirement) => requirement.name === "java")) {
|
|
229
|
-
requirements.push(req("java", ["--version"], "JVM runtime for build and test commands"));
|
|
230
|
-
}
|
|
231
|
-
if (!requirements.some((requirement) => requirement.name === "javac")) {
|
|
232
|
-
requirements.push(req("javac", ["--version"], "JVM compiler verification"));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function addJvmBuildToolRequirements(requirements, entries = []) {
|
|
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"));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function pushUnique(requirements, requirement) {
|
|
250
|
-
if (!requirements.some((candidate) => candidate.name === requirement.name)) requirements.push(requirement);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Kept for API compatibility. Callers may show these commands, but buildRequirements
|
|
254
|
-
// no longer attaches an unpinned command to an automatically executable requirement.
|
|
255
|
-
export function composerInstall(packages) {
|
|
256
|
-
return { command: "composer", args: ["require", "--dev", ...packages] };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function pythonInstall(uv, project, tool, platform = process.platform) {
|
|
260
|
-
if (uv && project) return { command: "uv", args: ["add", "--dev", tool] };
|
|
261
|
-
if (uv) return { command: "uv", args: ["tool", "install", tool] };
|
|
262
|
-
if (platform === "win32") return { command: "py", args: ["-3", "-m", "pip", "install", "--user", tool] };
|
|
263
|
-
return { command: "python3", args: ["-m", "pip", "install", "--user", tool] };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function jdtlsInstall(_platform = process.platform) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function req(command, args, reason, installCommand = null, options = {}) {
|
|
271
|
-
return {
|
|
272
|
-
name: command,
|
|
273
|
-
command,
|
|
274
|
-
args,
|
|
275
|
-
reason,
|
|
276
|
-
install: installCommand,
|
|
277
|
-
installUnavailableReason: options.installUnavailableReason ?? null,
|
|
278
|
-
localOnly: options.localOnly ?? false,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function unsafeReq(command, args, reason, installUnavailableReason) {
|
|
283
|
-
return req(command, args, reason, null, { installUnavailableReason });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function packageManager(entries = []) {
|
|
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)";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function installDev(pm, packages) {
|
|
296
|
-
if (pm === "bun") return { command: "bun", args: ["add", "-d", ...packages] };
|
|
297
|
-
if (pm === "pnpm") return { command: "pnpm", args: ["add", "-D", ...packages] };
|
|
298
|
-
if (pm === "yarn") return { command: "yarn", args: ["add", "-D", ...packages] };
|
|
299
|
-
return { command: "npm", args: ["install", "--save-dev", ...packages] };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function inspectCommand(requirement, {
|
|
303
|
-
root = process.cwd(),
|
|
304
|
-
runner = run,
|
|
305
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
306
|
-
localChecker = localExecutableAvailable,
|
|
307
|
-
platform = process.platform,
|
|
308
|
-
} = {}) {
|
|
309
|
-
const candidates = commandCandidates(requirement.command, root, platform);
|
|
310
|
-
const localOnly = requirement.localOnly || isRepositoryCommand(requirement.command, root);
|
|
311
|
-
|
|
312
|
-
if (!localOnly) {
|
|
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
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
for (const candidate of candidates.slice(1)) {
|
|
328
|
-
if (localChecker(candidate)) {
|
|
329
|
-
return {
|
|
330
|
-
ok: true,
|
|
331
|
-
availability: "discovered",
|
|
332
|
-
source: "repository",
|
|
333
|
-
candidate,
|
|
334
|
-
diagnostic,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
ok: false,
|
|
341
|
-
availability: diagnostic?.timedOut ? "timeout" : "missing",
|
|
342
|
-
source: null,
|
|
343
|
-
candidate: null,
|
|
344
|
-
diagnostic,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
for (const candidate of repositoryCandidates(requirement.command, root)) {
|
|
349
|
-
if (localChecker(candidate)) {
|
|
350
|
-
return {
|
|
351
|
-
ok: true,
|
|
352
|
-
availability: "discovered",
|
|
353
|
-
source: "repository",
|
|
354
|
-
candidate,
|
|
355
|
-
diagnostic: null,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
ok: false,
|
|
362
|
-
availability: "missing",
|
|
363
|
-
source: null,
|
|
364
|
-
candidate: null,
|
|
365
|
-
diagnostic: null,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function commandOk(command, args, root = process.cwd(), runner = run) {
|
|
370
|
-
return inspectCommand(req(command, args, "tool availability"), { root, runner }).ok;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export function commandCandidates(command, root = process.cwd(), platform = process.platform) {
|
|
374
|
-
const candidates = isRepositoryCommand(command, root)
|
|
375
|
-
? repositoryCandidates(command, root)
|
|
376
|
-
: [
|
|
377
|
-
command,
|
|
378
|
-
join(root, "node_modules", ".bin", command),
|
|
379
|
-
join(root, "vendor", "bin", command),
|
|
380
|
-
join(root, ".cairn", "tools", "bin", command),
|
|
381
|
-
];
|
|
382
|
-
if (platform !== "win32") return [...new Set(candidates)];
|
|
383
|
-
const extensions = [".cmd", ".bat", ".exe"];
|
|
384
|
-
return [...new Set(candidates.flatMap((candidate) => hasWindowsExecutableExtension(candidate) ? [candidate] : [candidate, ...extensions.map((extension) => `${candidate}${extension}`)]))];
|
|
385
|
-
}
|
|
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
|
-
|
|
392
|
-
export function localExecutableAvailable(command, platform = process.platform) {
|
|
393
|
-
try {
|
|
394
|
-
const info = statSync(command);
|
|
395
|
-
if (!info.isFile()) return false;
|
|
396
|
-
accessSync(command, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
397
|
-
return true;
|
|
398
|
-
} catch {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export function run(command, args, { cwd = process.cwd(), timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
404
|
-
const startedAt = Date.now();
|
|
405
|
-
const result = spawnSync(command, args, {
|
|
406
|
-
cwd,
|
|
407
|
-
stdio: "ignore",
|
|
408
|
-
encoding: "utf8",
|
|
409
|
-
shell: shouldUseShell(command),
|
|
410
|
-
timeout: timeoutMs,
|
|
411
|
-
env: safeEnvironment(cwd),
|
|
412
|
-
});
|
|
413
|
-
return { ...result, durationMs: Date.now() - startedAt };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
export function shouldUseShell(command, platform = process.platform) {
|
|
417
|
-
return platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function execute(command, args, { root, runner, timeoutMs }) {
|
|
421
|
-
const startedAt = Date.now();
|
|
422
|
-
try {
|
|
423
|
-
const result = runner(command, args, { cwd: root, timeoutMs });
|
|
424
|
-
return {
|
|
425
|
-
status: Number.isInteger(result?.status) ? result.status : null,
|
|
426
|
-
signal: result?.signal ?? null,
|
|
427
|
-
errorCode: result?.error?.code ?? null,
|
|
428
|
-
timedOut: result?.error?.code === "ETIMEDOUT",
|
|
429
|
-
durationMs: Number.isFinite(result?.durationMs) ? result.durationMs : Date.now() - startedAt,
|
|
430
|
-
};
|
|
431
|
-
} catch (error) {
|
|
432
|
-
return {
|
|
433
|
-
status: null,
|
|
434
|
-
signal: null,
|
|
435
|
-
errorCode: error && typeof error === "object" && "code" in error ? String(error.code) : "RUNNER_ERROR",
|
|
436
|
-
timedOut: false,
|
|
437
|
-
durationMs: Date.now() - startedAt,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function repositoryCandidates(command, root) {
|
|
443
|
-
const normalizedCommand = command.replace(/^\.\//, "");
|
|
444
|
-
const direct = isAbsolute(command) ? command : resolve(root, normalizedCommand);
|
|
445
|
-
return [...new Set([direct])];
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function isRepositoryCommand(command, root) {
|
|
449
|
-
if (/^(?:\.\.?[\\/])/.test(command)) return true;
|
|
450
|
-
if (/^(?:gradlew|gradlew\.bat|mvnw|mvnw\.cmd|mvnw\.bat)$/i.test(command)) return true;
|
|
451
|
-
return isAbsolute(command) && isWithin(root, command);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export function isWithin(root, candidate, platform = process.platform) {
|
|
455
|
-
const pathApi = platform === "win32" ? win32 : posix;
|
|
456
|
-
const path = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate));
|
|
457
|
-
return path === "" || (!pathApi.isAbsolute(path) && !path.startsWith(`..${pathApi.sep}`) && path !== "..");
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function safeEnvironment(root) {
|
|
461
|
-
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
462
|
-
const safePath = (process.env[pathKey] ?? "")
|
|
463
|
-
.split(delimiter)
|
|
464
|
-
.filter(Boolean)
|
|
465
|
-
.filter((entry) => isAbsolute(entry) && !isWithin(root, entry))
|
|
466
|
-
.join(delimiter);
|
|
467
|
-
return { ...process.env, [pathKey]: safePath };
|
|
468
|
-
}
|
|
469
|
-
|
|
470
196
|
function printReport(report) {
|
|
471
197
|
console.log(`Cairn toolcheck: ${report.root}`);
|
|
472
198
|
console.log(`Detected stacks: ${report.detected.length > 0 ? report.detected.join(", ") : "none"}`);
|
|
@@ -493,74 +219,3 @@ function isCliEntry() {
|
|
|
493
219
|
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
494
220
|
}
|
|
495
221
|
}
|
|
496
|
-
|
|
497
|
-
function normalizePath(path) {
|
|
498
|
-
return path.split(sep).join("/");
|
|
499
|
-
}
|
|
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
|
-
|
|
564
|
-
function hasWindowsExecutableExtension(command) {
|
|
565
|
-
return /\.(cmd|bat|exe)$/i.test(command);
|
|
566
|
-
}
|
package/scripts/cairn.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
@@ -8,8 +8,17 @@ import { parseRootArgs, resolvePluginRoot } from "./cairn-paths.mjs";
|
|
|
8
8
|
|
|
9
9
|
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
10
10
|
const scriptDir = join(pluginRoot, "scripts");
|
|
11
|
+
const version = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")).version;
|
|
11
12
|
|
|
12
13
|
export async function main(command = "help", args = [], { runner = spawnSync } = {}) {
|
|
14
|
+
if (command === "help" || command === "--help") {
|
|
15
|
+
console.log(message("usage"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (command === "version" || command === "--version") {
|
|
19
|
+
console.log(`cairn ${version}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
13
22
|
if (["install", "upgrade", "doctor", "uninstall"].includes(command)) {
|
|
14
23
|
const result = runner(process.execPath, [join(scriptDir, "cairn-lifecycle.mjs"), command, ...args], { stdio: "inherit" });
|
|
15
24
|
process.exitCode = result.status ?? 1;
|
|
@@ -40,7 +49,7 @@ export async function main(command = "help", args = [], { runner = spawnSync } =
|
|
|
40
49
|
console.log(message(command));
|
|
41
50
|
return;
|
|
42
51
|
}
|
|
43
|
-
|
|
52
|
+
throw new Error(`unknown command: ${command}`);
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
export function message(key, locale = localeValue()) {
|
|
@@ -140,5 +149,10 @@ const messages = {
|
|
|
140
149
|
|
|
141
150
|
if (isCliEntry()) {
|
|
142
151
|
const [command = "help", ...args] = process.argv.slice(2);
|
|
143
|
-
|
|
152
|
+
try {
|
|
153
|
+
await main(command, args);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`Cairn error: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
process.exitCode = 2;
|
|
157
|
+
}
|
|
144
158
|
}
|
|
@@ -17,7 +17,7 @@ When subagent tools are available, each agent may recursively delegate bounded s
|
|
|
17
17
|
|
|
18
18
|
## Runtime Location
|
|
19
19
|
|
|
20
|
-
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 look for Cairn runtime files in the target repository or derive the plugin root from the current working directory.
|
|
20
|
+
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 look for Cairn runtime files in the target repository or derive the plugin root from the current working directory. If the shared runtime or locator is missing, unreadable, or inconsistent, stop every dependent mirror and recover from the published/global package with `cairn doctor` followed by `cairn upgrade`; do not use another mirror as a repair source.
|
|
21
21
|
|
|
22
22
|
Keep the target repository root separate. Pass it explicitly with `--root <repoRoot>` when invoking the installed runtime. Repository artifacts include `MEMORY.md`, `PLAN.md`, `docs/memory/`, and `docs/plan/`; runtime goal state lives only under the user-home Cairn directory.
|
|
23
23
|
|
|
@@ -31,7 +31,8 @@ Keep the target repository root separate. Pass it explicitly with `--root <repoR
|
|
|
31
31
|
- Recursive subagent delegation is allowed only for bounded sub-tasks when the current surface supports it.
|
|
32
32
|
4. Write detailed facts to `docs/memory/<domain>.md`.
|
|
33
33
|
5. Keep only one-line links and summaries in `MEMORY.md`.
|
|
34
|
-
6.
|
|
34
|
+
6. When a lesson or correction recurs and is machine-checkable, encode it as a test, lint rule, script, or hook instead of more prose; memory keeps judgment rules and a pointer to the enforcing check.
|
|
35
|
+
7. Write user-visible responses and generated or updated documentation, plans, and memory artifacts in the OS locale unless the user asks for another language.
|
|
35
36
|
|
|
36
37
|
## Delegation Prompt Format
|
|
37
38
|
|
|
@@ -23,7 +23,7 @@ At the start and after compaction, restart, or handoff, restore context in this
|
|
|
23
23
|
|
|
24
24
|
## Runtime Location
|
|
25
25
|
|
|
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.
|
|
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. If the shared runtime or locator is missing, unreadable, or inconsistent, stop every dependent mirror and recover from the published/global package with `cairn doctor` followed by `cairn upgrade`; do not use another mirror as a repair source.
|
|
27
27
|
|
|
28
28
|
Use logical resource IDs such as `cairn://templates/work-plan.md` and `cairn://docs/model-guidance/codex.md` in repository plans. Resolve those IDs through `pluginRoot` only while executing. Keep `repoRoot` separate and pass it explicitly as `--root <repoRoot>`.
|
|
29
29
|
|
|
@@ -33,7 +33,10 @@ For a non-trivial implementation request or continuation of already planned work
|
|
|
33
33
|
node "<pluginRoot>/scripts/cairn.mjs" goal start --root "<repoRoot>" --goal "<objective>" --plan "docs/plan/<topic>.md" --tasks '[{"id":"triage-plan","title":"Run planned triage and finalize the plan","requiredEvidence":["planArtifact","triageDecision"]},{"id":"implement","title":"Implement the finalized plan"},{"id":"verify","title":"Verify and review the result"}]' --criteria "<completion criterion>" --session "<current session_id>"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Pass `--session` only with a session ID the surface actually exposes to the model; never guess or invent one, because a mismatched owner makes every later hook treat the goal as foreign. If no stable session ID is exposed, omit `--session` and record that the goal is repository-scoped.
|
|
37
|
+
|
|
38
|
+
If an unfinished repository goal already exists when a new implementation request, steer, or drift arrives, do not cancel it to start another. Fold the new work into it with `goal replan` (completed tasks and their evidence are preserved; resume a paused or blocked goal first), synchronize the plan artifact and native UI roadmap, and keep advancing until goal final review. Start a separate goal only when the user explicitly switches work. When a hook reports that another session owns the goal and this session continues that same work, take ownership with `goal claim --session <id>` using the ID shown by the hook.
|
|
39
|
+
If Codex UI plan/goal tools are unavailable, record that limitation and continue with the repository plan and goal; never claim UI synchronization that did not occur.
|
|
37
40
|
|
|
38
41
|
## Procedure
|
|
39
42
|
|
|
@@ -72,8 +75,8 @@ Use the current Codex hook `session_id` when it is available. If the surface doe
|
|
|
72
75
|
- Models always inherit the host/user default; never configure or override them.
|
|
73
76
|
- Light Path planning, implementation, and verification request `medium`.
|
|
74
77
|
- Heavy Path planning, review, and implementation request `high`; final verification and review request `xhigh`.
|
|
75
|
-
- Every module task records `Requested reasoning effort` and `Effective reasoning effort` in `docs/plan/<topic>.md`.
|
|
76
|
-
- Pass requested effort only when dispatching a new task/worker and the host exposes a reasoning-effort option or host-native equivalent. Omit model overrides.
|
|
78
|
+
- Every module task records `Requested reasoning effort` and `Effective reasoning effort` in `docs/plan/<topic>.md`. Requested is Cairn's desired profile; Effective is what the dispatch result confirms.
|
|
79
|
+
- Pass requested effort only when dispatching a new task/worker and the host exposes a reasoning-effort option or host-native equivalent. Omit model overrides. Effective `inherited` means no task-level override was confirmed, not proof of missing host capability. Describe capability only from the exposed interface or dispatch response, distinguish Cairn non-integration from host non-support, and leave model/global config unchanged.
|
|
77
80
|
- On every route change, synchronize the plan artifact, repository goal task roadmap through `goal replan`, native UI plan, and reasoning effort profile before mutation. Completed task profiles are preserved as audit history; incomplete task profiles are recalculated for the new path.
|
|
78
81
|
|
|
79
82
|
## Complexity Triage
|