@cairnkeep/cli 2.13.1 → 2.15.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 (70) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +29 -6
  3. package/claude/commands/cairn-work.md +19 -0
  4. package/docs/context-packs.md +13 -3
  5. package/docs/design/team-mode.md +145 -0
  6. package/docs/harness-compatibility.md +12 -11
  7. package/docs/learning/CURRICULUM-MAP.md +8 -4
  8. package/docs/learning/FEATURE-GUIDE.md +13 -5
  9. package/docs/learning/README.md +3 -1
  10. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  11. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  12. package/docs/learning/lessons/L02-installation.md +2 -2
  13. package/docs/learning/lessons/L03-first-project.md +1 -1
  14. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  15. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  16. package/docs/learning/lessons/L21-context-packs.md +5 -1
  17. package/docs/learning/lessons/L22-native-windows.md +2 -2
  18. package/docs/learning/lessons/L23-guided-setup.md +2 -2
  19. package/docs/learning/lessons/L24-okf-exchange.md +111 -0
  20. package/docs/learning/lessons/L25-playbooks.md +114 -0
  21. package/docs/learning/tracks/operator.md +8 -2
  22. package/docs/learning/tracks/practitioner.md +5 -3
  23. package/docs/learning/video-scripts/V24-okf-exchange.md +31 -0
  24. package/docs/learning/video-scripts/V25-playbooks.md +38 -0
  25. package/docs/native-windows.md +12 -0
  26. package/docs/open-knowledge-format.md +100 -0
  27. package/docs/operating.md +68 -0
  28. package/docs/privacy-and-data-flow.md +25 -0
  29. package/docs/quickstart.md +17 -1
  30. package/docs/storage.md +28 -0
  31. package/examples/eval/bundled-fake.json +1 -1
  32. package/kimi/skills/cairn-work/SKILL.md +13 -0
  33. package/mcp-memory-server/dist/context-pack-cli.js +54 -1
  34. package/mcp-memory-server/dist/context-pack.js +243 -47
  35. package/mcp-memory-server/dist/index.js +13 -1
  36. package/mcp-memory-server/dist/mcp-tool-catalog.js +1 -0
  37. package/mcp-memory-server/dist/note-store.js +44 -2
  38. package/mcp-memory-server/dist/okf.js +432 -0
  39. package/mcp-memory-server/dist/playbook-cli.js +321 -0
  40. package/mcp-memory-server/dist/playbook-receipt.js +245 -0
  41. package/mcp-memory-server/dist/playbook-schema.js +56 -0
  42. package/mcp-memory-server/dist/playbook.js +383 -0
  43. package/mcp-memory-server/package.json +4 -1
  44. package/opencode/command/cairn-work.md +13 -0
  45. package/package.json +2 -1
  46. package/pi/prompts/cairn-work.md +12 -0
  47. package/schemas/context-pack.schema.json +9 -0
  48. package/schemas/playbook.schema.json +29 -0
  49. package/scripts/bootstrap.sh +17 -0
  50. package/scripts/cairn-cli.mjs +2 -0
  51. package/scripts/completion.sh +26 -6
  52. package/scripts/doctor.sh +14 -1
  53. package/scripts/playbook-instructions.mjs +108 -0
  54. package/scripts/setup-core.mjs +1 -0
  55. package/scripts/setup.mjs +8 -0
  56. package/scripts/sync-kimi-assets.sh +3 -3
  57. package/scripts/sync-opencode-memory-assets.sh +2 -1
  58. package/scripts/sync-pi-assets.sh +2 -1
  59. package/scripts/test-cli-dispatch.sh +2 -0
  60. package/scripts/test-completion.sh +11 -2
  61. package/scripts/test-graph-harness-adapters.sh +9 -3
  62. package/scripts/test-harness-registry.sh +11 -0
  63. package/scripts/test-learning-docs.sh +23 -2
  64. package/scripts/test-package-install.sh +26 -0
  65. package/scripts/test-playbook-instructions.sh +60 -0
  66. package/scripts/test-windows-native.mjs +7 -0
  67. package/scripts/uninstall.sh +16 -1
  68. package/scripts/windows-platform.mjs +24 -3
  69. package/templates/playbook-agent-instructions.md.template +14 -0
  70. package/templates/playbooks.json.template +5 -0
