@cairnkeep/cli 2.15.3 → 2.16.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 +36 -0
- package/CONTRIBUTING.md +12 -0
- package/README.md +84 -589
- package/docs/README.md +79 -0
- package/docs/agents.md +173 -0
- package/docs/ecosystem.md +23 -0
- package/docs/harness-compatibility.md +4 -3
- package/docs/learning/CURRICULUM-MAP.md +2 -2
- package/docs/learning/FEATURE-GUIDE.md +6 -6
- package/docs/learning/README.md +3 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +3 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +9 -10
- 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 +2 -2
- package/docs/learning/lessons/L23-guided-setup.md +18 -5
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +2 -2
- package/docs/learning/video-scripts/V25-playbooks.md +1 -1
- package/docs/native-windows.md +5 -0
- package/docs/operating.md +24 -5
- package/docs/quickstart.md +15 -6
- package/docs/releasing.md +27 -19
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/playbook-cli.js +12 -5
- package/package.json +1 -1
- package/scripts/cairn-cli.mjs +1 -0
- package/scripts/setup.mjs +92 -13
- package/scripts/terminal-prompts.mjs +172 -0
- package/scripts/test-cli-dispatch.sh +4 -0
- package/scripts/test-docs-navigation.sh +63 -0
- package/scripts/test-learning-docs.sh +1 -1
- package/scripts/test-package-install.sh +1 -1
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-playbook-instructions.sh +4 -2
- package/scripts/test-release-sbom.sh +41 -16
- package/scripts/test-setup-preflight.mjs +130 -0
- package/scripts/test-windows-native.mjs +1 -1
- package/scripts/verify-docs-parity.sh +28 -28
- package/templates/playbook-agent-instructions.md.template +5 -4
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
-
|
|
5
|
+
CANDIDATE_WORKFLOW="$ROOT/.github/workflows/ci.yml"
|
|
6
|
+
PUBLISH_WORKFLOW="$ROOT/.github/workflows/publish.yml"
|
|
6
7
|
TEMP_DIR=$(mktemp -d)
|
|
7
8
|
trap 'rm -f "$TEMP_DIR/cairnkeep.cdx.json"; rmdir "$TEMP_DIR"' EXIT
|
|
8
9
|
|
|
@@ -11,12 +12,13 @@ fail() {
|
|
|
11
12
|
exit 1
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
node - "$ROOT/package.json" "$ROOT/package-lock.json" "$
|
|
15
|
+
node - "$ROOT/package.json" "$ROOT/package-lock.json" "$CANDIDATE_WORKFLOW" "$PUBLISH_WORKFLOW" <<'NODE'
|
|
15
16
|
const fs = require("node:fs");
|
|
16
17
|
|
|
17
18
|
const manifest = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
18
19
|
const lock = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));
|
|
19
|
-
const
|
|
20
|
+
const candidateWorkflow = fs.readFileSync(process.argv[4], "utf8");
|
|
21
|
+
const publishWorkflow = fs.readFileSync(process.argv[5], "utf8");
|
|
20
22
|
const expected = "6.0.0";
|
|
21
23
|
|
|
22
24
|
if (manifest.devDependencies?.["@cyclonedx/cyclonedx-npm"] !== expected) {
|
|
@@ -29,24 +31,47 @@ if (lock.packages?.["node_modules/@cyclonedx/cyclonedx-npm"]?.version !== expect
|
|
|
29
31
|
throw new Error(`package-lock.json must resolve @cyclonedx/cyclonedx-npm ${expected}`);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (!candidateWorkflow.includes("release-candidate:")
|
|
35
|
+
|| !candidateWorkflow.includes("- name: Build release candidate")
|
|
36
|
+
|| !candidateWorkflow.includes("name: release-candidate-${{ steps.candidate.outputs.tree }}")) {
|
|
37
|
+
throw new Error("CI must build and upload a tree-addressed release candidate after its required jobs");
|
|
38
|
+
}
|
|
39
|
+
if (!publishWorkflow.includes("- name: Download the CI-verified release candidate")
|
|
40
|
+
|| !publishWorkflow.includes("jq -r '.conclusion'")
|
|
41
|
+
|| !publishWorkflow.includes("== success")
|
|
42
|
+
|| !publishWorkflow.includes("sha256sum --check SHA256SUMS")
|
|
43
|
+
|| !publishWorkflow.includes("candidate.tree !== tree")) {
|
|
44
|
+
throw new Error("release publication must verify the successful CI run, checksums, version, and exact tree");
|
|
45
|
+
}
|
|
46
|
+
const publishStep = publishWorkflow.indexOf("- name: Publish to npm with provenance");
|
|
47
|
+
const attachStep = publishWorkflow.indexOf("- name: Attach package and SBOM to GitHub release");
|
|
48
|
+
if (publishStep < 0 || attachStep < 0 || publishStep >= attachStep) {
|
|
49
|
+
throw new Error("verified artifacts must publish to npm before release attachment");
|
|
50
|
+
}
|
|
51
|
+
if (!publishWorkflow.includes('npm publish "./artifacts/cairnkeep-cli-$VERSION.tgz"')) {
|
|
52
|
+
throw new Error("npm publication must use an explicit relative tarball path");
|
|
53
|
+
}
|
|
54
|
+
if (publishWorkflow.includes("check:public")) {
|
|
55
|
+
throw new Error("release workflow must promote the verified candidate without repeating the public suite");
|
|
56
|
+
}
|
|
57
|
+
if (!publishWorkflow.includes("name: containers (${{ matrix.target }})")
|
|
58
|
+
|| !publishWorkflow.includes("- target: server")
|
|
59
|
+
|| !publishWorkflow.includes("- target: workspace")
|
|
60
|
+
|| !publishWorkflow.includes("needs: prepare")) {
|
|
61
|
+
throw new Error("npm and both container targets must fan out after candidate preparation");
|
|
37
62
|
}
|
|
38
63
|
NODE
|
|
39
64
|
|
|
40
|
-
grep -Fq './node_modules/.bin/cyclonedx-npm' "$
|
|
41
|
-
grep -Fq -- '--package-lock-only' "$
|
|
42
|
-
grep -Fq -- '--omit dev' "$
|
|
43
|
-
grep -Fq -- '--output-reproducible' "$
|
|
44
|
-
grep -Fq -- '--spec-version 1.6' "$
|
|
45
|
-
grep -Fq -- '--validate' "$
|
|
46
|
-
if grep -Fq 'npm sbom' "$
|
|
65
|
+
grep -Fq './node_modules/.bin/cyclonedx-npm' "$CANDIDATE_WORKFLOW" || fail "candidate workflow does not use the locked CycloneDX generator"
|
|
66
|
+
grep -Fq -- '--package-lock-only' "$CANDIDATE_WORKFLOW" || fail "candidate workflow does not use the package lock"
|
|
67
|
+
grep -Fq -- '--omit dev' "$CANDIDATE_WORKFLOW" || fail "candidate workflow does not omit development dependencies"
|
|
68
|
+
grep -Fq -- '--output-reproducible' "$CANDIDATE_WORKFLOW" || fail "candidate workflow does not request reproducible output"
|
|
69
|
+
grep -Fq -- '--spec-version 1.6' "$CANDIDATE_WORKFLOW" || fail "candidate workflow does not emit CycloneDX 1.6"
|
|
70
|
+
grep -Fq -- '--validate' "$CANDIDATE_WORKFLOW" || fail "candidate workflow does not validate the generated SBOM"
|
|
71
|
+
if grep -Fq 'npm sbom' "$CANDIDATE_WORKFLOW" "$PUBLISH_WORKFLOW"; then
|
|
47
72
|
fail "release workflow still uses npm sbom"
|
|
48
73
|
fi
|
|
49
|
-
if grep -Fqi 'sbomsmith' "$
|
|
74
|
+
if grep -Fqi 'sbomsmith' "$CANDIDATE_WORKFLOW" "$PUBLISH_WORKFLOW"; then
|
|
50
75
|
fail "release workflow must not depend on an external converter"
|
|
51
76
|
fi
|
|
52
77
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
4
5
|
import {
|
|
5
6
|
existsSync,
|
|
6
7
|
lstatSync,
|
|
@@ -20,6 +21,7 @@ const RED_MARKER = "PHASE26_RED:SETUP_CORE_MISSING";
|
|
|
20
21
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
const setupCorePath = join(here, "setup-core.mjs");
|
|
22
23
|
const setupPath = join(here, "setup.mjs");
|
|
24
|
+
const terminalPromptsPath = join(here, "terminal-prompts.mjs");
|
|
23
25
|
const MANAGED_PATHS = [".ai", ".planning", ".agentfs"];
|
|
24
26
|
|
|
25
27
|
function assertNoManagedPaths(target, label) {
|
|
@@ -81,6 +83,10 @@ async function loadSetup() {
|
|
|
81
83
|
return import(pathToFileURL(setupPath).href);
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
async function loadTerminalPrompts() {
|
|
87
|
+
return import(pathToFileURL(terminalPromptsPath).href);
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
function assertExports(core) {
|
|
85
91
|
for (const name of [
|
|
86
92
|
"parseSetupArgs",
|
|
@@ -225,6 +231,126 @@ async function runInteractive(setup, args, responses) {
|
|
|
225
231
|
return { status, prompts, output: output.read(), error: error.read() };
|
|
226
232
|
}
|
|
227
233
|
|
|
234
|
+
class FakeTTY extends EventEmitter {
|
|
235
|
+
constructor() {
|
|
236
|
+
super();
|
|
237
|
+
this.isTTY = true;
|
|
238
|
+
this.isRaw = false;
|
|
239
|
+
this.paused = true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setRawMode(value) { this.isRaw = value; }
|
|
243
|
+
isPaused() { return this.paused; }
|
|
244
|
+
resume() { this.paused = false; }
|
|
245
|
+
pause() { this.paused = true; }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function testTerminalSelectors(prompts) {
|
|
249
|
+
const input = new FakeTTY();
|
|
250
|
+
const output = captureStream();
|
|
251
|
+
output.stream.isTTY = true;
|
|
252
|
+
assert.equal(prompts.supportsTerminalPrompts(input, output.stream, { TERM: "xterm-256color" }), true);
|
|
253
|
+
assert.equal(prompts.supportsTerminalPrompts(input, output.stream, { TERM: "dumb" }), false);
|
|
254
|
+
|
|
255
|
+
const multi = prompts.selectManyPrompt({
|
|
256
|
+
message: "Coding harnesses",
|
|
257
|
+
choices: [
|
|
258
|
+
{ value: "claude", label: "Claude Code" },
|
|
259
|
+
{ value: "codex", label: "Codex CLI" },
|
|
260
|
+
],
|
|
261
|
+
input,
|
|
262
|
+
output: output.stream,
|
|
263
|
+
});
|
|
264
|
+
setImmediate(() => {
|
|
265
|
+
input.emit("keypress", " ", { name: "space" });
|
|
266
|
+
input.emit("keypress", "", { name: "down" });
|
|
267
|
+
input.emit("keypress", " ", { name: "space" });
|
|
268
|
+
input.emit("keypress", "", { name: "return" });
|
|
269
|
+
});
|
|
270
|
+
assert.deepEqual(await multi, ["claude", "codex"]);
|
|
271
|
+
assert.equal(input.isRaw, false);
|
|
272
|
+
assert.equal(input.isPaused(), true);
|
|
273
|
+
assert.match(output.read(), /\[x\] Claude Code/);
|
|
274
|
+
assert.match(output.read(), /Coding harnesses: Claude Code, Codex CLI/);
|
|
275
|
+
|
|
276
|
+
const single = prompts.selectOnePrompt({
|
|
277
|
+
message: "Memory mode",
|
|
278
|
+
choices: [
|
|
279
|
+
{ value: "local", label: "Local" },
|
|
280
|
+
{ value: "none", label: "None" },
|
|
281
|
+
],
|
|
282
|
+
initialValue: "local",
|
|
283
|
+
input,
|
|
284
|
+
output: output.stream,
|
|
285
|
+
});
|
|
286
|
+
setImmediate(() => {
|
|
287
|
+
input.emit("keypress", "", { name: "down" });
|
|
288
|
+
input.emit("keypress", "", { name: "return" });
|
|
289
|
+
});
|
|
290
|
+
assert.equal(await single, "none");
|
|
291
|
+
assert.match(output.read(), /Memory mode: None/);
|
|
292
|
+
|
|
293
|
+
const cancelled = prompts.selectOnePrompt({
|
|
294
|
+
message: "Cancel me",
|
|
295
|
+
choices: [{ value: "only", label: "Only choice" }],
|
|
296
|
+
input,
|
|
297
|
+
output: output.stream,
|
|
298
|
+
});
|
|
299
|
+
setImmediate(() => input.emit("keypress", "", { name: "escape" }));
|
|
300
|
+
await assert.rejects(cancelled, (error) => error?.status === 130 && /cancelled/i.test(error.message));
|
|
301
|
+
assert.equal(input.isRaw, false);
|
|
302
|
+
assert.equal(input.isPaused(), true);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function testStructuredSetupSelectors(setup, fixture) {
|
|
306
|
+
const selections = [];
|
|
307
|
+
const output = captureStream();
|
|
308
|
+
const error = captureStream();
|
|
309
|
+
const status = await setup.runSetup([fixture.empty], {
|
|
310
|
+
isTTY: true,
|
|
311
|
+
input: { isTTY: true },
|
|
312
|
+
output: output.stream,
|
|
313
|
+
error: error.stream,
|
|
314
|
+
selectOne: async (options) => {
|
|
315
|
+
selections.push(options);
|
|
316
|
+
if (options.message === "Git integration") return "init";
|
|
317
|
+
if (options.message === "Memory mode") return "local";
|
|
318
|
+
return "no";
|
|
319
|
+
},
|
|
320
|
+
selectMany: async (options) => {
|
|
321
|
+
selections.push(options);
|
|
322
|
+
return ["codex", "opencode"];
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
assert.equal(status, 0);
|
|
326
|
+
assert.equal(error.read(), "");
|
|
327
|
+
assert.match(output.read(), /Setup cancelled; no files were changed/);
|
|
328
|
+
assert.deepEqual(selections.map(({ message }) => message), [
|
|
329
|
+
"Git integration",
|
|
330
|
+
"Coding harnesses",
|
|
331
|
+
"Memory mode",
|
|
332
|
+
"Apply this setup plan?",
|
|
333
|
+
]);
|
|
334
|
+
const harnessPrompt = selections.find(({ message }) => message === "Coding harnesses");
|
|
335
|
+
assert.deepEqual(harnessPrompt.choices.map(({ value }) => value), ["claude", "opencode", "pi", "kimi", "qwen", "codex"]);
|
|
336
|
+
assert.match(harnessPrompt.choices.find(({ value }) => value === "codex").label, /Codex CLI/);
|
|
337
|
+
assertNoManagedPaths(fixture.empty, "cancelled structured setup");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function testRedirectedOutputIsNoninteractive(setup, fixture) {
|
|
341
|
+
const output = captureStream();
|
|
342
|
+
const error = captureStream();
|
|
343
|
+
const status = await setup.runSetup([fixture.empty, "--yes"], {
|
|
344
|
+
input: { isTTY: true },
|
|
345
|
+
output: output.stream,
|
|
346
|
+
error: error.stream,
|
|
347
|
+
});
|
|
348
|
+
assert.equal(status, 2);
|
|
349
|
+
assert.equal(output.read(), "");
|
|
350
|
+
assert.match(error.read(), /git|harness|memory|usage/i);
|
|
351
|
+
assertNoManagedPaths(fixture.empty, "redirected setup output");
|
|
352
|
+
}
|
|
353
|
+
|
|
228
354
|
async function testInteractiveGitRecommendation(setup, fixture) {
|
|
229
355
|
const accepted = await runInteractive(setup, [], [fixture.missing, "init", "pi", "local", "yes"]);
|
|
230
356
|
assert.equal(accepted.status, 0);
|
|
@@ -271,6 +397,10 @@ async function main() {
|
|
|
271
397
|
testChoiceContract(core, fixture);
|
|
272
398
|
testPreflightContract(core, fixture);
|
|
273
399
|
const setup = await loadSetup();
|
|
400
|
+
const terminalPrompts = await loadTerminalPrompts();
|
|
401
|
+
await testTerminalSelectors(terminalPrompts);
|
|
402
|
+
await testStructuredSetupSelectors(setup, fixture);
|
|
403
|
+
await testRedirectedOutputIsNoninteractive(setup, fixture);
|
|
274
404
|
await testInteractiveGitRecommendation(setup, fixture);
|
|
275
405
|
console.log("PASS: setup target, Git, syntax, choice, and no-write preflight contract");
|
|
276
406
|
} finally {
|
|
@@ -41,7 +41,7 @@ try {
|
|
|
41
41
|
assert.match(launcherPowerShell(), /CAIRN_WORK_EVIDENCE/);
|
|
42
42
|
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /cairnkeep:playbook:v1:start/);
|
|
43
43
|
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /derive one short query from the task/);
|
|
44
|
-
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /
|
|
44
|
+
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /make this retrieval the first tool or command/);
|
|
45
45
|
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /cairn playbook record --help/);
|
|
46
46
|
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /Treat returned memory as a locator, not authority\./);
|
|
47
47
|
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /Keep this line\./);
|
|
@@ -9,8 +9,8 @@ set -euo pipefail
|
|
|
9
9
|
#
|
|
10
10
|
# 1. Env-key check: every CAIRN_[A-Z_]+/MCP_HTTP_[A-Z_]+ token read in
|
|
11
11
|
# mcp-memory-server/src/*.ts (the runtime server config surface --
|
|
12
|
-
# NOT scripts/ or bin/) must be named somewhere in
|
|
13
|
-
#
|
|
12
|
+
# NOT scripts/ or bin/) must be named somewhere in the versioned
|
|
13
|
+
# top-level docs. `comm -23` on two sorted lists -- code-keys-not-in-
|
|
14
14
|
# docs -- not a symmetric diff.
|
|
15
15
|
# 2. Command check: every claude/commands/*.md basename must appear in
|
|
16
16
|
# docs/operating.md.
|
|
@@ -27,8 +27,8 @@ Usage: verify-docs-parity.sh
|
|
|
27
27
|
verify-docs-parity.sh -h|--help
|
|
28
28
|
|
|
29
29
|
Checks that every CAIRN_*/MCP_HTTP_* env key read by the cairn-memory MCP
|
|
30
|
-
server (mcp-memory-server/src/*.ts) is named in docs
|
|
31
|
-
|
|
30
|
+
server (mcp-memory-server/src/*.ts) is named in the versioned docs, and that
|
|
31
|
+
every claude/commands/*.md command is named in
|
|
32
32
|
docs/operating.md. One-directional: doc-only names are not a failure.
|
|
33
33
|
Prints every missing key/command by name, then exits non-zero on any
|
|
34
34
|
drift, or 0 if the docs are fully in sync with the shipped code.
|
|
@@ -264,7 +264,7 @@ check_env_keys() {
|
|
|
264
264
|
local code_keys doc_keys missing
|
|
265
265
|
|
|
266
266
|
code_keys=$(grep -ohE "\\b${ENV_KEY_PATTERN}\\b" mcp-memory-server/src/*.ts | grep -v '^CAIRN_TEST_' | sort -u)
|
|
267
|
-
doc_keys=$(grep -ohE "\\b${ENV_KEY_PATTERN}\\b" docs
|
|
267
|
+
doc_keys=$(grep -ohE "\\b${ENV_KEY_PATTERN}\\b" docs/*.md | sort -u)
|
|
268
268
|
|
|
269
269
|
missing=$(comm -23 <(printf '%s\n' "$code_keys") <(printf '%s\n' "$doc_keys"))
|
|
270
270
|
|
|
@@ -276,7 +276,7 @@ check_env_keys() {
|
|
|
276
276
|
return 1
|
|
277
277
|
fi
|
|
278
278
|
|
|
279
|
-
echo "[env-keys] OK: every mcp-memory-server/src env key is named in
|
|
279
|
+
echo "[env-keys] OK: every mcp-memory-server/src env key is named in the versioned docs"
|
|
280
280
|
return 0
|
|
281
281
|
}
|
|
282
282
|
|
|
@@ -316,12 +316,12 @@ check_typed_contract() {
|
|
|
316
316
|
failed=1
|
|
317
317
|
fi
|
|
318
318
|
done <<'EOF'
|
|
319
|
-
CAIRN_TYPED_MEMORY_NODES|
|
|
320
|
-
memory_import|
|
|
321
|
-
address_space|
|
|
322
|
-
node_types|
|
|
323
|
-
tags_all|
|
|
324
|
-
tags_any|
|
|
319
|
+
CAIRN_TYPED_MEMORY_NODES|docs/operating.md
|
|
320
|
+
memory_import|docs/operating.md docs/privacy-and-data-flow.md
|
|
321
|
+
address_space|docs/operating.md docs/privacy-and-data-flow.md
|
|
322
|
+
node_types|docs/operating.md
|
|
323
|
+
tags_all|docs/operating.md
|
|
324
|
+
tags_any|docs/operating.md
|
|
325
325
|
schemas/memory-node.schema.json|docs/storage.md
|
|
326
326
|
cairn_node_metadata_v1|docs/storage.md
|
|
327
327
|
cairn_node_import_replays_v1|docs/storage.md
|
|
@@ -350,11 +350,11 @@ check_artifact_contract() {
|
|
|
350
350
|
echo "FATAL: artifact default '$key=$value' is missing from env.example" >&2
|
|
351
351
|
failed=1
|
|
352
352
|
}
|
|
353
|
-
grep -qF "$key"
|
|
353
|
+
grep -qF "$key" docs/operating.md || {
|
|
354
354
|
echo "FATAL: artifact env key '$key' is missing from public configuration docs" >&2
|
|
355
355
|
failed=1
|
|
356
356
|
}
|
|
357
|
-
grep -qF "$value"
|
|
357
|
+
grep -qF "$value" docs/operating.md || {
|
|
358
358
|
echo "FATAL: artifact default '$key=$value' is missing from public configuration docs" >&2
|
|
359
359
|
failed=1
|
|
360
360
|
}
|
|
@@ -368,7 +368,7 @@ CAIRN_ARTIFACT_GENERATED_FILE_SNAPSHOT_MAX_BYTES|262144|GENERATED_FILE_MAX_SNAPS
|
|
|
368
368
|
EOF
|
|
369
369
|
|
|
370
370
|
for key in CAIRN_COMPACTION_CAPTURE CAIRN_ARTIFACT_STORE CAIRN_ARTIFACT_HTTP; do
|
|
371
|
-
grep -qF "$key" mcp-memory-server/src/artifact-schema.ts
|
|
371
|
+
grep -qF "$key" mcp-memory-server/src/artifact-schema.ts docs/operating.md docs/privacy-and-data-flow.md || {
|
|
372
372
|
echo "FATAL: artifact feature flag '$key' is not source/docs complete" >&2
|
|
373
373
|
failed=1
|
|
374
374
|
}
|
|
@@ -376,13 +376,13 @@ EOF
|
|
|
376
376
|
|
|
377
377
|
for source_term in artifact_write artifact_read artifact_list artifact_delete; do
|
|
378
378
|
grep -qF "\"$source_term\"" mcp-memory-server/src/index.ts || failed=1
|
|
379
|
-
grep -qF "$source_term"
|
|
379
|
+
grep -qF "$source_term" docs/operating.md docs/privacy-and-data-flow.md || failed=1
|
|
380
380
|
done
|
|
381
381
|
for source_term in 'list [--kind K] [--session REF] [--json]' 'show <artifact-id-or-prefix> [--json]' 'delete <artifact-id-or-prefix> [--dry-run] [--json]' 'prune [--dry-run] [--include-protected] [--json]'; do
|
|
382
382
|
grep -qF "$source_term" mcp-memory-server/src/artifact-cli.ts || failed=1
|
|
383
383
|
done
|
|
384
384
|
grep -qF '.agentfs/artifacts.db' docs/storage.md docs/privacy-and-data-flow.md || failed=1
|
|
385
|
-
grep -qF 'CAIRN_ARTIFACT_HTTP' mcp-memory-server/src/index.ts
|
|
385
|
+
grep -qF 'CAIRN_ARTIFACT_HTTP' mcp-memory-server/src/index.ts docs/operating.md docs/privacy-and-data-flow.md || failed=1
|
|
386
386
|
grep -qE 'redact[^.]{0,180}(before|then)[^.]{0,180}(digest|index|write)' docs/privacy-and-data-flow.md || failed=1
|
|
387
387
|
grep -qE 'default uninstall retains|Default uninstall retains' docs/storage.md docs/privacy-and-data-flow.md || failed=1
|
|
388
388
|
grep -qF -- '--purge-memory PROJECT' docs/storage.md || failed=1
|
|
@@ -390,10 +390,10 @@ EOF
|
|
|
390
390
|
|
|
391
391
|
for version in 2.1.219 2.1.220; do
|
|
392
392
|
grep -qF "version: \"$version\"" mcp-memory-server/src/compaction-normalize.ts || failed=1
|
|
393
|
-
grep -qF "$version"
|
|
393
|
+
grep -qF "$version" docs/operating.md docs/privacy-and-data-flow.md || failed=1
|
|
394
394
|
done
|
|
395
395
|
grep -qF 'version: "1.17.20"' mcp-memory-server/src/compaction-normalize.ts || failed=1
|
|
396
|
-
grep -qF '1.17.20'
|
|
396
|
+
grep -qF '1.17.20' docs/operating.md docs/privacy-and-data-flow.md || failed=1
|
|
397
397
|
|
|
398
398
|
[[ "$failed" -eq 0 ]] && echo "[artifact-contract] OK: artifact flags/defaults/tools/paths/privacy/uninstall/version pins match source"
|
|
399
399
|
return "$failed"
|
|
@@ -440,8 +440,8 @@ check_work_evidence_contract() {
|
|
|
440
440
|
grep -qF "$key" mcp-memory-server/src/work-evidence-schema.ts || failed=1
|
|
441
441
|
grep -qF "$source_term" mcp-memory-server/src/work-evidence-schema.ts || failed=1
|
|
442
442
|
grep -qxF "# $key=$value" templates/env.example.template || failed=1
|
|
443
|
-
grep -qF "$key"
|
|
444
|
-
grep -qF "$value"
|
|
443
|
+
grep -qF "$key" docs/operating.md docs/work-evidence.md || failed=1
|
|
444
|
+
grep -qF "$value" docs/operating.md docs/work-evidence.md || failed=1
|
|
445
445
|
done <<'EOF'
|
|
446
446
|
CAIRN_WORK_EVIDENCE_RETENTION_DAYS|30|WORK_EVIDENCE_DEFAULT_RETENTION_DAYS = 30
|
|
447
447
|
CAIRN_WORK_EVIDENCE_STORE_MAX_BYTES|67108864|WORK_EVIDENCE_DEFAULT_STORE_MAX_BYTES = 64 * 1024 * 1024
|
|
@@ -450,11 +450,11 @@ CAIRN_WORK_EVIDENCE_PATCH_MAX_BYTES|1048576|WORK_EVIDENCE_DEFAULT_PATCH_MAX_BYTE
|
|
|
450
450
|
EOF
|
|
451
451
|
|
|
452
452
|
for key in CAIRN_WORK_EVIDENCE CAIRN_WORK_EVIDENCE_PATCH; do
|
|
453
|
-
grep -qF "$key" mcp-memory-server/src/work-evidence-schema.ts templates/env.example.template
|
|
453
|
+
grep -qF "$key" mcp-memory-server/src/work-evidence-schema.ts templates/env.example.template docs/operating.md docs/privacy-and-data-flow.md || failed=1
|
|
454
454
|
done
|
|
455
455
|
for source_term in work_evidence_list work_evidence_read; do
|
|
456
456
|
grep -qF "\"$source_term\"" mcp-memory-server/src/index.ts || failed=1
|
|
457
|
-
grep -qF "$source_term"
|
|
457
|
+
grep -qF "$source_term" docs/operating.md docs/privacy-and-data-flow.md docs/work-evidence.md || failed=1
|
|
458
458
|
done
|
|
459
459
|
for source_term in 'list [--status pending|complete] [--json]' 'show <evidence-id-or-prefix> [--json]' 'delete <evidence-id-or-prefix> [--dry-run] [--json]' 'prune [--dry-run] [--json]' 'doctor [--repair] [--json]'; do
|
|
460
460
|
grep -qF "$source_term" mcp-memory-server/src/work-evidence-cli.ts || failed=1
|
|
@@ -478,7 +478,7 @@ check_capability_contract() {
|
|
|
478
478
|
mcp-memory-server/src/capability-store.ts \
|
|
479
479
|
mcp-memory-server/src/capability-adapter.ts \
|
|
480
480
|
schemas/capability-callback.schema.json \
|
|
481
|
-
docs/operating.md docs/storage.md docs/privacy-and-data-flow.md
|
|
481
|
+
docs/operating.md docs/storage.md docs/privacy-and-data-flow.md; do
|
|
482
482
|
if [[ ! -f "$file" ]]; then
|
|
483
483
|
echo "FATAL: capability parity input is missing: $file" >&2
|
|
484
484
|
failed=1
|
|
@@ -491,11 +491,11 @@ check_capability_contract() {
|
|
|
491
491
|
echo "FATAL: canonical capability ID is missing from source: $id" >&2
|
|
492
492
|
failed=1
|
|
493
493
|
}
|
|
494
|
-
grep -qF "$id"
|
|
494
|
+
grep -qF "$id" docs/operating.md || {
|
|
495
495
|
echo "FATAL: canonical capability ID is undocumented: $id" >&2
|
|
496
496
|
failed=1
|
|
497
497
|
}
|
|
498
|
-
grep -qF "$env"
|
|
498
|
+
grep -qF "$env" docs/operating.md templates/env.example.template || {
|
|
499
499
|
echo "FATAL: capability environment key is not source/docs complete: $env" >&2
|
|
500
500
|
failed=1
|
|
501
501
|
}
|
|
@@ -511,7 +511,7 @@ context.explore|CAIRN_CAPABILITY_CONTEXT_EXPLORE
|
|
|
511
511
|
EOF
|
|
512
512
|
|
|
513
513
|
for term in CAIRN_CAPABILITY_CONTRACT CAIRN_CAPABILITY_LOGGING; do
|
|
514
|
-
grep -qF "$term" mcp-memory-server/src/capability-config.ts
|
|
514
|
+
grep -qF "$term" mcp-memory-server/src/capability-config.ts docs/operating.md templates/env.example.template || {
|
|
515
515
|
echo "FATAL: capability master/logging flag is not source/docs complete: $term" >&2
|
|
516
516
|
failed=1
|
|
517
517
|
}
|
|
@@ -520,7 +520,7 @@ EOF
|
|
|
520
520
|
while IFS='|' read -r id tool; do
|
|
521
521
|
grep -qF "\"$id\"" mcp-memory-server/src/index.ts || failed=1
|
|
522
522
|
grep -qF "\"$tool\"" mcp-memory-server/src/index.ts || failed=1
|
|
523
|
-
grep -qF "$tool"
|
|
523
|
+
grep -qF "$tool" docs/operating.md || {
|
|
524
524
|
echo "FATAL: omitted MCP tool is undocumented: $tool" >&2
|
|
525
525
|
failed=1
|
|
526
526
|
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
At the start of a nontrivial task that may depend on existing project
|
|
5
5
|
decisions, conventions, constraints, recurring failures, or prior work,
|
|
6
|
-
|
|
7
|
-
commands such as `rg --files`, `find`,
|
|
6
|
+
make this retrieval the first tool or command. Do not run repository location,
|
|
7
|
+
inventory, or search commands such as `pwd`, `ls`, `rg --files`, `find`,
|
|
8
|
+
`tree`, or broad text searches first:
|
|
8
9
|
|
|
9
10
|
- If `memory_search` is available, derive one short query from the task and
|
|
10
11
|
search `scope: project` once. Derive it from the user's task; do not inspect
|
|
@@ -22,9 +23,9 @@ commands such as `rg --files`, `find`, `tree`, or broad text searches:
|
|
|
22
23
|
Use Cairnkeep's bounded playbook policy to select workflow steps. It advises or
|
|
23
24
|
enforces existing capabilities; it does not execute a workflow for you.
|
|
24
25
|
|
|
25
|
-
- At task start, run `cairn playbook check start --session SESSION --complexity
|
|
26
|
+
- At task start, run `cairn playbook check start --session SESSION --complexity trivial|standard|complex --familiarity known|mixed|unfamiliar` with one value from each bounded set, and follow every applicable `must` action. Apply `should` actions unless there is a concrete reason to skip them; use judgment for `may` actions.
|
|
26
27
|
- Re-run `cairn playbook check check` when scope, familiarity, complexity, or risk changes materially.
|
|
27
|
-
- Before claiming completion, run `cairn playbook check finish --session SESSION --changed PATH... --risk
|
|
28
|
+
- Before claiming completion, run `cairn playbook check finish --session SESSION --changed PATH... --risk low|normal|high|security --public-change --completed ACTION... --skipped ACTION=REASON... --enforce` with one bounded risk value and accurate signals and evidence.
|
|
28
29
|
- A non-zero enforcement result means applicable `must` evidence is missing. Perform the action and check again; do not relabel a skipped or failed action as completed.
|
|
29
30
|
- After a successful finish check, record only material outcomes with `cairn playbook record --policy POLICY_DIGEST --decision DECISION_DIGEST --event finish --action ACTION --outcome OUTCOME --session SESSION [--reason REASON]`, using one call per recorded action and the exact digests returned by the check. Run `cairn playbook record --help` for the bounded values. Actor identity is an unverified local assertion in this release.
|
|
30
31
|
- Existing approval and capability boundaries still apply. A playbook cannot enable a disabled capability, grant approval, write durable memory automatically, run arbitrary commands, or authorize destructive work.
|