@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
package/docs/storage.md CHANGED
@@ -18,6 +18,10 @@ registration, memory remains on that computer.
18
18
  | Scope | Database path |
19
19
  |---|---|
20
20
  | `project` | `<server working directory>/.agentfs/project.db` |
21
+ | Opt-in session trajectories and capability callback state | `<harness project root>/.agentfs/trajectory.db` |
22
+ | Opt-in local artifacts and compaction revisions | `<project root>/.agentfs/artifacts.db` |
23
+ | Opt-in evaluation reports and note snapshots | `<project root>/.agentfs/eval/experiments/<experiment-id>/` |
24
+ | Opt-in derived hindsight notes | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/notes/` |
21
25
  | Any named/global scope, such as `identity` or `work` | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/<scope>.db` |
22
26
 
23
27
  For the local launchers, the server working directory is normally the project
@@ -29,6 +33,180 @@ services used to process/search memory. They do not change where the SQLite
29
33
  databases are stored. Git-provider and routing configuration do not change
30
34
  storage either.
31
35
 
36
+ Trajectory storage is deliberately separate from MCP memory. The harness hook
37
+ writes `.agentfs/trajectory.db` on the client machine where Claude Code,
38
+ OpenCode, or Pi is running; it is never redirected to the remote HTTP memory server.
39
+ The database contains versioned full-session records and small time-ordered
40
+ indexes. Defaults are 5 MiB per serialized session, 256 MiB logical total and
41
+ 30 days retention. Capture and `cairn trajectory prune` remove expired records
42
+ and then the oldest records needed to meet the logical budget. `prune
43
+ --dry-run` reports the same decision without changing the database.
44
+
45
+ ## Capability callback storage
46
+
47
+ Managed operating callbacks use two separate versioned KV namespaces inside
48
+ the existing project-local `.agentfs/trajectory.db`:
49
+
50
+ - `capability-callback/v1/pending/` contains bounded transient issuance
51
+ markers for operating starts.
52
+ - `capability-callback/v1/record/` contains immutable retained final callback
53
+ records.
54
+
55
+ No capability state exists in exact legacy master-off operation. With the
56
+ master contract on, a target-enabled invocation creates no lease or final when
57
+ either measurement consent is off. A target-disabled invocation still blocks
58
+ before owner I/O in that unmeasured branch, but likewise writes no state. Only
59
+ all three consents may create the recoverable lease and, for a disabled target,
60
+ atomically settle it as exactly one value-free `disabled` final.
61
+
62
+ An issuance marker contains only the strict operating-handle allow-list:
63
+ schema version, capability/invocation/correlation identifiers,
64
+ harness/source/transport classifications, start time, effective state source,
65
+ and configuration digest. It contains no arguments, results, errors, paths,
66
+ prompts, queries, secrets, or arbitrary metadata. A marker is not a trajectory
67
+ session, trajectory start, or callback start record. It is consumed once by a
68
+ matching finish or defensive invalidation and is never exposed as final
69
+ callback evidence.
70
+
71
+ Both namespaces are project-local state, not trajectory sessions: the existing
72
+ `trajectory/v1` session envelopes, keys, indexes, CLI, and path are unchanged.
73
+ There is no database migration, mirror, or second callback database. The file
74
+ is created with mode `0600`; its SQLite `-wal` and `-shm` files remain in the
75
+ same sensitive backup boundary.
76
+
77
+ Each eligible completed invocation consumes its exact recoverable lease and
78
+ appends one immutable final record in the same immediate transaction. This
79
+ settlement is atomic and idempotent: duplicate success, error, timeout,
80
+ disabled, or abandonment attempts cannot append or rewrite another final.
81
+ Crash recovery and SessionEnd/plugin-disposal cleanup consume only unfinished
82
+ leases as abandonment; they never replace a settled terminal. Callback age retention reuses
83
+ `CAIRN_TRAJECTORY_RETENTION_DAYS` (default 30 days). Each namespace has an
84
+ independent 10,000-record cap that prunes its oldest keys.
85
+ Expired issuance is consumed without a final record, and a final record older
86
+ than the current retention cutoff is not appended. Existing trajectory session
87
+ byte limits and total-store accounting remain session contracts; callback
88
+ state does not change their schema or path.
89
+
90
+ The namespaces are strictly local. Stdio MCP callbacks, offline notes, and
91
+ guarded local harness commands may persist final evidence only after all three
92
+ consents pass. Pending issuance and final records are never redirected through
93
+ HTTP or remote project storage. Authenticated HTTP callbacks are skipped:
94
+ there is no remote/HTTP callback persistence, server-base-directory mapping,
95
+ payload delivery, telemetry, export, or network delivery. Disabling logging
96
+ stops future records, invalidates an already-issued operating marker at finish,
97
+ and does not delete retained final rows.
98
+
99
+ Default uninstall retains `.agentfs/trajectory.db` and its sidecars exactly.
100
+ It removes the managed `.ai/capabilities.json` only after backing it up, and
101
+ the generated `revert.sh` restores the exact bytes and mode. An explicit
102
+ `cairn uninstall --purge-memory PROJECT` backs up and removes the entire
103
+ project `.agentfs/` boundary, including trajectory sessions and callback rows;
104
+ its `revert.sh` restores that boundary byte-for-byte. Back up the database and
105
+ sidecars together while moving or inspecting a project.
106
+
107
+ Hindsight notes are also separate derived data. They are human-readable
108
+ Markdown below `notes/projects/` and `notes/shared/`, with a local schema-v1
109
+ manifest and lock directories below `notes/.cairnkeep/`. Project paths are
110
+ represented in visible filenames by readable slugs plus stable local hashes;
111
+ the hidden local manifest retains the canonical project root needed for later
112
+ all-project runs. Occurrence provenance is capped at 1024 entries per note and
113
+ the processed-session ledger at 4096 digests per project. Note bodies do not
114
+ expire automatically: inspect/delete them according to your own retention
115
+ policy. Trajectory pruning does not delete already-derived notes.
116
+
117
+ ## Artifact storage
118
+
119
+ Artifacts are deliberately separate from memory and trajectories. Local
120
+ compaction hooks, recovery, operator commands, and stdio MCP tools use
121
+ `<project>/.agentfs/artifacts.db` with restrictive permissions. Existing
122
+ memory/trajectory databases are not migrated, rewritten, or mirrored into it.
123
+ SQLite `-wal` and `-shm` sidecars share the same sensitive backup boundary.
124
+
125
+ The schema-v1 authoritative rows are immutable full envelopes containing ID,
126
+ kind, creation time, stable session reference, optional typed-node reference,
127
+ media type, logical/stored bytes, digest, provenance, redaction/truncation
128
+ metadata, optional supersession, and bounded content. Small derived namespaces
129
+ hold created/session/kind indexes, request-dedupe bindings, monotonic
130
+ `compaction/sequence/<session>` counters, and latest session/project pointers.
131
+ Artifact indexes contain value-minimized metadata and references, never bodies.
132
+ Full record, indexes, dedupe, counter, and pointer updates share one immediate
133
+ transaction. An identical retry returns the existing ID; changed content
134
+ appends a new immutable revision. Deleted revision numbers are never reused.
135
+
136
+ Defaults are 1 MiB per artifact, 16 MiB per session, 256 MiB total, 30 days,
137
+ eight compaction revisions per session, and a 256 KiB generated-file snapshot
138
+ cap (or the lower artifact cap). Automatic retention applies age and revision
139
+ limits, then removes the oldest eligible records to meet session/store budgets.
140
+ It protects the newest valid project compaction. Explicit delete or
141
+ `cairn artifact prune --include-protected` may remove that record. Hard delete
142
+ removes content, indexes, dedupe rows, and affected pointers; there is no
143
+ tombstone or hidden retained body.
144
+
145
+ `cairn doctor` validates SQLite integrity, schema, authoritative envelopes and
146
+ digests, derived indexes, dedupe state, revision pointers, and retention state.
147
+ `--repair` rebuilds only safely derived indexes/dedupe/pointers from valid full
148
+ records. Unsupported schema, invalid full records, digest mismatch, or SQLite
149
+ corruption remains failed and untouched; preserve `.agentfs/artifacts.db` and
150
+ its sidecars before manual inspection.
151
+
152
+ Generated-file `path_label` values are contained project-relative labels only.
153
+ They are never dereferenced by MCP, CLI, capture, doctor, or recovery. Binary
154
+ or oversized candidates become metadata-only; optional inline text is bounded.
155
+
156
+ For separately enabled HTTP artifact access, the server requires a validated
157
+ `X-Cairn-Project` and derives the database under the server-side configured
158
+ base directory as `${CAIRN_AGENTFS_BASE_DIR}/<project-id>/.agentfs/artifacts.db`.
159
+ Clients cannot supply a filesystem path, and different project identities use
160
+ separate derived roots. The bearer token still defines the trust domain.
161
+ Neither `CAIRN_ARTIFACT_HTTP` nor remote memory registration redirects local
162
+ compaction hooks or trajectories.
163
+ Artifact backup follows the project `.agentfs/` backup boundary described below.
164
+
165
+ ## Evaluation report and note-snapshot storage
166
+
167
+ Evaluation is disabled by default. When `CAIRN_EVAL` is explicitly enabled and
168
+ an operator confirms a run, each experiment is stored below
169
+ `<project>/.agentfs/eval/experiments/<experiment-id>/`. `report.json` is the
170
+ canonical complete replacement checkpoint; it is rewritten atomically after
171
+ schedule creation and every terminal, distillation, cancellation, and cleanup
172
+ transition. An interrupted run therefore remains an inspectable `partial`
173
+ report rather than an inferred success or failure.
174
+
175
+ The eval root and experiment directories are mode `0700`; `report.json` is
176
+ mode `0600`. A report is limited to 16 MiB by default (64 MiB is the hard
177
+ implementation ceiling), and one root admits at most 10,000 experiment
178
+ directories. Task sets contain at most 10,000 tasks and input/configuration
179
+ files at most 4 MiB. Experiment-owned note snapshots live under
180
+ `<experiment-id>/snapshots/`; one snapshot contains at most 10,000 files and
181
+ 16 MiB. Snapshot files are mode `0400` and their directories are made
182
+ read-only after creation. Each Run 2 receives a separate read-only task-local
183
+ copy; digests are checked before and after exposure.
184
+
185
+ Reports contain versioned observations, reproducibility digests, optional
186
+ metrics, populations, missingness, warnings, and strict component identifiers.
187
+ Snapshots contain only the same task's distilled note files plus their
188
+ relative-path/byte/digest manifest. Both remain sensitive local data even
189
+ though prompts, model outputs, arbitrary stdout/stderr, environment snapshots,
190
+ and secret values are structurally excluded from reports.
191
+
192
+ There is no automatic age deletion. Operators inspect with `cairn eval report`,
193
+ remove one validated contained experiment with `cairn eval delete`, or apply
194
+ oldest-eligible removal with `cairn eval prune --older-than-days N` (default
195
+ 30 days). Both destructive commands have `--dry-run`; malformed, tampered,
196
+ unsafe-mode, symlinked, or path-escaping trees fail closed. A hard delete or
197
+ prune removes the selected experiment directory, including its report and note
198
+ snapshots, without a hidden tombstone. Cancellation retains the partial report
199
+ and completed snapshot checkpoints until one of these explicit operations or
200
+ an uninstall purge removes them.
201
+
202
+ Normal `cairn uninstall PROJECT` retains the entire `.agentfs/` tree, including
203
+ evaluation reports and snapshots, byte-for-byte. `--dry-run` changes no file,
204
+ mode, or directory entry. Explicit `cairn uninstall --purge-memory PROJECT`
205
+ backs up the complete project `.agentfs/` boundary before removing it; the
206
+ generated `revert.sh` restores the exact evaluation bytes, modes, and layout
207
+ alongside adjacent project memory, trajectory, and artifact data. Evaluation
208
+ has no separate destructive uninstall shortcut.
209
+
32
210
  When the memory server runs in the official container, the same rule applies:
