@cairnkeep/cli 2.3.1 → 2.5.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 (151) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +232 -6
  3. package/bin/cairn +43 -2
  4. package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
  5. package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
  6. package/claude/hooks/compaction-capture.sh +34 -0
  7. package/claude/hooks/memory-capture.sh +47 -5
  8. package/claude/hooks/memory-wakeup.sh +55 -2
  9. package/docs/building-an-overlay.md +4 -3
  10. package/docs/design/meta-agent.md +439 -0
  11. package/docs/harness-compatibility.md +117 -0
  12. package/docs/learning/CURRICULUM-MAP.md +91 -0
  13. package/docs/learning/FEATURE-GUIDE.md +86 -0
  14. package/docs/learning/PRODUCTION-PLAN.md +45 -14
  15. package/docs/learning/README.md +20 -1
  16. package/docs/learning/instructor-guide.md +6 -0
  17. package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
  18. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  19. package/docs/learning/lessons/L02-installation.md +23 -5
  20. package/docs/learning/lessons/L03-first-project.md +7 -2
  21. package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
  22. package/docs/learning/lessons/L05-daily-workflow.md +4 -0
  23. package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
  24. package/docs/learning/lessons/L10-context-exploration.md +4 -2
  25. package/docs/learning/lessons/L12-managed-overlays.md +2 -0
  26. package/docs/learning/lessons/L13-session-evidence.md +71 -0
  27. package/docs/learning/lessons/L14-typed-memory.md +51 -0
  28. package/docs/learning/lessons/L15-capability-governance.md +56 -0
  29. package/docs/learning/lessons/L16-evaluation.md +67 -0
  30. package/docs/learning/lessons/L17-agent-boundary.md +55 -0
  31. package/docs/learning/templates/lesson-template.md +4 -1
  32. package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
  33. package/docs/learning/tracks/operator.md +8 -2
  34. package/docs/learning/tracks/practitioner.md +6 -1
  35. package/docs/learning/tracks/quickstart.md +3 -0
  36. package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
  37. package/docs/learning/video-scripts/L02-installation.md +9 -2
  38. package/docs/learning/video-scripts/L03-first-project.md +3 -0
  39. package/docs/operating.md +567 -7
  40. package/docs/privacy-and-data-flow.md +417 -0
  41. package/docs/storage.md +252 -0
  42. package/examples/eval/README.md +42 -0
  43. package/examples/eval/adapter.json +12 -0
  44. package/examples/eval/bundled-fake.json +1 -0
  45. package/examples/eval/task-set.json +1 -0
  46. package/mcp-memory-server/dist/artifact-cli.js +184 -0
  47. package/mcp-memory-server/dist/artifact-schema.js +218 -0
  48. package/mcp-memory-server/dist/artifact-store.js +812 -0
  49. package/mcp-memory-server/dist/capability-adapter.js +286 -0
  50. package/mcp-memory-server/dist/capability-cli.js +361 -0
  51. package/mcp-memory-server/dist/capability-config.js +310 -0
  52. package/mcp-memory-server/dist/capability-harness.js +496 -0
  53. package/mcp-memory-server/dist/capability-registry.js +123 -0
  54. package/mcp-memory-server/dist/capability-schema.js +65 -0
  55. package/mcp-memory-server/dist/capability-store.js +431 -0
  56. package/mcp-memory-server/dist/compaction-normalize.js +267 -0
  57. package/mcp-memory-server/dist/eval-cli.js +523 -0
  58. package/mcp-memory-server/dist/eval-plan.js +346 -0
  59. package/mcp-memory-server/dist/eval-process.js +273 -0
  60. package/mcp-memory-server/dist/eval-report.js +609 -0
  61. package/mcp-memory-server/dist/eval-runner.js +827 -0
  62. package/mcp-memory-server/dist/eval-schema.js +368 -0
  63. package/mcp-memory-server/dist/eval-statistics.js +161 -0
  64. package/mcp-memory-server/dist/eval-workspace.js +313 -0
  65. package/mcp-memory-server/dist/failure-signature.js +141 -0
  66. package/mcp-memory-server/dist/index.js +600 -55
  67. package/mcp-memory-server/dist/node-cli.js +84 -0
  68. package/mcp-memory-server/dist/node-schema.js +170 -0
  69. package/mcp-memory-server/dist/node-store.js +455 -0
  70. package/mcp-memory-server/dist/note-cli.js +235 -0
  71. package/mcp-memory-server/dist/note-distiller.js +177 -0
  72. package/mcp-memory-server/dist/note-enrichment.js +93 -0
  73. package/mcp-memory-server/dist/note-schema.js +4 -0
  74. package/mcp-memory-server/dist/note-store.js +985 -0
  75. package/mcp-memory-server/dist/trajectory-cli.js +141 -0
  76. package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
  77. package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
  78. package/mcp-memory-server/dist/trajectory-schema.js +73 -0
  79. package/mcp-memory-server/dist/trajectory-store.js +339 -0
  80. package/mcp-memory-server/package.json +24 -2
  81. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
  82. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
  83. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
  84. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
  85. package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
  86. package/opencode/capability-contract/plugins/capability-command.ts +319 -0
  87. package/opencode/command/graphify.md +25 -14
  88. package/opencode/plugins/memory-capture.ts +76 -10
  89. package/opencode/plugins/memory-wakeup.ts +58 -6
  90. package/package.json +8 -4
  91. package/pi/extensions/cairnkeep-trajectory.ts +55 -0
  92. package/schemas/artifact.schema.json +133 -0
  93. package/schemas/capability-callback.schema.json +68 -0
  94. package/schemas/capability-contract.schema.json +126 -0
  95. package/schemas/eval-adapter.schema.json +43 -0
  96. package/schemas/eval-protocol.schema.json +114 -0
  97. package/schemas/eval-report.schema.json +423 -0
  98. package/schemas/eval-task-set.schema.json +114 -0
  99. package/schemas/memory-node.schema.json +90 -0
  100. package/schemas/note.schema.json +112 -0
  101. package/schemas/trajectory-redaction.schema.json +25 -0
  102. package/schemas/trajectory.schema.json +82 -0
  103. package/scripts/bootstrap.sh +6 -0
  104. package/scripts/completion.sh +119 -5
  105. package/scripts/doctor.sh +284 -0
  106. package/scripts/fake-eval-adapter.mjs +120 -0
  107. package/scripts/fixtures/capability-harness-contracts.json +408 -0
  108. package/scripts/install-audit-timer.sh +3 -1
  109. package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
  110. package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
  111. package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
  112. package/scripts/memory-wiki-audit.sh +49 -0
  113. package/scripts/smoke-bash32.sh +9 -0
  114. package/scripts/sync-claude-assets.sh +99 -6
  115. package/scripts/sync-opencode-graphify-assets.sh +24 -3
  116. package/scripts/sync-opencode-plugin-assets.sh +31 -5
  117. package/scripts/sync-opencode-security-assets.sh +24 -3
  118. package/scripts/sync-opencode-wiki-assets.sh +24 -3
  119. package/scripts/sync-pi-assets.sh +72 -0
  120. package/scripts/test-audit-timer.sh +98 -0
  121. package/scripts/test-bootstrap-untracked.sh +49 -0
  122. package/scripts/test-cli-dispatch.sh +41 -2
  123. package/scripts/test-compaction-hooks.sh +495 -0
  124. package/scripts/test-completion.sh +77 -0
  125. package/scripts/test-doctor.sh +348 -1
  126. package/scripts/test-eval-cli.sh +175 -0
  127. package/scripts/test-launcher-hooks.sh +630 -3
  128. package/scripts/test-learning-docs.sh +20 -4
  129. package/scripts/test-notes-cli.sh +114 -0
  130. package/scripts/test-package-install.sh +254 -0
  131. package/scripts/test-phase16-node-lifecycle.sh +50 -0
  132. package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
  133. package/scripts/test-phase18-capability-lifecycle.sh +504 -0
  134. package/scripts/test-phase18-harness-boundary.sh +1060 -0
  135. package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
  136. package/scripts/test-trajectory-cli.sh +87 -0
  137. package/scripts/test-trajectory-hooks.sh +142 -0
  138. package/scripts/test-uninstall.sh +231 -5
  139. package/scripts/uninstall.sh +38 -5
  140. package/scripts/verify-dependency-security.mjs +121 -0
  141. package/scripts/verify-docs-parity.sh +527 -1
  142. package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
  143. package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
  144. package/templates/capabilities.json.template +7 -0
  145. package/templates/env.example.template +51 -0
  146. package/templates/memory-wiki-audit.service.template +1 -1
  147. package/templates/start-claude.sh.template +36 -0
  148. package/templates/start-kimi.sh.template +39 -0
  149. package/templates/start-opencode.sh.template +47 -5
  150. package/templates/start-pi.sh.template +30 -0
  151. package/templates/trajectory-redaction.json.template +4 -0
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ # Public CLI contract for `cairn trajectory list|show|prune`.
3
+ set -euo pipefail
4
+
5
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
6
+ tmp=$(mktemp -d)
7
+ trap 'rm -rf "$tmp"' EXIT
8
+ fail() { echo "FAIL: $1" >&2; exit 1; }
9
+
10
+ cairn="$ROOT/bin/cairn"
11
+ fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-opencode.json"
12
+ cli="$ROOT/mcp-memory-server/dist/trajectory-cli.js"
13
+ repo="$tmp/project"
14
+ mkdir -p "$repo/.agentfs"
15
+
16
+ "$cairn" help | grep -q "cairn trajectory" || fail "help missing trajectory"
17
+
18
+ for shell in bash zsh fish; do
19
+ "$cairn" completion "$shell" > "$tmp/completion-$shell"
20
+ grep -q "trajectory" "$tmp/completion-$shell" || fail "$shell completion missing trajectory"
21
+ grep -q "list.*show.*prune\|list show prune" "$tmp/completion-$shell" \
22
+ || fail "$shell completion missing trajectory subcommands"
23
+ done
24
+
25
+ bootstrap_repo="$tmp/bootstrap"
26
+ mkdir -p "$bootstrap_repo"
27
+ "$cairn" bootstrap "$bootstrap_repo" >/dev/null
28
+ [[ -f "$bootstrap_repo/.ai/trajectory-redaction.json" ]] \
29
+ || fail "bootstrap missing optional trajectory redaction template"
30
+ if grep -R -E -q '^[[:space:]]*CAIRN_TRAJECTORY_CAPTURE[[:space:]]*=[[:space:]]*1' "$bootstrap_repo"; then
31
+ fail "bootstrap enabled trajectory capture"
32
+ fi
33
+ printf '{"version":1,"patterns":[]}\n' > "$bootstrap_repo/.ai/trajectory-redaction.json"
34
+ "$cairn" bootstrap "$bootstrap_repo" >/dev/null
35
+ grep -q '"patterns":\[\]' "$bootstrap_repo/.ai/trajectory-redaction.json" \
36
+ || fail "bootstrap overwrote an existing redaction configuration"
37
+
38
+ printf '%s' "$(cat "$fixture")" | node "$cli" capture-opencode "$repo" >/dev/null
39
+
40
+ (cd "$repo" && "$cairn" trajectory list --json) > "$tmp/list.json"
41
+ node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(j.sessions?.[0]?.session_id!=="opencode-session-001")process.exit(1)' "$tmp/list.json" \
42
+ || fail "trajectory list JSON missing captured session"
43
+
44
+ (cd "$repo" && "$cairn" trajectory show opencode-session-001 --json) > "$tmp/show.json"
45
+ node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(j.schema_version!==1||!Array.isArray(j.events))process.exit(1)' "$tmp/show.json" \
46
+ || fail "trajectory show JSON is not a v1 session"
47
+
48
+ if ! (set -o pipefail; cd "$repo" && "$cairn" trajectory show opencode-session-001 --json | head -c 0 >/dev/null); then
49
+ fail "trajectory CLI did not treat a closed output pipe as a normal consumer exit"
50
+ fi
51
+
52
+ (cd "$repo" && "$cairn" trajectory prune --dry-run --json) > "$tmp/prune.json"
53
+ node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(!Array.isArray(j.removed)||j.dry_run!==true)process.exit(1)' "$tmp/prune.json" \
54
+ || fail "trajectory prune dry-run JSON invalid"
55
+
56
+ if (cd "$repo" && "$cairn" trajectory show does-not-exist --json) >"$tmp/missing.out" 2>"$tmp/missing.err"; then
57
+ fail "show of missing session should fail"
58
+ fi
59
+ grep -q "not found" "$tmp/missing.err" || fail "missing-session error is not actionable"
60
+
61
+ # Doctor detects missing trajectory metadata and repairs indexes explicitly,
62
+ # preserving the valid full session record.
63
+ (cd "$ROOT/mcp-memory-server" && node --input-type=module -e '
64
+ import { AgentFS } from "agentfs-sdk"
65
+ const agent = await AgentFS.open({ id: "trajectory", path: process.argv[1] })
66
+ for (const row of await agent.kv.list("trajectory/index/")) await agent.kv.delete(row.key)
67
+ await agent.kv.delete("trajectory/meta/schema-version")
68
+ await agent.close()
69
+ ' "$repo/.agentfs/trajectory.db")
70
+
71
+ mkdir -p "$tmp/home"
72
+ if (cd "$repo" && env -u CAIRN_LLM_API_URL -u CAIRN_MEMORY_EMBEDDING_URL \
73
+ -u CAIRN_GIT_PROVIDER HOME="$tmp/home" "$cairn" doctor) >"$tmp/doctor-bad.out" 2>&1; then
74
+ fail "doctor accepted missing trajectory metadata"
75
+ fi
76
+ grep -q 'cairn doctor --repair' "$tmp/doctor-bad.out" \
77
+ || fail "doctor did not offer an actionable trajectory repair"
78
+
79
+ (cd "$repo" && env -u CAIRN_LLM_API_URL -u CAIRN_MEMORY_EMBEDDING_URL \
80
+ -u CAIRN_GIT_PROVIDER HOME="$tmp/home" "$cairn" doctor --repair) >"$tmp/doctor-repair.out" 2>&1 \
81
+ || fail "doctor failed to repair trajectory metadata/indexes"
82
+ grep -q '\[PASS\] trajectory store repaired' "$tmp/doctor-repair.out" \
83
+ || fail "doctor did not report trajectory repair"
84
+ (cd "$repo" && "$cairn" trajectory show opencode-session-001 --json) >/dev/null \
85
+ || fail "trajectory record was lost during doctor repair"
86
+
87
+ echo "PASS: cairn trajectory CLI dispatch and JSON contract"
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env bash
2
+ # Cross-harness trajectory capture and disabled-path regression test.
3
+ set -euo pipefail
4
+
5
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
6
+ tmp=$(mktemp -d)
7
+ trap 'rm -rf "$tmp"' EXIT
8
+ fail() { echo "FAIL: $1" >&2; exit 1; }
9
+
10
+ claude_fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-claude.jsonl"
11
+ opencode_fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-opencode.json"
12
+ pi_fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-pi.json"
13
+
14
+ render() {
15
+ sed "s|@@INFRA_ROOT@@|$ROOT|g" "$1" > "$2"
16
+ }
17
+
18
+ # Claude: flag off must not read stdin, create a store, or perturb the existing
19
+ # no-model/no-project-db path.
20
+ claude_repo="$tmp/claude-off"
21
+ mkdir -p "$claude_repo/.agentfs"
22
+ render "$ROOT/claude/hooks/memory-capture.sh" "$tmp/memory-capture.sh"
23
+ chmod +x "$tmp/memory-capture.sh"
24
+ before=$(find "$claude_repo" -type f -print | sort)
25
+ (cd "$claude_repo" && printf '%s\n' '{"transcript_path":"/does/not/exist"}' | "$tmp/memory-capture.sh") >"$tmp/claude-off.out" 2>"$tmp/claude-off.err"
26
+ after=$(find "$claude_repo" -type f -print | sort)
27
+ [[ "$before" == "$after" ]] || fail "Claude flag-off path changed files"
28
+ [[ ! -s "$tmp/claude-off.out" && ! -s "$tmp/claude-off.err" ]] || fail "Claude flag-off path emitted output"
29
+
30
+ # In a bootstrapped project, the original no-model path also exits before
31
+ # parsing stdin. A python spy makes that no-process guarantee observable.
32
+ claude_bootstrapped_off="$tmp/claude-bootstrapped-off"
33
+ mkdir -p "$claude_bootstrapped_off/.agentfs" "$tmp/spy-bin"
34
+ touch "$claude_bootstrapped_off/.agentfs/project.db"
35
+ cat > "$tmp/spy-bin/python3" <<EOF
36
+ #!/usr/bin/env bash
37
+ touch "$tmp/python-was-called"
38
+ exit 1
39
+ EOF
40
+ chmod +x "$tmp/spy-bin/python3"
41
+ (cd "$claude_bootstrapped_off" && printf '%s\n' '{"transcript_path":"/does/not/exist"}' \
42
+ | PATH="$tmp/spy-bin:$PATH" "$tmp/memory-capture.sh") >/dev/null 2>&1
43
+ [[ ! -e "$tmp/python-was-called" ]] || fail "Claude flag-off no-model path parsed stdin"
44
+
45
+ # Claude: enabled capture works without project.db, API key, extraction model,
46
+ # or network service.
47
+ claude_repo="$tmp/claude-on"
48
+ mkdir -p "$claude_repo/.agentfs"
49
+ hook_input=$(node -e 'console.log(JSON.stringify({transcript_path:process.argv[1],hook_event_name:"SessionEnd"}))' "$claude_fixture")
50
+ (cd "$claude_repo" && CAIRN_TRAJECTORY_CAPTURE=1 printf '%s\n' "$hook_input" | CAIRN_TRAJECTORY_CAPTURE=1 "$tmp/memory-capture.sh") >"$tmp/claude-on.out" 2>"$tmp/claude-on.err"
51
+ [[ -f "$claude_repo/.agentfs/trajectory.db" ]] || fail "Claude enabled hook did not create trajectory store"
52
+ (cd "$claude_repo" && node "$ROOT/mcp-memory-server/dist/trajectory-cli.js" show claude-session-001 --json) >/dev/null \
53
+ || fail "Claude hook trajectory not readable"
54
+
55
+ # OpenCode: render the installed plugin shape and drive a real session.idle
56
+ # callback with the SDK fixture. Type-only imports are stripped by Node 22+.
57
+ render "$ROOT/opencode/plugins/memory-capture.ts" "$tmp/memory-capture.ts"
58
+ opencode_off="$tmp/opencode-off"
59
+ mkdir -p "$opencode_off/.agentfs"
60
+ node --experimental-strip-types "$ROOT/scripts/lib/trajectory-opencode-plugin-harness.mjs" \
61
+ "$tmp/memory-capture.ts" "$opencode_off" "$opencode_fixture" >/dev/null 2>"$tmp/opencode-off.err"
62
+ [[ ! -f "$opencode_off/.agentfs/trajectory.db" ]] || fail "OpenCode flag-off path created trajectory store"
63
+
64
+ opencode_on="$tmp/opencode-on"
65
+ mkdir -p "$opencode_on/.agentfs"
66
+ CAIRN_TRAJECTORY_CAPTURE=1 node --experimental-strip-types \
67
+ "$ROOT/scripts/lib/trajectory-opencode-plugin-harness.mjs" \
68
+ "$tmp/memory-capture.ts" "$opencode_on" "$opencode_fixture" >/dev/null 2>"$tmp/opencode-on.err"
69
+ [[ -f "$opencode_on/.agentfs/trajectory.db" ]] || fail "OpenCode enabled plugin did not create trajectory store"
70
+ (cd "$opencode_on" && node "$ROOT/mcp-memory-server/dist/trajectory-cli.js" show opencode-session-001 --json) >/dev/null \
71
+ || fail "OpenCode plugin trajectory not readable"
72
+
73
+ # Exercise the real backup-first sync paths twice and compare their rendered
74
+ # installed assets, rather than relying only on the source-level render above.
75
+ claude_live="$tmp/claude-live"
76
+ "$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$claude_live" >/dev/null
77
+ "$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$claude_live" >/dev/null
78
+ render "$ROOT/claude/hooks/memory-capture.sh" "$tmp/expected-claude-capture.sh"
79
+ cmp -s "$tmp/expected-claude-capture.sh" "$claude_live/hooks/memory-capture.sh" \
80
+ || fail "installed Claude trajectory hook differs from rendered source"
81
+ claude_capture_registrations=$(node -e '
82
+ const fs = require("fs")
83
+ const settings = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
84
+ let count = 0
85
+ for (const entry of settings.hooks?.SessionEnd ?? []) {
86
+ for (const hook of entry.hooks ?? []) {
87
+ if (String(hook.command ?? "").includes("memory-capture.sh")) count += 1
88
+ }
89
+ }
90
+ process.stdout.write(String(count))
91
+ ' "$claude_live/settings.json")
92
+ [[ "$claude_capture_registrations" -eq 1 ]] \
93
+ || fail "Claude SessionEnd memory-capture registration is missing or duplicated"
94
+
95
+ opencode_live="$tmp/opencode-live"
96
+ "$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$opencode_live" >/dev/null
97
+ "$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$opencode_live" >/dev/null
98
+ render "$ROOT/opencode/plugins/memory-capture.ts" "$tmp/expected-opencode-capture.ts"
99
+ cmp -s "$tmp/expected-opencode-capture.ts" "$opencode_live/plugins/memory-capture.ts" \
100
+ || fail "installed OpenCode trajectory plugin differs from rendered source"
101
+ [[ $(find "$opencode_live" -type f -name 'memory-capture.ts' | wc -l) -eq 1 ]] \
102
+ || fail "OpenCode memory-capture plugin is duplicated"
103
+
104
+ # Pi: the native extension must not touch SessionManager when disabled and
105
+ # must send the active branch to the same local capture CLI when enabled.
106
+ render "$ROOT/pi/extensions/cairnkeep-trajectory.ts" "$tmp/cairnkeep-trajectory.ts"
107
+ pi_off="$tmp/pi-off"
108
+ mkdir -p "$pi_off/.agentfs"
109
+ node --experimental-strip-types "$ROOT/scripts/lib/trajectory-pi-extension-harness.mjs" \
110
+ "$tmp/cairnkeep-trajectory.ts" "$pi_off" "$pi_fixture" >"$tmp/pi-off.out" 2>"$tmp/pi-off.err"
111
+ [[ ! -f "$pi_off/.agentfs/trajectory.db" ]] || fail "Pi flag-off path created trajectory store"
112
+ [[ ! -s "$tmp/pi-off.out" && ! -s "$tmp/pi-off.err" ]] || fail "Pi flag-off path emitted output"
113
+
114
+ pi_on="$tmp/pi-on"
115
+ mkdir -p "$pi_on/.agentfs"
116
+ CAIRN_TRAJECTORY_CAPTURE=1 node --experimental-strip-types \
117
+ "$ROOT/scripts/lib/trajectory-pi-extension-harness.mjs" \
118
+ "$tmp/cairnkeep-trajectory.ts" "$pi_on" "$pi_fixture" >"$tmp/pi-on.out" 2>"$tmp/pi-on.err"
119
+ [[ -f "$pi_on/.agentfs/trajectory.db" ]] || fail "Pi enabled extension did not create trajectory store"
120
+ pi_json=$(cd "$pi_on" && node "$ROOT/mcp-memory-server/dist/trajectory-cli.js" show pi-session-001 --json)
121
+ [[ "$pi_json" != *"sk-live-pi-141414"* ]] || fail "Pi sentinel survived redaction"
122
+ [[ "$pi_json" != *"private pi reasoning"* ]] || fail "Pi reasoning survived omission"
123
+
124
+ pi_live="$tmp/pi-live"
125
+ "$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$pi_live" >/dev/null
126
+ "$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$pi_live" >/dev/null
127
+ render "$ROOT/pi/extensions/cairnkeep-trajectory.ts" "$tmp/expected-pi-capture.ts"
128
+ cmp -s "$tmp/expected-pi-capture.ts" "$pi_live/extensions/cairnkeep-trajectory.ts" \
129
+ || fail "installed Pi trajectory extension differs from rendered source"
130
+ [[ $(find "$pi_live" -type f -name 'cairnkeep-trajectory.ts' | wc -l) -eq 1 ]] \
131
+ || fail "Pi trajectory extension is duplicated"
132
+
133
+ # Integration must reuse the existing capture registration rather than add a
134
+ # second SessionEnd hook or plugin asset.
135
+ [[ $(grep -c 'memory-capture.sh)' "$ROOT/scripts/sync-claude-assets.sh") -eq 1 ]] \
136
+ || fail "Claude memory-capture hook registration is missing or duplicated"
137
+ [[ $(grep -c 'plugins/memory-capture.ts' "$ROOT/scripts/sync-opencode-plugin-assets.sh") -eq 1 ]] \
138
+ || fail "OpenCode memory-capture plugin asset is missing or duplicated"
139
+ [[ $(grep -c 'extensions/cairnkeep-trajectory.ts' "$ROOT/scripts/sync-pi-assets.sh") -eq 1 ]] \
140
+ || fail "Pi trajectory extension asset is missing or duplicated"
141
+
142
+ echo "PASS: Claude/OpenCode/Pi trajectory hooks and flag-off regression"
@@ -16,6 +16,63 @@ fails=0
16
16
  ok() { printf ' [PASS] %s\n' "$1"; }
17
17
  bad() { printf ' [FAIL] %s\n' "$1"; fails=$((fails + 1)); }
18
18
  check() { if [[ "$2" == "$3" ]]; then ok "$1 ($2)"; else bad "$1 (got '$2', want '$3')"; fi; }
19
+ tree_hash() { find "$1" -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1; }
20
+ tree_identity() {
21
+ node - "$1" <<'NODE'
22
+ const { createHash } = require("crypto")
23
+ const { lstatSync, readdirSync, readFileSync } = require("fs")
24
+ const { join, relative } = require("path")
25
+
26
+ const root = process.argv[2]
27
+ const entries = []
28
+ const visit = (path) => {
29
+ const stat = lstatSync(path)
30
+ const label = relative(root, path) || "."
31
+ const mode = (stat.mode & 0o777).toString(8).padStart(3, "0")
32
+ if (stat.isDirectory()) {
33
+ entries.push(`d\t${mode}\t${label}`)
34
+ for (const name of readdirSync(path).sort()) visit(join(path, name))
35
+ return
36
+ }
37
+ const digest = createHash("sha256").update(readFileSync(path)).digest("hex")
38
+ entries.push(`f\t${mode}\t${label}\t${digest}`)
39
+ }
40
+ visit(root)
41
+ process.stdout.write(createHash("sha256").update(`${entries.join("\n")}\n`).digest("hex"))
42
+ NODE
43
+ }
44
+ managed_hook_count() {
45
+ node -e '
46
+ const fs = require("fs")
47
+ const settings = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
48
+ const names = process.argv.slice(2)
49
+ let count = 0
50
+ for (const entries of Object.values(settings.hooks ?? {})) {
51
+ for (const entry of Array.isArray(entries) ? entries : []) {
52
+ for (const hook of Array.isArray(entry.hooks) ? entry.hooks : []) {
53
+ if (typeof hook.command === "string" && names.some((name) => hook.command.includes(name))) count += 1
54
+ }
55
+ }
56
+ }
57
+ process.stdout.write(String(count))
58
+ ' "$1" memory-wakeup.sh memory-capture.sh compaction-capture.sh memory-recall.sh context-explore-pretask.sh
59
+ }
60
+
61
+ capability_hook_count() {
62
+ node -e '
63
+ const fs = require("fs")
64
+ const settings = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
65
+ let count = 0
66
+ for (const entries of Object.values(settings.hooks ?? {})) {
67
+ for (const entry of Array.isArray(entries) ? entries : []) {
68
+ for (const hook of Array.isArray(entry.hooks) ? entry.hooks : []) {
69
+ if (typeof hook.command === "string" && /capability-command-(start|finish)\.sh/.test(hook.command)) count += 1
70
+ }
71
+ }
72
+ }
73
+ process.stdout.write(String(count))
74
+ ' "$1"
75
+ }
19
76
 
20
77
  # Isolated environment: fake HOME + stubbed system commands.
21
78
  mkdir -p "$SB/home" "$SB/bin"
@@ -24,38 +81,205 @@ printf '#!/usr/bin/env bash\ntrue\n' >"$SB/bin/systemctl"
24
81
  chmod +x "$SB/bin/"*
25
82
  export HOME="$SB/home" XDG_CONFIG_HOME="$SB/home/.config" PATH="$SB/bin:$PATH"
26
83
  LIVE="$SB/live"
84
+ PI_LIVE="$SB/pi-live"
27
85
 
28
86
  echo "test-uninstall"
29
87
 
30
88
  # --- install the operating layer -------------------------------------------
31
89
  "$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --live-root "$LIVE" >/dev/null 2>&1
90
+ "$ROOT_DIR/scripts/sync-pi-assets.sh" --apply --live-root "$PI_LIVE" >/dev/null 2>&1
32
91
  md_installed=$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')
33
92
  check "assets installed" "$([[ $md_installed -gt 0 ]] && echo yes || echo no)" "yes"
34
- check "hooks registered" "$(grep -c 'hooks/' "$LIVE/settings.json")" "4"
93
+ check "managed hooks registered" "$(managed_hook_count "$LIVE/settings.json")" "5"
94
+ node - "$LIVE/settings.json" <<'NODE'
95
+ const fs = require("fs")
96
+ const path = process.argv[2]
97
+ const settings = JSON.parse(fs.readFileSync(path, "utf8"))
98
+ settings.hooks ??= {}
99
+ settings.hooks.PostToolUse ??= []
100
+ settings.hooks.PostToolUse.push({
101
+ matcher: "Read",
102
+ hooks: [{ type: "command", command: "bash /external/hooks/keep-me.sh" }],
103
+ })
104
+ fs.writeFileSync(path, JSON.stringify(settings, null, 2) + "\n")
105
+ NODE
35
106
  cp "$LIVE/settings.json" "$SB/settings.before.json"
107
+ cp "$PI_LIVE/extensions/cairnkeep-trajectory.ts" "$SB/pi.before.ts"
108
+ mkdir -p "$SB/home/.cairnkeep/notes/projects/example/hindsight"
109
+ printf 'durable note bytes\n' >"$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md"
110
+ mkdir -p "$SB/home/.cairnkeep/notes/.cairnkeep/history/project-notes/knowledge/example" \
111
+ "$SB/home/.cairnkeep/notes/.cairnkeep/transactions/prepared/backups"
112
+ printf 'typed sqlite bytes\n' >"$SB/home/.cairnkeep/identity.db"
113
+ printf 'typed history bytes\n' >"$SB/home/.cairnkeep/notes/.cairnkeep/history/project-notes/knowledge/example/1.json"
114
+ printf 'prepared journal bytes\n' >"$SB/home/.cairnkeep/notes/.cairnkeep/transactions/prepared/journal-v1.json"
115
+ printf 'backup bytes\n' >"$SB/home/.cairnkeep/notes/.cairnkeep/transactions/prepared/backups/0.bin"
116
+ note_before=$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)
117
+ store_before=$(tree_hash "$SB/home/.cairnkeep")
118
+ PROJECT_DATA="$SB/project-data"
119
+ ADJACENT_PROJECT="$SB/adjacent-project"
120
+ mkdir -p "$PROJECT_DATA/.ai" \
121
+ "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one" \
122
+ "$PROJECT_DATA/.agentfs/eval/experiments/partial" \
123
+ "$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent"
124
+ printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$PROJECT_DATA/.ai/capabilities.json"
125
+ chmod 600 "$PROJECT_DATA/.ai/capabilities.json"
126
+ printf 'operator-owned ai bytes\000must-survive\377\n' >"$PROJECT_DATA/.ai/operator-state.bin"
127
+ printf 'artifact-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/artifacts.db"
128
+ printf 'session-and-callback-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/trajectory.db"
129
+ printf 'callback-wal-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/trajectory.db-wal"
130
+ printf 'unrelated-memory-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/project.db"
131
+ printf 'complete-report-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/complete/report.json"
132
+ printf 'partial-report-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/partial/report.json"
133
+ printf 'note-snapshot-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one/note.md"
134
+ printf 'adjacent-eval-v1\000must-survive\377bytes\n' >"$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent/report.json"
135
+ chmod 700 "$PROJECT_DATA/.agentfs" "$PROJECT_DATA/.agentfs/eval" \
136
+ "$PROJECT_DATA/.agentfs/eval/experiments" \
137
+ "$PROJECT_DATA/.agentfs/eval/experiments/complete" \
138
+ "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots" \
139
+ "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one" \
140
+ "$PROJECT_DATA/.agentfs/eval/experiments/partial"
141
+ chmod 600 "$PROJECT_DATA/.agentfs/project.db" \
142
+ "$PROJECT_DATA/.agentfs/eval/experiments/complete/report.json" \
143
+ "$PROJECT_DATA/.agentfs/eval/experiments/partial/report.json"
144
+ chmod 400 "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one/note.md"
145
+ chmod 700 "$ADJACENT_PROJECT/.agentfs" "$ADJACENT_PROJECT/.agentfs/eval" \
146
+ "$ADJACENT_PROJECT/.agentfs/eval/experiments" \
147
+ "$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent"
148
+ chmod 600 "$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent/report.json"
149
+ cp "$PROJECT_DATA/.ai/capabilities.json" "$SB/capabilities.before.json"
150
+ cp "$PROJECT_DATA/.ai/operator-state.bin" "$SB/operator-state.before.bin"
151
+ cp "$PROJECT_DATA/.agentfs/artifacts.db" "$SB/artifacts.before.db"
152
+ cp "$PROJECT_DATA/.agentfs/trajectory.db" "$SB/trajectory.before.db"
153
+ cp "$PROJECT_DATA/.agentfs/trajectory.db-wal" "$SB/trajectory.before.db-wal"
154
+ agentfs_before=$(tree_identity "$PROJECT_DATA/.agentfs")
155
+ eval_before=$(tree_identity "$PROJECT_DATA/.agentfs/eval")
156
+ adjacent_before=$(tree_identity "$ADJACENT_PROJECT/.agentfs")
157
+
158
+ # Normal and master-off sync are the same inert installation. Replaying an
159
+ # explicit overlay request with the master off must not change any legacy byte,
160
+ # registration, process state, block state, or measurement state.
161
+ CLAUDE_INERT="$SB/claude-inert"
162
+ OPENCODE_INERT="$SB/opencode-inert"
163
+ env -u CAIRN_CAPABILITY_CONTRACT \
164
+ "$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --live-root "$CLAUDE_INERT" >/dev/null
165
+ env -u CAIRN_CAPABILITY_CONTRACT \
166
+ "$ROOT_DIR/scripts/sync-opencode-wiki-assets.sh" --apply --live-root "$OPENCODE_INERT" >/dev/null
167
+ env -u CAIRN_CAPABILITY_CONTRACT \
168
+ "$ROOT_DIR/scripts/sync-opencode-graphify-assets.sh" --apply --live-root "$OPENCODE_INERT" >/dev/null
169
+ env -u CAIRN_CAPABILITY_CONTRACT \
170
+ "$ROOT_DIR/scripts/sync-opencode-security-assets.sh" --apply --live-root "$OPENCODE_INERT" >/dev/null
171
+ cp -a "$CLAUDE_INERT" "$SB/claude-inert.before"
172
+ cp -a "$OPENCODE_INERT" "$SB/opencode-inert.before"
173
+ CAIRN_CAPABILITY_CONTRACT=0 \
174
+ "$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --capability-overlay --live-root "$CLAUDE_INERT" >/dev/null
175
+ CAIRN_CAPABILITY_CONTRACT=0 \
176
+ "$ROOT_DIR/scripts/sync-opencode-wiki-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_INERT" >/dev/null
177
+ CAIRN_CAPABILITY_CONTRACT=0 \
178
+ "$ROOT_DIR/scripts/sync-opencode-graphify-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_INERT" >/dev/null
179
+ CAIRN_CAPABILITY_CONTRACT=0 \
180
+ "$ROOT_DIR/scripts/sync-opencode-security-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_INERT" >/dev/null
181
+ check "master-off Claude sync is exact normal identity" "$(diff -qr "$SB/claude-inert.before" "$CLAUDE_INERT" >/dev/null && echo yes || echo no)" "yes"
182
+ check "master-off OpenCode sync is exact normal identity" "$(diff -qr "$SB/opencode-inert.before" "$OPENCODE_INERT" >/dev/null && echo yes || echo no)" "yes"
183
+ check "inert Claude sync has no native hooks" "$([[ -e "$CLAUDE_INERT/hooks/capability-command-start.sh" || -e "$CLAUDE_INERT/hooks/capability-command-finish.sh" ]] && echo no || echo yes)" "yes"
184
+ check "inert OpenCode sync has no native plugin" "$([[ -e "$OPENCODE_INERT/plugins/capability-command.ts" ]] && echo no || echo yes)" "yes"
185
+ check "inert sync creates no measurement state" "$([[ -e "$SB/inert-state" ]] && echo no || echo yes)" "yes"
186
+
187
+ # Enabled sync owns one isolated project overlay containing legacy operating
188
+ # assets plus the native hook/plugin registrations. The whole overlay is a
189
+ # reversible managed unit; normal roots above are never cleanup targets.
190
+ CLAUDE_OVERLAY="$PROJECT_DATA/.ai/capability-contract/claude"
191
+ OPENCODE_OVERLAY="$PROJECT_DATA/.ai/capability-contract/opencode"
192
+ CAIRN_CAPABILITY_CONTRACT=1 \
193
+ "$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --capability-overlay --live-root "$CLAUDE_OVERLAY" >/dev/null
194
+ CAIRN_CAPABILITY_CONTRACT=1 \
195
+ "$ROOT_DIR/scripts/sync-opencode-wiki-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_OVERLAY" >/dev/null
196
+ CAIRN_CAPABILITY_CONTRACT=1 \
197
+ "$ROOT_DIR/scripts/sync-opencode-graphify-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_OVERLAY" >/dev/null
198
+ CAIRN_CAPABILITY_CONTRACT=1 \
199
+ "$ROOT_DIR/scripts/sync-opencode-security-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_OVERLAY" >/dev/null
200
+ check "enabled overlay registers five Claude hook events" "$(capability_hook_count "$CLAUDE_OVERLAY/settings.json")" "5"
201
+ check "enabled overlay registers one OpenCode plugin" "$([[ -f "$OPENCODE_OVERLAY/plugins/capability-command.ts" ]] && echo yes || echo no)" "yes"
202
+ check "enabled overlay contains no retired prompt owner" "$([[ -d "$CLAUDE_OVERLAY/capability-contract/commands" || -d "$OPENCODE_OVERLAY/capability-contract/command" || -d "$OPENCODE_OVERLAY/capability-contract/workflows" ]] && echo no || echo yes)" "yes"
203
+ overlay_before=$(tree_hash "$PROJECT_DATA/.ai/capability-contract")
204
+ project_before=$(tree_hash "$PROJECT_DATA")
36
205
 
37
206
  # --- dry-run must change nothing -------------------------------------------
38
- "$ROOT_DIR/scripts/uninstall.sh" --dry-run --live-root "$LIVE" >/dev/null 2>&1
207
+ "$ROOT_DIR/scripts/uninstall.sh" --dry-run --live-root "$LIVE" --pi-live-root "$PI_LIVE" "$PROJECT_DATA" >/dev/null 2>&1
39
208
  check "dry-run leaves assets" "$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')" "$md_installed"
40
209
  check "dry-run makes no bundle" "$(ls -d "$SB/home/.cairnkeep-uninstall-"* 2>/dev/null | wc -l | tr -d ' ')" "0"
210
+ check "dry-run leaves Pi extension" "$([[ -f "$PI_LIVE/extensions/cairnkeep-trajectory.ts" ]] && echo yes || echo no)" "yes"
211
+ check "dry-run leaves settings identical" "$(cmp -s "$SB/settings.before.json" "$LIVE/settings.json" && echo yes || echo no)" "yes"
212
+ check "dry-run leaves artifact bytes exact" "$(cmp -s "$SB/artifacts.before.db" "$PROJECT_DATA/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
213
+ check "dry-run leaves eval tree, bytes, and modes exact" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
214
+ check "dry-run leaves unrelated project memory exact" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
215
+ check "dry-run leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
216
+ check "dry-run leaves project state byte-identical" "$(tree_hash "$PROJECT_DATA")" "$project_before"
41
217
 
42
218
  # --- real uninstall ---------------------------------------------------------
43
- "$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$LIVE" >/dev/null 2>&1
219
+ "$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$LIVE" --pi-live-root "$PI_LIVE" "$PROJECT_DATA" >/dev/null 2>&1
44
220
  check "assets removed" "$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')" "0"
45
- check "hooks de-registered" "$(grep -c 'hooks/' "$LIVE/settings.json" 2>/dev/null || true)" "0"
221
+ check "managed hooks de-registered" "$(managed_hook_count "$LIVE/settings.json")" "0"
222
+ check "unrelated hook preserved" "$(grep -cF 'keep-me.sh' "$LIVE/settings.json" 2>/dev/null || true)" "1"
223
+ check "Pi extension removed" "$([[ -e "$PI_LIVE/extensions/cairnkeep-trajectory.ts" ]] && echo no || echo yes)" "yes"
224
+ check "default uninstall keeps notes" "$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)" "$note_before"
225
+ check "default uninstall keeps all typed and journal bytes" "$(tree_hash "$SB/home/.cairnkeep")" "$store_before"
226
+ check "default uninstall keeps artifact bytes exact" "$(cmp -s "$SB/artifacts.before.db" "$PROJECT_DATA/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
227
+ check "default uninstall removes managed capability config" "$([[ -e "$PROJECT_DATA/.ai/capabilities.json" ]] && echo no || echo yes)" "yes"
228
+ check "default uninstall removes managed capability overlay" "$([[ -e "$PROJECT_DATA/.ai/capability-contract" ]] && echo no || echo yes)" "yes"
229
+ check "default uninstall leaves normal Claude installation exact" "$(diff -qr "$SB/claude-inert.before" "$CLAUDE_INERT" >/dev/null && echo yes || echo no)" "yes"
230
+ check "default uninstall leaves normal OpenCode installation exact" "$(diff -qr "$SB/opencode-inert.before" "$OPENCODE_INERT" >/dev/null && echo yes || echo no)" "yes"
231
+ check "default uninstall keeps unrelated .ai bytes exact" "$(cmp -s "$SB/operator-state.before.bin" "$PROJECT_DATA/.ai/operator-state.bin" && echo yes || echo no)" "yes"
232
+ check "default uninstall keeps callback DB exact" "$(cmp -s "$SB/trajectory.before.db" "$PROJECT_DATA/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
233
+ check "default uninstall keeps callback WAL exact" "$(cmp -s "$SB/trajectory.before.db-wal" "$PROJECT_DATA/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
234
+ check "default uninstall keeps complete and partial eval reports, snapshots, modes, and layout exact" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
235
+ check "default uninstall keeps unrelated project memory exact" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
236
+ check "default uninstall leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
46
237
  BK=$(ls -d "$SB/home/.cairnkeep-uninstall-"* 2>/dev/null | head -1)
47
238
  check "revert.sh generated" "$([[ -n "$BK" && -f "$BK/revert.sh" ]] && echo yes || echo no)" "yes"
239
+ check "capability config backup is exact" "$(cmp -s "$SB/capabilities.before.json" "$BK/files/${PROJECT_DATA#/}/.ai/capabilities.json" && echo yes || echo no)" "yes"
48
240
 
49
241
  # --- revert restores everything --------------------------------------------
50
242
  bash "$BK/revert.sh" >/dev/null 2>&1
51
243
  check "assets restored" "$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')" "$md_installed"
52
244
  check "settings.json identical" "$(cmp -s "$SB/settings.before.json" "$LIVE/settings.json" && echo yes || echo no)" "yes"
245
+ check "Pi extension restored" "$(cmp -s "$SB/pi.before.ts" "$PI_LIVE/extensions/cairnkeep-trajectory.ts" && echo yes || echo no)" "yes"
246
+ check "capability config bytes restored" "$(cmp -s "$SB/capabilities.before.json" "$PROJECT_DATA/.ai/capabilities.json" && echo yes || echo no)" "yes"
247
+ check "capability overlay bytes restored" "$(tree_hash "$PROJECT_DATA/.ai/capability-contract")" "$overlay_before"
248
+ check "capability hook registrations restored" "$(capability_hook_count "$CLAUDE_OVERLAY/settings.json")" "5"
249
+ check "capability plugin restored" "$([[ -f "$OPENCODE_OVERLAY/plugins/capability-command.ts" ]] && echo yes || echo no)" "yes"
250
+ check "capability config mode restored" "$(stat -c '%a' "$PROJECT_DATA/.ai/capabilities.json" 2>/dev/null || stat -f '%Lp' "$PROJECT_DATA/.ai/capabilities.json")" "600"
251
+ check "unrelated .ai bytes remain exact after revert" "$(cmp -s "$SB/operator-state.before.bin" "$PROJECT_DATA/.ai/operator-state.bin" && echo yes || echo no)" "yes"
252
+ check "default-uninstall revert leaves retained eval exact" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
253
+
254
+ # --- project artifact purge is backup-first and exactly reversible ---------
255
+ ARTIFACT_HOME="$SB/artifact-home"
256
+ mkdir -p "$ARTIFACT_HOME"
257
+ HOME="$ARTIFACT_HOME" XDG_CONFIG_HOME="$ARTIFACT_HOME/.config" \
258
+ "$ROOT_DIR/scripts/uninstall.sh" --yes --purge-memory --live-root "$SB/unused-live" --pi-live-root "$SB/unused-pi" "$PROJECT_DATA" >/dev/null 2>&1
259
+ check "artifact purge removes project store" "$([[ -e "$PROJECT_DATA/.agentfs" ]] && echo no || echo yes)" "yes"
260
+ ARTIFACT_BK=$(ls -dt "$ARTIFACT_HOME/.cairnkeep-uninstall-"* 2>/dev/null | head -1)
261
+ check "artifact purge backup is exact" "$(cmp -s "$SB/artifacts.before.db" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
262
+ check "callback purge backup is exact" "$(cmp -s "$SB/trajectory.before.db" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
263
+ check "callback WAL purge backup is exact" "$(cmp -s "$SB/trajectory.before.db-wal" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
264
+ check "purge backup contains exact eval tree, bytes, and modes" "$(tree_identity "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/eval")" "$eval_before"
265
+ check "purge backup contains exact unrelated project memory" "$(tree_identity "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs")" "$agentfs_before"
266
+ check "selected-project purge leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
267
+ HOME="$ARTIFACT_HOME" XDG_CONFIG_HOME="$ARTIFACT_HOME/.config" bash "$ARTIFACT_BK/revert.sh" >/dev/null 2>&1
268
+ check "artifact revert restores exact bytes" "$(cmp -s "$SB/artifacts.before.db" "$PROJECT_DATA/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
269
+ check "callback revert restores exact bytes" "$(cmp -s "$SB/trajectory.before.db" "$PROJECT_DATA/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
270
+ check "callback WAL revert restores exact bytes" "$(cmp -s "$SB/trajectory.before.db-wal" "$PROJECT_DATA/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
271
+ check "purge revert restores exact eval tree, bytes, and modes" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
272
+ check "purge revert restores exact unrelated project memory" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
273
+ check "purge revert leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
53
274
 
54
275
  # --- project scaffold + memory purge round-trip ----------------------------
55
276
  PROJ="$SB/proj"; mkdir -p "$PROJ"; git -C "$PROJ" init -q
56
277
  "$ROOT_DIR/scripts/bootstrap.sh" --untracked "$PROJ" >/dev/null 2>&1
57
278
  EXCL="$PROJ/.git/info/exclude"
58
- mkdir -p "$SB/home/.cairnkeep"; echo data >"$SB/home/.cairnkeep/db"
279
+ mkdir -p "$SB/home/.cairnkeep/notes/projects/example/hindsight"; echo data >"$SB/home/.cairnkeep/db"
280
+ printf 'purge and restore exact note bytes\n' >"$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md"
281
+ purge_note_before=$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)
282
+ purge_store_before=$(tree_hash "$SB/home/.cairnkeep")
59
283
  "$ROOT_DIR/scripts/uninstall.sh" --yes --purge-memory --live-root "$SB/home/.claude" "$PROJ" >/dev/null 2>&1
60
284
  check "project .ai removed" "$([[ -e "$PROJ/.ai" ]] && echo no || echo yes)" "yes"
61
285
  check "project .agentfs removed" "$([[ -e "$PROJ/.agentfs" ]] && echo no || echo yes)" "yes"
@@ -66,6 +290,8 @@ bash "$BK2/revert.sh" >/dev/null 2>&1
66
290
  check "project scaffold restored" "$([[ -d "$PROJ/.ai" ]] && echo yes || echo no)" "yes"
67
291
  check "project ignore restored" "$([[ -f "$PROJ/.agentfs/.gitignore" ]] && echo yes || echo no)" "yes"
68
292
  check "memory store restored" "$(cat "$SB/home/.cairnkeep/db" 2>/dev/null)" "data"
293
+ check "note store restored exactly" "$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)" "$purge_note_before"
294
+ check "typed and journal state restored exactly" "$(tree_hash "$SB/home/.cairnkeep")" "$purge_store_before"
69
295
 
70
296
  echo
71
297
  if [[ "$fails" -gt 0 ]]; then echo "test-uninstall: $fails check(s) failed."; exit 1; fi
@@ -21,6 +21,7 @@ CLAUDE_SOURCE="$CAIRN_ROOT/claude"
21
21
  TPL="$CAIRN_ROOT/templates"
22
22
 
23
23
  LIVE_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
24
+ PI_LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
24
25
  STORE_DIR="${CAIRN_AGENTFS_BASE_DIR:-$HOME/.cairnkeep}"
25
26
  STORE_DIR="${STORE_DIR/#\~/$HOME}"
26
27
  DRY_RUN=0
@@ -29,20 +30,35 @@ PURGE_MEMORY=0
29
30
  PROJECTS=()
30
31
 
31
32
  # The hooks cairnkeep registers (basename is the settings.json match token).
32
- HOOK_NAMES=(memory-wakeup.sh memory-capture.sh memory-recall.sh context-explore-pretask.sh)
33
+ HOOK_NAMES=(memory-wakeup.sh memory-capture.sh compaction-capture.sh memory-recall.sh context-explore-pretask.sh)
34
+
35
+ # Exact project-local files installed by bootstrap. Remove these individually so
36
+ # operator-owned files under .ai (notably .env and local state) are never swept
37
+ # up with Cairnkeep's managed scaffold.
38
+ PROJECT_AI_FILES=(
39
+ start-claude.sh
40
+ start-opencode.sh
41
+ start-pi.sh
42
+ start-kimi.sh
43
+ env.example
44
+ trajectory-redaction.json
45
+ capabilities.json
46
+ )
33
47
 
34
48
  usage() {
35
49
  cat <<'EOF'
36
- Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--live-root PATH] [PROJECT ...]
50
+ Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--live-root PATH]
51
+ [--pi-live-root PATH] [PROJECT ...]
37
52
 
