@cairnkeep/cli 2.9.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 +46 -1
- package/README.md +54 -25
- package/bin/cairn +6 -165
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +41 -4
- package/docs/containers.md +10 -0
- package/docs/harness-compatibility.md +30 -2
- package/docs/learning/CURRICULUM-MAP.md +9 -3
- package/docs/learning/FEATURE-GUIDE.md +9 -3
- package/docs/learning/README.md +2 -0
- 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 +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- 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 +8 -3
- package/docs/learning/tracks/quickstart.md +7 -3
- package/docs/native-windows.md +122 -0
- package/docs/operating.md +119 -18
- package/docs/privacy-and-data-flow.md +34 -1
- package/docs/storage.md +14 -2
- 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.js +84 -25
- 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 +7 -5
- package/mcp-memory-server/dist/graph-cli.js +7 -1
- package/mcp-memory-server/dist/index.js +19 -20
- package/mcp-memory-server/dist/mcp-tool-profile.js +6 -4
- 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 +2 -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/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 +12 -3
- package/scripts/doctor.sh +57 -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-completion.sh +39 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-learning-docs.sh +52 -3
- package/scripts/test-package-install.sh +64 -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 +50 -0
- package/scripts/test-windows-native.mjs +136 -0
- package/scripts/uninstall.sh +69 -6
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- package/scripts/windows-platform.mjs +725 -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"
|
|
@@ -118,5 +118,44 @@ if "$ROOT/bin/cairn" completion unsupported >/dev/null 2>&1; then
|
|
|
118
118
|
exit 1
|
|
119
119
|
fi
|
|
120
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
|
+
|
|
121
160
|
git -C "$ROOT" diff --check
|
|
122
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"
|
|
@@ -16,13 +16,13 @@ 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
18
|
course-04-operation course-05-evidence course-06-governance course-07-evaluation \
|
|
19
|
-
course-08-graph course-09-skill course-10-trust-context; do
|
|
19
|
+
course-08-graph course-09-skill course-10-trust-context course-11-windows; do
|
|
20
20
|
grep -qF "\`$checkpoint\`" "$coverage"
|
|
21
21
|
done
|
|
22
22
|
|
|
23
23
|
ready=0
|
|
24
24
|
brief=0
|
|
25
|
-
for number in $(seq -w 0
|
|
25
|
+
for number in $(seq -w 0 22); do
|
|
26
26
|
matches=(docs/learning/lessons/L"$number"-*.md)
|
|
27
27
|
[[ ${#matches[@]} -eq 1 && -f ${matches[0]} ]] || {
|
|
28
28
|
echo "learning path must contain exactly one L$number lesson" >&2
|
|
@@ -53,7 +53,7 @@ grep -qF '`cairn sync-kimi --apply`' "$graph_lesson"
|
|
|
53
53
|
grep -qF '`cairn sync-pi --apply`' "$graph_lesson"
|
|
54
54
|
grep -qF '`graphify-out/`' "$graph_lesson"
|
|
55
55
|
|
|
56
|
-
[[ $ready -eq
|
|
56
|
+
[[ $ready -eq 8 && $brief -eq 15 ]] || {
|
|
57
57
|
echo "unexpected learning status totals: ready=$ready brief=$brief" >&2
|
|
58
58
|
exit 1
|
|
59
59
|
}
|
|
@@ -94,4 +94,53 @@ for (const file of markdownFiles("docs/learning")) {
|
|
|
94
94
|
}
|
|
95
95
|
NODE
|
|
96
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
|
+
|
|
97
146
|
echo "PASS: public learning path structure, readiness, links, and version alignment"
|
|
@@ -331,4 +331,68 @@ fi
|
|
|
331
331
|
[[ "$(sha256sum "$ROOT/package-lock.json" | cut -d' ' -f1)" == "$root_lock_before" ]] || fail "root lockfile changed during pack/install"
|
|
332
332
|
[[ "$(sha256sum "$ROOT/mcp-memory-server/package-lock.json" | cut -d' ' -f1)" == "$server_lock_before" ]] || fail "server lockfile changed during pack/install"
|
|
333
333
|
|
|
334
|
+
phase26_surface_complete=true
|
|
335
|
+
for required in \
|
|
336
|
+
scripts/setup.mjs \
|
|
337
|
+
scripts/setup-core.mjs \
|
|
338
|
+
scripts/setup-reconcile.mjs \
|
|
339
|
+
schemas/cairnkeep-setup.schema.json \
|
|
340
|
+
schemas/cairnkeep-setup-policy.schema.json \
|
|
341
|
+
mcp-memory-server/dist/pi-mcp-bridge.js \
|
|
342
|
+
pi/extensions/cairnkeep-memory.ts \
|
|
343
|
+
docs/operating.md \
|
|
344
|
+
docs/harness-compatibility.md \
|
|
345
|
+
docs/native-windows.md \
|
|
346
|
+
docs/building-an-overlay.md
|
|
347
|
+
do
|
|
348
|
+
[[ -f "$ROOT/$required" ]] || phase26_surface_complete=false
|
|
349
|
+
done
|
|
350
|
+
|
|
351
|
+
if [[ "$phase26_surface_complete" != true ]]; then
|
|
352
|
+
if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
|
|
353
|
+
echo "PHASE26_RED:SETUP_PACKAGE_MISSING"
|
|
354
|
+
exit 86
|
|
355
|
+
fi
|
|
356
|
+
echo "SKIP: packaged guided setup and Pi bridge surface is not complete"
|
|
357
|
+
echo "PASS: npm tarball installs a self-contained CLI and MCP server"
|
|
358
|
+
exit 0
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
for required in \
|
|
362
|
+
scripts/setup.mjs \
|
|
363
|
+
scripts/setup-core.mjs \
|
|
364
|
+
scripts/setup-reconcile.mjs \
|
|
365
|
+
schemas/cairnkeep-setup.schema.json \
|
|
366
|
+
schemas/cairnkeep-setup-policy.schema.json \
|
|
367
|
+
mcp-memory-server/dist/pi-mcp-bridge.js \
|
|
368
|
+
pi/extensions/cairnkeep-memory.ts \
|
|
369
|
+
docs/operating.md \
|
|
370
|
+
docs/harness-compatibility.md \
|
|
371
|
+
docs/native-windows.md \
|
|
372
|
+
docs/building-an-overlay.md
|
|
373
|
+
do
|
|
374
|
+
[[ -f "$installed_root/$required" ]] || fail "npm tarball omitted Phase 26 asset $required"
|
|
375
|
+
done
|
|
376
|
+
|
|
377
|
+
installed_setup_target="$tmp/Installed Setup – Unicode"
|
|
378
|
+
(cd "$tmp" && cairn setup "$installed_setup_target" \
|
|
379
|
+
--git none --harness claude,pi --memory local --yes --json) >"$tmp/installed-setup.json" \
|
|
380
|
+
|| fail "installed cairn setup failed outside the source checkout"
|
|
381
|
+
node - "$tmp/installed-setup.json" "$installed_setup_target" <<'NODE'
|
|
382
|
+
const fs = require("fs");
|
|
383
|
+
const [resultPath, target] = process.argv.slice(2);
|
|
384
|
+
const result = JSON.parse(fs.readFileSync(resultPath, "utf8"));
|
|
385
|
+
if (result.schema_version !== 1 || result.status !== "limited") process.exit(1);
|
|
386
|
+
if (result.git !== "none" || result.memory !== "local") process.exit(1);
|
|
387
|
+
if (JSON.stringify(result.harnesses) !== JSON.stringify(["claude", "pi"])) process.exit(1);
|
|
388
|
+
if (!fs.existsSync(`${target}/.ai/cairnkeep.json`)) process.exit(1);
|
|
389
|
+
NODE
|
|
390
|
+
|
|
391
|
+
if find "$installed_root" -type f \( \
|
|
392
|
+
-name '.env' -o -name '*.tmp' -o -name '*.bak' -o -name 'cairnkeep.json' \
|
|
393
|
+
-o -path '*/.planning/*' -o -path '*/.agentfs/*' \
|
|
394
|
+
\) | grep -q .; then
|
|
395
|
+
fail "npm tarball included temporary or project-private setup state"
|
|
396
|
+
fi
|
|
397
|
+
|
|
334
398
|
echo "PASS: npm tarball installs a self-contained CLI and MCP server"
|
|
@@ -48,7 +48,7 @@ run_shell_syntax_baseline() {
|
|
|
48
48
|
local script
|
|
49
49
|
while IFS= read -r script; do
|
|
50
50
|
bash -n "$script" || return 1
|
|
51
|
-
done < <(find "$ROOT/bin" "$ROOT/scripts" -type f
|
|
51
|
+
done < <(find "$ROOT/bin" "$ROOT/scripts" -type f -name '*.sh' | sort)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
run_baseline() {
|
|
@@ -142,7 +142,7 @@ run_baseline() {
|
|
|
142
142
|
run_baseline_check "$ROOT/scripts/test-completion.sh"
|
|
143
143
|
run_baseline_check "$ROOT/scripts/test-package-install.sh"
|
|
144
144
|
run_baseline_check "$ROOT/scripts/test-uninstall.sh"
|
|
145
|
-
run_baseline_check
|
|
145
|
+
run_baseline_check node --check "$ROOT/bin/cairn"
|
|
146
146
|
run_master_off_identity
|
|
147
147
|
echo "PASS: Phase 18 master-off CLI/package/lifecycle identity baseline"
|
|
148
148
|
}
|