33
211
  the process stores databases below `/data`, normally backed by the
34
212
  `cairnkeep-data` named volume. Replacing the container preserves that volume;
@@ -118,6 +296,80 @@ sqlite3 /path/to/project/.agentfs/project.db \
118
296
  Treat every database and export archive as sensitive. They may contain source
119
297
  paths, decisions, incident details, and other project context.
120
298
 
299
+ The same backup boundary applies to `<project>/.agentfs/trajectory.db`, which
300
+ is not included in `cairn memory export`. `cairn uninstall PROJECT` retains the
301
+ whole `.agentfs/` directory by default. `cairn uninstall --purge-memory PROJECT`
302
+ backs it up and removes it, including memory and trajectories; the generated
303
+ `revert.sh` can restore it.
304
+
305
+ The same boundary includes `<project>/.agentfs/artifacts.db` and its sidecars.
306
+ `cairn memory export` does not include artifacts. Default uninstall retains the
307
+ whole `.agentfs/` tree. `cairn uninstall --purge-memory PROJECT` backs it up
308
+ before removal, and the generated `revert.sh` restores the exact durable bytes.
309
+ No automatic export or migration of existing artifact stores is performed.
310
+
311
+ The same boundary includes `<project>/.agentfs/eval/experiments/`. `cairn
312
+ memory export` does not include reports or note snapshots. Default uninstall
313
+ retains them; `--purge-memory PROJECT` backs them up with the whole project
314
+ `.agentfs/` tree before removal, and the generated `revert.sh` restores their
315
+ exact bytes, modes, and directory layout.
316
+
317
+ Global hindsight notes share the `${CAIRN_AGENTFS_BASE_DIR}` durable-store
318
+ boundary. `cairn uninstall` keeps them by default. `cairn uninstall
319
+ --purge-memory` backs up and removes the entire boundary, including `notes/`,
320
+ and the generated bundle's `revert.sh` restores the exact files. Back up both
321
+ the global store and each project's `.agentfs/` when moving a complete setup;
322
+ `cairn memory export` covers SQLite memory scopes, not the Markdown note tree.
323
+
324
+ ## Typed metadata, import replay, and history
325
+
326
+ With `CAIRN_TYPED_MEMORY_NODES=1`, each concrete AgentFS database keeps raw
327
+ live values in the existing `kv_store`. The same SQLite transaction stores
328
+ only additive metadata in `cairn_node_metadata_v1` and optional import-ID
329
+ bindings in `cairn_node_import_replays_v1`. A legacy row with no metadata is
330
+ projected as schema-v1 type `memory` with no tags; reading it creates no table,
331
+ row, cache, or rewritten value. There is no ORM or schema-push step: tables are
332
+ created lazily only by enabled mutations.
333
+ The public portable contract is `schemas/memory-node.schema.json`; matching
334
+ runtime validation lives in the packaged server.
335
+
336
+ Supersede, metadata-only supersede, reviewed changes, import replacement, and
337
+ delete write complete value/type/tag snapshots under hidden `__history__` keys
338
+ in the same immediate transaction. Delete removes the live metadata row but
339
+ retains its final snapshot; recreating the key is supported. Import commit
340
+ rechecks conflicts and replay inside one concrete store transaction. Dry-run
341
+ does not create the database or metadata tables.
342
+
343
+ SQLite backups and `cairn memory export` include metadata, replay bindings, and
344
+ history. `cairn doctor` treats absent tables as healthy legacy state. Repair
345
+ may remove provably orphaned derived metadata, but never rewrites raw KV,
346
+ infers non-default types, discards corrupt authoritative metadata, or repairs
347
+ a divergent replay digest.
348
+
349
+ ## Canonical note lifecycle and recovery
350
+
351
+ Note address spaces remain file-backed. The canonical set includes Markdown
352
+ leaves, `README.md` indexes, `.cairnkeep/manifest-v1.json`, typed history,
353
+ `note-import-replays-v1.json`, and transaction directories below
354
+ `notes/.cairnkeep/transactions/<transaction-id>/`. Create, supersede, delete,
355
+ and import render immutable path/byte/pre-image/hash plans and install them
356
+ through one journal primitive, with the manifest last. There is no AgentFS
357
+ mirror and clients never supply filesystem paths.
358
+
359
+ Staged same-filesystem bytes, backups, and a `prepared` journal are durable
360
+ before live replacement. `committing` records completed paths; `committed`
361
+ records intended final hashes. Any pending journal blocks later mutations.
362
+ Doctor repair restores verified pre-images for prepared/committing state. For
363
+ committed-before-cleanup state it verifies final hashes, preserves the completed
364
+ operation, and removes only transaction artifacts. Failed verification leaves
365
+ the journal and live state untouched; a committed operation is never treated
366
+ as an uncommitted rollback candidate.
367
+
368
+ Generated managed blocks may change, but unmarked maintainer bytes after the
369
+ managed marker are preserved exactly. Default uninstall retains databases,
370
+ note history, replay ledgers, and journals. `--purge-memory` uses the existing
371
+ backup-first whole-store boundary; `revert.sh` restores their exact bytes.
372
+
121
373
  ## Reviewed-memory integration
