@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CONTRIBUTING.md +12 -0
  3. package/README.md +84 -589
  4. package/docs/README.md +79 -0
  5. package/docs/agents.md +173 -0
  6. package/docs/ecosystem.md +23 -0
  7. package/docs/harness-compatibility.md +4 -3
  8. package/docs/learning/CURRICULUM-MAP.md +2 -2
  9. package/docs/learning/FEATURE-GUIDE.md +6 -6
  10. package/docs/learning/README.md +3 -0
  11. package/docs/learning/lessons/L00-why-cairnkeep.md +3 -1
  12. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  13. package/docs/learning/lessons/L02-installation.md +2 -2
  14. package/docs/learning/lessons/L03-first-project.md +9 -10
  15. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  16. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  17. package/docs/learning/lessons/L21-context-packs.md +1 -1
  18. package/docs/learning/lessons/L22-native-windows.md +2 -2
  19. package/docs/learning/lessons/L23-guided-setup.md +18 -5
  20. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  21. package/docs/learning/lessons/L25-playbooks.md +2 -2
  22. package/docs/learning/video-scripts/V25-playbooks.md +1 -1
  23. package/docs/native-windows.md +5 -0
  24. package/docs/operating.md +24 -5
  25. package/docs/quickstart.md +15 -6
  26. package/docs/releasing.md +27 -19
  27. package/examples/eval/bundled-fake.json +1 -1
  28. package/mcp-memory-server/dist/playbook-cli.js +12 -5
  29. package/package.json +1 -1
  30. package/scripts/cairn-cli.mjs +1 -0
  31. package/scripts/setup.mjs +92 -13
  32. package/scripts/terminal-prompts.mjs +172 -0
  33. package/scripts/test-cli-dispatch.sh +4 -0
  34. package/scripts/test-docs-navigation.sh +63 -0
  35. package/scripts/test-learning-docs.sh +1 -1
  36. package/scripts/test-package-install.sh +1 -1
  37. package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
  38. package/scripts/test-playbook-instructions.sh +4 -2
  39. package/scripts/test-release-sbom.sh +41 -16
  40. package/scripts/test-setup-preflight.mjs +130 -0
  41. package/scripts/test-windows-native.mjs +1 -1
  42. package/scripts/verify-docs-parity.sh +28 -28
  43. 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
- WORKFLOW="$ROOT/.github/workflows/publish.yml"
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" "$WORKFLOW" <<'NODE'
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 workflow = fs.readFileSync(process.argv[4], "utf8");
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
- const artifactStep = workflow.indexOf("- name: Build and validate release artifacts");
33
- const publishStep = workflow.indexOf("- name: Publish to npm with provenance");
34
- const attachStep = workflow.indexOf("- name: Attach package and SBOM to GitHub release");
35
- if (artifactStep < 0 || publishStep < 0 || attachStep < 0 || !(artifactStep < publishStep && publishStep < attachStep)) {
36
- throw new Error("release artifacts must validate before npm publication and attach only after publication");
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' "$WORKFLOW" || fail "release workflow does not use the locked CycloneDX generator"
41
- grep -Fq -- '--package-lock-only' "$WORKFLOW" || fail "release workflow does not use the package lock"
42
- grep -Fq -- '--omit dev' "$WORKFLOW" || fail "release workflow does not omit development dependencies"
43
- grep -Fq -- '--output-reproducible' "$WORKFLOW" || fail "release workflow does not request reproducible output"
44
- grep -Fq -- '--spec-version 1.6' "$WORKFLOW" || fail "release workflow does not emit CycloneDX 1.6"
45
- grep -Fq -- '--validate' "$WORKFLOW" || fail "release workflow does not validate the generated SBOM"
46
- if grep -Fq 'npm sbom' "$WORKFLOW"; then
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' "$WORKFLOW"; then
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"), /before running repository inventory or search/);
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 docs/operating.md
13
- # or README.md. `comm -23` on two sorted lists -- code-keys-not-in-
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/operating.md or
31
- README.md, and that every claude/commands/*.md command is named in
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/operating.md README.md | sort -u)
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 docs/operating.md or README.md"
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|README.md docs/operating.md
320
- memory_import|README.md docs/operating.md docs/privacy-and-data-flow.md
321
- address_space|README.md docs/operating.md docs/privacy-and-data-flow.md
322
- node_types|README.md docs/operating.md
323
- tags_all|README.md docs/operating.md
324
- tags_any|README.md docs/operating.md
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" README.md docs/operating.md || {
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" README.md docs/operating.md || {
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 README.md docs/operating.md docs/privacy-and-data-flow.md || {
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" README.md docs/operating.md docs/privacy-and-data-flow.md || failed=1
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 README.md docs/operating.md docs/privacy-and-data-flow.md || failed=1
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" README.md docs/operating.md docs/privacy-and-data-flow.md || failed=1
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' README.md docs/operating.md docs/privacy-and-data-flow.md || failed=1
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" README.md docs/operating.md docs/work-evidence.md || failed=1
444
- grep -qF "$value" README.md docs/operating.md docs/work-evidence.md || failed=1
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 README.md docs/operating.md docs/privacy-and-data-flow.md || failed=1
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" README.md docs/operating.md docs/privacy-and-data-flow.md docs/work-evidence.md || failed=1
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 README.md; do
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" README.md docs/operating.md || {
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" README.md docs/operating.md templates/env.example.template || {
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 README.md docs/operating.md templates/env.example.template || {
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" README.md docs/operating.md || {
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
- perform this retrieval step before running repository inventory or search
7
- commands such as `rg --files`, `find`, `tree`, or broad text searches:
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 LEVEL --familiarity LEVEL` and follow every applicable `must` action. Apply `should` actions unless there is a concrete reason to skip them; use judgment for `may` actions.
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 LEVEL --public-change --completed ACTION... --skipped ACTION=REASON... --enforce` with accurate signals and evidence.
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.