@cairnkeep/cli 2.8.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +82 -27
  3. package/bin/cairn +6 -153
  4. package/bin/cairn-container +8 -0
  5. package/docs/building-an-overlay.md +41 -4
  6. package/docs/containers.md +10 -0
  7. package/docs/context-packs.md +94 -0
  8. package/docs/harness-compatibility.md +37 -3
  9. package/docs/learning/CURRICULUM-MAP.md +16 -5
  10. package/docs/learning/FEATURE-GUIDE.md +15 -4
  11. package/docs/learning/README.md +8 -2
  12. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  13. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  14. package/docs/learning/lessons/L02-installation.md +11 -2
  15. package/docs/learning/lessons/L03-first-project.md +1 -1
  16. package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
  17. package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
  18. package/docs/learning/lessons/L21-context-packs.md +79 -0
  19. package/docs/learning/lessons/L22-native-windows.md +83 -0
  20. package/docs/learning/lessons/L23-guided-setup.md +138 -0
  21. package/docs/learning/tracks/operator.md +10 -3
  22. package/docs/learning/tracks/quickstart.md +7 -3
  23. package/docs/mcp-tool-profiles.md +43 -0
  24. package/docs/native-windows.md +122 -0
  25. package/docs/operating.md +157 -18
  26. package/docs/privacy-and-data-flow.md +51 -1
  27. package/docs/storage.md +29 -0
  28. package/examples/eval/bundled-fake.json +1 -1
  29. package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
  30. package/mcp-memory-server/dist/artifact-store.js +196 -55
  31. package/mcp-memory-server/dist/capability-cli.js +2 -1
  32. package/mcp-memory-server/dist/capability-config.js +4 -3
  33. package/mcp-memory-server/dist/capability-harness.js +34 -7
  34. package/mcp-memory-server/dist/context-pack-cli.js +185 -0
  35. package/mcp-memory-server/dist/context-pack.js +889 -0
  36. package/mcp-memory-server/dist/eval-cli.js +6 -5
  37. package/mcp-memory-server/dist/eval-plan.js +35 -13
  38. package/mcp-memory-server/dist/eval-process.js +11 -4
  39. package/mcp-memory-server/dist/eval-report.js +8 -5
  40. package/mcp-memory-server/dist/eval-runner.js +2 -0
  41. package/mcp-memory-server/dist/eval-schema.js +1 -0
  42. package/mcp-memory-server/dist/graph-cli.js +7 -1
  43. package/mcp-memory-server/dist/index.js +87 -37
  44. package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
  45. package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
  46. package/mcp-memory-server/dist/mcp-tool-profile.js +164 -0
  47. package/mcp-memory-server/dist/note-store.js +12 -3
  48. package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
  49. package/mcp-memory-server/dist/platform-security.js +102 -0
  50. package/mcp-memory-server/dist/skill-store.js +27 -12
  51. package/mcp-memory-server/package.json +4 -1
  52. package/package.json +5 -4
  53. package/pi/extensions/cairnkeep-memory.ts +61 -0
  54. package/schemas/cairnkeep-setup-policy.schema.json +55 -0
  55. package/schemas/cairnkeep-setup.schema.json +36 -0
  56. package/schemas/context-pack.schema.json +33 -0
  57. package/schemas/eval-report.schema.json +1 -0
  58. package/scripts/cairn-cli.mjs +119 -0
  59. package/scripts/cairn-container-cli.mjs +121 -0
  60. package/scripts/cairn-hook.mjs +199 -0
  61. package/scripts/completion.sh +33 -5
  62. package/scripts/doctor.sh +96 -0
  63. package/scripts/fix-permissions.mjs +12 -0
  64. package/scripts/memory-wiki-audit.mjs +95 -0
  65. package/scripts/phase26-test-manifest.mjs +26 -0
  66. package/scripts/run-repository-tests.mjs +76 -0
  67. package/scripts/setup-core.mjs +310 -0
  68. package/scripts/setup-reconcile.mjs +331 -0
  69. package/scripts/setup.mjs +306 -0
  70. package/scripts/sync-pi-assets.sh +19 -4
  71. package/scripts/test-cli-dispatch.sh +6 -0
  72. package/scripts/test-completion.sh +44 -0
  73. package/scripts/test-doctor.sh +66 -0
  74. package/scripts/test-learning-docs.sh +53 -3
  75. package/scripts/test-package-install.sh +81 -0
  76. package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
  77. package/scripts/test-phase18-capability-lifecycle.sh +1 -1
  78. package/scripts/test-pi-lifecycle.mjs +190 -0
  79. package/scripts/test-setup-compatibility.mjs +186 -0
  80. package/scripts/test-setup-output.sh +131 -0
  81. package/scripts/test-setup-overlay.sh +113 -0
  82. package/scripts/test-setup-preflight.mjs +281 -0
  83. package/scripts/test-setup-reconcile.mjs +257 -0
  84. package/scripts/test-uninstall.sh +71 -0
  85. package/scripts/test-windows-native.mjs +136 -0
  86. package/scripts/uninstall.sh +95 -7
  87. package/scripts/verify-pi-mcp-bridge.mjs +588 -0
  88. package/scripts/windows-platform.mjs +725 -0
  89. package/templates/env.example.template +11 -0
