cairn-ai 0.2.6 → 0.2.7
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 +1 -1
- package/.agents/workflows/cairn-plan.md +2 -2
- package/.agents/workflows/cairn-review.md +1 -1
- package/.agents/workflows/cairn-work.md +3 -3
- 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 +7 -6
- package/README.md +5 -6
- package/README.pt.md +3 -2
- package/README.zh.md +3 -2
- package/agents/worker.md +1 -1
- package/commands/cairn-install.md +2 -1
- package/commands/cairn-memory.md +1 -1
- package/commands/cairn-plan.md +2 -2
- package/commands/cairn-review.md +1 -1
- package/commands/cairn-work.md +3 -3
- package/docs/model-guidance/README.md +4 -4
- package/docs/model-guidance/claude.md +1 -1
- package/docs/model-guidance/codex.md +2 -2
- package/package.json +4 -2
- package/scripts/cairn-cleanup.mjs +12 -6
- package/scripts/cairn-goal-cli.mjs +233 -0
- package/scripts/cairn-goal-evidence.mjs +179 -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 +177 -0
- package/scripts/cairn-goal-policy.mjs +33 -0
- package/scripts/cairn-goal-state-validation.mjs +199 -0
- package/scripts/cairn-goal-state.mjs +89 -0
- package/scripts/cairn-goal.mjs +32 -1078
- package/scripts/cairn-lifecycle-commands.mjs +200 -0
- package/scripts/cairn-lifecycle-config.mjs +149 -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 +148 -0
- package/scripts/cairn-state-hook.mjs +24 -0
- package/scripts/cairn-state-init.mjs +137 -0
- package/scripts/cairn-state.mjs +6 -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 +1 -1
- package/skills/cairn-plan/SKILL.md +3 -3
- package/skills/cairn-review/SKILL.md +1 -1
- package/skills/cairn-work/SKILL.md +4 -4
- 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
|
|
|
@@ -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
|
|
|
@@ -72,8 +72,8 @@ Use the current Codex hook `session_id` when it is available. If the surface doe
|
|
|
72
72
|
- Models always inherit the host/user default; never configure or override them.
|
|
73
73
|
- Light Path planning, implementation, and verification request `medium`.
|
|
74
74
|
- 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.
|
|
75
|
+
- 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.
|
|
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. 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
77
|
- 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
78
|
|
|
79
79
|
## Complexity Triage
|
|
@@ -17,7 +17,7 @@ At the start and after compaction, restart, delegation, or handoff, restore cont
|
|
|
17
17
|
|
|
18
18
|
When subagent tools are available, each agent may recursively delegate bounded review sub-tasks to subagents. Every child subagent reads project-root `MEMORY.md` when present and continues when absent, keeps the assigned scope, and preserves others' edits.
|
|
19
19
|
|
|
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.
|
|
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. 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
|
## Procedure
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ When subagent tools are available, each agent may recursively delegate bounded s
|
|
|
29
29
|
|
|
30
30
|
## Runtime Location
|
|
31
31
|
|
|
32
|
-
Resolve Cairn's read-only runtime from `references/cairn-runtime.json` next to this `SKILL.md`. Its `pluginRoot` contains Cairn's scripts 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 runtime files or derive the plugin root from the current working directory. Resolve `cairn://...` plan resources through this runtime and pass the target repository separately with `--root <repoRoot>`.
|
|
32
|
+
Resolve Cairn's read-only runtime from `references/cairn-runtime.json` next to this `SKILL.md`. Its `pluginRoot` contains Cairn's scripts 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 runtime files or derive the plugin root from the current working directory. Resolve `cairn://...` plan resources through this runtime and pass the target repository separately with `--root <repoRoot>`. 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.
|
|
33
33
|
|
|
34
34
|
Use the installed CLI for durable state. Important forms are:
|
|
35
35
|
|
|
@@ -46,7 +46,7 @@ node "<pluginRoot>/scripts/cairn.mjs" goal complete --quiet --root "<repoRoot>"
|
|
|
46
46
|
|
|
47
47
|
`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.
|
|
48
48
|
|
|
49
|
-
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
|
+
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. With no explicit watch, Git-ignored files are excluded; pass each ignored artifact required by the claim with `--watch <path>` or explicitly watch its containing directory.
|
|
50
50
|
|
|
51
51
|
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>"`.
|
|
52
52
|
|
|
@@ -72,8 +72,8 @@ Any relevant mutation after a gate makes that result stale evidence. Rerun the a
|
|
|
72
72
|
- Models always inherit the host/user default; never configure or override them.
|
|
73
73
|
- Light Path planning, implementation, and verification request `medium`.
|
|
74
74
|
- 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.
|
|
75
|
+
- 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.
|
|
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. 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
77
|
- On every route change, synchronize the plan artifact, repository goal task roadmap through `goal replan`, native UI plan, and reasoning effort profile before edits resume. Completed task profiles are preserved as audit history; incomplete task profiles are recalculated for the new path.
|
|
78
78
|
|
|
79
79
|
## Procedure
|