38
53
  Reverse cairnkeep's install. Everything removed/edited is backed up into
39
54
  $HOME/.cairnkeep-uninstall-<ts>/ with a revert.sh before any change.
40
55
 
41
56
  --dry-run Print what would happen; touch nothing.
42
57
  --yes Do not prompt for confirmation.
43
- --purge-memory Also remove the global memory store and .agentfs/ memory in
44
- each PROJECT. Backed up first; off by default.
58
+ --purge-memory Also remove the global memory store and .agentfs/ memory and
59
+ trajectories in each PROJECT. Backed up first; off by default.
45
60
  --live-root PATH Claude root to clean (default: $CLAUDE_CONFIG_DIR or ~/.claude).
61
+ --pi-live-root PATH Pi agent root to clean (default: $PI_CODING_AGENT_DIR or ~/.pi/agent).
46
62
  PROJECT ... Also revert `cairn bootstrap` in these project dirs
47
63
  (.ai/, .planning/, and any .git/info/exclude entries).
48
64
  -h, --help Show this help.
@@ -55,6 +71,7 @@ while [[ $# -gt 0 ]]; do
55
71
  --yes) ASSUME_YES=1; shift ;;
56
72
  --purge-memory) PURGE_MEMORY=1; shift ;;
57
73
  --live-root) LIVE_ROOT="$2"; shift 2 ;;
