@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
package/scripts/doctor.sh CHANGED
@@ -9,6 +9,14 @@
9
9
  set -uo pipefail
10
10
 
11
11
  CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
12
+ REPAIR_STORES=0
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --repair) REPAIR_STORES=1; shift ;;
16
+ -h|--help) echo "Usage: cairn doctor [--repair] # repair derived trajectory/artifact/typed metadata, indexes, and interrupted note transactions"; exit 0 ;;
17
+ *) echo "Unknown doctor option: $1" >&2; echo "Usage: cairn doctor [--repair]" >&2; exit 2 ;;
18
+ esac
19
+ done
12
20
 
13
21
  # Load the project's .ai/.env if present (does not override already-set env).
14
22
  if [[ -f "$PWD/.ai/.env" ]]; then
@@ -116,6 +124,282 @@ else
116
124
  warn "sqlite3 not found — memory export unavailable (runtime and import unaffected)"
117
125
  fi
118
126
 
127
+ # 7. Project-local trajectory store. Absence is healthy because capture is
128
+ # opt-in. Existing stores must pass SQLite, schema and index checks. Repair
129
+ # is explicit and only reconstructs metadata/indexes from valid full records.
130
+ trajectory_cli="$CAIRN_ROOT/mcp-memory-server/dist/trajectory-cli.js"
131
+ trajectory_dir="$PWD/.agentfs"
132
+ if [[ -d "$trajectory_dir" && ! -w "$trajectory_dir" ]]; then
133
+ fail "trajectory store directory is not writable: $trajectory_dir"
134
+ elif [[ ! -d "$trajectory_dir" && ! -w "$PWD" ]]; then
135
+ fail "trajectory store parent is not writable: $PWD"
136
+ elif [[ ! -f "$trajectory_cli" ]]; then
137
+ fail "trajectory diagnostics unavailable — rebuild mcp-memory-server"
138
+ else
139
+ trajectory_args=(doctor --json)
140
+ [[ $REPAIR_STORES -eq 1 ]] && trajectory_args+=(--repair)
141
+ trajectory_json=$(node "$trajectory_cli" "${trajectory_args[@]}" 2>/dev/null)
142
+ trajectory_status=$?
143
+ trajectory_state=$(node -e '
144
+ try {
145
+ const value = JSON.parse(process.argv[1])
146
+ if (!value.exists) process.stdout.write("absent")
147
+ else if (value.ok && value.repaired) process.stdout.write("repaired")
148
+ else if (value.ok) process.stdout.write("ok")
149
+ else process.stdout.write("broken")
150
+ } catch { process.stdout.write("invalid") }
151
+ ' "$trajectory_json" 2>/dev/null)
152
+ case "$trajectory_state" in
153
+ absent) skip "trajectory store (not present — capture is opt-in)" ;;
154
+ repaired) pass "trajectory store repaired (metadata/indexes rebuilt; full records preserved)" ;;
155
+ ok) pass "trajectory store integrity, schema and indexes are valid" ;;
156
+ *)
157
+ if [[ $REPAIR_STORES -eq 1 ]]; then
158
+ fail "trajectory store could not be repaired safely; preserve .agentfs/trajectory.db and inspect it manually"
159
+ else
160
+ fail "trajectory store metadata/schema/index check failed — run: cairn doctor --repair"
161
+ fi
162
+ ;;
163
+ esac
164
+ [[ $trajectory_status -eq 0 || "$trajectory_state" == "broken" ]] || true
165
+ fi
166
+
167
+ # 8. Project-local artifact store. Absence is healthy because compaction
168
+ # continuity and explicit artifact access are both opt-in. Repair may only
169
+ # rebuild derived indexes, dedupe rows, and latest pointers from valid full
170
+ # records; authoritative schema, digest, full-record, or SQLite corruption
171
+ # is never modified automatically.
172
+ artifact_cli="$CAIRN_ROOT/mcp-memory-server/dist/artifact-cli.js"
173
+ if [[ ! -f "$artifact_cli" ]]; then
174
+ fail "artifact diagnostics unavailable — rebuild mcp-memory-server"
175
+ else
176
+ artifact_args=(doctor --json)
177
+ [[ $REPAIR_STORES -eq 1 ]] && artifact_args+=(--repair)
178
+ artifact_json=$(node "$artifact_cli" "${artifact_args[@]}" 2>/dev/null)
179
+ artifact_status=$?
180
+ artifact_state=$(node -e '
181
+ try {
182
+ const value = JSON.parse(process.argv[1])
183
+ if (!value.exists) process.stdout.write("absent")
184
+ else if (value.ok && value.repaired) process.stdout.write("repaired")
185
+ else if (value.ok) process.stdout.write("ok")
186
+ else process.stdout.write("broken")
187
+ } catch { process.stdout.write("invalid") }
188
+ ' "$artifact_json" 2>/dev/null)
189
+ case "$artifact_state" in
190
+ absent) skip "artifact store (not present — compaction capture and artifact access are opt-in)" ;;
191
+ repaired) pass "artifact store repaired (derived indexes, dedupe rows, and pointers rebuilt; authoritative records preserved)" ;;
192
+ ok) pass "artifact store integrity, schema, digests, indexes, pointers, and retention state are valid" ;;
193
+ *)
194
+ if [[ $REPAIR_STORES -eq 1 ]]; then
195
+ fail "artifact store could not be repaired safely; preserve .agentfs/artifacts.db and inspect authoritative corruption manually"
196
+ else
197
+ fail "artifact store authoritative/schema/digest/index check failed — preserve .agentfs/artifacts.db, then run: cairn doctor --repair"
198
+ fi
199
+ ;;
200
+ esac
201
+ [[ $artifact_status -eq 0 || "$artifact_state" == "broken" ]] || true
202
+ fi
203
+
204
+ # 9. Typed metadata overlays and canonical-note transaction state. The internal
205
+ # JSON seam enumerates only contained local databases and the one notes root;
206
+ # raw KV cells and unmarked Markdown bytes are never repair inputs.
207
+ node_cli="$CAIRN_ROOT/mcp-memory-server/dist/node-cli.js"
208
+ if [[ ! -f "$node_cli" ]]; then
209
+ fail "typed-node diagnostics unavailable — rebuild mcp-memory-server"
210
+ else
211
+ node_args=(doctor --project-root "$PWD")
212
+ [[ $REPAIR_STORES -eq 1 ]] && node_args+=(--repair)
213
+ node_json=$(node "$node_cli" "${node_args[@]}" 2>/dev/null)
214
+ node_status=$?
215
+ node_state=$(node -e '
216
+ try {
217
+ const value = JSON.parse(process.argv[1])
218
+ if (!value.exists) process.stdout.write("absent")
219
+ else if (value.ok && value.repaired) process.stdout.write("repaired")
220
+ else if (value.ok) process.stdout.write("ok")
221
+ else process.stdout.write("broken")
222
+ } catch { process.stdout.write("invalid") }
223
+ ' "$node_json" 2>/dev/null)
224
+ case "$node_state" in
225
+ absent) skip "typed metadata and note transaction state (not present)" ;;
226
+ repaired) pass "typed metadata/indexes and note transactions repaired safely" ;;
227
+ ok) pass "typed metadata, replay state, and note transactions are valid" ;;
228
+ *)
229
+ if [[ $REPAIR_STORES -eq 1 ]]; then
230
+ fail "typed/note state could not be repaired safely; preserve the memory store and recovery journals before manual inspection"
231
+ else
232
+ fail "typed metadata or note transaction check failed — run: cairn doctor --repair"
233
+ fi
234
+ ;;
235
+ esac
236
+ [[ $node_status -eq 0 || "$node_state" == "broken" ]] || true
237
+ fi
238
+
239
+ # 10. Managed capability configuration and the independent callback namespace.
240
+ # The TypeScript doctor owns all config/database reads. This shell layer accepts
241
+ # only its fixed states, codes, canonical IDs, and setting name; raw values,
242
+ # records, paths, parser errors, and SQLite details never cross the boundary.
243
+ capability_cli="$CAIRN_ROOT/mcp-memory-server/dist/capability-cli.js"
244
+ if [[ ! -f "$capability_cli" ]]; then
245
+ fail "capability diagnostics unavailable — rebuild mcp-memory-server"
246
+ else
247
+ capability_json=$(node "$capability_cli" doctor --json 2>/dev/null)
248
+ capability_status=$?
249
+ if ! capability_mapped=$(node - "$capability_json" <<'NODE'
250
+ const ids = new Set([
251
+ "memory.write", "memory.search", "notes.distill", "wiki",
252
+ "graph", "security.audit", "route.check", "context.explore",
253
+ ]);
254
+ const configCodes = new Set([
255
+ "unknown-capability", "invalid-capability-value", "invalid-logging-value",
256
+ "invalid-config", "unsafe-permissions",
257
+ ]);
258
+ const callbackCodes = new Set([
259
+ "sqlite-integrity-failed", "schema-missing", "schema-unsupported",
260
+ "invalid-record", "diagnostic-failed",
261
+ ]);
262
+ try {
263
+ const value = JSON.parse(process.argv[2]);
264
+ const configuration = value.configuration;
265
+ const callbacks = value.callbacks;
266
+ if (value.schema_version !== 1
267
+ || !configuration || configuration.name !== ".ai/capabilities.json"
268
+ || !callbacks || callbacks.name !== ".agentfs/trajectory.db"
269
+ || typeof configuration.exists !== "boolean"
270
+ || typeof callbacks.exists !== "boolean"
271
+ || !["PASS", "WARN", "FAIL"].includes(configuration.state)
272
+ || !["PASS", "FAIL"].includes(callbacks.state)
273
+ || !Array.isArray(configuration.issues)
274
+ || !Array.isArray(callbacks.issues)) throw new Error("invalid");
275
+
276
+ const configRows = configuration.issues.map((issue) => {
277
+ if (!issue || typeof issue !== "object" || !configCodes.has(issue.code)) throw new Error("invalid");
278
+ if (issue.code === "invalid-capability-value") {
279
+ if (!ids.has(issue.capability_id) || issue.setting !== undefined) throw new Error("invalid");
280
+ return [issue.code, issue.capability_id];
281
+ }
282
+ if (issue.code === "invalid-logging-value") {
283
+ if (issue.setting !== "logging.callbacks" || issue.capability_id !== undefined) throw new Error("invalid");
284
+ return [issue.code, issue.setting];
285
+ }
286
+ if (issue.capability_id !== undefined || issue.setting !== undefined) throw new Error("invalid");
287
+ return [issue.code, "-"];
288
+ });
289
+ if ((configuration.state === "PASS") !== (configRows.length === 0)) throw new Error("invalid");
290
+ if (configuration.state === "WARN" && configRows.some(([code]) => code === "invalid-config" || code === "unsafe-permissions")) throw new Error("invalid");
291
+ if (configuration.state === "FAIL" && !configRows.some(([code]) => code === "invalid-config" || code === "unsafe-permissions")) throw new Error("invalid");
292
+
293
+ const callbackRows = callbacks.issues.map((code) => {
294
+ if (typeof code !== "string" || !callbackCodes.has(code)) throw new Error("invalid");
295
+ return code;
296
+ });
297
+ if ((callbacks.state === "PASS") !== (callbackRows.length === 0)) throw new Error("invalid");
298
+
299
+ console.log(["configuration", configuration.state, configuration.exists ? "present" : "absent"].join("\t"));
300
+ for (const [code, subject] of configRows) console.log(["configuration-issue", code, subject].join("\t"));
301
+ console.log(["callbacks", callbacks.state, callbacks.exists ? "present" : "absent"].join("\t"));
302
+ for (const code of callbackRows) console.log(["callback-issue", code, "-"].join("\t"));
303
+ } catch {
304
+ process.exit(1);
305
+ }
306
+ NODE
307
+ ); then
308
+ fail "capability diagnostics returned an invalid value-free response"
309
+ else
310
+ while IFS=$'\t' read -r area state detail; do
311
+ case "$area:$state:$detail" in
312
+ configuration:PASS:absent)
313
+ pass "capability configuration (not present — managed overrides are unused)"
314
+ ;;
315
+ configuration:PASS:present)
316
+ pass "capability configuration schema and permissions are valid"
317
+ ;;
318
+ configuration:WARN:*|configuration:FAIL:*)
319
+ ;;
320
+ configuration-issue:unknown-capability:-)
321
+ warn "capability configuration contains an unknown override ID"
322
+ ;;
323
+ configuration-issue:invalid-capability-value:*)
324
+ warn "capability configuration has an invalid value for $detail"
325
+ ;;
326
+ configuration-issue:invalid-logging-value:logging.callbacks)
327
+ warn "capability configuration has an invalid value for logging.callbacks"
328
+ ;;
329
+ configuration-issue:invalid-config:-)
330
+ fail "capability configuration is invalid or uses an unsupported schema; preserve .ai/capabilities.json before reset"
331
+ ;;
332
+ configuration-issue:unsafe-permissions:-)
333
+ fail "capability configuration type or permissions are unsafe; preserve .ai/capabilities.json before recovery"
334
+ ;;
335
+ callbacks:PASS:absent)
336
+ pass "capability callback namespace (not present — callback logging is unused)"
337
+ ;;
338
+ callbacks:PASS:present)
339
+ pass "capability callback namespace schema and SQLite integrity are valid"
340
+ ;;
341
+ callbacks:FAIL:*)
342
+ ;;
343
+ callback-issue:sqlite-integrity-failed:-)
344
+ fail "capability callback namespace SQLite integrity failed; preserve .agentfs/trajectory.db before recovery"
345
+ ;;
346
+ callback-issue:schema-missing:-)
347
+ fail "capability callback namespace schema marker is missing; preserve .agentfs/trajectory.db before recovery"
348
+ ;;
349
+ callback-issue:schema-unsupported:-)
350
+ fail "capability callback namespace schema is unsupported; preserve .agentfs/trajectory.db before recovery"
351
+ ;;
352
+ callback-issue:invalid-record:-)
353
+ fail "capability callback namespace contains invalid records; preserve .agentfs/trajectory.db before recovery"
354
+ ;;
355
+ callback-issue:diagnostic-failed:-)
356
+ fail "capability callback namespace could not be diagnosed safely; preserve .agentfs/trajectory.db before recovery"
357
+ ;;
358
+ *)
359
+ fail "capability diagnostics returned an invalid value-free response"
360
+ ;;
361
+ esac
362
+ done <<< "$capability_mapped"
363
+ [[ $capability_status -eq 0 || "$capability_mapped" == *$'\tFAIL\t'* ]] ||
364
+ fail "capability diagnostics exited unexpectedly"
365
+ fi
366
+ fi
367
+
368
+ # 11. Evaluation reports. The private compiled operation resolves the owned
369
+ # project-local report root and returns one bounded, value-free state. The
370
+ # shell never reads report bytes and treats malformed output as unsafe.
371
+ eval_cli="$CAIRN_ROOT/mcp-memory-server/dist/eval-cli.js"
372
+ eval_state="unsafe"
373
+ if [[ -f "$eval_cli" ]]; then
374
+ eval_json=$(node "$eval_cli" doctor-diagnosis --root "$PWD" --json 2>/dev/null | head -c 257)
375
+ if parsed_eval_state=$(node - "$eval_json" <<'NODE'
376
+ const raw = process.argv[2];
377
+ try {
378
+ if (Buffer.byteLength(raw, "utf8") > 64) throw new Error("invalid");
379
+ const value = JSON.parse(raw);
380
+ const keys = Object.keys(value).sort();
381
+ if (JSON.stringify(keys) !== JSON.stringify(["diagnosis", "schema_version"])
382
+ || value.schema_version !== 1
383
+ || !["absent", "ok", "partial", "tampered", "unsafe"].includes(value.diagnosis)) {
384
+ throw new Error("invalid");
385
+ }
386
+ process.stdout.write(value.diagnosis);
387
+ } catch {
388
+ process.exit(1);
389
+ }
390
+ NODE
391
+ ); then
392
+ eval_state="$parsed_eval_state"
393
+ fi
394
+ fi
395
+ case "$eval_state" in
396
+ absent) skip "evaluation reports (not present — evaluation is opt-in)" ;;
397
+ ok) pass "evaluation reports are complete and valid" ;;
398
+ partial) warn "evaluation reports are partial" ;;
399
+ tampered) warn "evaluation report integrity check failed" ;;
400
+ unsafe) warn "evaluation report storage is unsafe" ;;
401
+ esac
402
+
119
403
  echo