@@ -303,6 +303,77 @@ check "memory store restored" "$(cat "$SB/home/.cairnkeep/db" 2>/dev/null)"
303
303
  check "note store restored exactly" "$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)" "$purge_note_before"
304
304
  check "typed and journal state restored exactly" "$(tree_hash "$SB/home/.cairnkeep")" "$purge_store_before"
305
305
 
306
+ # --- context-pack data requires its own explicit purge consent ---------------
307
+ PACK_HOME="$SB/pack-home"
308
+ PACK_STORE="$PACK_HOME/.cairnkeep/packs"
309
+ mkdir -p "$PACK_STORE/objects/example" "$PACK_HOME/.cairnkeep/memory"
310
+ printf 'immutable pack\n' >"$PACK_STORE/objects/example/marker"
311
+ printf 'memory\n' >"$PACK_HOME/.cairnkeep/memory/marker"
312
+ pack_output=$(HOME="$PACK_HOME" XDG_CONFIG_HOME="$PACK_HOME/.config" \
313
+ CAIRN_AGENTFS_BASE_DIR="$PACK_HOME/.cairnkeep" CAIRN_PACK_BASE_DIR="$PACK_STORE" \
314
+ "$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$SB/pack-live" --pi-live-root "$SB/pack-pi" --kimi-live-root "$SB/pack-kimi" 2>&1)
315
+ check "ordinary uninstall reports retained context packs" "$(grep -q 'kept context packs' <<<"$pack_output" && echo yes || echo no)" "yes"
316
+ check "ordinary uninstall retains context packs" "$([[ -f "$PACK_STORE/objects/example/marker" ]] && echo yes || echo no)" "yes"
317
+ HOME="$PACK_HOME" XDG_CONFIG_HOME="$PACK_HOME/.config" \
318
+ CAIRN_AGENTFS_BASE_DIR="$PACK_HOME/.cairnkeep" CAIRN_PACK_BASE_DIR="$PACK_STORE" \
319
+ "$ROOT_DIR/scripts/uninstall.sh" --yes --purge-memory --live-root "$SB/pack-live" --pi-live-root "$SB/pack-pi" --kimi-live-root "$SB/pack-kimi" >/dev/null 2>&1
320
+ check "memory purge retains context packs" "$([[ -f "$PACK_STORE/objects/example/marker" ]] && echo yes || echo no)" "yes"
321
+ check "memory purge removes non-pack memory" "$([[ -e "$PACK_HOME/.cairnkeep/memory" ]] && echo no || echo yes)" "yes"
322
+ HOME="$PACK_HOME" XDG_CONFIG_HOME="$PACK_HOME/.config" \
323
+ CAIRN_AGENTFS_BASE_DIR="$PACK_HOME/.cairnkeep" CAIRN_PACK_BASE_DIR="$PACK_STORE" \
324
+ "$ROOT_DIR/scripts/uninstall.sh" --yes --purge-packs --live-root "$SB/pack-live" --pi-live-root "$SB/pack-pi" --kimi-live-root "$SB/pack-kimi" >/dev/null 2>&1
325
+ check "explicit pack purge removes context packs" "$([[ -e "$PACK_STORE" ]] && echo no || echo yes)" "yes"
326
+
306
327
  echo
