@cairnkeep/cli 2.3.0 → 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 +78 -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 +9 -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
@@ -14,6 +14,8 @@ trap cleanup EXIT
14
14
  fail() { echo "FAIL: $1" >&2; exit 1; }
15
15
 
16
16
  doctor="$ROOT/scripts/doctor.sh"
17
+ export HOME="$tmp/home"
18
+ mkdir -p "$HOME"
17
19
  # Clean the inherited env so the test controls what is "configured".
18
20
  unset CAIRN_LLM_API_URL CAIRN_MEMORY_EMBEDDING_URL CAIRN_GIT_PROVIDER CAIRN_AGENTFS_BASE_DIR
19
21
 
@@ -21,6 +23,12 @@ unset CAIRN_LLM_API_URL CAIRN_MEMORY_EMBEDDING_URL CAIRN_GIT_PROVIDER CAIRN_AGEN
21
23
  proj="$tmp/clean"; mkdir -p "$proj"
22
24
  ( cd "$proj" && "$doctor" ) >"$tmp/out1" 2>&1 || fail "doctor exited non-zero with nothing configured:\n$(cat "$tmp/out1")"
23
25
  grep -q "\[SKIP\]" "$tmp/out1" || fail "expected SKIP lines when nothing is configured"
26
+ grep -q "typed metadata and note transaction state" "$tmp/out1" || fail "expected typed/note diagnostics in the public doctor output"
27
+ grep -q "artifact store (not present" "$tmp/out1" || fail "expected absent opt-in artifact diagnostics"
28
+ grep -q "capability configuration (not present — managed overrides are unused)" "$tmp/out1" ||
29
+ fail "expected absent capability configuration diagnostics"
30
+ grep -q "capability callback namespace (not present — callback logging is unused)" "$tmp/out1" ||
31
+ fail "expected absent capability callback diagnostics"
24
32
 
25
33
  # 2. An unsupported runtime is diagnosed before the server probe.
26
34
  mkdir -p "$tmp/old-node"
@@ -106,4 +114,343 @@ EOF
106
114
  fail "expected a functional embedding FAIL line"
107
115
  fi
108
116
 