122
374
 
123
375
  External review systems should not compose `memory_write`, `memory_read`, and
@@ -0,0 +1,42 @@
1
+ # Offline evaluation fixture
2
+
3
+ This directory contains Cairnkeep's deterministic, network-free evaluation
4
+ population. It validates the evaluation framework; its designed differences are
5
+ not performance, efficiency, quality, or live-product evidence.
6
+
7
+ `task-set.json` is the immutable `cairn-offline-fake-v1` source. The loader
8
+ requires its exact canonical bytes, the package version, and the SHA-256 digest
9
+ recorded by `bundled-fake.json` before it creates a workspace. `adapter.json`
10
+ selects the only executable adapter shipped by Cairnkeep,
11
+ `scripts/fake-eval-adapter.mjs`. Live harness adapters remain explicit
12
+ operator-owned program-plus-argument configurations.
13
+
14
+ The coordinator sends one bounded JSON request on standard input. The fake
15
+ emits exactly one schema-valid JSON result on standard output for ordinary
16
+ cases and uses standard error only for value-free diagnostics. Requests include
17
+ the task, arm, repetition, pass, seed, relative workspace/notes/output paths,
18
+ limits, and expected capability digest. Results can include terminal adapter
19
+ status, turn semantics, optional usage, component identities, the observed
20
+ capability digest, and a task-local trajectory reference. The independent task
21
+ verifier, never the adapter, assigns pass or fail.
22
+
23
+ The population has ten stable tasks covering verified pass, verifier failure,
24
+ missing tokens, successful note creation, no notes, distillation failure,
25
+ skipped distillation, timeout, structured adapter error, invalid result, and a
26
+ cancellation control. With one repetition the two-pass run estimates and then
27
+ performs exactly 20 serial adapter invocations. Run 2 starts only after all ten
28
+ Run 1 observations and offline distillation stages finish.
29
+
30
+ Validate without invoking the adapter:
31
+
32
+ ```bash
33
+ CAIRN_EVAL=1 node mcp-memory-server/dist/eval-cli.js validate \
34
+ --task-set examples/eval/task-set.json \
35
+ --adapter examples/eval/adapter.json \
36
+ --output .agentfs/eval/experiments --json
37
+ ```
38
+
39
+ Run only in a disposable checkout when exercising the fixture directly. The
40
+ result is always scoped as `offline-framework`; it cannot authorize a README or
41
+ release claim. No provider, endpoint, credential, model service, or network
42
+ default is included.
@@ -0,0 +1,12 @@
1
+ {
2
+ "schema_version": 1,
3
+ "id": "cairn-offline-fake",
4
+ "command": {
5
+ "program": "../../scripts/fake-eval-adapter.mjs",
6
+ "args": []
7
+ },
8
+ "turn_semantics": {
9
+ "id": "offline-fixture-turn",
10
+ "description": "One deterministic fixture state transition; meaningful only for offline framework verification."
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.4.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
@@ -0,0 +1 @@
1
+ {"id":"cairn-offline-fake-v1","schema_version":1,"source":{"files":[{"content":"cairn-offline-fixture-v1\n","path":"fixture.txt"}],"identifier":"cairn-offline-fake-v1","kind":"bundled_fake"},"tasks":[{"id":"offline-pass-note","input":"Produce one deterministic note-producing result.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-verifier-fail","input":"Produce a result rejected by the independent verifier.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-missing-tokens","input":"Omit optional usage fields.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-no-notes","input":"Close a trajectory with no distillable notes.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-distillation-failure","input":"Expose an exact but unreadable trajectory store.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-skipped-notes","input":"Complete without a trajectory reference.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-timeout","input":"Exceed the declared adapter timeout.","limits":{"elapsed_ms":100,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(0)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-adapter-error","input":"Return a structured adapter error.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(0)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-invalid-result","input":"Return a schema-invalid result.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(0)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-cancellation-control","input":"Complete normally unless the cancellation control is enabled.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}}]}
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { getArtifactLimits } from "./artifact-schema.js";
3
+ import { deleteArtifact, doctorArtifactStore, listArtifacts, pruneArtifacts, putArtifact, readArtifact, recordUnsupportedCompactionAdapter, } from "./artifact-store.js";
4
+ import { normalizeClaudePostCompact, normalizeOpenCodeCompaction, renderCompactionRecovery, selectCompactionRecovery, } from "./compaction-normalize.js";
5
+ process.stdout.on("error", (error) => {
6
+ if (error.code === "EPIPE")
7
+ process.exit(0);
8
+ throw error;
9
+ });
10
+ function usage() {
11
+ return `cairn artifact — local immutable artifacts
12
+
13
+ Usage:
14
+ cairn artifact list [--kind K] [--session REF] [--json]
15
+ cairn artifact show <artifact-id-or-prefix> [--json]
16
+ cairn artifact delete <artifact-id-or-prefix> [--dry-run] [--json]
17
+ cairn artifact prune [--dry-run] [--include-protected] [--json]
18
+ `;
19
+ }
20
+ function hasFlag(args, flag) {
21
+ return args.includes(flag);
22
+ }
23
+ function option(args, name) {
24
+ const index = args.indexOf(name);
25
+ return index >= 0 ? args[index + 1] : undefined;
26
+ }
27
+ async function readStdin() {
28
+ const chunks = [];
29
+ for await (const chunk of process.stdin)
30
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
31
+ return Buffer.concat(chunks).toString("utf8");
32
+ }
33
+ function publicList(value) {
34
+ return {
35
+ ...value,
36
+ artifacts: value.artifacts.map(({ content: _content, ...artifact }) => artifact),
37
+ };
38
+ }
39
+ function humanList(value) {
40
+ if (value.artifacts.length === 0)
41
+ return "No local artifacts found.";
42
+ return `${value.artifacts.map((artifact) => [artifact.artifact_id, artifact.kind, artifact.session_ref, artifact.created_at, `${artifact.logical_bytes} bytes`].join("\t")).join("\n")}\n${value.artifacts.length} artifact(s), ${value.logical_bytes} logical bytes`;
43
+ }
44
+ function humanShow(value) {
45
+ return [
46
+ `Artifact: ${value.artifact_id}`,
47
+ `Kind: ${value.kind}`,
48
+ `Session: ${value.session_ref}`,
49
+ `Created: ${value.created_at}`,
50
+ `Digest: ${value.content_digest}`,
51
+ "",
52
+ JSON.stringify(value.content, null, 2),
53
+ ].join("\n");
54
+ }
55
+ function artifactInput(normalized) {
56
+ return {
57
+ kind: "compaction_summary",
58
+ session_ref: normalized.session_ref,
59
+ media_type: "text/markdown",
60
+ provenance: {
61
+ producer: normalized.harness === "claude-code" ? "claude-post-compact" : "opencode-session-compacted",
62
+ source_event: normalized.source_event,
63
+ harness: normalized.harness,
64
+ harness_version: normalized.harness_version,
65
+ ...("native_id" in normalized ? { native_id: normalized.native_id } : {}),
66
+ },
67
+ content: {
68
+ raw_summary: normalized.raw_summary,
69
+ ...normalized.projection,
70
+ trigger: normalized.trigger,
71
+ },
72
+ };
73
+ }
74
+ async function captureClaude(projectRoot, args) {
75
+ const harnessVersion = option(args, "--harness-version") ?? "unknown";
76
+ const normalized = normalizeClaudePostCompact(JSON.parse(await readStdin()), { harnessVersion });
77
+ if (!normalized) {
78
+ await recordUnsupportedCompactionAdapter(projectRoot, {
79
+ harness: "claude-code",
80
+ harness_version: harnessVersion === "unknown" ? undefined : harnessVersion,
81
+ reason: ["2.1.219", "2.1.220"].includes(harnessVersion) ? "invalid_shape" : "unsupported_version",
82
+ });
83
+ return;
84
+ }
85
+ await putArtifact(projectRoot, artifactInput(normalized));
86
+ }
87
+ async function captureOpenCode(projectRoot) {
88
+ const raw = JSON.parse(await readStdin());
89
+ const version = typeof raw.harness_version === "string"
90
+ ? raw.harness_version
91
+ : typeof raw.session?.version === "string"
92
+ ? String(raw.session.version) : "1.17.20";
93
+ const normalized = normalizeOpenCodeCompaction(raw.event, raw.session, raw.messages, { harnessVersion: version });
94
+ if (!normalized) {
95
+ await recordUnsupportedCompactionAdapter(projectRoot, { harness: "opencode", harness_version: version, reason: "unsupported_version" });
96
+ return;
97
+ }
98
+ await putArtifact(projectRoot, artifactInput(normalized));
99
+ }
100
+ async function recover(projectRoot, args) {
101
+ const listed = await listArtifacts(projectRoot, { kind: "compaction_summary" });
102
+ const selected = selectCompactionRecovery(listed.artifacts, {
103
+ currentSessionRef: option(args, "--session-ref"),
104
+ now: new Date(),
105
+ staleAfterSeconds: 86400,
106
+ });
107
+ if (!selected)
108
+ return;
109
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(selected) : renderCompactionRecovery(selected)}${hasFlag(args, "--json") ? "\n" : ""}`);
110
+ }
111
+ async function main() {
112
+ const [command = "help", ...args] = process.argv.slice(2);
113
+ if (command === "capture-claude" || command === "capture-opencode") {
114
+ const projectRoot = args.find((arg) => !arg.startsWith("-"));
115
+ if (!projectRoot)
116
+ return;
117
+ try {
118
+ if (command === "capture-claude")
119
+ await captureClaude(projectRoot, args);
120
+ else
121
+ await captureOpenCode(projectRoot);
122
+ }
123
+ catch {
124
+ process.stderr.write("artifact capture skipped: local validation or persistence failed\n");
125
+ }
126
+ return;
127
+ }
128
+ try {
129
+ if (command === "list") {
130
+ const kind = option(args, "--kind");
131
+ const value = publicList(await listArtifacts(process.cwd(), { kind, session_ref: option(args, "--session") }));
132
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanList(value)}\n`);
133
+ return;
134
+ }
135
+ if (command === "show") {
136
+ const identifier = args.find((arg) => !arg.startsWith("-"));
137
+ if (!identifier)
138
+ throw new Error("show requires an artifact ID or unambiguous prefix.");
139
+ const value = await readArtifact(identifier, process.cwd());
140
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanShow(value)}\n`);
141
+ return;
142
+ }
143
+ if (command === "delete") {
144
+ const identifier = args.find((arg) => !arg.startsWith("-"));
145
+ if (!identifier)
146
+ throw new Error("delete requires an artifact ID or unambiguous prefix.");
147
+ const value = await deleteArtifact(identifier, process.cwd(), { dryRun: hasFlag(args, "--dry-run") });
148
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : `${value.deleted ? "Deleted" : "Would delete"} ${value.artifact_id}.`}\n`);
149
+ return;
150
+ }
151
+ if (command === "prune") {
152
+ const includeProtected = hasFlag(args, "--include-protected");
153
+ const value = await pruneArtifacts(process.cwd(), getArtifactLimits(), {
154
+ dryRun: hasFlag(args, "--dry-run"), includeProtected,
155
+ });
156
+ const output = { ...value, include_protected: includeProtected };
157
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(output) : `${value.dry_run ? "Would remove" : "Removed"} ${value.removed.length} artifact(s); ${value.remaining_artifacts} remain.`}\n`);
158
+ return;
159
+ }
160
+ if (command === "recover") {
161
+ const projectRoot = args.find((arg) => !arg.startsWith("-")) ?? process.cwd();
162
+ await recover(projectRoot, args);
163
+ return;
164
+ }
165
+ if (command === "doctor") {
166
+ const value = await doctorArtifactStore(process.cwd(), hasFlag(args, "--repair"));
167
+ process.stdout.write(`${JSON.stringify(value)}\n`);
168
+ if (!value.ok)
169
+ process.exitCode = 2;
170
+ return;
171
+ }
172
+ if (command === "help" || command === "--help" || command === "-h") {
173
+ process.stdout.write(usage());
174
+ return;
175
+ }
176
+ throw new Error(`unknown artifact command "${command}".`);
177
+ }
178
+ catch (error) {
179
+ const message = error instanceof Error ? error.message : "unknown error";
180
+ process.stderr.write(`cairn artifact: ${message}\n`);
181
+ process.exitCode = 2;
182
+ }
183
+ }
184
+ await main();