307
328
  if [[ "$fails" -gt 0 ]]; then echo "test-uninstall: $fails check(s) failed."; exit 1; fi
308
329
  echo "test-uninstall: OK"
330
+
331
+ phase26_uninstall_complete=true
332
+ [[ -f "$ROOT_DIR/pi/extensions/cairnkeep-memory.ts" ]] || phase26_uninstall_complete=false
333
+ grep -qF 'extensions/cairnkeep-memory.ts' "$ROOT_DIR/scripts/sync-pi-assets.sh" || phase26_uninstall_complete=false
334
+ grep -qF 'extensions/cairnkeep-memory.ts' "$ROOT_DIR/scripts/uninstall.sh" || phase26_uninstall_complete=false
335
+
336
+ if [[ "$phase26_uninstall_complete" != true ]]; then
337
+ if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
338
+ echo "PHASE26_RED:PI_UNINSTALL_MISSING"
339
+ exit 86
340
+ fi
341
+ echo "SKIP: Pi memory-extension uninstall ownership is not complete"
342
+ exit 0
343
+ fi
344
+
345
+ # Exercise the new Pi owner separately so the long-standing uninstall oracle
346
+ # above remains byte-for-byte focused on its established asset set.
347
+ PHASE26_HOME="$SB/phase26-home"
348
+ PHASE26_PI="$SB/phase26-pi"
349
+ PHASE26_LIVE="$SB/phase26-live"
350
+ PHASE26_STORE="$PHASE26_HOME/.cairnkeep"
351
+ PHASE26_PACKS="$PHASE26_STORE/packs"
352
+ mkdir -p "$PHASE26_HOME" "$PHASE26_PI/extensions" "$PHASE26_STORE/memory" "$PHASE26_PACKS/objects/fixture"
353
+ printf 'neighbor bytes\n' >"$PHASE26_PI/extensions/operator-extension.ts"
354
+ printf 'memory bytes\n' >"$PHASE26_STORE/memory/record"
355
+ printf 'pack bytes\n' >"$PHASE26_PACKS/objects/fixture/record"
356
+
357
+ HOME="$PHASE26_HOME" PI_CODING_AGENT_DIR="$PHASE26_PI" \
358
+ "$ROOT_DIR/scripts/sync-pi-assets.sh" --apply --live-root "$PHASE26_PI" >/dev/null 2>&1
359
+ check "Pi memory extension installed" "$([[ -f "$PHASE26_PI/extensions/cairnkeep-memory.ts" ]] && echo yes || echo no)" "yes"
360
+ printf '\n// operator-modified fixture\n' >>"$PHASE26_PI/extensions/cairnkeep-memory.ts"
361
+ cp "$PHASE26_PI/extensions/cairnkeep-memory.ts" "$SB/pi-memory-modified.before.ts"
362
+
363
+ HOME="$PHASE26_HOME" XDG_CONFIG_HOME="$PHASE26_HOME/.config" \
364
+ CAIRN_AGENTFS_BASE_DIR="$PHASE26_STORE" CAIRN_PACK_BASE_DIR="$PHASE26_PACKS" \
365
+ "$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$PHASE26_LIVE" --pi-live-root "$PHASE26_PI" >/dev/null 2>&1
366
+ check "modified Pi memory extension removed after backup" "$([[ -e "$PHASE26_PI/extensions/cairnkeep-memory.ts" ]] && echo no || echo yes)" "yes"
367
+ check "neighboring Pi extension preserved" "$(cat "$PHASE26_PI/extensions/operator-extension.ts")" "neighbor bytes"
368
+ check "default Pi uninstall preserves memory" "$(cat "$PHASE26_STORE/memory/record")" "memory bytes"
369
+ check "default Pi uninstall preserves context packs" "$(cat "$PHASE26_PACKS/objects/fixture/record")" "pack bytes"
370
+
371
+ PHASE26_BACKUP=$(ls -dt "$PHASE26_HOME/.cairnkeep-uninstall-"* 2>/dev/null | head -1)
372
+ check "Pi memory extension backup exists" "$([[ -f "$PHASE26_BACKUP/files/${PHASE26_PI#/}/extensions/cairnkeep-memory.ts" ]] && echo yes || echo no)" "yes"
373
+ check "Pi memory extension manifest entry exists" "$(grep -cF "$PHASE26_PI/extensions/cairnkeep-memory.ts" "$PHASE26_BACKUP/manifest.tsv" 2>/dev/null || true)" "1"
374
+ HOME="$PHASE26_HOME" bash "$PHASE26_BACKUP/revert.sh" >/dev/null 2>&1
375
+ check "modified Pi memory extension restored exactly" "$(cmp -s "$SB/pi-memory-modified.before.ts" "$PHASE26_PI/extensions/cairnkeep-memory.ts" && echo yes || echo no)" "yes"
376
+ check "neighbor remains exact after revert" "$(cat "$PHASE26_PI/extensions/operator-extension.ts")" "neighbor bytes"
377
+
378
+ if [[ "$fails" -gt 0 ]]; then echo "test-uninstall: $fails Phase 26 check(s) failed."; exit 1; fi
379
+ echo "PASS: Pi memory-extension backup-first uninstall and revert contract"
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from "node:assert/strict";
4
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { gzipSync } from "node:zlib";
6
+ import { join, resolve } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { fileURLToPath } from "node:url";
9
+ import {
10
+ bootstrapWindows,
11
+ createTar,
12
+ extractTar,
13
+ launcherModule,
14
+ launcherPowerShell,
15
+ powershellCompletion,
16
+ runWindowsCommand,
17
+ } from "./windows-platform.mjs";
18
+ import { runNativeContainer } from "./cairn-container-cli.mjs";
19
+
20
+ const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
21
+ const sandbox = mkdtempSync(join(tmpdir(), "cairn-windows-native-"));
22
+ const originalBase = process.env.CAIRN_AGENTFS_BASE_DIR;
23
+ const originalHome = process.env.HOME;
24
+ const originalProfile = process.env.USERPROFILE;
25
+ try {
26
+ process.env.HOME = sandbox;
27
+ process.env.USERPROFILE = sandbox;
28
+ const project = join(sandbox, "Project with spaces – Unicode");
29
+ mkdirSync(project, { recursive: true });
30
+ bootstrapWindows(root, [project]);
31
+ for (const harness of ["claude", "opencode", "pi", "kimi", "qwen"]) {
32
+ assert.ok(existsSync(join(project, ".ai", `start-${harness}.cmd`)), `${harness} cmd launcher`);
33
+ }
34
+ assert.match(readFileSync(join(project, ".ai", "start-claude.cmd"), "utf8"), /start-harness\.ps1/);
35
+ assert.match(launcherModule(), /spawnSync\(command/);
36
+ assert.match(launcherPowerShell(), /pre-launch\.ps1/);
37
+ assert.match(launcherPowerShell(), /post-exit\.ps1/);
38
+
39
+ const claudeRoot = join(sandbox, "Claude Config");
40
+ await runWindowsCommand({ command: "sync", args: ["--apply", "--live-root", claudeRoot], root });
41
+ assert.ok(existsSync(join(claudeRoot, "commands", "remember.md")));
42
+ assert.ok(existsSync(join(claudeRoot, "hooks", "memory-wakeup.cmd")));
43
+ const settings = JSON.parse(readFileSync(join(claudeRoot, "settings.json"), "utf8"));
44
+ assert.ok(settings.hooks.SessionStart.some((entry) => entry.hooks.some((hook) => /memory-wakeup/.test(hook.command))));
45
+ await runWindowsCommand({ command: "sync", args: ["--check", "--live-root", claudeRoot], root });
46
+
47
+ const payloads = [["project.db", Buffer.from("sqlite-a")], ["team.db", Buffer.from("sqlite-b")]];
48
+ const tar = createTar(payloads);
49
+ assert.deepEqual(extractTar(tar), payloads);
50
+ assert.throws(() => extractTar(Buffer.from(tar).fill(0x2e, 0, 12)), /checksum|unsafe|unsupported/);
51
+ const archive = join(sandbox, "memory.tgz");
52
+ writeFileSync(archive, gzipSync(tar));
53
+ const memory = join(sandbox, "Memory Store");
54
+ process.env.CAIRN_AGENTFS_BASE_DIR = memory;
55
+ await runWindowsCommand({ command: "memory", args: ["import", archive], root });
56
+ assert.equal(readFileSync(join(memory, "project.db"), "utf8"), "sqlite-a");
57
+ assert.match(powershellCompletion(), /Register-ArgumentCompleter/);
58
+
59
+ let containerArgs = [];
60
+ runNativeContainer(["stdio", "--image", "example/windows:1", "--volume", "windows-data"], root, (_engine, args) => { containerArgs = args; });
61
+ assert.ok(containerArgs.includes("--read-only"));
62
+ assert.ok(containerArgs.includes("windows-data:/data:Z,U"));
63
+ assert.ok(containerArgs.includes("example/windows:1"));
64
+
65
+ await runWindowsCommand({ command: "uninstall", args: ["--yes", "--live-root", claudeRoot, project], root });
66
+ assert.ok(!existsSync(join(project, ".ai", "start-claude.cmd")));
67
+ assert.ok(!existsSync(join(claudeRoot, "commands", "remember.md")));
68
+ const cleanedSettings = JSON.parse(readFileSync(join(claudeRoot, "settings.json"), "utf8"));
69
+ assert.ok(!JSON.stringify(cleanedSettings).includes("memory-wakeup"));
70
+
71
+ const setupSurfaceComplete = [
72
+ join(root, "scripts", "setup.mjs"),
73
+ join(root, "scripts", "setup-core.mjs"),
74
+ join(root, "scripts", "setup-reconcile.mjs"),
75
+ join(root, "schemas", "cairnkeep-setup.schema.json"),
76
+ join(root, "schemas", "cairnkeep-setup-policy.schema.json"),
77
+ ].every((candidate) => existsSync(candidate))
78
+ && /case\s+["']setup["']/.test(readFileSync(join(root, "scripts", "windows-platform.mjs"), "utf8"))
79
+ && /["']setup["']/.test(powershellCompletion());
80
+
81
+ if (!setupSurfaceComplete) {
82
+ if (process.env.CAIRN_PHASE26_RED === "1") {
83
+ console.log("PHASE26_RED:WINDOWS_SETUP_MISSING");
84
+ process.exitCode = 86;
85
+ } else {
86
+ console.log("SKIP: simulated Windows guided setup surface is not complete");
87
+ console.log("PASS: native Windows CLI lifecycle, paths, archive safety, hooks, and PowerShell completion");
88
+ }
89
+ } else {
90
+ const setupProject = join(sandbox, "Guided Project with spaces – Unicode");
91
+ let output = "";
92
+ const originalWrite = process.stdout.write.bind(process.stdout);
93
+ process.stdout.write = ((chunk, ...args) => {
94
+ output += String(chunk);
95
+ return true;
96
+ });
97
+ try {
98
+ const handled = await runWindowsCommand({
99
+ command: "setup",
100
+ args: [setupProject, "--git", "none", "--harness", "claude,pi", "--memory", "local", "--yes", "--json"],
101
+ root,
102
+ });
103
+ assert.equal(handled, true);
104
+ } finally {
105
+ process.stdout.write = originalWrite;
106
+ }
107
+ const setupResult = JSON.parse(output);
108
+ assert.equal(setupResult.schema_version, 1);
109
+ assert.equal(setupResult.status, "limited");
110
+ assert.equal(setupResult.git, "none");
111
+ assert.equal(setupResult.memory, "local");
112
+ assert.deepEqual(setupResult.harnesses, ["claude", "pi"]);
113
+ assert.equal(Array.isArray(setupResult.recovery), true);
114
+ assert.equal(setupResult.recovery.some((line) => /cairn (?:setup|sync)/.test(line)), true);
115
+
116
+ const setupStatePath = join(setupProject, ".ai", "cairnkeep.json");
117
+ assert.ok(existsSync(setupStatePath));
118
+ const setupState = JSON.parse(readFileSync(setupStatePath, "utf8"));
119
+ const serializedState = JSON.stringify(setupState);
120
+ assert.equal(serializedState.includes(setupProject), false);
121
+ assert.doesNotMatch(serializedState, /(?:token|secret|credential|endpoint|username|environment|https?:)/i);
122
+ assert.match(powershellCompletion(), /--git/);
123
+ assert.match(powershellCompletion(), /--harness/);
124
+ assert.match(powershellCompletion(), /--memory/);
125
+ assert.match(powershellCompletion(), /--policy/);
126
+ assert.match(powershellCompletion(), /--yes/);
127
+ assert.match(powershellCompletion(), /--json/);
128
+ console.log("PASS: simulated Windows setup parity, private state, recovery, and completion contract");
129
+ }
130
+ } finally {
131
+ if (originalBase === undefined) delete process.env.CAIRN_AGENTFS_BASE_DIR;
132
+ else process.env.CAIRN_AGENTFS_BASE_DIR = originalBase;
133
+ if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome;
134
+ if (originalProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = originalProfile;
135
+ rmSync(sandbox, { recursive: true, force: true });
136
+ }
@@ -25,9 +25,12 @@ PI_LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
25
25
  KIMI_LIVE_ROOT="${KIMI_CODE_HOME:-$HOME/.kimi-code}"
26
26
  STORE_DIR="${CAIRN_AGENTFS_BASE_DIR:-$HOME/.cairnkeep}"
27
27
  STORE_DIR="${STORE_DIR/#\~/$HOME}"
28
+ PACK_DIR="${CAIRN_PACK_BASE_DIR:-$HOME/.cairnkeep/packs}"
29
+ PACK_DIR="${PACK_DIR/#\~/$HOME}"
28
30
  DRY_RUN=0
29
31
  ASSUME_YES=0
30
32
  PURGE_MEMORY=0
33
+ PURGE_PACKS=0
31
34
  PROJECTS=()
32
35
 
33
36
  # The hooks cairnkeep registers (basename is the settings.json match token).
@@ -49,7 +52,7 @@ PROJECT_AI_FILES=(
49
52
 
50
53
  usage() {
51
54
  cat <<'EOF'
52
- Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--live-root PATH]
55
+ Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--purge-packs] [--live-root PATH]
53
56
  [--pi-live-root PATH] [--kimi-live-root PATH] [PROJECT ...]
54
57
 
55
58
  Reverse cairnkeep's install. Everything removed/edited is backed up into
@@ -59,6 +62,8 @@ $HOME/.cairnkeep-uninstall-<ts>/ with a revert.sh before any change.
59
62
  --yes Do not prompt for confirmation.
60
63
  --purge-memory Also remove the global memory store and .agentfs/ memory and
61
64
  trajectories in each PROJECT. Backed up first; off by default.
65
+ --purge-packs Also remove immutable context-pack objects and project
66
+ pointers. Backed up first; off by default.
62
67
  --live-root PATH Claude root to clean (default: $CLAUDE_CONFIG_DIR or ~/.claude).
63
68
  --pi-live-root PATH Pi agent root to clean (default: $PI_CODING_AGENT_DIR or ~/.pi/agent).
64
69
  --kimi-live-root PATH Kimi Code root to clean (default: $KIMI_CODE_HOME or ~/.kimi-code).
@@ -73,6 +78,7 @@ while [[ $# -gt 0 ]]; do
73
78
  --dry-run) DRY_RUN=1; shift ;;
74
79
  --yes) ASSUME_YES=1; shift ;;
75
80
  --purge-memory) PURGE_MEMORY=1; shift ;;
81
+ --purge-packs) PURGE_PACKS=1; shift ;;
76
82
  --live-root) LIVE_ROOT="$2"; shift 2 ;;
77
83
  --pi-live-root) PI_LIVE_ROOT="$2"; shift 2 ;;
78
84
  --kimi-live-root) KIMI_LIVE_ROOT="$2"; shift 2 ;;
