@cairnkeep/cli 2.8.0 → 2.11.0
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/CHANGELOG.md +68 -1
- package/README.md +82 -27
- package/bin/cairn +6 -153
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +41 -4
- package/docs/containers.md +10 -0
- package/docs/context-packs.md +94 -0
- package/docs/harness-compatibility.md +37 -3
- package/docs/learning/CURRICULUM-MAP.md +16 -5
- package/docs/learning/FEATURE-GUIDE.md +15 -4
- package/docs/learning/README.md +8 -2
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +11 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
- package/docs/learning/lessons/L21-context-packs.md +79 -0
- package/docs/learning/lessons/L22-native-windows.md +83 -0
- package/docs/learning/lessons/L23-guided-setup.md +138 -0
- package/docs/learning/tracks/operator.md +10 -3
- package/docs/learning/tracks/quickstart.md +7 -3
- package/docs/mcp-tool-profiles.md +43 -0
- package/docs/native-windows.md +122 -0
- package/docs/operating.md +157 -18
- package/docs/privacy-and-data-flow.md +51 -1
- package/docs/storage.md +29 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
- package/mcp-memory-server/dist/artifact-store.js +196 -55
- package/mcp-memory-server/dist/capability-cli.js +2 -1
- package/mcp-memory-server/dist/capability-config.js +4 -3
- package/mcp-memory-server/dist/capability-harness.js +34 -7
- package/mcp-memory-server/dist/context-pack-cli.js +185 -0
- package/mcp-memory-server/dist/context-pack.js +889 -0
- package/mcp-memory-server/dist/eval-cli.js +6 -5
- package/mcp-memory-server/dist/eval-plan.js +35 -13
- package/mcp-memory-server/dist/eval-process.js +11 -4
- package/mcp-memory-server/dist/eval-report.js +8 -5
- package/mcp-memory-server/dist/eval-runner.js +2 -0
- package/mcp-memory-server/dist/eval-schema.js +1 -0
- package/mcp-memory-server/dist/graph-cli.js +7 -1
- package/mcp-memory-server/dist/index.js +87 -37
- package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
- package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
- package/mcp-memory-server/dist/mcp-tool-profile.js +164 -0
- package/mcp-memory-server/dist/note-store.js +12 -3
- package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
- package/mcp-memory-server/dist/platform-security.js +102 -0
- package/mcp-memory-server/dist/skill-store.js +27 -12
- package/mcp-memory-server/package.json +4 -1
- package/package.json +5 -4
- package/pi/extensions/cairnkeep-memory.ts +61 -0
- package/schemas/cairnkeep-setup-policy.schema.json +55 -0
- package/schemas/cairnkeep-setup.schema.json +36 -0
- package/schemas/context-pack.schema.json +33 -0
- package/schemas/eval-report.schema.json +1 -0
- package/scripts/cairn-cli.mjs +119 -0
- package/scripts/cairn-container-cli.mjs +121 -0
- package/scripts/cairn-hook.mjs +199 -0
- package/scripts/completion.sh +33 -5
- package/scripts/doctor.sh +96 -0
- package/scripts/fix-permissions.mjs +12 -0
- package/scripts/memory-wiki-audit.mjs +95 -0
- package/scripts/phase26-test-manifest.mjs +26 -0
- package/scripts/run-repository-tests.mjs +76 -0
- package/scripts/setup-core.mjs +310 -0
- package/scripts/setup-reconcile.mjs +331 -0
- package/scripts/setup.mjs +306 -0
- package/scripts/sync-pi-assets.sh +19 -4
- package/scripts/test-cli-dispatch.sh +6 -0
- package/scripts/test-completion.sh +44 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-learning-docs.sh +53 -3
- package/scripts/test-package-install.sh +81 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-phase18-capability-lifecycle.sh +1 -1
- package/scripts/test-pi-lifecycle.mjs +190 -0
- package/scripts/test-setup-compatibility.mjs +186 -0
- package/scripts/test-setup-output.sh +131 -0
- package/scripts/test-setup-overlay.sh +113 -0
- package/scripts/test-setup-preflight.mjs +281 -0
- package/scripts/test-setup-reconcile.mjs +257 -0
- package/scripts/test-uninstall.sh +71 -0
- package/scripts/test-windows-native.mjs +136 -0
- package/scripts/uninstall.sh +95 -7
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- package/scripts/windows-platform.mjs +725 -0
- package/templates/env.example.template +11 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
fstatSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { createInterface } from "node:readline/promises";
|
|
15
|
+
import { stdin as defaultInput, stdout as defaultOutput, stderr as defaultError } from "node:process";
|
|
16
|
+
import {
|
|
17
|
+
buildSetupPlan,
|
|
18
|
+
classifySetupTarget,
|
|
19
|
+
parseSetupArgs,
|
|
20
|
+
readSetupPolicy,
|
|
21
|
+
resolveSetupChoices,
|
|
22
|
+
} from "./setup-core.mjs";
|
|
23
|
+
import { reconcileSetupPlan } from "./setup-reconcile.mjs";
|
|
24
|
+
|
|
25
|
+
const HARNESSES = Object.freeze(["claude", "opencode", "pi", "kimi", "qwen"]);
|
|
26
|
+
const GIT_MODES = Object.freeze(["init", "existing", "none"]);
|
|
27
|
+
const MEMORY_MODES = Object.freeze(["local", "none"]);
|
|
28
|
+
const STATE_KEYS = Object.freeze(["schema_version", "cairnkeep_version", "git", "memory", "harnesses", "assets"]);
|
|
29
|
+
const ASSET_KEYS = Object.freeze(["digest", "mode", "template"]);
|
|
30
|
+
const SAFE_ASSET = /^\.(?:ai|planning|agentfs)\/(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
|
|
31
|
+
const COMMON_SETUP_ASSETS = Object.freeze([
|
|
32
|
+
".ai/env.example",
|
|
33
|
+
".ai/trajectory-redaction.json",
|
|
34
|
+
".ai/capabilities.json",
|
|
35
|
+
".agentfs/.gitignore",
|
|
36
|
+
".planning/config.json",
|
|
37
|
+
".planning/PROJECT-BRIEF.md",
|
|
38
|
+
".planning/wiki/index.md",
|
|
39
|
+
".planning/wiki/policy.md",
|
|
40
|
+
".planning/wiki/CONTRADICTIONS.md",
|
|
41
|
+
".planning/wiki/LOG.md",
|
|
42
|
+
".planning/alignment/policy.md",
|
|
43
|
+
".planning/alignment/gap-register.yaml",
|
|
44
|
+
".planning/graphs/policy.md",
|
|
45
|
+
".planning/graphs/.gitignore",
|
|
46
|
+
".planning/security/policy.md",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function operational(kind, message) {
|
|
50
|
+
const error = new Error(message);
|
|
51
|
+
error.name = "SetupOperationalError";
|
|
52
|
+
error.kind = kind;
|
|
53
|
+
error.status = 1;
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeLine(stream, value = "") {
|
|
58
|
+
stream.write(`${value}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseInteractiveHarnesses(value) {
|
|
62
|
+
const selected = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
63
|
+
if (selected.length === 0 || new Set(selected).size !== selected.length || selected.some((item) => !HARNESSES.includes(item))) {
|
|
64
|
+
throw operational("interactive-input", `Harnesses must be a comma-separated subset of ${HARNESSES.join(", ")}.`);
|
|
65
|
+
}
|
|
66
|
+
return HARNESSES.filter((name) => selected.includes(name));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function promptSetupChoices(parsed, streams) {
|
|
70
|
+
const terminal = typeof streams.question === "function"
|
|
71
|
+
? { question: streams.question, close: streams.closePrompt ?? (() => {}) }
|
|
72
|
+
: createInterface({ input: streams.input, output: streams.output });
|
|
73
|
+
try {
|
|
74
|
+
const target = parsed.target ?? (await terminal.question("Target path: ")).trim();
|
|
75
|
+
if (!target) throw operational("interactive-input", "A setup target path is required.");
|
|
76
|
+
const preflight = classifySetupTarget(target);
|
|
77
|
+
const gitQuestion = preflight.targetState === "missing" || preflight.targetState === "empty"
|
|
78
|
+
? "Git mode (init recommended; choices: init, existing, none): "
|
|
79
|
+
: preflight.repository === "work-tree"
|
|
80
|
+
? "Git mode (existing recommended; choices: init, existing, none): "
|
|
81
|
+
: "Git mode (init requires explicit choice for existing non-Git target; choices: init, existing, none): ";
|
|
82
|
+
const git = parsed.git ?? (await terminal.question(gitQuestion)).trim();
|
|
83
|
+
const harnesses = parsed.harnesses ?? parseInteractiveHarnesses(await terminal.question(`Harnesses (${HARNESSES.join(", ")}): `));
|
|
84
|
+
const memory = parsed.memory ?? (await terminal.question("Memory mode (local, none): ")).trim();
|
|
85
|
+
return { target, git, harnesses, memory, preflight, terminal };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
terminal.close();
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderPlan(plan, stream) {
|
|
93
|
+
writeLine(stream, `Target: ${plan.target}`);
|
|
94
|
+
writeLine(stream, `Git mode: ${plan.git}`);
|
|
95
|
+
writeLine(stream, `Harnesses: ${plan.harnesses.join(", ")}`);
|
|
96
|
+
writeLine(stream, `Memory mode: ${plan.memory}`);
|
|
97
|
+
writeLine(stream, "Planned assets:");
|
|
98
|
+
for (const asset of plan.assets) writeLine(stream, ` ${asset.path}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function launchCommands(plan, platform) {
|
|
102
|
+
const suffix = platform === "win32" ? "cmd" : "sh";
|
|
103
|
+
return plan.harnesses.map((harness) => `.ai/start-${harness}.${suffix}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function setupRecovery(plan) {
|
|
107
|
+
return [`cairn setup . --git ${plan.git} --harness ${plan.harnesses.join(",")} --memory ${plan.memory} --yes`];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resultFor(plan, reconciliation, platform) {
|
|
111
|
+
const machineCommand = plan.harnesses.includes("pi") ? "cairn sync-pi --apply" : "cairn sync --apply";
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
schema_version: 1,
|
|
114
|
+
status: plan.limited ? "limited" : "complete",
|
|
115
|
+
target: plan.target,
|
|
116
|
+
git: plan.git,
|
|
117
|
+
memory: plan.memory,
|
|
118
|
+
harnesses: plan.harnesses,
|
|
119
|
+
counts: reconciliation.counts,
|
|
120
|
+
changes: reconciliation.changes,
|
|
121
|
+
verification: Object.freeze(["cairn doctor"]),
|
|
122
|
+
launch_commands: Object.freeze(launchCommands(plan, platform)),
|
|
123
|
+
recovery: Object.freeze(setupRecovery(plan)),
|
|
124
|
+
limitations: Object.freeze(plan.limited ? ["Git integration is disabled; repository-aware features are limited."] : []),
|
|
125
|
+
machine_sync: Object.freeze({ automatic: false, command: machineCommand }),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function renderSetupResult(result, stream) {
|
|
130
|
+
writeLine(stream, `Setup status: ${result.status}`);
|
|
131
|
+
writeLine(stream, `Harnesses: ${result.harnesses.join(", ")}`);
|
|
132
|
+
writeLine(stream, `Git mode: ${result.git}`);
|
|
133
|
+
writeLine(stream, `Memory mode: ${result.memory}`);
|
|
134
|
+
writeLine(stream, `Created: ${result.counts.created}`);
|
|
135
|
+
writeLine(stream, `Updated: ${result.counts.updated}`);
|
|
136
|
+
writeLine(stream, `Unchanged: ${result.counts.unchanged}`);
|
|
137
|
+
writeLine(stream, `Skipped: ${result.counts.skipped}`);
|
|
138
|
+
writeLine(stream, `Verification: ${result.verification.join(", ")}`);
|
|
139
|
+
for (const command of result.launch_commands) writeLine(stream, `Launch: ${command}`);
|
|
140
|
+
for (const command of result.recovery) writeLine(stream, `Recovery: ${command}`);
|
|
141
|
+
for (const limitation of result.limitations) writeLine(stream, `Limitation: ${limitation}`);
|
|
142
|
+
writeLine(stream, `Machine sync: ${result.machine_sync.command} (not run automatically)`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createTarget(target) {
|
|
146
|
+
try {
|
|
147
|
+
if (!existsSync(target)) mkdirSync(target, { recursive: true, mode: 0o755 });
|
|
148
|
+
const info = lstatSync(target);
|
|
149
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("target is not a regular directory");
|
|
150
|
+
} catch {
|
|
151
|
+
throw operational("target-denied", "Setup target could not be created safely.");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function initializeGit(target) {
|
|
156
|
+
const result = spawnSync("git", ["init", "-q", target], { encoding: "utf8", shell: false });
|
|
157
|
+
if (result.error?.code === "ENOENT") throw operational("missing-git", "Git executable is required for --git init.");
|
|
158
|
+
if (result.error || result.status !== 0) throw operational("git-init", "Git repository initialization failed.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function executeSetup(args, options) {
|
|
162
|
+
const isTTY = options.isTTY ?? Boolean(options.input.isTTY);
|
|
163
|
+
const parsed = parseSetupArgs(args, { isTTY });
|
|
164
|
+
const policy = parsed.policyPath ? readSetupPolicy(parsed.policyPath) : null;
|
|
165
|
+
|
|
166
|
+
let interactive = null;
|
|
167
|
+
let target = parsed.target;
|
|
168
|
+
let preflight = null;
|
|
169
|
+
if (isTTY && (!target || !parsed.git || !parsed.harnesses || !parsed.memory || !parsed.confirmed)) {
|
|
170
|
+
interactive = await promptSetupChoices(parsed, options);
|
|
171
|
+
target = interactive.target;
|
|
172
|
+
preflight = interactive.preflight;
|
|
173
|
+
}
|
|
174
|
+
preflight ??= classifySetupTarget(target);
|
|
175
|
+
const choices = resolveSetupChoices({
|
|
176
|
+
parsed,
|
|
177
|
+
preflight,
|
|
178
|
+
policy,
|
|
179
|
+
interactive: interactive ? { ...interactive, confirmed: true } : null,
|
|
180
|
+
});
|
|
181
|
+
let plan = buildSetupPlan({ target, preflight, choices });
|
|
182
|
+
if (options.augmentPlan) plan = await options.augmentPlan(plan);
|
|
183
|
+
|
|
184
|
+
if (interactive && !parsed.confirmed) {
|
|
185
|
+
renderPlan(plan, options.output);
|
|
186
|
+
const answer = (await interactive.terminal.question("Apply this setup plan? [y/N] ")).trim().toLowerCase();
|
|
187
|
+
interactive.terminal.close();
|
|
188
|
+
if (answer !== "y" && answer !== "yes") {
|
|
189
|
+
writeLine(options.output, "Setup cancelled; no files were changed.");
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
} else if (interactive) interactive.terminal.close();
|
|
193
|
+
|
|
194
|
+
createTarget(plan.target);
|
|
195
|
+
if (plan.git === "init") initializeGit(plan.target);
|
|
196
|
+
const reconciliation = await reconcileSetupPlan(plan);
|
|
197
|
+
return resultFor(plan, reconciliation, options.platform);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function runSetup(args, options = {}) {
|
|
201
|
+
const streams = {
|
|
202
|
+
input: options.input ?? defaultInput,
|
|
203
|
+
output: options.output ?? defaultOutput,
|
|
204
|
+
error: options.error ?? defaultError,
|
|
205
|
+
isTTY: options.isTTY,
|
|
206
|
+
platform: options.platform ?? process.platform,
|
|
207
|
+
augmentPlan: options.augmentPlan,
|
|
208
|
+
question: options.question,
|
|
209
|
+
closePrompt: options.closePrompt,
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
const result = await executeSetup(args, streams);
|
|
213
|
+
if (!result) return 0;
|
|
214
|
+
if (args.includes("--json")) writeLine(streams.output, JSON.stringify(result));
|
|
215
|
+
else renderSetupResult(result, streams.output);
|
|
216
|
+
return 0;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
writeLine(streams.error, error instanceof Error ? error.message : "Setup failed.");
|
|
219
|
+
return Number.isInteger(error?.status) ? error.status : 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function incompleteDiagnosis(recovery = "cairn setup . --git existing --harness claude --memory local --yes") {
|
|
224
|
+
return Object.freeze({ schema_version: 1, status: "incomplete", code: "setup-state", recovery: Object.freeze([recovery]) });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function exactKeys(value, expected) {
|
|
228
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
229
|
+
&& Object.keys(value).sort().join(",") === [...expected].sort().join(",");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function safeStateAsset(path, record) {
|
|
233
|
+
return typeof path === "string"
|
|
234
|
+
&& path.length <= 512
|
|
235
|
+
&& SAFE_ASSET.test(path)
|
|
236
|
+
&& !path.includes("\\")
|
|
237
|
+
&& !path.split("/").includes("..")
|
|
238
|
+
&& exactKeys(record, ASSET_KEYS)
|
|
239
|
+
&& typeof record.digest === "string"
|
|
240
|
+
&& /^[a-f0-9]{64}$/.test(record.digest)
|
|
241
|
+
&& Number.isInteger(record.mode)
|
|
242
|
+
&& record.mode >= 0
|
|
243
|
+
&& record.mode <= 0o777
|
|
244
|
+
&& typeof record.template === "string"
|
|
245
|
+
&& /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(record.template);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readPrivateState(path) {
|
|
249
|
+
const info = lstatSync(path);
|
|
250
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) throw new Error("unsafe");
|
|
251
|
+
if (process.platform !== "win32" && (info.mode & 0o777) !== 0o600) throw new Error("unsafe");
|
|
252
|
+
let descriptor;
|
|
253
|
+
try {
|
|
254
|
+
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
255
|
+
const opened = fstatSync(descriptor);
|
|
256
|
+
if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > 1024 * 1024) throw new Error("unsafe");
|
|
257
|
+
return JSON.parse(readFileSync(descriptor, "utf8"));
|
|
258
|
+
} finally {
|
|
259
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function diagnoseSetup(target = ".") {
|
|
264
|
+
const project = resolve(target);
|
|
265
|
+
const statePath = join(project, ".ai", "cairnkeep.json");
|
|
266
|
+
if (!existsSync(statePath)) return Object.freeze({ schema_version: 1, status: "absent", code: "not-configured", recovery: Object.freeze([]) });
|
|
267
|
+
try {
|
|
268
|
+
const state = readPrivateState(statePath);
|
|
269
|
+
if (!exactKeys(state, STATE_KEYS)
|
|
270
|
+
|| state.schema_version !== 1
|
|
271
|
+
|| typeof state.cairnkeep_version !== "string"
|
|
272
|
+
|| state.cairnkeep_version.length === 0
|
|
273
|
+
|| state.cairnkeep_version.length > 128
|
|
274
|
+
|| !GIT_MODES.includes(state.git)
|
|
275
|
+
|| !MEMORY_MODES.includes(state.memory)
|
|
276
|
+
|| !Array.isArray(state.harnesses)
|
|
277
|
+
|| state.harnesses.length === 0
|
|
278
|
+
|| new Set(state.harnesses).size !== state.harnesses.length
|
|
279
|
+
|| state.harnesses.some((name) => !HARNESSES.includes(name))
|
|
280
|
+
|| !state.assets
|
|
281
|
+
|| typeof state.assets !== "object"
|
|
282
|
+
|| Array.isArray(state.assets)
|
|
283
|
+
|| Object.keys(state.assets).length === 0) return incompleteDiagnosis();
|
|
284
|
+
|
|
285
|
+
const recovery = `cairn setup . --git ${state.git} --harness ${state.harnesses.join(",")} --memory ${state.memory} --yes`;
|
|
286
|
+
for (const [path, record] of Object.entries(state.assets)) {
|
|
287
|
+
if (!safeStateAsset(path, record)) return incompleteDiagnosis(recovery);
|
|
288
|
+
const destination = join(project, ...path.split("/"));
|
|
289
|
+
if (!existsSync(destination)) return incompleteDiagnosis(recovery);
|
|
290
|
+
const info = lstatSync(destination);
|
|
291
|
+
if (!info.isFile() || info.isSymbolicLink()) return incompleteDiagnosis(recovery);
|
|
292
|
+
const digest = createHash("sha256").update(readFileSync(destination)).digest("hex");
|
|
293
|
+
if (digest !== record.digest || (process.platform !== "win32" && (info.mode & 0o777) !== record.mode)) return incompleteDiagnosis(recovery);
|
|
294
|
+
}
|
|
295
|
+
const requiredAssets = [...COMMON_SETUP_ASSETS, ...state.harnesses.map((harness) => `.ai/start-${harness}.sh`)];
|
|
296
|
+
if (requiredAssets.some((path) => !Object.hasOwn(state.assets, path))) return incompleteDiagnosis(recovery);
|
|
297
|
+
return Object.freeze({
|
|
298
|
+
schema_version: 1,
|
|
299
|
+
status: state.git === "none" ? "limited" : "complete",
|
|
300
|
+
code: state.git === "none" ? "git-disabled" : "configured",
|
|
301
|
+
recovery: Object.freeze([]),
|
|
302
|
+
});
|
|
303
|
+
} catch {
|
|
304
|
+
return incompleteDiagnosis();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -21,6 +21,7 @@ SOURCE_ROOT="$ROOT_DIR/pi"
|
|
|
21
21
|
LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
22
22
|
MODE="check"
|
|
23
23
|
ASSETS=(
|
|
24
|
+
"extensions/cairnkeep-memory.ts"
|
|
24
25
|
"extensions/cairnkeep-trajectory.ts"
|
|
25
26
|
"prompts/graphify.md"
|
|
26
27
|
)
|
|
@@ -37,7 +38,7 @@ done
|
|
|
37
38
|
|
|
38
39
|
render_asset() {
|
|
39
40
|
local rel="$1"
|
|
40
|
-
if [[ "$rel" ==
|
|
41
|
+
if [[ "$rel" == extensions/*.ts ]]; then
|
|
41
42
|
sed "s|@@INFRA_ROOT@@|$ROOT_DIR|g" "$SOURCE_ROOT/$rel"
|
|
42
43
|
else
|
|
43
44
|
cat "$SOURCE_ROOT/$rel"
|
|
@@ -74,20 +75,34 @@ ensure_sources
|
|
|
74
75
|
if [[ "$MODE" == "apply" ]]; then
|
|
75
76
|
updated=0
|
|
76
77
|
unchanged=0
|
|
78
|
+
memory_updated=0
|
|
79
|
+
memory_unchanged=0
|
|
77
80
|
for rel in "${ASSETS[@]}"; do
|
|
78
81
|
mkdir -p "$(dirname "$LIVE_ROOT/$rel")"
|
|
79
82
|
if [[ -f "$LIVE_ROOT/$rel" ]] && cmp -s <(render_asset "$rel") "$LIVE_ROOT/$rel"; then
|
|
80
|
-
|
|
83
|
+
if [[ "$rel" == "extensions/cairnkeep-memory.ts" ]]; then
|
|
84
|
+
memory_unchanged=1
|
|
85
|
+
else
|
|
86
|
+
unchanged=$((unchanged + 1))
|
|
87
|
+
fi
|
|
81
88
|
continue
|
|
82
89
|
fi
|
|
83
90
|
rendered_tmp=$(mktemp)
|
|
84
91
|
render_asset "$rel" > "$rendered_tmp"
|
|
85
92
|
install -m 0644 "$rendered_tmp" "$LIVE_ROOT/$rel"
|
|
86
93
|
rm -f "$rendered_tmp"
|
|
87
|
-
|
|
94
|
+
if [[ "$rel" == "extensions/cairnkeep-memory.ts" ]]; then
|
|
95
|
+
memory_updated=1
|
|
96
|
+
else
|
|
97
|
+
updated=$((updated + 1))
|
|
98
|
+
fi
|
|
88
99
|
done
|
|
89
100
|
printf 'Applied %s Pi asset(s); %s already matched.\n' "$updated" "$unchanged"
|
|
101
|
+
printf 'Pi memory extension: %s updated; %s already matched.\n' "$memory_updated" "$memory_unchanged"
|
|
90
102
|
fi
|
|
91
103
|
|
|
92
|
-
check_assets
|
|
104
|
+
if ! check_assets; then
|
|
105
|
+
echo "Recovery: sync-pi-assets.sh --apply --live-root PATH" >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
93
108
|
printf 'Pi extension and graph prompt are in sync under %s\n' "$LIVE_ROOT"
|
|
@@ -16,6 +16,8 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
|
16
16
|
"$cairn" help | grep -q "cairn notes" || fail "help missing notes"
|
|
17
17
|
"$cairn" help | grep -q "cairn artifact <list|show|delete|prune>" || fail "help missing artifact"
|
|
18
18
|
"$cairn" help | grep -q "cairn capabilities <list|status|enable|disable|reset|logging>" || fail "help missing capabilities"
|
|
19
|
+
"$cairn" help | grep -q "cairn mcp-tools <list|status|set|reset>" || fail "help missing mcp-tools"
|
|
20
|
+
"$cairn" help | grep -q "cairn pack <init|lock|validate|install|list|show|remove|enable|disable|update|skills|approve-skill|revoke-skill>" || fail "help missing pack"
|
|
19
21
|
"$cairn" help | grep -q "cairn graph <build|query|status|diff|explain|path>" || fail "help missing graph"
|
|
20
22
|
"$cairn" sync --help >/dev/null 2>&1 || fail "cairn sync dispatch"
|
|
21
23
|
"$cairn" sync-pi --help >/dev/null 2>&1 || fail "cairn sync-pi dispatch"
|
|
@@ -23,6 +25,10 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
|
23
25
|
env -u CAIRN_NOTE_DISTILLATION "$cairn" notes --help >/dev/null 2>&1 || fail "cairn notes dispatch"
|
|
24
26
|
"$cairn" artifact --help | grep -q "cairn artifact list" || fail "cairn artifact dispatch"
|
|
25
27
|
"$cairn" capabilities --help >"$tmp/capabilities-help" 2>/dev/null || fail "cairn capabilities dispatch"
|
|
28
|
+
"$cairn" mcp-tools list --json >"$tmp/mcp-tools.json" || fail "cairn mcp-tools dispatch"
|
|
29
|
+
CAIRN_PACK_BASE_DIR="$tmp/pack-store" "$cairn" pack list --json >"$tmp/packs.json" || fail "cairn pack dispatch"
|
|
30
|
+
node -e 'const v=require(process.argv[1]);if(v.schema_version!==1||!Array.isArray(v.tools))process.exit(1)' "$tmp/mcp-tools.json" || fail "mcp-tools JSON contract"
|
|
31
|
+
node -e 'const v=require(process.argv[1]);if(v.schema_version!==1||!Array.isArray(v.packs))process.exit(1)' "$tmp/packs.json" || fail "pack JSON contract"
|
|
26
32
|
"$cairn" graph --help | grep -q "cairn graph explain <symbol>" || fail "cairn graph dispatch"
|
|
27
33
|
grep -qxF 'MCP capability changes require a memory-server restart. Operating capability' "$tmp/capabilities-help" || fail "capability help missing exact restart wording"
|
|
28
34
|
grep -qxF 'changes apply on the next invocation.' "$tmp/capabilities-help" || fail "capability help missing exact invocation wording"
|
|
@@ -43,6 +43,11 @@ for shell in bash zsh fish; do
|
|
|
43
43
|
for skill_flag in --candidate --exploration-task-set --confirmation-task-set --evaluation --application --confirm; do
|
|
44
44
|
grep -q -- "$skill_flag\|-l ${skill_flag#--}" "$tmp/$shell"
|
|
45
45
|
done
|
|
46
|
+
grep -q 'mcp-tools' "$tmp/$shell"
|
|
47
|
+
grep -q 'full.*read-only.*custom\|full read-only custom' "$tmp/$shell"
|
|
48
|
+
grep -q 'pack' "$tmp/$shell"
|
|
49
|
+
grep -q 'init.*lock.*validate.*install.*list.*show.*remove.*enable.*disable.*update.*skills.*approve-skill.*revoke-skill\|init lock validate install list show remove enable disable update skills approve-skill revoke-skill' "$tmp/$shell"
|
|
50
|
+
grep -q -- '--purge-packs\|-l purge-packs' "$tmp/$shell"
|
|
46
51
|
done
|
|
47
52
|
|
|
48
53
|
"$ROOT/bin/cairn" help >"$tmp/root-help"
|
|
@@ -113,5 +118,44 @@ if "$ROOT/bin/cairn" completion unsupported >/dev/null 2>&1; then
|
|
|
113
118
|
exit 1
|
|
114
119
|
fi
|
|
115
120
|
|
|
121
|
+
completion_surface_complete=true
|
|
122
|
+
for shell in bash zsh fish; do
|
|
123
|
+
if ! grep -qw 'setup' "$tmp/$shell"; then
|
|
124
|
+
completion_surface_complete=false
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
"$ROOT/bin/cairn" completion powershell >"$tmp/powershell"
|
|
128
|
+
if ! grep -qE "['\"]setup['\"]" "$tmp/powershell"; then
|
|
129
|
+
completion_surface_complete=false
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if [[ "$completion_surface_complete" != true ]]; then
|
|
133
|
+
if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
|
|
134
|
+
echo "PHASE26_RED:SETUP_COMPLETION_MISSING"
|
|
135
|
+
exit 86
|
|
136
|
+
fi
|
|
137
|
+
echo "SKIP: guided setup completion surface is not complete"
|
|
138
|
+
git -C "$ROOT" diff --check
|
|
139
|
+
echo "PASS: shell completion generation"
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
setup_flags='--git --harness --memory --policy --yes --json'
|
|
144
|
+
setup_values='init existing none claude opencode pi kimi qwen local'
|
|
145
|
+
for shell in bash zsh fish powershell; do
|
|
146
|
+
for flag in $setup_flags; do
|
|
147
|
+
grep -q -- "$flag\|${flag#--}" "$tmp/$shell" || {
|
|
148
|
+
echo "setup completion omitted $flag for $shell" >&2
|
|
149
|
+
exit 1
|
|
150
|
+
}
|
|
151
|
+
done
|
|
152
|
+
for value in $setup_values; do
|
|
153
|
+
grep -qF "$value" "$tmp/$shell" || {
|
|
154
|
+
echo "setup completion omitted $value for $shell" >&2
|
|
155
|
+
exit 1
|
|
156
|
+
}
|
|
157
|
+
done
|
|
158
|
+
done
|
|
159
|
+
|
|
116
160
|
git -C "$ROOT" diff --check
|
|
117
161
|
echo "PASS: shell completion generation"
|
package/scripts/test-doctor.sh
CHANGED
|
@@ -454,3 +454,69 @@ for sentinel in prompt-sentinel model-output-sentinel adapter-stderr-sentinel en
|
|
|
454
454
|
done
|
|
455
455
|
|
|
456
456
|
echo "PASS: cairn doctor (existing probes plus value-free capability/evaluation diagnostics)"
|
|
457
|
+
|
|
458
|
+
phase26_doctor_complete=true
|
|
459
|
+
for required in \
|
|
460
|
+
"$ROOT/scripts/setup.mjs" \
|
|
461
|
+
"$ROOT/schemas/cairnkeep-setup.schema.json" \
|
|
462
|
+
"$ROOT/pi/extensions/cairnkeep-memory.ts"; do
|
|
463
|
+
[[ -f "$required" ]] || phase26_doctor_complete=false
|
|
464
|
+
done
|
|
465
|
+
grep -qF 'cairnkeep.json' "$doctor" || phase26_doctor_complete=false
|
|
466
|
+
grep -qF 'cairnkeep-memory.ts' "$doctor" || phase26_doctor_complete=false
|
|
467
|
+
|
|
468
|
+
if [[ "$phase26_doctor_complete" != true ]]; then
|
|
469
|
+
if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
|
|
470
|
+
echo "PHASE26_RED:SETUP_DOCTOR_MISSING"
|
|
471
|
+
exit 86
|
|
472
|
+
fi
|
|
473
|
+
echo "SKIP: setup and Pi doctor diagnostics are not complete"
|
|
474
|
+
exit 0
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
setup_home="$tmp/setup-home"
|
|
478
|
+
setup_pi="$setup_home/.pi/agent"
|
|
479
|
+
mkdir -p "$setup_home" "$setup_pi"
|
|
480
|
+
HOME="$setup_home" "$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$setup_pi" >/dev/null
|
|
481
|
+
|
|
482
|
+
complete_project="$tmp/setup-complete"
|
|
483
|
+
HOME="$setup_home" "$ROOT/bin/cairn" setup "$complete_project" \
|
|
484
|
+
--git init --harness claude,pi --memory local --yes --json >/dev/null
|
|
485
|
+
(cd "$complete_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-complete.out" 2>&1 \
|
|
486
|
+
|| fail "doctor rejected complete guided setup"
|
|
487
|
+
grep -Eq '\[PASS\].*(project )?setup.*complete' "$tmp/setup-complete.out" || fail "doctor omitted complete setup state"
|
|
488
|
+
|
|
489
|
+
limited_project="$tmp/setup-limited"
|
|
490
|
+
HOME="$setup_home" "$ROOT/bin/cairn" setup "$limited_project" \
|
|
491
|
+
--git none --harness claude --memory none --yes --json >/dev/null
|
|
492
|
+
(cd "$limited_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-limited.out" 2>&1 \
|
|
493
|
+
|| fail "doctor treated intentional Git-less setup as fatal"
|
|
494
|
+
grep -Eq '\[WARN\].*(setup.*limited|Git-less)' "$tmp/setup-limited.out" || fail "doctor omitted Git-less limited state"
|
|
495
|
+
|
|
496
|
+
incomplete_project="$tmp/setup-incomplete"
|
|
497
|
+
mkdir -p "$incomplete_project/.ai"
|
|
498
|
+
printf '%s\n' '{"schema_version":1,"git":"existing","memory":"local","harnesses":["pi"],"assets":{}}' >"$incomplete_project/.ai/cairnkeep.json"
|
|
499
|
+
set +e
|
|
500
|
+
(cd "$incomplete_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-incomplete.out" 2>&1
|
|
501
|
+
incomplete_status=$?
|
|
502
|
+
set -e
|
|
503
|
+
[[ "$incomplete_status" -ne 0 ]] || fail "doctor accepted incomplete setup state"
|
|
504
|
+
grep -Eq '\[FAIL\].*(setup.*incomplete|setup state)' "$tmp/setup-incomplete.out" || fail "doctor omitted incomplete setup diagnosis"
|
|
505
|
+
grep -Eq 'cairn setup .*--git|cairn setup --git' "$tmp/setup-incomplete.out" || fail "incomplete setup lacks exact setup recovery"
|
|
506
|
+
|
|
507
|
+
rm -f "$setup_pi/extensions/cairnkeep-memory.ts"
|
|
508
|
+
set +e
|
|
509
|
+
(cd "$complete_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-pi-drift.out" 2>&1
|
|
510
|
+
pi_drift_status=$?
|
|
511
|
+
set -e
|
|
512
|
+
[[ "$pi_drift_status" -ne 0 ]] || fail "doctor accepted missing selected Pi memory extension"
|
|
513
|
+
grep -Eq '\[FAIL\].*Pi.*memory extension|\[FAIL\].*cairnkeep-memory' "$tmp/setup-pi-drift.out" || fail "doctor omitted Pi drift diagnosis"
|
|
514
|
+
grep -qF 'cairn sync-pi --apply' "$tmp/setup-pi-drift.out" || fail "Pi drift lacks exact sync recovery"
|
|
515
|
+
|
|
516
|
+
for output in "$tmp/setup-complete.out" "$tmp/setup-limited.out" "$tmp/setup-incomplete.out" "$tmp/setup-pi-drift.out"; do
|
|
517
|
+
if grep -Eqi 'token[=:]|secret[=:]|credential[=:]|https?://' "$output"; then
|
|
518
|
+
fail "setup doctor exposed unbounded private values"
|
|
519
|
+
fi
|
|
520
|
+
done
|
|
521
|
+
|
|
522
|
+
echo "PASS: complete, limited, incomplete, Pi drift, and recovery doctor contract"
|
|
@@ -15,13 +15,14 @@ grep -qF "**Baseline:** Cairnkeep $version" "$coverage"
|
|
|
15
15
|
grep -qF "**Baseline:** Cairnkeep $version" "$feature_guide"
|
|
16
16
|
grep -qF 'https://github.com/cairnkeep/cairnkeep-course-labs' "$landing"
|
|
17
17
|
for checkpoint in course-00-app course-01-bootstrap course-02-memory course-03-quality \
|
|
18
|
-
course-04-operation course-05-evidence course-06-governance course-07-evaluation
|
|
18
|
+
course-04-operation course-05-evidence course-06-governance course-07-evaluation \
|
|
19
|
+
course-08-graph course-09-skill course-10-trust-context course-11-windows; do
|
|
19
20
|
grep -qF "\`$checkpoint\`" "$coverage"
|
|
20
21
|
done
|
|
21
22
|
|
|
22
23
|
ready=0
|
|
23
24
|
brief=0
|
|
24
|
-
for number in $(seq -w 0
|
|
25
|
+
for number in $(seq -w 0 22); do
|
|
25
26
|
matches=(docs/learning/lessons/L"$number"-*.md)
|
|
26
27
|
[[ ${#matches[@]} -eq 1 && -f ${matches[0]} ]] || {
|
|
27
28
|
echo "learning path must contain exactly one L$number lesson" >&2
|
|
@@ -52,7 +53,7 @@ grep -qF '`cairn sync-kimi --apply`' "$graph_lesson"
|
|
|
52
53
|
grep -qF '`cairn sync-pi --apply`' "$graph_lesson"
|
|
53
54
|
grep -qF '`graphify-out/`' "$graph_lesson"
|
|
54
55
|
|
|
55
|
-
[[ $ready -eq
|
|
56
|
+
[[ $ready -eq 8 && $brief -eq 15 ]] || {
|
|
56
57
|
echo "unexpected learning status totals: ready=$ready brief=$brief" >&2
|
|
57
58
|
exit 1
|
|
58
59
|
}
|
|
@@ -93,4 +94,53 @@ for (const file of markdownFiles("docs/learning")) {
|
|
|
93
94
|
}
|
|
94
95
|
NODE
|
|
95
96
|
|
|
97
|
+
l23=docs/learning/lessons/L23-guided-setup.md
|
|
98
|
+
quickstart=docs/learning/tracks/quickstart.md
|
|
99
|
+
operator=docs/learning/tracks/operator.md
|
|
100
|
+
phase26_learning_complete=true
|
|
101
|
+
[[ -f "$l23" ]] || phase26_learning_complete=false
|
|
102
|
+
grep -qF 'L23-guided-setup.md' "$quickstart" || phase26_learning_complete=false
|
|
103
|
+
grep -qF 'L23-guided-setup.md' "$operator" || phase26_learning_complete=false
|
|
104
|
+
|
|
105
|
+
if [[ "$phase26_learning_complete" != true ]]; then
|
|
106
|
+
if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
|
|
107
|
+
echo "PHASE26_RED:GUIDED_SETUP_LEARNING_MISSING"
|
|
108
|
+
exit 86
|
|
109
|
+
fi
|
|
110
|
+
echo "SKIP: Ready L23 guided-setup lesson and track routing are not complete"
|
|
111
|
+
echo "PASS: public learning path structure, readiness, links, and version alignment"
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
grep -q '^\*\*Status:\*\* Ready' "$l23"
|
|
116
|
+
grep -qF "Tested with:** Cairnkeep $version" "$l23"
|
|
117
|
+
for heading in '## Outcome' '## Exercise' '## Recovery exercise' '## Common failures' '## Privacy and trust boundary'; do
|
|
118
|
+
grep -qxF "$heading" "$l23" || {
|
|
119
|
+
echo "L23 missing required heading: $heading" >&2
|
|
120
|
+
exit 1
|
|
121
|
+
}
|
|
122
|
+
done
|
|
123
|
+
for command in \
|
|
124
|
+
'cairn setup' \
|
|
125
|
+
'--git init' \
|
|
126
|
+
'--git none' \
|
|
127
|
+
'--harness' \
|
|
128
|
+
'--memory' \
|
|
129
|
+
'--yes' \
|
|
130
|
+
'cairn sync-pi --apply' \
|
|
131
|
+
'cairn doctor' \
|
|
132
|
+
'cairn uninstall'; do
|
|
133
|
+
grep -qF -- "$command" "$l23" || {
|
|
134
|
+
echo "L23 missing guided setup command or mode: $command" >&2
|
|
135
|
+
exit 1
|
|
136
|
+
}
|
|
137
|
+
done
|
|
138
|
+
grep -Eqi 'missing|empty' "$l23"
|
|
139
|
+
grep -Eqi 'deterministic|non-interactive|non-TTY' "$l23"
|
|
140
|
+
grep -Eqi 'local.*stdio|stdio.*local' "$l23"
|
|
141
|
+
grep -Eqi 'limited|Git-less' "$l23"
|
|
142
|
+
grep -Eqi 'cancel|shutdown|child process' "$l23"
|
|
143
|
+
grep -Eqi '0\.84\.1' "$l23"
|
|
144
|
+
grep -Eqi 'credentials|secrets|private state' "$l23"
|
|
145
|
+
|
|
96
146
|
echo "PASS: public learning path structure, readiness, links, and version alignment"
|