109
- echo "PASS: cairn doctor (skip unconfigured, fail unreachable, probe embeddings)"
117
+ # 5. Artifact doctor passes a valid store, repairs only derived state, and
118
+ # refuses authoritative full-record corruption without leaking stored values.
119
+ artifact_cli="$ROOT/mcp-memory-server/dist/artifact-cli.js"
120
+ artifact_fixture="$ROOT/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json"
121
+ artifact_project="$tmp/artifact-project"
122
+ mkdir -p "$artifact_project"
123
+ node "$artifact_cli" capture-claude "$artifact_project" --harness-version 2.1.219 <"$artifact_fixture" >/dev/null 2>&1 || \
124
+ fail "could not create the artifact doctor fixture"
125
+ ( cd "$artifact_project" && "$doctor" ) >"$tmp/artifact-valid.out" 2>&1 || \
126
+ fail "doctor rejected a valid artifacts.db:\n$(cat "$tmp/artifact-valid.out")"
127
+ grep -q "\[PASS\] artifact store integrity" "$tmp/artifact-valid.out" || \
128
+ fail "expected the valid artifact PASS line"
129
+
130
+ node --input-type=module - "$ROOT" "$artifact_project" <<'NODE'
131
+ import { join } from "node:path";
132
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
133
+ const agent = await AgentFS.open({ id: "artifacts", path: join(process.argv[3], ".agentfs", "artifacts.db") });
134
+ const fullRows = await agent.kv.list("artifact/full/");
135
+ const dedupeRows = await agent.kv.list("artifact/index/dedupe/");
136
+ if (fullRows.length !== 1 || dedupeRows.length !== 1) throw new Error("missing derived doctor fixture");
137
+ const artifact = fullRows[0].value;
138
+ await agent.kv.delete(dedupeRows[0].key);
139
+ await agent.kv.set("artifact/index/dedupe/orphan-public-doctor", artifact.artifact_id);
140
+ await agent.kv.set(`compaction/sequence/${artifact.session_ref}`, 0);
141
+ await agent.close();
142
+ NODE
143
+ if ( cd "$artifact_project" && "$doctor" ) >"$tmp/artifact-derived.out" 2>&1; then
144
+ fail "doctor should fail before derived artifact repair:\n$(cat "$tmp/artifact-derived.out")"
145
+ fi
146
+ ( cd "$artifact_project" && "$doctor" --repair ) >"$tmp/artifact-repaired.out" 2>&1 || \
147
+ fail "doctor did not repair derived artifact state:\n$(cat "$tmp/artifact-repaired.out")"
148
+ grep -q "\[PASS\] artifact store repaired" "$tmp/artifact-repaired.out" || \
149
+ fail "expected the derived artifact repair PASS line"
150
+
151
+ node --input-type=module - "$ROOT" "$artifact_project" <<'NODE'
152
+ import { join } from "node:path";
153
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
154
+ const agent = await AgentFS.open({ id: "artifacts", path: join(process.argv[3], ".agentfs", "artifacts.db") });
155
+ const fullRows = await agent.kv.list("artifact/full/");
156
+ const dedupeRows = await agent.kv.list("artifact/index/dedupe/");
157
+ if (fullRows.length !== 1 || dedupeRows.length !== 1) throw new Error("derived repair did not restore the exact maps");
158
+ const artifact = fullRows[0].value;
159
+ const sequenceKey = `compaction/sequence/${artifact.session_ref}`;
160
+ if (await agent.kv.get(sequenceKey) !== artifact.content.revision) throw new Error("derived repair did not restore the retained sequence");
161
+ await agent.kv.set(sequenceKey, artifact.content.revision + 8);
162
+ await agent.close();
163
+ NODE
164
+ ( cd "$artifact_project" && "$doctor" --repair ) >"$tmp/artifact-higher-sequence.out" 2>&1 || \
165
+ fail "doctor rejected a valid higher historical sequence:\n$(cat "$tmp/artifact-higher-sequence.out")"
166
+ node --input-type=module - "$ROOT" "$artifact_project" <<'NODE'
167
+ import { join } from "node:path";
168
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
169
+ const agent = await AgentFS.open({ id: "artifacts", path: join(process.argv[3], ".agentfs", "artifacts.db") });
170
+ const [artifact] = (await agent.kv.list("artifact/full/")).map((row) => row.value);
171
+ if (await agent.kv.get(`compaction/sequence/${artifact.session_ref}`) !== artifact.content.revision + 8) {
172
+ throw new Error("doctor lowered a valid higher historical sequence");
173
+ }
174
+ await agent.close();
175
+ NODE
176
+
177
+ node --input-type=module - "$ROOT" "$artifact_project" <<'NODE'
178
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
179
+ const { putArtifact } = await import(process.argv[2] + "/mcp-memory-server/dist/artifact-store.js");
180
+ const { join } = await import("node:path");
181
+ const agent = await AgentFS.open({ id: "artifacts", path: join(process.argv[3], ".agentfs", "artifacts.db") });
182
+ const [stored] = (await agent.kv.list("artifact/full/")).map((row) => row.value);
183
+ await agent.close();
184
+ const { revision: _revision, ...content } = stored.content;
185
+ await putArtifact(process.argv[3], {
186
+ kind: stored.kind,
187
+ session_ref: stored.session_ref,
188
+ media_type: stored.media_type,
189
+ provenance: stored.provenance,
190
+ content: {
191
+ ...content,
192
+ raw_summary: `${content.raw_summary}\nretention-fixture`,
193
+ task_goals: [...content.task_goals, "retention-fixture"],
194
+ },
195
+ }, {
196
+ artifactMaxBytes: 4096,
197
+ sessionMaxBytes: 65536,
198
+ storeMaxBytes: 262144,
199
+ retentionDays: 3650,
200
+ compactionMaxRevisions: 8,
201
+ generatedFileSnapshotMaxBytes: 4096,
202
+ });
203
+ NODE
204
+ node --input-type=module - "$ROOT" "$artifact_project" "$tmp/artifact-full-before.json" <<'NODE'
205
+ import { writeFileSync } from "node:fs";
206
+ import { join } from "node:path";
207
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
208
+ const agent = await AgentFS.open({ id: "artifacts", path: join(process.argv[3], ".agentfs", "artifacts.db") });
209
+ writeFileSync(process.argv[4], JSON.stringify(await agent.kv.list("artifact/full/")));
210
+ await agent.close();
211
+ NODE
212
+ if ( cd "$artifact_project" && CAIRN_COMPACTION_MAX_REVISIONS=1 "$doctor" ) >"$tmp/artifact-retention.out" 2>&1; then
213
+ fail "doctor should fail for retained artifacts over the configured revision limit:\n$(cat "$tmp/artifact-retention.out")"
214
+ fi
215
+ if ( cd "$artifact_project" && CAIRN_COMPACTION_MAX_REVISIONS=1 "$doctor" --repair ) >"$tmp/artifact-retention-repair.out" 2>&1; then
216
+ fail "doctor repair should leave retention violations unresolved:\n$(cat "$tmp/artifact-retention-repair.out")"
217
+ fi
218
+ grep -q "\[FAIL\] artifact store could not be repaired safely" "$tmp/artifact-retention-repair.out" || \
219
+ fail "expected a failed non-destructive retention repair result"
220
+ node --input-type=module - "$ROOT" "$artifact_project" "$tmp/artifact-full-before.json" <<'NODE'
221
+ import { readFileSync } from "node:fs";
222
+ import { join } from "node:path";
223
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
224
+ const agent = await AgentFS.open({ id: "artifacts", path: join(process.argv[3], ".agentfs", "artifacts.db") });
225
+ const after = JSON.stringify(await agent.kv.list("artifact/full/"));
226
+ await agent.close();
227
+ if (after !== readFileSync(process.argv[4], "utf8")) throw new Error("doctor repair changed authoritative rows for a retention violation");
228
+ NODE
229
+
230
+ node --input-type=module - "$ROOT" "$artifact_project" <<'NODE'
231
+ import { join } from "node:path";
232
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
233
+ const agent = await AgentFS.open({ id: "artifacts", path: join(process.argv[3], ".agentfs", "artifacts.db") });
234
+ const rows = await agent.kv.list("artifact/full/");
235
+ if (rows.length === 0) throw new Error("missing authoritative record fixture");
236
+ await agent.kv.set(rows[0].key, { ...rows[0].value, content_digest: "0".repeat(64) });
237
+ await agent.close();
238
+ NODE
239
+ if ( cd "$artifact_project" && "$doctor" --repair ) >"$tmp/artifact-authoritative.out" 2>&1; then
240
+ fail "doctor should reject authoritative artifact corruption:\n$(cat "$tmp/artifact-authoritative.out")"
241
+ fi
242
+ grep -q "\[FAIL\] artifact store could not be repaired safely" "$tmp/artifact-authoritative.out" || \
243
+ fail "expected the authoritative artifact corruption FAIL line"
244
+ if grep -qF 'compact_summary' "$tmp/artifact-authoritative.out"; then
245
+ fail "artifact doctor leaked a stored value"
246
+ fi
247
+
248
+ # 6. Capability diagnostics consume only the private fixed doctor envelope and
249
+ # map config/store states without exposing malformed values or callback rows.
250
+ capability_valid="$tmp/capability-valid"
251
+ mkdir -p "$capability_valid/.ai"
252
+ printf '%s\n' '{"schema_version":1,"capabilities":{},"logging":{}}' >"$capability_valid/.ai/capabilities.json"
253
+ chmod 600 "$capability_valid/.ai/capabilities.json"
254
+ ( cd "$capability_valid" && "$doctor" ) >"$tmp/capability-valid.out" 2>&1 ||
255
+ fail "doctor rejected valid capability state:\n$(cat "$tmp/capability-valid.out")"
256
+ grep -q "\[PASS\] capability configuration schema and permissions are valid" "$tmp/capability-valid.out" ||
257
+ fail "expected valid capability configuration PASS"
258
+
259
+ capability_warn="$tmp/capability-warn"
260
+ mkdir -p "$capability_warn/.ai"
261
+ printf '%s\n' '{"schema_version":1,"capabilities":{"memory.write":"PHASE18_CONFIG_VALUE_SENTINEL","PHASE18_UNKNOWN_ID_SENTINEL":true},"logging":{"callbacks":"PHASE18_LOGGING_VALUE_SENTINEL"}}' >"$capability_warn/.ai/capabilities.json"
262
+ chmod 600 "$capability_warn/.ai/capabilities.json"
263
+ ( cd "$capability_warn" && "$doctor" ) >"$tmp/capability-warn.out" 2>&1 ||
264
+ fail "doctor treated row-local capability warnings as fatal:\n$(cat "$tmp/capability-warn.out")"
265
+ grep -q "\[WARN\] capability configuration has an invalid value for memory.write" "$tmp/capability-warn.out" ||
266
+ fail "expected canonical capability warning"
267
+ grep -q "\[WARN\] capability configuration has an invalid value for logging.callbacks" "$tmp/capability-warn.out" ||
268
+ fail "expected fixed callback setting warning"
269
+ grep -q "\[WARN\] capability configuration contains an unknown override ID" "$tmp/capability-warn.out" ||
270
+ fail "expected value-free unknown-ID warning"
271
+ if grep -Eq 'PHASE18_(CONFIG_VALUE|UNKNOWN_ID|LOGGING_VALUE)_SENTINEL' "$tmp/capability-warn.out"; then
272
+ fail "capability doctor exposed malformed configuration contents"
273
+ fi
274
+
275
+ capability_unsafe="$tmp/capability-unsafe"
276
+ mkdir -p "$capability_unsafe/.ai"
277
+ printf '%s\n' '{"schema_version":2,"capabilities":{"wiki":false},"logging":{}}' >"$capability_unsafe/.ai/capabilities.json"
278
+ chmod 644 "$capability_unsafe/.ai/capabilities.json"
279
+ if ( cd "$capability_unsafe" && "$doctor" ) >"$tmp/capability-unsafe.out" 2>&1; then
280
+ fail "doctor accepted unsafe or unsupported capability configuration"
281
+ fi
282
+ grep -q "\[FAIL\] capability configuration is invalid or uses an unsupported schema; preserve .ai/capabilities.json before reset" "$tmp/capability-unsafe.out" ||
283
+ fail "expected unsupported-schema backup guidance"
284
+ grep -q "\[FAIL\] capability configuration type or permissions are unsafe; preserve .ai/capabilities.json before recovery" "$tmp/capability-unsafe.out" ||
285
+ fail "expected unsafe-permissions backup guidance"
286
+
287
+ callback_project="$tmp/capability-callbacks"
288
+ mkdir -p "$callback_project"
289
+ node --input-type=module - "$ROOT" "$callback_project" <<'NODE'
290
+ const { appendCapabilityRecord } = await import(process.argv[2] + "/mcp-memory-server/dist/capability-store.js");
291
+ const now = new Date().toISOString();
292
+ await appendCapabilityRecord(process.argv[3], {
293
+ schema_version: 1,
294
+ capability_id: "wiki",
295
+ invocation_id: "cap:11111111-1111-4111-8111-111111111111",
296
+ correlation_id: "cairn:doctor-fixture",
297
+ harness: "other",
298
+ source: "operating-command",
299
+ transport: "local-process",
300
+ started_at: now,
301
+ finished_at: now,
302
+ duration_ms: 0,
303
+ outcome: "success",
304
+ state_source: "project",
305
+ configuration_digest: "1".repeat(64),
306
+ });
307
+ NODE
308
+ ( cd "$callback_project" && "$doctor" ) >"$tmp/callback-valid.out" 2>&1 ||
309
+ fail "doctor rejected a valid capability callback namespace:\n$(cat "$tmp/callback-valid.out")"
310
+ grep -q "\[PASS\] capability callback namespace schema and SQLite integrity are valid" "$tmp/callback-valid.out" ||
311
+ fail "expected valid capability callback PASS"
312
+ if grep -qF 'doctor-fixture' "$tmp/callback-valid.out"; then
313
+ fail "capability doctor exposed a callback record"
314
+ fi
315
+
316
+ node --input-type=module - "$ROOT" "$callback_project" <<'NODE'
317
+ import { join } from "node:path";
318
+ const { AgentFS } = await import(process.argv[2] + "/mcp-memory-server/node_modules/agentfs-sdk/dist/index_node.js");
319
+ const agent = await AgentFS.open({ id: "trajectory", path: join(process.argv[3], ".agentfs", "trajectory.db") });
320
+ await agent.kv.set("capability-callback/meta/schema-version", 2);
321
+ await agent.close();
322
+ NODE
323
+ cp "$callback_project/.agentfs/trajectory.db" "$tmp/callback-unsupported.before"
324
+ if ( cd "$callback_project" && "$doctor" ) >"$tmp/callback-unsupported.out" 2>&1; then
325
+ fail "doctor accepted an unsupported callback namespace"
326
+ fi
327
+ grep -q "\[FAIL\] capability callback namespace schema is unsupported; preserve .agentfs/trajectory.db before recovery" "$tmp/callback-unsupported.out" ||
328
+ fail "expected unsupported callback schema guidance"
329
+ if grep -qF 'doctor-fixture' "$tmp/callback-unsupported.out"; then
330
+ fail "unsupported callback diagnostics exposed a stored record"
331
+ fi
332
+ cmp -s "$tmp/callback-unsupported.before" "$callback_project/.agentfs/trajectory.db" ||
333
+ fail "callback diagnostics mutated an unsupported namespace"
334
+
335
+ callback_corrupt="$tmp/capability-callback-corrupt"
336
+ mkdir -p "$callback_corrupt/.agentfs"
337
+ printf '%s\n' 'PHASE18_CALLBACK_ROW_SENTINEL' >"$callback_corrupt/.agentfs/trajectory.db"
338
+ cp "$callback_corrupt/.agentfs/trajectory.db" "$tmp/callback-corrupt.before"
339
+ if ( cd "$callback_corrupt" && "$doctor" ) >"$tmp/callback-corrupt.out" 2>&1; then
340
+ fail "doctor accepted SQLite callback-store corruption"
341
+ fi
342
+ grep -q "\[FAIL\] capability callback namespace could not be diagnosed safely; preserve .agentfs/trajectory.db before recovery" "$tmp/callback-corrupt.out" ||
343
+ fail "expected value-free callback corruption guidance"
344
+ if grep -qF 'PHASE18_CALLBACK_ROW_SENTINEL' "$tmp/callback-corrupt.out"; then
345
+ fail "callback corruption diagnostics exposed stored bytes"
346
+ fi
347
+ cmp -s "$tmp/callback-corrupt.before" "$callback_corrupt/.agentfs/trajectory.db" ||
348
+ fail "callback diagnostics mutated a corrupt database"
349
+
350
+ # 7. Evaluation diagnostics consume only the private two-key envelope, map all
351
+ # five report states, and collapse malformed CLI output without reflection.
352
+ create_eval_report_fixture() {
353
+ local project="$1"
354
+ local state="$2"
355
+ mkdir -p "$project"
356
+ node --input-type=module - "$ROOT" "$project" "$state" <<'NODE'
357
+ import { chmod, symlink, writeFile } from "node:fs/promises";
358
+ import { join } from "node:path";
359
+
360
+ const root = process.argv[2];
361
+ const project = process.argv[3];
362
+ const state = process.argv[4];
363
+ const reportApi = await import(`${root}/mcp-memory-server/dist/eval-report.js`);
364
+ const store = await reportApi.createEvalReportStore({ project_root: project, experiment_id: "fixture-experiment" });
365
+ if (state === "unsafe") {
366
+ const target = join(project, "environment-sentinel");
367
+ await writeFile(target, "environment-sentinel\n", { mode: 0o600 });
368
+ await symlink(target, store.report_path);
369
+ process.exit(0);
370
+ }
371
+ if (state === "tampered") {
372
+ await writeFile(store.report_path, '{"adapter-stderr":"adapter-stderr-sentinel"}\n', { mode: 0o600 });
373
+ await chmod(store.report_path, 0o600);
374
+ process.exit(0);
375
+ }
376
+ const report = {
377
+ schema_version: 1,
378
+ experiment_id: store.experiment_id,
379
+ status: state === "partial" ? "partial" : "final",
380
+ experiment_kind: "two_pass",
381
+ task_set_digest: "0".repeat(64), adapter_config_digest: "1".repeat(64),
382
+ source_revision: "2".repeat(40), schedule_digest: "3".repeat(64),
383
+ created_at: "2026-01-01T00:00:00.000Z", updated_at: "2026-01-01T00:00:01.000Z",
384
+ runtime: { platform: "linux", arch: "x64", node: "22.0.0", cairnkeep: "0.0.0" },
385
+ schedule: [], observations: [], aggregates: [],
386
+ missingness: { digest: "4".repeat(64), count: 0, reasons: [] }, warnings: [],
387
+ evidence: {
388
+ schema_version: 1, evidence_scope: "offline-framework", source_commit: "2".repeat(40),
389
+ package_version: "0.0.0", runtime_id: "node-22-linux-x64", task_set_digest: "0".repeat(64),
390
+ report_digest: "5".repeat(64), schema_digests: ["6".repeat(64), "7".repeat(64)], note_snapshot_digests: [],
391
+ missingness_digest: "4".repeat(64), claim_anchors: [],
392
+ },
393
+ };
394
+ await reportApi.checkpointEvalReport(store, report);
395
+ NODE
396
+ }
397
+
398
+ for state in ok partial tampered unsafe; do
399
+ eval_project="$tmp/eval-$state"
400
+ create_eval_report_fixture "$eval_project" "$state"
401
+ ( cd "$eval_project" && env -u CAIRN_EVAL "$doctor" ) >"$tmp/eval-$state.out" 2>"$tmp/eval-$state.err" ||
402
+ fail "doctor rejected value-free evaluation state $state"
403
+ done
404
+ grep -q "\[PASS\] evaluation reports are complete and valid" "$tmp/eval-ok.out" ||
405
+ fail "expected complete evaluation report PASS"
406
+ grep -q "\[WARN\] evaluation reports are partial" "$tmp/eval-partial.out" ||
407
+ fail "expected partial evaluation report warning"
408
+ grep -q "\[WARN\] evaluation report integrity check failed" "$tmp/eval-tampered.out" ||
409
+ fail "expected tampered evaluation report warning"
410
+ grep -q "\[WARN\] evaluation report storage is unsafe" "$tmp/eval-unsafe.out" ||
411
+ fail "expected unsafe evaluation report warning"
412
+ grep -q "\[SKIP\] evaluation reports (not present — evaluation is opt-in)" "$tmp/out1" ||
413
+ fail "expected disabled/absent evaluation report health result"
414
+
415
+ real_node=$(command -v node)
416
+ mkdir -p "$tmp/eval-node-shim"
417
+ cat >"$tmp/eval-node-shim/node" <<'EOF'
418
+ #!/usr/bin/env bash
419
+ if [[ "${1:-}" == */eval-cli.js && "${2:-}" == doctor-diagnosis ]]; then
420
+ printf '%s\n' "$CAIRN_TEST_EVAL_OUTPUT"
421
+ [[ -z "${CAIRN_TEST_EVAL_STDERR:-}" ]] || printf '%s\n' "$CAIRN_TEST_EVAL_STDERR" >&2
422
+ exit "${CAIRN_TEST_EVAL_EXIT:-0}"
423
+ fi
424
+ exec "$CAIRN_TEST_REAL_NODE" "$@"
425
+ EOF
426
+ chmod 755 "$tmp/eval-node-shim/node"
427
+
428
+ oversized=$(printf '%01024d' 0)
429
+ for malformed_output in \
430
+ '{"schema_version":1,"diagnosis":"unknown"}' \
431
+ '{"schema_version":1,"diagnosis":"ok","prompt":"prompt-sentinel"}' \
432
+ "$oversized"; do
433
+ ( cd "$proj" && PATH="$tmp/eval-node-shim:$PATH" CAIRN_TEST_REAL_NODE="$real_node" \
434
+ CAIRN_TEST_EVAL_OUTPUT="$malformed_output" "$doctor" ) >"$tmp/eval-malformed.out" 2>"$tmp/eval-malformed.err" ||
435
+ fail "doctor treated malformed private diagnosis output as a configured dependency failure"
436
+ grep -q "\[WARN\] evaluation report storage is unsafe" "$tmp/eval-malformed.out" ||
437
+ fail "malformed private diagnosis output did not fail closed"
438
+ if grep -Eq 'prompt-sentinel|model-output-sentinel' "$tmp/eval-malformed.out" "$tmp/eval-malformed.err"; then
439
+ fail "malformed private diagnosis output reflected fixture bytes"
440
+ fi
441
+ done
442
+
443
+ ( cd "$proj" && PATH="$tmp/eval-node-shim:$PATH" CAIRN_TEST_REAL_NODE="$real_node" \
444
+ CAIRN_TEST_EVAL_OUTPUT='' CAIRN_TEST_EVAL_STDERR='model-output-sentinel' CAIRN_TEST_EVAL_EXIT=9 "$doctor" ) \
445
+ >"$tmp/eval-error.out" 2>"$tmp/eval-error.err" ||
446
+ fail "doctor treated private diagnosis execution failure as a configured dependency failure"
447
+ grep -q "\[WARN\] evaluation report storage is unsafe" "$tmp/eval-error.out" ||
448
+ fail "private diagnosis execution failure did not fail closed"
449
+
450
+ for sentinel in prompt-sentinel model-output-sentinel adapter-stderr-sentinel environment-sentinel; do
451
+ if grep -F "$sentinel" "$tmp"/eval-*.out "$tmp"/eval-*.err >/dev/null; then
452
+ fail "evaluation doctor exposed $sentinel"
453
+ fi
454
+ done
455
+
456
+ echo "PASS: cairn doctor (existing probes plus value-free capability/evaluation diagnostics)"
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bash
2
+ # Installed-style public and private evaluation CLI contract.
3
+ set -euo pipefail
4
+
5
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
6
+ MODE="${1:-baseline}"
7
+ [[ $# -gt 0 ]] && shift
8
+
9
+ usage() {
10
+ echo 'Usage: test-eval-cli.sh [--expected-red|--doctor-diagnosis]' >&2
11
+ }
12
+ fail() { echo "FAIL: $1" >&2; exit 1; }
13
+
14
+ case "$MODE" in
15
+ baseline|--expected-red|--doctor-diagnosis) [[ $# -eq 0 ]] || { usage; exit 2; } ;;
16
+ *) usage; exit 2 ;;
17
+ esac
18
+
19
+ tmp=$(mktemp -d)
20
+ trap 'rm -rf "$tmp"' EXIT
21
+ cli="$ROOT/mcp-memory-server/dist/eval-cli.js"
22
+
23
+ baseline() {
24
+ bash -n "$ROOT/scripts/test-eval-cli.sh"
25
+ "$ROOT/bin/cairn" help >/dev/null
26
+ [[ ! -e "$tmp/project" && ! -e "$tmp/output" && ! -e "$tmp/database" && ! -e "$tmp/network" ]] || \
27
+ fail "baseline created a side-effect sentinel"
28
+ # Once the compiled owner lands, default execution exercises the installed-
29
+ # style surface while Wave 0 remains green without changing root composition.
30
+ if [[ -f "$cli" ]]; then
31
+ node "$cli" --help >"$tmp/help"
32
+ for operation in validate run ablate report prune delete; do
33
+ grep -q "$operation" "$tmp/help" || fail "help omitted $operation"
34
+ done
35
+ grep -q 'doctor-diagnosis' "$tmp/help" && fail "public help exposed private diagnosis"
36
+ env -u CAIRN_EVAL node "$cli" validate --task-set "$tmp/unread-task-set" --adapter "$tmp/unread-adapter" --output "$tmp/unwritten" --json >"$tmp/disabled.json"
37
+ node -e 'const v=require(process.argv[1]);if(v.schema_version!==1||v.enabled!==false)process.exit(1)' "$tmp/disabled.json" \
38
+ || fail "disabled JSON is not stable"
39
+ [[ ! -e "$tmp/unwritten" ]] || fail "disabled CLI created output"
40
+ if node "$cli" unknown-operation >"$tmp/unknown.out" 2>"$tmp/unknown.err"; then
41
+ fail "unknown operation succeeded"
42
+ fi
43
+ fi
44
+ echo "PASS: Phase 19 eval CLI Wave 0/default contract"
45
+ }
46
+
47
+ doctor_diagnosis() {
48
+ [[ -f "$cli" ]] || fail "compiled eval CLI is absent"
49
+
50
+ create_report_fixture() {
51
+ local project="$1"
52
+ local state="$2"
53
+ mkdir -p "$project"
54
+ node --input-type=module - "$ROOT" "$project" "$state" <<'NODE'
55
+ import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
56
+ import { join } from "node:path";
57
+
58
+ const root = process.argv[2];
59
+ const project = process.argv[3];
60
+ const state = process.argv[4];
61
+ const reportApi = await import(`${root}/mcp-memory-server/dist/eval-report.js`);
62
+ const store = await reportApi.createEvalReportStore({
63
+ project_root: project,
64
+ experiment_id: "fixture-experiment",
65
+ });
66
+ if (state === "unsafe") {
67
+ const target = join(project, "unsafe-target");
68
+ await writeFile(target, "absolute-path-sentinel\n", { mode: 0o600 });
69
+ await symlink(target, store.report_path);
70
+ process.exit(0);
71
+ }
72
+ if (state === "tampered") {
73
+ await writeFile(store.report_path, '{"prompt":"prompt-sentinel"}\n', { mode: 0o600 });
74
+ await chmod(store.report_path, 0o600);
75
+ process.exit(0);
76
+ }
77
+ const report = {
78
+ schema_version: 1,
79
+ experiment_id: store.experiment_id,
80
+ status: state === "partial" ? "partial" : "final",
81
+ experiment_kind: "two_pass",
82
+ task_set_digest: "0".repeat(64),
83
+ adapter_config_digest: "1".repeat(64),
84
+ source_revision: "2".repeat(40),
85
+ schedule_digest: "3".repeat(64),
86
+ created_at: "2026-01-01T00:00:00.000Z",
87
+ updated_at: "2026-01-01T00:00:01.000Z",
88
+ runtime: { platform: "linux", arch: "x64", node: "22.0.0", cairnkeep: "0.0.0" },
89
+ schedule: [], observations: [], aggregates: [],
90
+ missingness: { digest: "4".repeat(64), count: 0, reasons: [] },
91
+ warnings: [],
92
+ evidence: {
93
+ schema_version: 1,
94
+ evidence_scope: "offline-framework",
95
+ source_commit: "2".repeat(40),
96
+ package_version: "0.0.0",
97
+ runtime_id: "node-22-linux-x64",
98
+ task_set_digest: "0".repeat(64),
99
+ report_digest: "5".repeat(64),
100
+ schema_digests: ["6".repeat(64), "7".repeat(64)],
101
+ note_snapshot_digests: [],
102
+ missingness_digest: "4".repeat(64),
103
+ claim_anchors: [],
104
+ },
105
+ };
106
+ await reportApi.checkpointEvalReport(store, report);
107
+ NODE
108
+ }
109
+
110
+ for state in absent ok partial tampered unsafe; do
111
+ project="$tmp/$state"
112
+ mkdir -p "$project"
113
+ [[ "$state" == "absent" ]] || create_report_fixture "$project" "$state"
114
+ env -u CAIRN_EVAL node "$cli" doctor-diagnosis --root "$project" --json >"$tmp/$state.json" 2>"$tmp/$state.err"
115
+ node - "$tmp/$state.json" "$state" <<'NODE'
116
+ const value = require(process.argv[2]);
117
+ if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(["diagnosis", "schema_version"])) process.exit(1);
118
+ if (value.schema_version !== 1 || value.diagnosis !== process.argv[3]) process.exit(1);
119
+ NODE
120
+ [[ ! -s "$tmp/$state.err" ]] || fail "diagnosis wrote diagnostics for $state"
121
+ [[ $(wc -c <"$tmp/$state.json") -le 64 ]] || fail "diagnosis output exceeded its fixed bound"
122
+ done
123
+
124
+ mkdir -p "$tmp/symlink-target"
125
+ mkdir -p "$tmp/symlink-root"
126
+ ln -s "$tmp/symlink-target" "$tmp/symlink-root/.agentfs"
127
+ env -u CAIRN_EVAL node "$cli" doctor-diagnosis --root "$tmp/symlink-root" --json >"$tmp/symlink-root.json"
128
+ node -e 'const v=require(process.argv[1]);if(v.schema_version!==1||v.diagnosis!=="unsafe"||Object.keys(v).length!==2)process.exit(1)' \
129
+ "$tmp/symlink-root.json" || fail "symlinked evaluation ancestor did not fail closed"
130
+
131
+ for malformed in \
132
+ "doctor-diagnosis --json" \
133
+ "doctor-diagnosis --root prompt-sentinel" \
134
+ "doctor-diagnosis --root $tmp/absent --json --extra model-output-sentinel"; do
135
+ # shellcheck disable=SC2086
136
+ env -u CAIRN_EVAL node "$cli" $malformed >"$tmp/malformed.json" 2>"$tmp/malformed.err" ||
137
+ fail "malformed diagnosis invocation did not collapse safely"
138
+ node -e 'const v=require(process.argv[1]);if(JSON.stringify(v)!==JSON.stringify({schema_version:1,diagnosis:"unsafe"}))process.exit(1)' \
139
+ "$tmp/malformed.json" || fail "malformed diagnosis invocation returned a non-canonical envelope"
140
+ [[ ! -s "$tmp/malformed.err" ]] || fail "malformed diagnosis invocation exposed stderr"
141
+ done
142
+
143
+ help_and_completions="$tmp/public-vocabulary"
144
+ {
145
+ node "$cli" --help
146
+ sed -n '1,220p' "$ROOT/scripts/completion.sh"
147
+ } >"$help_and_completions"
148
+ ! grep -qF 'doctor-diagnosis' "$help_and_completions" || fail "private diagnosis entered public discovery"
149
+
150
+ for sentinel in prompt-sentinel model-output-sentinel adapter-stderr-sentinel environment-sentinel absolute-path-sentinel; do
151
+ ! grep -F "$sentinel" "$tmp"/*.json "$tmp"/*.err >/dev/null || fail "diagnosis reflected $sentinel"
152
+ done
153
+ echo "PASS: Phase 19 private value-free five-state diagnosis contract"
154
+ }
155
+
156
+ case "$MODE" in
157
+ --expected-red)
158
+ baseline >/dev/null
159
+ set +e
160
+ node --input-type=module - "$cli" >"$tmp/red.out" 2>"$tmp/red.err" <<'NODE'
161
+ import { pathToFileURL } from "node:url";
162
+ await import(pathToFileURL(process.argv[2]).href);
163
+ NODE
164
+ status=$?
165
+ set -e
166
+ if [[ "$status" -ne 0 ]] && grep -qF '/dist/eval-cli.js' "$tmp/red.err" && grep -qF 'ERR_MODULE_NOT_FOUND' "$tmp/red.err"; then
167
+ echo 'PHASE19_RED:EVAL_CLI_MISSING'
168
+ exit 0
169
+ fi
170
+ cat "$tmp/red.out" "$tmp/red.err" >&2
171
+ fail "expected only the missing compiled eval CLI"
172
+ ;;
173
+ --doctor-diagnosis) doctor_diagnosis ;;
174
+ baseline) baseline ;;
175
+ esac