@@ -119,6 +125,48 @@ remove_path() {
119
125
  echo " removed: $abs"
120
126
  }
121
127
 
128
+ # Print only package-known relative paths from a valid guided-setup ownership
129
+ # record. The record identifies candidates, but never grants authority to an
130
+ # arbitrary path. Modified candidates are still backed up before removal.
131
+ setup_owned_paths() {
132
+ node --input-type=module - "$1" <<'NODE'
133
+ import { lstatSync, readFileSync } from "node:fs";
134
+ const path = process.argv[2];
135
+ const info = lstatSync(path);
136
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) process.exit(1);
137
+ if (process.platform !== "win32" && (info.mode & 0o777) !== 0o600) process.exit(1);
138
+ const state = JSON.parse(readFileSync(path, "utf8"));
139
+ const exact = (value, keys) => value && typeof value === "object" && !Array.isArray(value)
140
+ && Object.keys(value).sort().join(",") === [...keys].sort().join(",");
141
+ if (!exact(state, ["schema_version", "cairnkeep_version", "git", "memory", "harnesses", "assets"])
142
+ || state.schema_version !== 1
143
+ || !["init", "existing", "none"].includes(state.git)
144
+ || !["local", "none"].includes(state.memory)
145
+ || !Array.isArray(state.harnesses)
146
+ || !state.assets
147
+ || typeof state.assets !== "object"
148
+ || Array.isArray(state.assets)) process.exit(1);
149
+ const allowed = new Set([
150
+ ".ai/env.example", ".ai/trajectory-redaction.json", ".ai/capabilities.json",
151
+ ".agentfs/.gitignore", ".planning/config.json", ".planning/PROJECT-BRIEF.md",
152
+ ".planning/wiki/index.md", ".planning/wiki/policy.md", ".planning/wiki/CONTRADICTIONS.md",
153
+ ".planning/wiki/LOG.md", ".planning/alignment/policy.md", ".planning/alignment/gap-register.yaml",
154
+ ".planning/graphs/policy.md", ".planning/graphs/.gitignore", ".planning/security/policy.md",
155
+ ".ai/start-harness.mjs", ".ai/start-harness.ps1",
156
+ ...["claude", "opencode", "pi", "kimi", "qwen"].flatMap((name) => [`.ai/start-${name}.sh`, `.ai/start-${name}.cmd`]),
157
+ ]);
158
+ for (const [asset, record] of Object.entries(state.assets)) {
159
+ if (!allowed.has(asset)
160
+ || !exact(record, ["digest", "mode", "template"])
161
+ || !/^[a-f0-9]{64}$/.test(record.digest)
162
+ || !Number.isInteger(record.mode)
163
+ || record.mode < 0 || record.mode > 0o777
164
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(record.template)) process.exit(1);
165
+ }
166
+ process.stdout.write(`${Object.keys(state.assets).sort().join("\n")}\n`);
167
+ NODE
168
+ }
169
+
122
170
  note() { [[ $DRY_RUN -eq 1 ]] || { ensure_bundle; printf '%s\n' "$1" >>"$NOTES"; }; }
