@cairnkeep/cli 2.3.1 → 2.4.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 (125) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +221 -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 +1 -1
  10. package/docs/design/meta-agent.md +439 -0
  11. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  12. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  13. package/docs/learning/lessons/L02-installation.md +2 -2
  14. package/docs/learning/lessons/L03-first-project.md +1 -1
  15. package/docs/operating.md +531 -4
  16. package/docs/privacy-and-data-flow.md +417 -0
  17. package/docs/storage.md +252 -0
  18. package/examples/eval/README.md +42 -0
  19. package/examples/eval/adapter.json +12 -0
  20. package/examples/eval/bundled-fake.json +1 -0
  21. package/examples/eval/task-set.json +1 -0
  22. package/mcp-memory-server/dist/artifact-cli.js +184 -0
  23. package/mcp-memory-server/dist/artifact-schema.js +218 -0
  24. package/mcp-memory-server/dist/artifact-store.js +812 -0
  25. package/mcp-memory-server/dist/capability-adapter.js +286 -0
  26. package/mcp-memory-server/dist/capability-cli.js +361 -0
  27. package/mcp-memory-server/dist/capability-config.js +310 -0
  28. package/mcp-memory-server/dist/capability-harness.js +496 -0
  29. package/mcp-memory-server/dist/capability-registry.js +123 -0
  30. package/mcp-memory-server/dist/capability-schema.js +65 -0
  31. package/mcp-memory-server/dist/capability-store.js +431 -0
  32. package/mcp-memory-server/dist/compaction-normalize.js +267 -0
  33. package/mcp-memory-server/dist/eval-cli.js +523 -0
  34. package/mcp-memory-server/dist/eval-plan.js +346 -0
  35. package/mcp-memory-server/dist/eval-process.js +273 -0
  36. package/mcp-memory-server/dist/eval-report.js +609 -0
  37. package/mcp-memory-server/dist/eval-runner.js +827 -0
  38. package/mcp-memory-server/dist/eval-schema.js +368 -0
  39. package/mcp-memory-server/dist/eval-statistics.js +161 -0
  40. package/mcp-memory-server/dist/eval-workspace.js +313 -0
  41. package/mcp-memory-server/dist/failure-signature.js +141 -0
  42. package/mcp-memory-server/dist/index.js +600 -55
  43. package/mcp-memory-server/dist/node-cli.js +84 -0
  44. package/mcp-memory-server/dist/node-schema.js +170 -0
  45. package/mcp-memory-server/dist/node-store.js +455 -0
  46. package/mcp-memory-server/dist/note-cli.js +235 -0
  47. package/mcp-memory-server/dist/note-distiller.js +177 -0
  48. package/mcp-memory-server/dist/note-enrichment.js +93 -0
  49. package/mcp-memory-server/dist/note-schema.js +4 -0
  50. package/mcp-memory-server/dist/note-store.js +985 -0
  51. package/mcp-memory-server/dist/trajectory-cli.js +141 -0
  52. package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
  53. package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
  54. package/mcp-memory-server/dist/trajectory-schema.js +73 -0
  55. package/mcp-memory-server/dist/trajectory-store.js +339 -0
  56. package/mcp-memory-server/package.json +24 -2
  57. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
  58. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
  59. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
  60. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
  61. package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
  62. package/opencode/capability-contract/plugins/capability-command.ts +319 -0
  63. package/opencode/command/graphify.md +25 -14
  64. package/opencode/plugins/memory-capture.ts +76 -10
  65. package/opencode/plugins/memory-wakeup.ts +58 -6
  66. package/package.json +8 -4
  67. package/pi/extensions/cairnkeep-trajectory.ts +55 -0
  68. package/schemas/artifact.schema.json +133 -0
  69. package/schemas/capability-callback.schema.json +68 -0
  70. package/schemas/capability-contract.schema.json +126 -0
  71. package/schemas/eval-adapter.schema.json +43 -0
  72. package/schemas/eval-protocol.schema.json +114 -0
  73. package/schemas/eval-report.schema.json +423 -0
  74. package/schemas/eval-task-set.schema.json +114 -0
  75. package/schemas/memory-node.schema.json +90 -0
  76. package/schemas/note.schema.json +112 -0
  77. package/schemas/trajectory-redaction.schema.json +25 -0
  78. package/schemas/trajectory.schema.json +82 -0
  79. package/scripts/bootstrap.sh +4 -0
  80. package/scripts/completion.sh +119 -5
  81. package/scripts/doctor.sh +284 -0
  82. package/scripts/fake-eval-adapter.mjs +120 -0
  83. package/scripts/fixtures/capability-harness-contracts.json +408 -0
  84. package/scripts/install-audit-timer.sh +3 -1
  85. package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
  86. package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
  87. package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
  88. package/scripts/memory-wiki-audit.sh +49 -0
  89. package/scripts/smoke-bash32.sh +9 -0
  90. package/scripts/sync-claude-assets.sh +99 -6
  91. package/scripts/sync-opencode-graphify-assets.sh +24 -3
  92. package/scripts/sync-opencode-plugin-assets.sh +31 -5
  93. package/scripts/sync-opencode-security-assets.sh +24 -3
  94. package/scripts/sync-opencode-wiki-assets.sh +24 -3
  95. package/scripts/sync-pi-assets.sh +72 -0
  96. package/scripts/test-audit-timer.sh +98 -0
  97. package/scripts/test-bootstrap-untracked.sh +48 -0
  98. package/scripts/test-cli-dispatch.sh +41 -2
  99. package/scripts/test-compaction-hooks.sh +495 -0
  100. package/scripts/test-completion.sh +77 -0
  101. package/scripts/test-doctor.sh +348 -1
  102. package/scripts/test-eval-cli.sh +175 -0
  103. package/scripts/test-launcher-hooks.sh +602 -3
  104. package/scripts/test-notes-cli.sh +114 -0
  105. package/scripts/test-package-install.sh +251 -0
  106. package/scripts/test-phase16-node-lifecycle.sh +50 -0
  107. package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
  108. package/scripts/test-phase18-capability-lifecycle.sh +504 -0
  109. package/scripts/test-phase18-harness-boundary.sh +1060 -0
  110. package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
  111. package/scripts/test-trajectory-cli.sh +87 -0
  112. package/scripts/test-trajectory-hooks.sh +142 -0
  113. package/scripts/test-uninstall.sh +231 -5
  114. package/scripts/uninstall.sh +37 -5
  115. package/scripts/verify-dependency-security.mjs +121 -0
  116. package/scripts/verify-docs-parity.sh +527 -1
  117. package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
  118. package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
  119. package/templates/capabilities.json.template +7 -0
  120. package/templates/env.example.template +51 -0
  121. package/templates/memory-wiki-audit.service.template +1 -1
  122. package/templates/start-claude.sh.template +36 -0
  123. package/templates/start-opencode.sh.template +47 -5
  124. package/templates/start-pi.sh.template +30 -0
  125. package/templates/trajectory-redaction.json.template +4 -0
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env bash
2
+ # Phase 18 capability CLI, lifecycle, operating-overlay, and ablation contract.
3
+ # No-argument execution is the complete GREEN contract. Explicit expected-RED
4
+ # modes remain available only to classify historical missing-owner simulations.
5
+ set -uo pipefail
6
+
7
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
8
+ EXPECTED_RED_EXIT=86
9
+ RED_MARKER="PHASE18_RED:CAPABILITY_LIFECYCLE_MISSING"
10
+ MODE="${1:-full}"
11
+ [[ $# -gt 0 ]] && shift
12
+ RUNTIME=0
13
+ HARNESS=""
14
+ CAPABILITY=""
15
+
16
+ usage() {
17
+ cat <<'USAGE'
18
+ Usage: test-phase18-capability-lifecycle.sh [baseline|--expect-red-contract|contract|matrix|bootstrap|uninstall|lifecycle [--runtime]|claude-native|native-boundary|owner-retirement|full]
19
+ test-phase18-capability-lifecycle.sh operating --harness claude|opencode-command|opencode-workflow --capability wiki|graph|security.audit
20
+ USAGE
21
+ }
22
+
23
+ fail() {
24
+ echo "FAIL: $1" >&2
25
+ exit 1
26
+ }
27
+
28
+ case "$MODE" in
29
+ baseline|--expect-red-contract|contract|matrix|bootstrap|uninstall|claude-native|native-boundary|owner-retirement|full)
30
+ [[ $# -eq 0 ]] || { usage >&2; exit 2; }
31
+ ;;
32
+ lifecycle)
33
+ if [[ "${1:-}" == "--runtime" ]]; then RUNTIME=1; shift; fi
34
+ [[ $# -eq 0 ]] || { usage >&2; exit 2; }
35
+ ;;
36
+ operating)
37
+ while [[ $# -gt 0 ]]; do
38
+ case "$1" in
39
+ --harness) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; HARNESS="$2"; shift 2 ;;
40
+ --capability) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; CAPABILITY="$2"; shift 2 ;;
41
+ *) usage >&2; exit 2 ;;
42
+ esac
43
+ done
44
+ case "$HARNESS" in claude|opencode-command|opencode-workflow) ;; *) usage >&2; exit 2 ;; esac
45
+ case "$CAPABILITY" in wiki|graph|security.audit) ;; *) usage >&2; exit 2 ;; esac
46
+ if [[ "$HARNESS" == "opencode-workflow" && "$CAPABILITY" == "graph" ]]; then
47
+ echo "graph has no directly invokable OpenCode workflow" >&2
48
+ exit 2
49
+ fi
50
+ ;;
51
+ *) usage >&2; exit 2 ;;
52
+ esac
53
+
54
+ # Selector validation above intentionally precedes all fixture mutation.
55
+ tmp=$(mktemp -d)
56
+ trap 'rm -rf "$tmp"' EXIT
57
+ baseline_number=0
58
+
59
+ run_baseline_check() {
60
+ baseline_number=$((baseline_number + 1))
61
+ local output="$tmp/baseline-$baseline_number.out"
62
+ if ! env -u CAIRN_CAPABILITY_CONTRACT "$@" >"$output" 2>&1; then
63
+ cat "$output" >&2
64
+ fail "baseline failed: $*"
65
+ fi
66
+ if grep -qF 'PHASE18_RED:' "$output"; then
67
+ cat "$output" >&2
68
+ fail "baseline emitted a Phase 18 RED marker: $*"
69
+ fi
70
+ }
71
+
72
+ assert_same_file() {
73
+ cmp -s "$1" "$2" || fail "installed bytes differ: $2"
74
+ }
75
+
76
+ run_master_off_identity() {
77
+ local project="$tmp/identity-project"
78
+ local fake_bin="$tmp/identity-bin"
79
+ local trace="$tmp/identity.trace"
80
+ local claude_live="$tmp/claude-live"
81
+ local opencode_live="$tmp/opencode-live"
82
+ mkdir -p "$project" "$fake_bin"
83
+ git -C "$project" init -q
84
+ "$ROOT/scripts/bootstrap.sh" "$project" >"$tmp/bootstrap.out"
85
+ printf '%s' '{"schema_version":1,"capabilities":' >"$project/.ai/capabilities.json"
86
+ chmod 600 "$project/.ai/capabilities.json"
87
+ sha256sum "$project/.ai/capabilities.json" >"$tmp/config.before"
88
+
89
+ for executable in claude opencode; do
90
+ cat >"$fake_bin/$executable" <<'STUB'
91
+ #!/usr/bin/env bash
92
+ printf '%s|%s' "$(basename "$0")" "$PWD" >>"$TRACE_FILE"
93
+ printf '|%s' "$@" >>"$TRACE_FILE"
94
+ printf '\n' >>"$TRACE_FILE"
95
+ exit "${HARNESS_EXIT:-0}"
96
+ STUB
97
+ chmod 755 "$fake_bin/$executable"
98
+ done
99
+
100
+ : >"$trace"
101
+ if ! env -u CAIRN_CAPABILITY_CONTRACT PATH="$fake_bin:$PATH" TRACE_FILE="$trace" \
102
+ "$project/.ai/start-claude.sh" --identity >"$tmp/claude.out" 2>"$tmp/claude.err"; then
103
+ fail "master-off Claude launcher changed exit behavior"
104
+ fi
105
+ if ! env -u CAIRN_CAPABILITY_CONTRACT PATH="$fake_bin:$PATH" TRACE_FILE="$trace" \
106
+ "$project/.ai/start-opencode.sh" --identity >"$tmp/opencode.out" 2>"$tmp/opencode.err"; then
107
+ fail "master-off OpenCode launcher changed exit behavior"
108
+ fi
109
+ [[ ! -s "$tmp/claude.out" && ! -s "$tmp/claude.err" && ! -s "$tmp/opencode.out" && ! -s "$tmp/opencode.err" ]] || \
110
+ fail "master-off launcher added output"
111
+ printf 'claude|%s|--identity\nopencode|%s|--identity\n' "$project" "$project" >"$tmp/identity.expected"
112
+ cmp -s "$tmp/identity.expected" "$trace" || fail "master-off launcher process/argv trace changed"
113
+
114
+ env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$claude_live" >/dev/null
115
+ for rel in commands/wiki-ingest.md commands/wiki-query.md commands/wiki-lint.md commands/graphify.md commands/security-audit.md; do
116
+ assert_same_file "$ROOT/claude/$rel" "$claude_live/$rel"
117
+ done
118
+ env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-opencode-wiki-assets.sh" --apply --live-root "$opencode_live" >/dev/null
119
+ env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-opencode-graphify-assets.sh" --apply --live-root "$opencode_live" >/dev/null
120
+ env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-opencode-security-assets.sh" --apply --live-root "$opencode_live" >/dev/null
121
+ for rel in \
122
+ command/wiki-ingest.md command/wiki-query.md command/wiki-lint.md \
123
+ workflows/wiki-ingest-workflow.md workflows/wiki-query-workflow.md workflows/wiki-lint-workflow.md \
124
+ command/graphify.md command/security-audit.md workflows/security-audit-workflow.md
125
+ do
126
+ assert_same_file "$ROOT/opencode/$rel" "$opencode_live/$rel"
127
+ done
128
+
129
+ sha256sum -c "$tmp/config.before" >/dev/null || fail "master-off path changed malformed capability config"
130
+ if find "$project/.agentfs" -type f \( -name '*.db' -o -name '*.db-wal' -o -name '*.db-shm' \) | grep -q .; then
131
+ fail "master-off path created a capability/logging store"
132
+ fi
133
+ if grep -R -qF '{"schema_version":1,"capabilities":' "$tmp/claude.out" "$tmp/claude.err" "$tmp/opencode.out" "$tmp/opencode.err"; then
134
+ fail "master-off path exposed configuration contents"
135
+ fi
136
+ }
137
+
138
+ run_baseline() {
139
+ run_baseline_check "$ROOT/scripts/test-cli-dispatch.sh"
140
+ run_baseline_check "$ROOT/scripts/test-bootstrap-untracked.sh"
141
+ run_baseline_check "$ROOT/scripts/test-doctor.sh"
142
+ run_baseline_check "$ROOT/scripts/test-completion.sh"
143
+ run_baseline_check "$ROOT/scripts/test-package-install.sh"
144
+ run_baseline_check "$ROOT/scripts/test-uninstall.sh"
145
+ run_baseline_check bash -n "$ROOT/bin/cairn"
146
+ run_master_off_identity
147
+ echo "PASS: Phase 18 master-off CLI/package/lifecycle identity baseline"
148
+ }
149
+
150
+ new_project() {
151
+ local project="$1"
152
+ mkdir -p "$project"
153
+ git -C "$project" init -q
154
+ "$ROOT/scripts/bootstrap.sh" "$project" >/dev/null
155
+ }
156
+
157
+ run_capability() {
158
+ local project="$1"
159
+ shift
160
+ (cd "$project" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities "$@")
161
+ }
162
+
163
+ enable_all_capabilities() {
164
+ local project="$1" id
165
+ for id in memory.write memory.search notes.distill wiki graph security.audit route.check context.explore; do
166
+ run_capability "$project" enable "$id" >/dev/null
167
+ done
168
+ }
169
+
170
+ run_contract() {
171
+ local project="$tmp/contract-project"
172
+ local config_path="$project/.ai/capabilities.json"
173
+ local status="$tmp/status.json"
174
+ local before after
175
+ new_project "$project"
176
+
177
+ run_capability "$project" list >"$tmp/list.out" 2>"$tmp/list.err" || fail "capabilities list failed"
178
+ run_capability "$project" status >"$tmp/status.out" 2>"$tmp/status.err" || fail "human status failed"
179
+ run_capability "$project" status --json >"$status" 2>"$tmp/status-json.err" || fail "JSON status failed"
180
+ node - "$status" <<'NODE'
181
+ const fs = require("fs");
182
+ const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
183
+ const ids = ["memory.write", "memory.search", "notes.distill", "wiki", "graph", "security.audit", "route.check", "context.explore"];
184
+ if (value.schema_version !== 1 || value.contract_enabled !== true) process.exit(1);
185
+ if (JSON.stringify(value.capabilities.map((row) => row.id)) !== JSON.stringify(ids)) process.exit(1);
186
+ if (!/^[0-9a-f]{64}$/.test(value.configuration_digest)) process.exit(1);
187
+ for (const row of value.capabilities) {
188
+ if (row.restart_required !== (row.kind === "mcp-tool")) process.exit(1);
189
+ if (!["environment", "project", "compatibility"].includes(row.source)) process.exit(1);
190
+ }
191
+ NODE
192
+
193
+ before=$(sha256sum "$config_path" | cut -d' ' -f1)
194
+ if run_capability "$project" disable unknown.owner >"$tmp/unknown.out" 2>"$tmp/unknown.err"; then
195
+ fail "unknown capability ID was accepted"
196
+ fi
197
+ after=$(sha256sum "$config_path" | cut -d' ' -f1)
198
+ [[ "$before" == "$after" ]] || fail "invalid ID mutated configuration"
199
+
200
+ run_capability "$project" disable memory.write >/dev/null || fail "disable failed"
201
+ run_capability "$project" enable wiki >/dev/null || fail "enable failed"
202
+ run_capability "$project" reset memory.write >/dev/null || fail "reset failed"
203
+ run_capability "$project" logging enable >/dev/null || fail "logging enable failed"
204
+ run_capability "$project" logging disable >/dev/null || fail "logging disable failed"
205
+ run_capability "$project" logging reset >/dev/null || fail "logging reset failed"
206
+ node - "$config_path" <<'NODE'
207
+ const fs = require("fs");
208
+ const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
209
+ if (Object.hasOwn(value.capabilities, "memory.write")) process.exit(1);
210
+ if (value.capabilities.wiki !== true) process.exit(1);
211
+ if (Object.hasOwn(value.logging, "callbacks")) process.exit(1);
212
+ NODE
213
+
214
+ run_capability "$project" disable memory.search >"$tmp/concurrent-one.out" 2>&1 &
215
+ local first_pid=$!
216
+ run_capability "$project" disable security.audit >"$tmp/concurrent-two.out" 2>&1 &
217
+ local second_pid=$!
218
+ wait "$first_pid" || fail "first concurrent mutation failed"
219
+ wait "$second_pid" || fail "second concurrent mutation failed"
220
+ node - "$config_path" <<'NODE'
221
+ const fs = require("fs");
222
+ const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
223
+ if (value.capabilities["memory.search"] !== false || value.capabilities["security.audit"] !== false) process.exit(1);
224
+ NODE
225
+
226
+ printf '%s\n' '{"schema_version":1,"capabilities":{"memory.write":"PHASE18_RAW_SENTINEL","wiki":false},"logging":{"callbacks":false}}' >"$config_path"
227
+ chmod 600 "$config_path"
228
+ run_capability "$project" status --json >"$tmp/invalid-status.out" 2>"$tmp/invalid-status.err" || fail "row-local invalid status failed"
229
+ (cd "$project" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" doctor) >"$tmp/invalid-doctor.out" 2>"$tmp/invalid-doctor.err" || true
230
+ if grep -R -qF 'PHASE18_RAW_SENTINEL' "$tmp/invalid-status.out" "$tmp/invalid-status.err" "$tmp/invalid-doctor.out" "$tmp/invalid-doctor.err"; then
231
+ fail "status or doctor exposed raw configuration contents"
232
+ fi
233
+ grep -qF 'invalid-capability-value' "$tmp/invalid-status.out" || fail "status omitted the fixed invalid-row issue"
234
+ grep -q '"id":"wiki".*"enabled":false\|"enabled":false.*"id":"wiki"' "$tmp/invalid-status.out" || fail "one invalid row poisoned an unrelated valid row"
235
+
236
+ cp "$config_path" "$tmp/config-before-rerun"
237
+ "$ROOT/scripts/bootstrap.sh" "$project" >/dev/null
238
+ cmp -s "$tmp/config-before-rerun" "$config_path" || fail "bootstrap rerun overwrote capability state"
239
+ [[ $(stat -c '%a' "$config_path" 2>/dev/null || stat -f '%Lp' "$config_path") == 600 ]] || fail "capability config is not mode 0600"
240
+ echo "PASS: Phase 18 capability CLI/configuration contract"
241
+ }
242
+
243
+ run_bootstrap() {
244
+ local project="$tmp/bootstrap-project"
245
+ local config_path="$project/.ai/capabilities.json"
246
+ new_project "$project"
247
+ [[ -f "$config_path" ]] || fail "fresh bootstrap omitted capabilities.json"
248
+ cmp -s "$ROOT/templates/capabilities.json.template" "$config_path" || fail "fresh capability config differs from template"
249
+ [[ $(stat -c '%a' "$config_path" 2>/dev/null || stat -f '%Lp' "$config_path") == 600 ]] || fail "fresh capability config is not mode 0600"
250
+ printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$config_path"
251
+ chmod 600 "$config_path"
252
+ cp "$config_path" "$tmp/bootstrap-operator-state"
253
+ "$ROOT/scripts/bootstrap.sh" "$project" >/dev/null
254
+ cmp -s "$tmp/bootstrap-operator-state" "$config_path" || fail "bootstrap rerun replaced operator state"
255
+
256
+ local untracked="$tmp/bootstrap-untracked"
257
+ mkdir -p "$untracked"
258
+ git -C "$untracked" init -q
259
+ "$ROOT/scripts/bootstrap.sh" --untracked "$untracked" >/dev/null
260
+ [[ -f "$untracked/.ai/capabilities.json" ]] || fail "untracked bootstrap omitted capability config"
261
+ [[ -z $(git -C "$untracked" status --porcelain) ]] || fail "untracked capability config became visible to git"
262
+ echo "PASS: Phase 18 capability bootstrap fresh/rerun contract"
263
+ }
264
+
265
+ run_uninstall() {
266
+ local project="$tmp/uninstall-project"
267
+ local fixture_home="$tmp/uninstall-home"
268
+ local fixture_bin="$tmp/uninstall-bin"
269
+ local live="$tmp/uninstall-live"
270
+ local config_before="$tmp/capabilities.before"
271
+ local db_before="$tmp/callbacks.before"
272
+ local unrelated_before="$tmp/unrelated-ai.before"
273
+ local project_before="$tmp/uninstall-project.before"
274
+ local bundle
275
+ new_project "$project"
276
+ mkdir -p "$fixture_home" "$fixture_bin" "$project/.agentfs"
277
+ printf '#!/usr/bin/env bash\nexit 0\n' >"$fixture_bin/claude"
278
+ printf '#!/usr/bin/env bash\nexit 0\n' >"$fixture_bin/systemctl"
279
+ chmod 755 "$fixture_bin/claude" "$fixture_bin/systemctl"
280
+ printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$project/.ai/capabilities.json"
281
+ chmod 600 "$project/.ai/capabilities.json"
282
+ printf 'operator-owned ai bytes\000must-survive\377\n' >"$project/.ai/operator-state.bin"
283
+ printf 'capability-callback-v1\000durable\377bytes\n' >"$project/.agentfs/trajectory.db"
284
+ printf 'capability-wal-v1\000durable\377bytes\n' >"$project/.agentfs/trajectory.db-wal"
285
+ cp "$project/.ai/capabilities.json" "$config_before"
286
+ cp "$project/.ai/operator-state.bin" "$unrelated_before"
287
+ cp "$project/.agentfs/trajectory.db" "$db_before"
288
+ cp -a "$project" "$project_before"
289
+
290
+ HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" PATH="$fixture_bin:$PATH" \
291
+ "$ROOT/scripts/uninstall.sh" --dry-run --live-root "$live" "$project" >/dev/null 2>&1 || fail "capability uninstall dry-run failed"
292
+ diff -qr "$project_before" "$project" >/dev/null || fail "capability uninstall dry-run changed project bytes"
293
+
294
+ HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" PATH="$fixture_bin:$PATH" \
295
+ "$ROOT/scripts/uninstall.sh" --yes --live-root "$live" "$project" >/dev/null 2>&1 || fail "default capability uninstall failed"
296
+ [[ ! -e "$project/.ai/capabilities.json" ]] || fail "default uninstall retained project capability config"
297
+ cmp -s "$unrelated_before" "$project/.ai/operator-state.bin" || fail "default uninstall changed unrelated .ai bytes"
298
+ cmp -s "$db_before" "$project/.agentfs/trajectory.db" || fail "default uninstall changed callback store bytes"
299
+ bundle=$(find "$fixture_home" -maxdepth 1 -type d -name '.cairnkeep-uninstall-*' | sort | tail -1)
300
+ [[ -n "$bundle" && -x "$bundle/revert.sh" ]] || fail "default uninstall created no reversible backup"
301
+ cmp -s "$config_before" "$bundle/files/${project#/}/.ai/capabilities.json" || fail "capability config backup is not exact"
302
+ HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" bash "$bundle/revert.sh" >/dev/null 2>&1 || fail "capability revert failed"
303
+ cmp -s "$config_before" "$project/.ai/capabilities.json" || fail "capability revert did not restore config bytes"
304
+ [[ $(stat -c '%a' "$project/.ai/capabilities.json" 2>/dev/null || stat -f '%Lp' "$project/.ai/capabilities.json") == 600 ]] || fail "capability revert did not restore config mode"
305
+ cmp -s "$unrelated_before" "$project/.ai/operator-state.bin" || fail "capability revert changed unrelated .ai bytes"
306
+
307
+ HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" PATH="$fixture_bin:$PATH" \
308
+ "$ROOT/scripts/uninstall.sh" --yes --purge-memory --live-root "$live" "$project" >/dev/null 2>&1 || fail "capability purge failed"
309
+ [[ ! -e "$project/.agentfs" ]] || fail "purge retained callback store"
310
+ bundle=$(find "$fixture_home" -maxdepth 1 -type d -name '.cairnkeep-uninstall-*' | sort | tail -1)
311
+ cmp -s "$db_before" "$bundle/files/${project#/}/.agentfs/trajectory.db" || fail "callback store purge backup is not exact"
312
+ HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" bash "$bundle/revert.sh" >/dev/null 2>&1 || fail "capability purge revert failed"
313
+ cmp -s "$db_before" "$project/.agentfs/trajectory.db" || fail "capability purge revert did not restore callback bytes"
314
+ cmp -s "$unrelated_before" "$project/.ai/operator-state.bin" || fail "capability purge revert changed unrelated .ai bytes"
315
+ echo "PASS: Phase 18 capability uninstall keep/purge/revert contract"
316
+ }
317
+
318
+ run_operating() {
319
+ if [[ "$HARNESS" == "claude" ]]; then
320
+ "$ROOT/scripts/test-phase18-harness-boundary.sh" claude-delegate-calls >/dev/null || \
321
+ fail "Claude native delegate-call boundary failed"
322
+ else
323
+ "$ROOT/scripts/test-phase18-harness-boundary.sh" opencode-delegate-calls >/dev/null || \
324
+ fail "OpenCode native delegate-call boundary failed"
325
+ fi
326
+ echo "PASS: Phase 18 $HARNESS $CAPABILITY deterministic policy/boundary coverage (not live owner execution)"
327
+ }
328
+
329
+ run_claude_native() {
330
+ local start="$ROOT/claude/capability-contract/hooks/capability-command-start.sh"
331
+ local finish="$ROOT/claude/capability-contract/hooks/capability-command-finish.sh"
332
+ [[ -x "$start" && -x "$finish" ]] || fail "Claude native capability hooks are absent"
333
+ grep -qF 'harness-before' "$start" || fail "Claude admission hook does not delegate to the coordinator"
334
+ grep -qF 'harness-terminal' "$finish" || fail "Claude terminal hook does not delegate to the coordinator"
335
+ grep -qF 'harness-cwd' "$finish" || fail "Claude cwd hook does not delegate to the coordinator"
336
+ "$ROOT/scripts/test-phase18-harness-boundary.sh" claude-hooks >/dev/null || fail "Claude native lifecycle outcomes failed"
337
+ echo "PASS: Phase 18 Claude native hook delegation and durable outcomes"
338
+ }
339
+
340
+ run_owner_retirement() {
341
+ if find "$ROOT/claude/capability-contract/commands" -type f -print -quit 2>/dev/null | grep -q .; then
342
+ fail "obsolete Claude Markdown capability owner remains"
343
+ fi
344
+ echo "PASS: Phase 18 Claude Markdown owner retirement"
345
+ }
346
+
347
+ run_matrix() {
348
+ local fixture="$ROOT/mcp-memory-server/scripts/fixtures/capabilities/ablation-snapshots.json"
349
+ local project="$tmp/matrix-project"
350
+ [[ -f "$fixture" ]] || fail "missing ablation snapshot fixture"
351
+ new_project "$project"
352
+ node - "$fixture" <<'NODE'
353
+ const { createHash } = require("crypto");
354
+ const fs = require("fs");
355
+ const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
356
+ const ids = ["memory.write", "memory.search", "notes.distill", "wiki", "graph", "security.audit", "route.check", "context.explore"];
357
+ const tools = { "memory.write": "memory_write", "memory.search": "memory_search", "route.check": "route_check", "context.explore": "context_explore" };
358
+ const digest = (snapshot) => createHash("sha256").update(JSON.stringify({
359
+ schema_version: 1,
360
+ contract_enabled: true,
361
+ capabilities: snapshot.capabilities.map(({ id, enabled }) => ({ id, enabled })),
362
+ logging: { callbacks: snapshot.logging.enabled },
363
+ })).digest("hex");
364
+ if (value.schema_version !== 1 || !value.all_enabled || JSON.stringify(Object.keys(value.one_disabled)) !== JSON.stringify(ids)) process.exit(1);
365
+ if (value.all_enabled.configuration_digest !== digest(value.all_enabled)) process.exit(1);
366
+ if (JSON.stringify(value.all_enabled.capabilities.map((row) => row.id)) !== JSON.stringify(ids)) process.exit(1);
367
+ if (JSON.stringify(value.all_enabled.registered_mcp_tools) !== JSON.stringify(Object.values(tools))) process.exit(1);
368
+ for (const id of ids) {
369
+ const snapshot = value.one_disabled[id];
370
+ if (snapshot.configuration_digest !== digest(snapshot) || snapshot.configuration_digest === value.all_enabled.configuration_digest) process.exit(1);
371
+ if (snapshot.logging.enabled !== value.all_enabled.logging.enabled || snapshot.logging.source !== value.all_enabled.logging.source) process.exit(1);
372
+ if (snapshot.capabilities.filter((row) => !row.enabled).map((row) => row.id).join() !== id) process.exit(1);
373
+ for (const row of snapshot.capabilities) {
374
+ const baseline = value.all_enabled.capabilities.find((candidate) => candidate.id === row.id);
375
+ if (!baseline || row.source !== baseline.source || row.restart_required !== baseline.restart_required || row.kind !== baseline.kind) process.exit(1);
376
+ if (row.id !== id && row.enabled !== baseline.enabled) process.exit(1);
377
+ }
378
+ const expectedTools = Object.entries(tools).filter(([capability]) => capability !== id).map(([, tool]) => tool);
379
+ if (JSON.stringify(snapshot.registered_mcp_tools) !== JSON.stringify(expectedTools)) process.exit(1);
380
+ if (tools[id]) {
381
+ if (snapshot.omission_evidence?.tool !== tools[id] || snapshot.omission_evidence?.configuration_digest !== snapshot.configuration_digest) process.exit(1);
382
+ } else if (snapshot.omission_evidence !== null) process.exit(1);
383
+ }
384
+ NODE
385
+ CAIRN_CAPABILITY_CONTRACT=1 node "$ROOT/mcp-memory-server/scripts/smoke-capability-contract.mjs" --baseline >/dev/null
386
+ run_capability "$project" reset --all >/dev/null
387
+ enable_all_capabilities "$project"
388
+ local id output="$tmp/matrix-status.json" matrix_states=1
389
+ run_capability "$project" status --json >"$output"
390
+ node - "$output" "$fixture" <<'NODE'
391
+ const fs = require("fs");
392
+ const [statusPath, fixturePath] = process.argv.slice(2);
393
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
394
+ const expected = JSON.parse(fs.readFileSync(fixturePath, "utf8")).all_enabled;
395
+ if (status.configuration_digest !== expected.configuration_digest) process.exit(1);
396
+ if (JSON.stringify(status.logging) !== JSON.stringify(expected.logging)) process.exit(1);
397
+ if (JSON.stringify(status.capabilities) !== JSON.stringify(expected.capabilities)) process.exit(1);
398
+ NODE
399
+ for id in memory.write memory.search notes.distill wiki graph security.audit route.check context.explore; do
400
+ run_capability "$project" reset --all >/dev/null
401
+ enable_all_capabilities "$project"
402
+ run_capability "$project" disable "$id" >/dev/null
403
+ run_capability "$project" status --json >"$output"
404
+ node - "$output" "$fixture" "$id" <<'NODE'
405
+ const fs = require("fs");
406
+ const [statusPath, fixturePath, disabled] = process.argv.slice(2);
407
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
408
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
409
+ const expected = fixture.one_disabled[disabled];
410
+ if (!expected || status.configuration_digest !== expected.configuration_digest) process.exit(1);
411
+ if (status.capabilities.filter((row) => !row.enabled).map((row) => row.id).join() !== disabled) process.exit(1);
412
+ for (const row of status.capabilities) {
413
+ const wanted = expected.capabilities.find((item) => item.id === row.id);
414
+ if (!wanted || wanted.enabled !== row.enabled || wanted.source !== row.source || wanted.restart_required !== row.restart_required) process.exit(1);
415
+ }
416
+ NODE
417
+ matrix_states=$((matrix_states + 1))
418
+ done
419
+ [[ "$matrix_states" -eq 9 ]] || fail "matrix did not execute exactly one all-enabled and eight one-disabled states"
420
+ echo "PASS: Phase 18 all-enabled and eight one-disabled policy/status matrix (not live owner execution)"
421
+ }
422
+
423
+ run_runtime() {
424
+ node --version | grep -qE '^v(22|24|26)\.' || fail "lifecycle runtime is outside the supported Node matrix"
425
+ bash --version | head -1
426
+ bash -n "$ROOT/scripts/test-phase18-capability-lifecycle.sh"
427
+ echo "PASS: Phase 18 local runtime contract"
428
+ }
429
+
430
+ run_lifecycle() {
431
+ run_contract
432
+ run_matrix
433
+ run_bootstrap
434
+ run_uninstall
435
+ [[ "$RUNTIME" -eq 0 ]] || run_runtime
436
+ echo "PASS: Phase 18 complete capability lifecycle"
437
+ }
438
+
439
+ run_native_boundary() {
440
+ local evidence="$tmp/native-boundary-evidence.log"
441
+ run_matrix
442
+ "$ROOT/scripts/test-phase18-harness-boundary.sh" evidence-scope >"$evidence" || {
443
+ cat "$evidence" >&2
444
+ fail "native delegate-call evidence failed"
445
+ }
446
+ node - "$evidence" <<'NODE' || fail "native boundary evidence overstated its acceptance scope"
447
+ const fs = require("node:fs");
448
+ const lines = fs.readFileSync(process.argv[2], "utf8").trim().split(/\n/);
449
+ const parse = (prefix) => {
450
+ const line = lines.find((candidate) => candidate.startsWith(`${prefix}:`));
451
+ if (!line) process.exit(1);
452
+ return JSON.parse(line.slice(prefix.length + 1));
453
+ };
454
+ const boundary = parse("DETERMINISTIC_BOUNDARY_EVIDENCE");
455
+ const live = parse("PHASE18_REQUIRED_LIVE_MATRIX");
456
+ if (boundary.schema_version !== 1 || boundary.status !== "pass") process.exit(1);
457
+ if (boundary.scope !== "native-delegate-order" || boundary.owner_execution !== "simulated-after-admission") process.exit(1);
458
+ if (live.schema_version !== 1 || live.required_cells !== 56 || live.passing_cells !== 0) process.exit(1);
459
+ if (live.status !== "blocking" || live.acceptance !== false || live.replacement_plan !== "18-27") process.exit(1);
460
+ NODE
461
+ cat "$evidence"
462
+ echo "PASS: deterministic native-boundary coverage; live real-owner acceptance remains blocked"
463
+ }
464
+
465
+ case "$MODE" in
466
+ baseline)
467
+ run_baseline
468
+ ;;
469
+ --expect-red-contract)
470
+ run_baseline
471
+ set +e
472
+ (cd "$tmp" && env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/bin/cairn" capabilities status --json) >"$tmp/red.out" 2>"$tmp/red.err"
473
+ red_status=$?
474
+ set -e
475
+ if [[ "$red_status" -eq 2 ]] && grep -qxF 'Unknown command: capabilities' "$tmp/red.err"; then
476
+ printf '%s\n' "$RED_MARKER" >&2
477
+ exit "$EXPECTED_RED_EXIT"
478
+ fi
479
+ cat "$tmp/red.out" "$tmp/red.err" >&2
480
+ [[ "$red_status" -ne 0 ]] || fail "expected missing capability CLI, but the command succeeded"
481
+ fail "capability CLI failed for a reason other than the absent dispatch surface"
482
+ ;;
483
+ contract) run_contract ;;
484
+ matrix) run_matrix ;;
485
+ operating) run_operating ;;
486
+ bootstrap) run_bootstrap ;;
487
+ uninstall) run_uninstall ;;
488
+ claude-native) run_claude_native ;;
489
+ native-boundary) run_native_boundary ;;
490
+ owner-retirement) run_owner_retirement ;;
491
+ lifecycle) run_lifecycle ;;
492
+ full)
493
+ run_baseline
494
+ run_lifecycle
495
+ HARNESS=claude CAPABILITY=wiki run_operating
496
+ HARNESS=claude CAPABILITY=graph run_operating
497
+ HARNESS=claude CAPABILITY=security.audit run_operating
498
+ HARNESS=opencode-command CAPABILITY=wiki run_operating
499
+ HARNESS=opencode-command CAPABILITY=graph run_operating
500
+ HARNESS=opencode-command CAPABILITY=security.audit run_operating
501
+ HARNESS=opencode-workflow CAPABILITY=wiki run_operating
502
+ HARNESS=opencode-workflow CAPABILITY=security.audit run_operating
503
+ ;;
504
+ esac