74
+ --pi-live-root) PI_LIVE_ROOT="$2"; shift 2 ;;
58
75
  -h|--help) usage; exit 0 ;;
59
76
  -*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
60
77
  *) PROJECTS+=("$1"); shift ;;
@@ -118,6 +135,7 @@ asset_dests() {
118
135
  echo "cairn uninstall"
119
136
  [[ $DRY_RUN -eq 1 ]] && echo "(dry run — nothing will change)"
120
137
  echo " live root: $LIVE_ROOT"
138
+ echo " Pi live root: $PI_LIVE_ROOT"
121
139
  echo " memory store: $STORE_DIR ($([[ $PURGE_MEMORY -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
122
140
  [[ ${#PROJECTS[@]} -gt 0 ]] && echo " projects: ${PROJECTS[*]}"
123
141
 
@@ -131,6 +149,11 @@ fi
131
149
  echo "Operating layer (assets):"
132
150
  while IFS= read -r dst; do remove_path "$dst"; done < <(asset_dests)
133
151
 
152
+ # The Pi adapter is one precisely owned extension; do not touch any other Pi
153
+ # configuration or extensions in the same user root.
154
+ echo "Pi extension:"
155
+ remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-trajectory.ts"
156
+
134
157
  # 2. settings.json hook registrations (back up whole file, then de-register).
135
158
  SETTINGS="$LIVE_ROOT/settings.json"
136
159
  if [[ -f "$SETTINGS" ]]; then
@@ -207,7 +230,17 @@ for proj in "${PROJECTS[@]:-}"; do
207
230
  [[ -n "$proj" && -d "$proj" ]] || continue
208
231
  proj=$(cd "$proj" && pwd)
209
232
  echo "Project scaffold: $proj"
210
- remove_path "$proj/.ai"
233
+ # Truthy launchers create this project-local root only for the explicit
234
+ # capability overlay. Back up the complete managed unit so its native Claude
235
+ # hook registrations and OpenCode plugin bytes restore together exactly.
236
+ # Normal and master-off installations never create it, so absence is inert.
237
+ remove_path "$proj/.ai/capability-contract"
238
+ for rel in "${PROJECT_AI_FILES[@]}"; do
239
+ remove_path "$proj/.ai/$rel"
240
+ done
241
+ if [[ $DRY_RUN -eq 0 && -d "$proj/.ai" ]]; then
242
+ rmdir "$proj/.ai" 2>/dev/null || true
243
+ fi
211
244
  remove_path "$proj/.planning"
212
245
  if [[ $PURGE_MEMORY -eq 1 ]]; then
213
246
  remove_path "$proj/.agentfs"