123
171
 
124
172
  # ---- what will be removed ----------------------------------------------------
@@ -142,6 +190,7 @@ echo " live root: $LIVE_ROOT"
142
190
  echo " Pi live root: $PI_LIVE_ROOT"
143
191
  echo " Kimi root: $KIMI_LIVE_ROOT"
144
192
  echo " memory store: $STORE_DIR ($([[ $PURGE_MEMORY -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
193
+ echo " context packs: $PACK_DIR ($([[ $PURGE_PACKS -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
145
194
  [[ ${#PROJECTS[@]} -gt 0 ]] && echo " projects: ${PROJECTS[*]}"
146
195
 
147
196
  if [[ $DRY_RUN -eq 0 && $ASSUME_YES -eq 0 ]]; then
@@ -157,6 +206,7 @@ while IFS= read -r dst; do remove_path "$dst"; done < <(asset_dests)
157
206
  # These are precisely owned adapter paths; do not touch neighboring harness
158
207
  # configuration in either user root.
159
208
  echo "Pi adapters:"
209
+ remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-memory.ts"
160
210
  remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-trajectory.ts"
161
211
  remove_path "$PI_LIVE_ROOT/prompts/graphify.md"
162
212
  echo "Kimi adapter:"
@@ -231,6 +281,24 @@ fi
231
281
  if [[ $PURGE_MEMORY -eq 1 ]]; then
232
282
  echo "Memory store:"
233
283
  remove_path "$STORE_DIR"
284
+ if [[ $PURGE_PACKS -eq 0 && "$PACK_DIR" == "$STORE_DIR"/* && $DRY_RUN -eq 0 ]]; then
285
+ pack_backup="$BACKUP_DIR/files/${PACK_DIR#/}"
286
+ if [[ -d "$pack_backup" ]]; then
287
+ mkdir -p "$(dirname "$PACK_DIR")"
288
+ cp -a "$pack_backup" "$PACK_DIR"
289
+ echo " retained context packs: $PACK_DIR"
290
+ fi
291
+ fi
292
+ fi
293
+
294
+ if [[ $PURGE_MEMORY -eq 0 && $PURGE_PACKS -eq 0 && -d "$PACK_DIR" ]]; then
295
+ echo "Context packs:"
296
+ echo " kept context packs: $PACK_DIR (use --purge-packs to remove)"
297
+ fi
298
+
299
+ if [[ $PURGE_PACKS -eq 1 && ! ( $PURGE_MEMORY -eq 1 && "$PACK_DIR" == "$STORE_DIR"/* ) ]]; then
300
+ echo "Context packs:"
301
+ remove_path "$PACK_DIR"
234
302
  fi
235
303
 
236
304
  # 6. Per-project bootstrap scaffold.
@@ -243,13 +311,33 @@ for proj in "${PROJECTS[@]:-}"; do
243
311
  # hook registrations and OpenCode plugin bytes restore together exactly.
244
312
  # Normal and master-off installations never create it, so absence is inert.
245
313
  remove_path "$proj/.ai/capability-contract"
246
- for rel in "${PROJECT_AI_FILES[@]}"; do
247
- remove_path "$proj/.ai/$rel"
248
- done
249
- if [[ $DRY_RUN -eq 0 && -d "$proj/.ai" ]]; then
250
- rmdir "$proj/.ai" 2>/dev/null || true
314
+ setup_state="$proj/.ai/cairnkeep.json"
315
+ if [[ -e "$setup_state" ]]; then
316
+ if owned_paths=$(setup_owned_paths "$setup_state" 2>/dev/null); then
317
+ while IFS= read -r rel; do
318
+ [[ -n "$rel" ]] && remove_path "$proj/$rel"
319
+ done <<< "$owned_paths"
320
+ remove_path "$setup_state"
321
+ if [[ $DRY_RUN -eq 0 ]]; then
322
+ for managed_dir in \
323
+ "$proj/.planning/wiki" "$proj/.planning/alignment" "$proj/.planning/graphs" \
324
+ "$proj/.planning/security" "$proj/.planning" "$proj/.ai" "$proj/.agentfs"; do
325
+ rmdir "$managed_dir" 2>/dev/null || true
326
+ done
327
+ fi
328
+ else
329
+ echo " kept guided setup scaffold: ownership state is invalid or unsafe"
330
+ fi
331
+ else
332
+ # Compatibility path for projects created by the legacy bootstrap command.
333
+ for rel in "${PROJECT_AI_FILES[@]}"; do
334
+ remove_path "$proj/.ai/$rel"
335
+ done
336
+ if [[ $DRY_RUN -eq 0 && -d "$proj/.ai" ]]; then
337
+ rmdir "$proj/.ai" 2>/dev/null || true
338
+ fi
339
+ remove_path "$proj/.planning"
251
340
  fi
252
- remove_path "$proj/.planning"
253
341
  if [[ $PURGE_MEMORY -eq 1 ]]; then
254
342
  remove_path "$proj/.agentfs"
255
343
  elif [[ -d "$proj/.agentfs" ]]; then