120
404
  if [[ "$fails" -gt 0 ]]; then
121
405
  echo "cairn doctor: $fails configured dependency check(s) failed."
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic offline-framework adapter. It is fixture code, never live evidence.
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+
7
+ const MAX_REQUEST_BYTES = 1024 * 1024;
8
+ const chunks = [];
9
+ let requestBytes = 0;
10
+ for await (const chunk of process.stdin) {
11
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
12
+ requestBytes += bytes.byteLength;
13
+ if (requestBytes > MAX_REQUEST_BYTES) {
14
+ process.stderr.write("offline-framework: request limit exceeded\n");
15
+ process.exit(2);
16
+ }
17
+ chunks.push(bytes);
18
+ }
19
+
20
+ let request;
21
+ try {
22
+ request = JSON.parse(Buffer.concat(chunks).toString("utf8"));
23
+ } catch {
24
+ process.stderr.write("offline-framework: invalid request\n");
25
+ process.exit(2);
26
+ }
27
+
28
+ const required = [
29
+ "schema_version", "experiment_id", "task_id", "arm", "repetition", "pass",
30
+ "workspace_path", "notes_path", "input", "limits", "seed",
31
+ "expected_capability_digest", "output_path",
32
+ ];
33
+ if (!request || typeof request !== "object"
34
+ || Object.keys(request).sort().join("\0") !== [...required].sort().join("\0")) {
35
+ process.stderr.write("offline-framework: unsupported request shape\n");
36
+ process.exit(2);
37
+ }
38
+
39
+ const workspace = resolve(process.cwd(), request.workspace_path);
40
+ const sessionId = `${request.arm}-r${request.repetition}-${request.pass}-${request.task_id}`;
41
+ if (process.env.CAIRN_FAKE_PID_FILE) writeFileSync(process.env.CAIRN_FAKE_PID_FILE, `${process.pid}\n`, { mode: 0o600 });
42
+
43
+ function trajectory(events) {
44
+ const agentfs = join(workspace, ".agentfs");
45
+ mkdirSync(agentfs, { recursive: true, mode: 0o700 });
46
+ const database = new DatabaseSync(join(agentfs, "trajectory.db"));
47
+ database.exec("CREATE TABLE kv_store (key TEXT PRIMARY KEY, value TEXT NOT NULL, created_at INTEGER DEFAULT (unixepoch ()), updated_at INTEGER DEFAULT (unixepoch ()))");
48
+ const startedAt = "2026-01-01T00:00:00.000Z";
49
+ const endedAt = "2026-01-01T00:00:01.000Z";
50
+ const session = {
51
+ schema_version: 1,
52
+ session_id: sessionId,
53
+ harness: "pi",
54
+ project_root: workspace,
55
+ started_at: startedAt,
56
+ ended_at: endedAt,
57
+ events,
58
+ capture: {
59
+ captured_at: endedAt,
60
+ omitted_reasoning_blocks: 0,
61
+ omitted_unknown_records: 0,
62
+ truncated: false,
63
+ },
64
+ };
65
+ const index = {
66
+ schema_version: 1,
67
+ session_id: sessionId,
68
+ harness: "pi",
69
+ started_at: startedAt,
70
+ ended_at: endedAt,
71
+ event_count: events.length,
72
+ logical_bytes: Buffer.byteLength(JSON.stringify(session)),
73
+ full_key: `trajectory/session/${sessionId}`,
74
+ };
75
+ const insert = database.prepare("INSERT INTO kv_store(key,value) VALUES(?,?)");
76
+ insert.run("trajectory/meta/schema-version", "1");
77
+ insert.run(`trajectory/session/${sessionId}`, JSON.stringify(session));
78
+ insert.run(`trajectory/index/${String(Date.parse(endedAt)).padStart(16, "0")}/${sessionId}`, JSON.stringify(index));
79
+ database.close();
80
+ }
81
+
82
+ if (process.env.CAIRN_FAKE_CANCEL_ALL === "1"
83
+ || request.task_id === "offline-timeout"
84
+ || (request.task_id === "offline-cancellation-control" && process.env.CAIRN_FAKE_CANCEL === "1")) {
85
+ await new Promise((resolveWait) => setTimeout(resolveWait, 60_000));
86
+ }
87
+ if (request.task_id === "offline-invalid-result") {
88
+ process.stdout.write('{"schema_version":1,"status":"completed","unexpected":true}');
89
+ process.exit(0);
90
+ }
91
+ if (request.task_id === "offline-adapter-error") {
92
+ process.stdout.write(JSON.stringify({ schema_version: 1, status: "adapter_error", error_code: "designed_adapter_error" }));
93
+ process.exit(0);
94
+ }
95
+
96
+ writeFileSync(join(workspace, "adapter-result.txt"), `${request.task_id}:${request.pass}\n`, { mode: 0o600 });
97
+ if (request.task_id === "offline-distillation-failure") {
98
+ const agentfs = join(workspace, ".agentfs");
99
+ mkdirSync(agentfs, { recursive: true, mode: 0o700 });
100
+ writeFileSync(join(agentfs, "trajectory.db"), "designed-invalid-store", { mode: 0o600 });
101
+ } else if (request.task_id !== "offline-skipped-notes") {
102
+ const events = request.task_id === "offline-pass-note" ? [
103
+ { sequence: 0, kind: "tool_invocation", payload: { call_id: "fixture-call", tool_name: "fixture-check", input: { task: request.task_id } } },
104
+ { sequence: 1, kind: "tool_result", payload: { call_id: "fixture-call", is_error: true, error: "Deterministic fixture failure" } },
105
+ ] : [];
106
+ trajectory(events);
107
+ }
108
+
109
+ const result = {
110
+ schema_version: 1,
111
+ status: "completed",
112
+ turns: { value: request.pass === "run1" ? 2 : 1, semantics: "offline-fixture-turn" },
113
+ harness: { id: "cairn-offline-fake" },
114
+ adapter: { id: "cairn-offline-fake" },
115
+ model: { id: "deterministic-fixture" },
116
+ observed_capability_digest: request.expected_capability_digest,
117
+ ...(request.task_id === "offline-skipped-notes" ? {} : { trajectory_ref: sessionId }),
118
+ ...(request.task_id === "offline-missing-tokens" ? {} : { usage: { total_tokens: request.pass === "run1" ? 20 : 10 } }),
119
+ };
120
+ process.stdout.write(JSON.stringify(result));