package/scripts/doctor.sh CHANGED
@@ -230,7 +230,20 @@ try {
230
230
  [[ $evidence_status -eq 0 || "$evidence_state" == "broken" ]] || true
231
231
  fi
232
232
 
233
- # 10. Typed metadata overlays and canonical-note transaction state. The internal
233
+ # 10. Project playbook policy and private receipts. Absence is healthy because
234
+ # the built-in balanced profile is the compatibility default.
235
+ playbook_cli="$CAIRN_ROOT/mcp-memory-server/dist/playbook-cli.js"
236
+ if [[ ! -f "$playbook_cli" ]]; then
237
+ fail "playbook diagnostics unavailable — rebuild mcp-memory-server"
238
+ elif playbook_json=$(node "$playbook_cli" doctor --project "$PWD" --json 2>/dev/null) \
239
+ && node -e 'const v=JSON.parse(process.argv[1]);if(v.schema_version!==1||v.ok!==true)process.exit(1)' "$playbook_json"; then
240
+ playbook_count=$(node -e 'const v=JSON.parse(process.argv[1]);process.stdout.write(String(v.receipts.count))' "$playbook_json")
241
+ pass "playbook policy and private receipts are valid ($playbook_count receipt(s))"
242
+ else
243
+ fail "playbook policy or receipts failed validation — inspect with: cairn playbook doctor --json"
244
+ fi
245
+
246
+ # 11. Typed metadata overlays and canonical-note transaction state. The internal
234
247
  # JSON seam enumerates only contained local databases and the one notes root;
235
248
  # raw KV cells and unmarked Markdown bytes are never repair inputs.
236
249
  node_cli="$CAIRN_ROOT/mcp-memory-server/dist/node-cli.js"
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import { randomBytes } from "node:crypto";
3
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ const TEMPLATE = join(ROOT, "templates", "playbook-agent-instructions.md.template");
9
+ const START = "<!-- cairnkeep:playbook:v1:start -->";
10
+ const END = "<!-- cairnkeep:playbook:v1:end -->";
11
+ const MAX_BYTES = 1024 * 1024;
12
+
13
+ function safeRead(path) {
14
+ const info = lstatSync(path);
15
+ if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_BYTES) throw new Error("AGENTS.md is not a safe bounded regular file.");
16
+ let descriptor;
17
+ try {
18
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
19
+ const opened = fstatSync(descriptor);
20
+ if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > MAX_BYTES) throw new Error("AGENTS.md changed during validation.");
21
+ return { text: readFileSync(descriptor, "utf8"), mode: info.mode & 0o777 };
22
+ } finally {
23
+ if (descriptor !== undefined) closeSync(descriptor);
24
+ }
25
+ }
26
+
27
+ function count(text, marker) {
28
+ return text.split(marker).length - 1;
29
+ }
30
+
31
+ function render(current, block) {
32
+ const starts = count(current, START);
33
+ const ends = count(current, END);
34
+ if (starts === 0 && ends === 0) return `${current.trimEnd()}${current.trim() ? "\n\n" : ""}${block.trim()}\n`;
35
+ if (starts !== 1 || ends !== 1) throw new Error("AGENTS.md contains malformed or duplicate Cairnkeep playbook markers.");
36
+ const start = current.indexOf(START);
37
+ const end = current.indexOf(END, start);
38
+ if (end < start) throw new Error("AGENTS.md contains reversed Cairnkeep playbook markers.");
39
+ return `${current.slice(0, start)}${block.trim()}${current.slice(end + END.length)}`.replace(/\s*$/, "\n");
40
+ }
41
+
42
+ function removeBlock(current) {
43
+ const starts = count(current, START);
44
+ const ends = count(current, END);
45
+ if (starts === 0 && ends === 0) return current;
46
+ if (starts !== 1 || ends !== 1) throw new Error("AGENTS.md contains malformed or duplicate Cairnkeep playbook markers.");
47
+ const start = current.indexOf(START);
48
+ const end = current.indexOf(END, start);
49
+ if (end < start) throw new Error("AGENTS.md contains reversed Cairnkeep playbook markers.");
50
+ return `${current.slice(0, start)}${current.slice(end + END.length)}`.replace(/\n{3,}/g, "\n\n").trimEnd();
51
+ }
52
+
53
+ function atomicWrite(path, text, mode) {
54
+ const temporary = join(dirname(path), `.AGENTS.md.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
55
+ try {
56
+ writeFileSync(temporary, text, { encoding: "utf8", mode, flag: "wx" });
57
+ renameSync(temporary, path);
58
+ } finally {
59
+ rmSync(temporary, { force: true });
60
+ }
61
+ }
62
+
63
+ export function reconcilePlaybookInstructions(projectRoot = process.cwd(), options = {}) {
64
+ const root = resolve(projectRoot);
65
+ const path = join(root, "AGENTS.md");
66
+ if (dirname(path) !== root) throw new Error("AGENTS.md path escapes the project root.");
67
+ const block = readFileSync(TEMPLATE, "utf8");
68
+ const existing = existsSync(path) ? safeRead(path) : { text: "", mode: 0o644 };
69
+ const desired = render(existing.text, block);
70
+ const status = !existsSync(path) ? "created" : desired === existing.text ? "unchanged" : "updated";
71
+ if (!options.check && status !== "unchanged") atomicWrite(path, desired, existing.mode || 0o644);
72
+ return Object.freeze({ schema_version: 1, path, status: options.check && status !== "unchanged" ? `would-${status}` : status });
73
+ }
74
+
75
+ export function removePlaybookInstructions(projectRoot = process.cwd(), options = {}) {
76
+ const root = resolve(projectRoot);
77
+ const path = join(root, "AGENTS.md");
78
+ if (!existsSync(path)) return Object.freeze({ schema_version: 1, path, status: "unchanged" });
79
+ const existing = safeRead(path);
80
+ const desired = removeBlock(existing.text);
81
+ const status = desired === existing.text ? "unchanged" : "removed";
82
+ if (!options.check && status === "removed") {
83
+ if (desired.trim()) atomicWrite(path, `${desired}\n`, existing.mode || 0o644);
84
+ else rmSync(path);
85
+ }
86
+ return Object.freeze({ schema_version: 1, path, status: options.check && status === "removed" ? "would-remove" : status });
87
+ }
88
+
89
+ async function main() {
90
+ const args = process.argv.slice(2);
91
+ const check = args.includes("--check");
92
+ const remove = args.includes("--remove");
93
+ const json = args.includes("--json");
94
+ const values = args.filter((arg) => !["--check", "--remove", "--json"].includes(arg));
95
+ if (values.length > 1) throw new Error("Usage: playbook-instructions.mjs [PROJECT] [--check] [--json]");
96
+ const value = remove
97
+ ? removePlaybookInstructions(values[0] ?? process.cwd(), { check })
98
+ : reconcilePlaybookInstructions(values[0] ?? process.cwd(), { check });
99
+ process.stdout.write(`${json ? JSON.stringify(value) : `${value.status}: ${value.path}`}\n`);
100
+ if (check && value.status !== "unchanged") process.exitCode = 1;
101
+ }
102
+
103
+ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
104
+ main().catch((error) => {
105
+ process.stderr.write(`cairn playbook instructions: ${error instanceof Error ? error.message : String(error)}\n`);
106
+ process.exitCode = 2;
107
+ });
108
+ }
@@ -16,6 +16,7 @@ const COMMON_ASSETS = Object.freeze([
16
16
  ["env.example.template", ".ai/env.example", 0o644],
17
17
  ["trajectory-redaction.json.template", ".ai/trajectory-redaction.json", 0o644],
18
18
  ["capabilities.json.template", ".ai/capabilities.json", 0o600],
19
+ ["playbooks.json.template", ".ai/playbooks.json", 0o600],
19
20
  ["agentfs-gitignore.template", ".agentfs/.gitignore", 0o644],
20
21
  ["planning-config.json.template", ".planning/config.json", 0o644],
21
22
  ["project-brief.md.template", ".planning/PROJECT-BRIEF.md", 0o644],
package/scripts/setup.mjs CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  } from "./setup-core.mjs";
23
23
  import { reconcileSetupPlan } from "./setup-reconcile.mjs";
24
24
  import { HARNESS_IDS, machineSyncCommand, requiredHarnessAssetPaths } from "./harness-registry.mjs";
25
+ import { reconcilePlaybookInstructions } from "./playbook-instructions.mjs";
25
26
 
26
27
  const HARNESSES = HARNESS_IDS;
27
28
  const GIT_MODES = Object.freeze(["init", "existing", "none"]);
@@ -33,6 +34,7 @@ const COMMON_SETUP_ASSETS = Object.freeze([
33
34
  ".ai/env.example",
34
35
  ".ai/trajectory-redaction.json",
35
36
  ".ai/capabilities.json",
37
+ ".ai/playbooks.json",
36
38
  ".agentfs/.gitignore",
37
39
  ".planning/config.json",
38
40
  ".planning/PROJECT-BRIEF.md",
@@ -186,6 +188,11 @@ async function executeSetup(args, options) {
186
188
  });
187
189
  let plan = buildSetupPlan({ target, preflight, choices });
188
190
  if (options.augmentPlan) plan = await options.augmentPlan(plan);
191
+ try {
192
+ reconcilePlaybookInstructions(plan.target, { check: true });
193
+ } catch (error) {
194
+ throw operational("unsafe-instructions", `Project AGENTS.md cannot be reconciled safely: ${error instanceof Error ? error.message : String(error)}`);
195
+ }
189
196
 
190
197
  if (interactive && !parsed.confirmed) {
191
198
  renderPlan(plan, options.output);
@@ -200,6 +207,7 @@ async function executeSetup(args, options) {
200
207
  createTarget(plan.target);
201
208
  if (plan.git === "init") initializeGit(plan.target);
202
209
  const reconciliation = await reconcileSetupPlan(plan);
210
+ reconcilePlaybookInstructions(plan.target);
203
211
  return resultFor(plan, reconciliation, options.platform);
204
212
  }
205
213
 
@@ -5,7 +5,7 @@ usage() {
5
5
  cat <<'EOF'
6
6
  Usage: sync-kimi-assets.sh [--check|--apply] [--live-root PATH]
7
7
 
8
- Compare or sync Cairnkeep's thin graph Skill against the live Kimi Code tree.
8
+ Compare or sync Cairnkeep's thin managed Skills against the live Kimi Code tree.
9
9
 
10
10
  Options:
11
11
  --check Verify managed live assets match the repo copies (default)
@@ -19,7 +19,7 @@ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
19
19
  SOURCE_ROOT="$ROOT_DIR/kimi"
20
20
  LIVE_ROOT="${KIMI_CODE_HOME:-$HOME/.kimi-code}"
21
21
  MODE="check"
22
- ASSETS=("skills/graphify/SKILL.md")
22
+ ASSETS=("skills/graphify/SKILL.md" "skills/cairn-work/SKILL.md")
23
23
 
24
24
  while [[ $# -gt 0 ]]; do
25
25
  case "$1" in
@@ -74,4 +74,4 @@ if [[ "$MODE" == "apply" ]]; then
74
74
  fi
75
75
 
76
76
  check_assets
77
- printf 'Kimi graph Skill is in sync under %s\n' "$LIVE_ROOT"
77
+ printf 'Kimi managed Skills are in sync under %s\n' "$LIVE_ROOT"
@@ -16,7 +16,7 @@ Options:
16
16
 
17
17
  Notes:
18
18
  - The repo-managed source of truth lives under ./opencode/
19
- - This script manages the memory-sync / memory-review / code-review commands,
19
+ - This script manages the memory-sync / memory-review / code-review / cairn-work commands,
20
20
  the code-reviewer agent, and the memory-review workflow. The memory-wakeup
21
21
  plugin is handled by sync-opencode-plugin-assets.sh.
22
22
  EOF
@@ -33,6 +33,7 @@ ASSETS=(
33
33
  "command/code-review.md"
34
34
  "command/remember.md"
35
35
  "command/recall.md"
36
+ "command/cairn-work.md"
36
37
  "agents/code-reviewer.md"
37
38
  "workflows/memory-review-workflow.md"
38
39
  )
@@ -24,6 +24,7 @@ ASSETS=(
24
24
  "extensions/cairnkeep-memory.ts"
25
25
  "extensions/cairnkeep-trajectory.ts"
26
26
  "prompts/graphify.md"
27
+ "prompts/cairn-work.md"
27
28
  )
28
29
 
29
30
  while [[ $# -gt 0 ]]; do
@@ -105,4 +106,4 @@ if ! check_assets; then
105
106
  echo "Recovery: sync-pi-assets.sh --apply --live-root PATH" >&2
106
107
  exit 1
107
108
  fi
108
- printf 'Pi extension and graph prompt are in sync under %s\n' "$LIVE_ROOT"
109
+ printf 'Pi extensions and managed prompts are in sync under %s\n' "$LIVE_ROOT"
@@ -16,6 +16,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
16
16
  "$cairn" help | grep -q "cairn notes" || fail "help missing notes"
17
17
  "$cairn" help | grep -q "cairn artifact <list|show|delete|prune>" || fail "help missing artifact"
18
18
  "$cairn" help | grep -q "cairn evidence <list|show|delete|prune|doctor>" || fail "help missing evidence"
19
+ "$cairn" help | grep -q "cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>" || fail "help missing playbook"
19
20
  "$cairn" help | grep -q "cairn capabilities <list|status|enable|disable|reset|logging>" || fail "help missing capabilities"
20
21
  "$cairn" help | grep -q "cairn mcp-tools <list|status|set|reset>" || fail "help missing mcp-tools"
21
22
  "$cairn" help | grep -q "cairn pack <init|lock|validate|install|list|show|remove|enable|disable|update|skills|approve-skill|revoke-skill>" || fail "help missing pack"
@@ -26,6 +27,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
26
27
  env -u CAIRN_NOTE_DISTILLATION "$cairn" notes --help >/dev/null 2>&1 || fail "cairn notes dispatch"
27
28
  "$cairn" artifact --help | grep -q "cairn artifact list" || fail "cairn artifact dispatch"
28
29
  "$cairn" evidence --help | grep -q "cairn evidence list" || fail "cairn evidence dispatch"
30
+ "$cairn" playbook --help | grep -q "cairn playbook list" || fail "cairn playbook dispatch"
29
31
  "$cairn" capabilities --help >"$tmp/capabilities-help" 2>/dev/null || fail "cairn capabilities dispatch"
30
32
  "$cairn" mcp-tools list --json >"$tmp/mcp-tools.json" || fail "cairn mcp-tools dispatch"
31
33
  CAIRN_PACK_BASE_DIR="$tmp/pack-store" "$cairn" pack list --json >"$tmp/packs.json" || fail "cairn pack dispatch"
@@ -22,7 +22,7 @@ for shell in bash zsh fish; do
22
22
  for capability_id in $capability_ids; do
23
23
  grep -qF "$capability_id" "$tmp/$shell"
24
24
  done
25
- if grep -Eq '(^|[[:space:]"'"'"'])(guard|start|finish)([[:space:]"'"'"']|$)' "$tmp/$shell"; then
25
+ if grep -Eq '(^|[[:space:]"'"'"'])guard([[:space:]"'"'"']|$)' "$tmp/$shell"; then
26
26
  echo "completion exposed a private capability operation for $shell" >&2
27
27
  exit 1
28
28
  fi
@@ -34,7 +34,7 @@ for shell in bash zsh fish; do
34
34
  for capability_id in $capability_ids; do
35
35
  grep -qF "$capability_id" "$tmp/$shell"
36
36
  done
37
- if grep -Eq '(^|[[:space:]"'"'"'])(doctor-diagnosis|guard|start|finish)([[:space:]"'"'"']|$)' "$tmp/$shell"; then
37
+ if grep -Eq '(^|[[:space:]"'"'"'])(doctor-diagnosis|guard)([[:space:]"'"'"']|$)' "$tmp/$shell"; then
38
38
  echo "completion exposed a private eval lifecycle operation for $shell" >&2
39
39
  exit 1
40
40
  fi
@@ -51,6 +51,14 @@ for shell in bash zsh fish; do
51
51
  grep -q 'evidence' "$tmp/$shell"
52
52
  grep -q 'list.*show.*delete.*prune.*doctor\|list show delete prune doctor' "$tmp/$shell"
53
53
  grep -q -- '--status\|-l status' "$tmp/$shell"
54
+ grep -q 'playbook' "$tmp/$shell"
55
+ grep -q 'list.*status.*init.*set.*enable.*disable.*reset.*check.*record.*receipts.*instructions.*doctor\|list status init set enable disable reset check record receipts instructions doctor' "$tmp/$shell"
56
+ for playbook_value in minimal balanced strict context.recall verify.tests review.security learning.capture; do
57
+ grep -qF "$playbook_value" "$tmp/$shell"
58
+ done
59
+ for playbook_flag in --project --json --enforce --changed --complexity --familiarity --risk --public-change --completed --skipped --failed; do
60
+ grep -q -- "$playbook_flag\|-l ${playbook_flag#--}" "$tmp/$shell"
61
+ done
54
62
  done
55
63
 
56
64
  "$ROOT/bin/cairn" help >"$tmp/root-help"
@@ -68,6 +76,7 @@ if grep -q 'doctor-diagnosis' "$tmp/eval-help"; then
68
76
  fi
69
77
  grep -qF 'cairn skill <harvest|list|show|review|propose|evaluate|apply|rollback>' "$tmp/root-help"
70
78
  grep -qF 'cairn evidence <list|show|delete|prune|doctor>' "$tmp/root-help"
79
+ grep -qF 'cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>' "$tmp/root-help"
71
80
  node "$ROOT/mcp-memory-server/dist/skill-cli.js" --help >"$tmp/skill-help"
72
81
  for command in $skill_commands; do
73
82
  grep -q "cairn skill $command" "$tmp/skill-help"
@@ -9,9 +9,13 @@ fail() { echo "FAIL: $1" >&2; exit 1; }
9
9
 
10
10
  PI_SOURCE="$ROOT/pi/prompts/graphify.md"
11
11
  KIMI_SOURCE="$ROOT/kimi/skills/graphify/SKILL.md"
12
+ PI_PLAYBOOK_SOURCE="$ROOT/pi/prompts/cairn-work.md"
13
+ KIMI_PLAYBOOK_SOURCE="$ROOT/kimi/skills/cairn-work/SKILL.md"
12
14
 
13
15
  [[ -f "$PI_SOURCE" ]] || fail "Pi /graphify prompt source is missing"
14
16
  [[ -f "$KIMI_SOURCE" ]] || fail "Kimi graphify skill source is missing"
17
+ [[ -f "$PI_PLAYBOOK_SOURCE" ]] || fail "Pi cairn-work prompt source is missing"
18
+ [[ -f "$KIMI_PLAYBOOK_SOURCE" ]] || fail "Kimi cairn-work Skill source is missing"
15
19
 
16
20
  for adapter in "$PI_SOURCE" "$KIMI_SOURCE"; do
17
21
  grep -qF 'cairn graph' "$adapter" || fail "adapter does not delegate to cairn graph: $adapter"
@@ -31,16 +35,18 @@ PI_LIVE="$tmp/pi"
31
35
  "$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$PI_LIVE" >"$tmp/pi-first"
32
36
  [[ -f "$PI_LIVE/extensions/cairnkeep-trajectory.ts" ]] || fail "Pi sync omitted trajectory extension"
33
37
  cmp -s "$PI_SOURCE" "$PI_LIVE/prompts/graphify.md" || fail "Pi sync changed graphify prompt bytes"
38
+ cmp -s "$PI_PLAYBOOK_SOURCE" "$PI_LIVE/prompts/cairn-work.md" || fail "Pi sync changed cairn-work prompt bytes"
34
39
  "$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$PI_LIVE" >"$tmp/pi-second"
35
- grep -qF 'Applied 0 Pi asset(s); 2 already matched.' "$tmp/pi-second" || fail "Pi sync is not idempotent"
40
+ grep -qF 'Applied 0 Pi asset(s); 3 already matched.' "$tmp/pi-second" || fail "Pi sync is not idempotent"
36
41
 
37
42
  KIMI_LIVE="$tmp/kimi"
38
43
  "$ROOT/scripts/sync-kimi-assets.sh" --apply --live-root "$KIMI_LIVE" >"$tmp/kimi-first"
39
44
  cmp -s "$KIMI_SOURCE" "$KIMI_LIVE/skills/graphify/SKILL.md" || fail "Kimi sync changed graphify skill bytes"
45
+ cmp -s "$KIMI_PLAYBOOK_SOURCE" "$KIMI_LIVE/skills/cairn-work/SKILL.md" || fail "Kimi sync changed cairn-work Skill bytes"
40
46
  "$ROOT/scripts/sync-kimi-assets.sh" --apply --live-root "$KIMI_LIVE" >"$tmp/kimi-second"
41
- grep -qF 'Applied 0 Kimi asset(s); 1 already matched.' "$tmp/kimi-second" || fail "Kimi sync is not idempotent"
47
+ grep -qF 'Applied 0 Kimi asset(s); 2 already matched.' "$tmp/kimi-second" || fail "Kimi sync is not idempotent"
42
48
 
43
49
  "$ROOT/bin/cairn" help | grep -qF 'cairn sync-kimi' || fail "CLI help omits sync-kimi"
44
50
  "$ROOT/bin/cairn" sync-kimi --help >/dev/null || fail "CLI does not dispatch sync-kimi"
45
51
 
46
- echo "PASS: thin Pi and Kimi graph adapters delegate only to cairn graph"
52
+ echo "PASS: thin Pi and Kimi graph/playbook adapters are bounded and idempotent"
@@ -13,6 +13,17 @@ import { spawnSync } from "node:child_process";
13
13
  import { pathToFileURL } from "node:url";
14
14
 
15
15
  const [root, sandbox] = process.argv.slice(2);
16
+ for (const path of [
17
+ "claude/commands/cairn-work.md",
18
+ "opencode/command/cairn-work.md",
19
+ "pi/prompts/cairn-work.md",
20
+ "kimi/skills/cairn-work/SKILL.md",
21
+ ]) {
22
+ const content = readFileSync(join(root, path), "utf8");
23
+ assert.match(content, /cairn playbook check/);
24
+ assert.match(content, /must/);
25
+ assert.match(content, /approval/);
26
+ }
16
27
  const registry = await import(pathToFileURL(join(root, "scripts", "harness-registry.mjs")).href);
17
28
  assert.deepEqual(registry.HARNESS_IDS, ["claude", "opencode", "pi", "kimi", "qwen", "codex"]);
18
29
  assert.equal(new Set(registry.HARNESS_IDS).size, registry.HARNESS_IDS.length);
@@ -22,7 +22,7 @@ done
22
22
 
23
23
  ready=0
24
24
  brief=0
25
- for number in $(seq -w 0 22); do
25
+ for number in $(seq -w 0 25); 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 8 && $brief -eq 15 ]] || {
56
+ [[ $ready -eq 11 && $brief -eq 15 ]] || {
57
57
  echo "unexpected learning status totals: ready=$ready brief=$brief" >&2
58
58
  exit 1
59
59
  }
@@ -143,4 +143,25 @@ grep -Eqi 'cancel|shutdown|child process' "$l23"
143
143
  grep -Eqi '0\.84\.1' "$l23"
144
144
  grep -Eqi 'credentials|secrets|private state' "$l23"
145
145
 
146
+ l25=docs/learning/lessons/L25-playbooks.md
147
+ video25=docs/learning/video-scripts/V25-playbooks.md
148
+ for file in "$l25" "$video25"; do
149
+ [[ -f "$file" ]]
150
+ done
151
+ for phrase in \
152
+ 'cairn playbook check start' \
153
+ 'cairn playbook check finish' \
154
+ '--enforce' \
155
+ 'cairn playbook record' \
156
+ 'unauthenticated' \
157
+ 'does not run'; do
158
+ grep -qF -- "$phrase" "$l25" || {
159
+ echo "L25 missing playbook teaching boundary: $phrase" >&2
160
+ exit 1
161
+ }
162
+ done
163
+ grep -qF 'course-13-playbooks' "$coverage"
164
+ grep -qF 'L25-playbooks.md' docs/learning/tracks/practitioner.md
165
+ grep -qF 'L25-playbooks.md' docs/learning/tracks/operator.md
166
+
146
167
  echo "PASS: public learning path structure, readiness, links, and version alignment"
@@ -48,6 +48,8 @@ bootstrap_output=$(cairn bootstrap "$tmp/project")
48
48
  [[ -x "$tmp/project/.ai/start-qwen.sh" ]] || fail "bootstrap did not install an executable Qwen launcher"
49
49
  [[ -x "$tmp/project/.ai/start-codex.sh" ]] || fail "bootstrap did not install an executable Codex launcher"
50
50
  [[ -f "$tmp/project/.planning/config.json" ]] || fail "bootstrap did not install the planning scaffold"
51
+ [[ -f "$tmp/project/.ai/playbooks.json" ]] || fail "bootstrap did not install the playbook policy"
52
+ grep -qF '<!-- cairnkeep:playbook:v1:start -->' "$tmp/project/AGENTS.md" || fail "bootstrap did not install playbook agent guidance"
51
53
 
52
54
  (cd "$tmp/project" && cairn doctor) >/dev/null || fail "installed package failed cairn doctor"
53
55
  env -u CAIRN_NOTE_DISTILLATION cairn notes --help >/dev/null \
@@ -104,6 +106,30 @@ CAIRN_PACK_BASE_DIR="$tmp/installed-packs" cairn pack list --json >"$tmp/install
104
106
  cairn mcp-tools list --json >"$tmp/installed-mcp-tools.json" || \
105
107
  fail "installed package mcp-tools CLI failed"
106
108
 
109
+ for required in \
110
+ schemas/playbook.schema.json \
111
+ templates/playbooks.json.template \
112
+ templates/playbook-agent-instructions.md.template \
113
+ scripts/playbook-instructions.mjs \
114
+ mcp-memory-server/dist/playbook-schema.js \
115
+ mcp-memory-server/dist/playbook.js \
116
+ mcp-memory-server/dist/playbook-receipt.js \
117
+ mcp-memory-server/dist/playbook-cli.js \
118
+ claude/commands/cairn-work.md \
119
+ opencode/command/cairn-work.md \
120
+ pi/prompts/cairn-work.md \
121
+ kimi/skills/cairn-work/SKILL.md \
122
+ docs/design/team-mode.md \
123
+ docs/learning/lessons/L25-playbooks.md \
124
+ docs/learning/video-scripts/V25-playbooks.md
125
+ do
126
+ [[ -f "$installed_root/$required" ]] || fail "npm tarball omitted $required"
127
+ done
128
+ cairn playbook status --project "$tmp/project" --json >"$tmp/installed-playbook-status.json" || \
129
+ fail "installed package playbook status failed"
130
+ cairn playbook check start --project "$tmp/project" --session installed-playbook --json >"$tmp/installed-playbook-check.json" || \
131
+ fail "installed package playbook check failed"
132
+
107
133
  for required in \
108
134
  schemas/skill-adapter.schema.json \
109
135
  schemas/skill-proposal-protocol.schema.json \
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5
+ tmp=$(mktemp -d)
6
+ trap 'rm -rf "$tmp"' EXIT
7
+ project="$tmp/project"
8
+ mkdir -p "$project"
9
+ printf '# User rules\n\nKeep this line.\n' >"$project/AGENTS.md"
10
+ chmod 0640 "$project/AGENTS.md"
11
+
12
+ node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null
13
+ grep -qF 'Keep this line.' "$project/AGENTS.md"
14
+ [[ $(grep -cF '<!-- cairnkeep:playbook:v1:start -->' "$project/AGENTS.md") -eq 1 ]]
15
+ [[ $(stat -c '%a' "$project/AGENTS.md") == 640 ]]
16
+ before=$(sha256sum "$project/AGENTS.md" | cut -d' ' -f1)
17
+ node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null
18
+ after=$(sha256sum "$project/AGENTS.md" | cut -d' ' -f1)
19
+ [[ "$before" == "$after" ]]
20
+ "$ROOT/bin/cairn" playbook instructions check --project "$project" --json >/dev/null
21
+
22
+ node "$ROOT/scripts/playbook-instructions.mjs" "$project" --remove >/dev/null
23
+ grep -qF 'Keep this line.' "$project/AGENTS.md"
24
+ ! grep -qF 'cairnkeep:playbook' "$project/AGENTS.md"
25
+
26
+ printf '%s\n%s\n%s\n' \
27
+ '<!-- cairnkeep:playbook:v1:start -->' \
28
+ '<!-- cairnkeep:playbook:v1:start -->' \
29
+ '<!-- cairnkeep:playbook:v1:end -->' >"$project/AGENTS.md"
30
+ if node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null 2>&1; then
31
+ echo 'duplicate managed markers were accepted' >&2
32
+ exit 1
33
+ fi
34
+
35
+ rm "$project/AGENTS.md"
36
+ printf 'neighbor\n' >"$tmp/neighbor"
37
+ ln -s "$tmp/neighbor" "$project/AGENTS.md"
38
+ if node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null 2>&1; then
39
+ echo 'symlink AGENTS.md was accepted' >&2
40
+ exit 1
41
+ fi
42
+ [[ $(cat "$tmp/neighbor") == neighbor ]]
43
+
44
+ for command in bootstrap setup; do
45
+ unsafe="$tmp/unsafe-$command"
46
+ mkdir -p "$unsafe"
47
+ printf '%s\n%s\n' '<!-- cairnkeep:playbook:v1:start -->' '<!-- cairnkeep:playbook:v1:start -->' >"$unsafe/AGENTS.md"
48
+ if [[ "$command" == bootstrap ]]; then
49
+ if "$ROOT/bin/cairn" bootstrap "$unsafe" >/dev/null 2>&1; then
50
+ echo 'bootstrap accepted malformed managed markers' >&2
51
+ exit 1
52
+ fi
53
+ elif "$ROOT/bin/cairn" setup "$unsafe" --git init --harness codex --memory local --yes >/dev/null 2>&1; then
54
+ echo 'setup accepted malformed managed markers' >&2
55
+ exit 1
56
+ fi
57
+ [[ ! -e "$unsafe/.ai" ]]
58
+ done
59
+
60
+ echo 'PASS: ownership-safe playbook AGENTS.md reconciliation'
@@ -28,6 +28,7 @@ try {
28
28
  process.env.USERPROFILE = sandbox;
29
29
  const project = join(sandbox, "Project with spaces – Unicode");
30
30
  mkdirSync(project, { recursive: true });
31
+ writeFileSync(join(project, "AGENTS.md"), "# User rules\n\nKeep this line.\n");
31
32
  bootstrapWindows(root, [project]);
32
33
  for (const harness of HARNESS_IDS) {
33
34
  assert.ok(existsSync(join(project, ".ai", `start-${harness}.cmd`)), `${harness} cmd launcher`);
@@ -38,6 +39,8 @@ try {
38
39
  assert.match(launcherPowerShell(), /post-exit\.ps1/);
39
40
  assert.match(launcherPowerShell(), /evidence run --harness/);
40
41
  assert.match(launcherPowerShell(), /CAIRN_WORK_EVIDENCE/);
42
+ assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /cairnkeep:playbook:v1:start/);
43
+ assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /Keep this line\./);
41
44
 
42
45
  const claudeRoot = join(sandbox, "Claude Config");
43
46
  await runWindowsCommand({ command: "sync", args: ["--apply", "--live-root", claudeRoot], root });
@@ -59,6 +62,9 @@ try {
59
62
  assert.equal(readFileSync(join(memory, "project.db"), "utf8"), "sqlite-a");
60
63
  assert.match(powershellCompletion(), /Register-ArgumentCompleter/);
61
64
  assert.match(powershellCompletion(), /evidence/);
65
+ assert.match(powershellCompletion(), /playbook/);
66
+ assert.match(powershellCompletion(), /review\.security/);
67
+ assert.match(powershellCompletion(), /--enforce/);
62
68
 
63
69
  let containerArgs = [];
64
70
  runNativeContainer(["stdio", "--image", "example/windows:1", "--volume", "windows-data"], root, (_engine, args) => { containerArgs = args; });
@@ -68,6 +74,7 @@ try {
68
74
 
69
75
  await runWindowsCommand({ command: "uninstall", args: ["--yes", "--live-root", claudeRoot, project], root });
70
76
  assert.ok(!existsSync(join(project, ".ai", "start-claude.cmd")));
77
+ assert.equal(readFileSync(join(project, "AGENTS.md"), "utf8"), "# User rules\n\nKeep this line.\n");
71
78
  assert.ok(!existsSync(join(claudeRoot, "commands", "remember.md")));
72
79
  const cleanedSettings = JSON.parse(readFileSync(join(claudeRoot, "settings.json"), "utf8"));
73
80
  assert.ok(!JSON.stringify(cleanedSettings).includes("memory-wakeup"));
@@ -49,6 +49,7 @@ PROJECT_AI_FILES=(
49
49
  env.example
50
50
  trajectory-redaction.json
51
51
  capabilities.json
52
+ playbooks.json
52
53
  )
53
54
 
54
55
  usage() {
@@ -150,7 +151,7 @@ if (!exact(state, ["schema_version", "cairnkeep_version", "git", "memory", "harn
150
151
  || typeof state.assets !== "object"
151
152
  || Array.isArray(state.assets)) process.exit(1);
152
153
  const allowed = new Set([
153
- ".ai/env.example", ".ai/trajectory-redaction.json", ".ai/capabilities.json",
154
+ ".ai/env.example", ".ai/trajectory-redaction.json", ".ai/capabilities.json", ".ai/playbooks.json",
154
155
  ".agentfs/.gitignore", ".planning/config.json", ".planning/PROJECT-BRIEF.md",
155
156
  ".planning/wiki/index.md", ".planning/wiki/policy.md", ".planning/wiki/CONTRADICTIONS.md",
156
157
  ".planning/wiki/LOG.md", ".planning/alignment/policy.md", ".planning/alignment/gap-register.yaml",
@@ -213,8 +214,10 @@ echo "Pi adapters:"
213
214
  remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-memory.ts"
214
215
  remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-trajectory.ts"
215
216
  remove_path "$PI_LIVE_ROOT/prompts/graphify.md"
217
+ remove_path "$PI_LIVE_ROOT/prompts/cairn-work.md"
216
218
  echo "Kimi adapter:"
217
219
  remove_path "$KIMI_LIVE_ROOT/skills/graphify/SKILL.md"
220
+ remove_path "$KIMI_LIVE_ROOT/skills/cairn-work/SKILL.md"
218
221
 
219
222
  # 2. settings.json hook registrations (back up whole file, then de-register).
220
223
  SETTINGS="$LIVE_ROOT/settings.json"
@@ -310,6 +313,18 @@ for proj in "${PROJECTS[@]:-}"; do
310
313
  [[ -n "$proj" && -d "$proj" ]] || continue
311
314
  proj=$(cd "$proj" && pwd)
312
315
  echo "Project scaffold: $proj"
316
+ if [[ -f "$proj/AGENTS.md" && ! -L "$proj/AGENTS.md" ]] && grep -qF '<!-- cairnkeep:playbook:v1:start -->' "$proj/AGENTS.md" 2>/dev/null; then
317
+ if [[ $DRY_RUN -eq 1 ]]; then
318
+ echo " would remove Cairnkeep playbook block from $proj/AGENTS.md"
319
+ else
320
+ back_up "$proj/AGENTS.md"
321
+ if node "$CAIRN_ROOT/scripts/playbook-instructions.mjs" "$proj" --remove >/dev/null; then
322
+ echo " removed Cairnkeep playbook block from $proj/AGENTS.md"
323
+ else
324
+ echo " kept AGENTS.md: playbook markers could not be removed safely"
325
+ fi
326
+ fi
327
+ fi
313
328
  # Truthy launchers create this project-local root only for the explicit
314
329
  # capability overlay. Back up the complete managed unit so its native Claude
315
330
  # hook registrations and OpenCode plugin bytes restore together exactly.