@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
@@ -0,0 +1,267 @@
1
+ import { redactLocalValue } from "./trajectory-redaction.js";
2
+ export const SUPPORTED_COMPACTION_ADAPTERS = [
3
+ { harness: "claude-code", version: "2.1.219", event: "PostCompact" },
4
+ { harness: "claude-code", version: "2.1.220", event: "PostCompact" },
5
+ { harness: "opencode", version: "1.17.20", event: "session.compacted" },
6
+ ];
7
+ function object(value) {
8
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
9
+ }
10
+ function string(value) {
11
+ return typeof value === "string" && value.length > 0 ? value : undefined;
12
+ }
13
+ function cleanLine(value) {
14
+ return value.trim().replace(/^[-*+]\s+/, "").trim();
15
+ }
16
+ function stripLabel(value, label) {
17
+ const cleaned = cleanLine(value);
18
+ const match = new RegExp(`^${label}:\\s*(.+)$`, "i").exec(cleaned);
19
+ return match?.[1]?.trim() || undefined;
20
+ }
21
+ export function projectCompactionSummary(summary, options) {
22
+ const result = {
23
+ task_goals: [],
24
+ decisions_made: [],
25
+ open_todos: [],
26
+ critical_error_traces: [],
27
+ completeness: {
28
+ task_goals: "missing",
29
+ decisions_made: "missing",
30
+ open_todos: "missing",
31
+ critical_error_traces: "missing",
32
+ },
33
+ };
34
+ const claudeTemplate = options.template.startsWith("claude-code-");
35
+ const headings = claudeTemplate
36
+ ? new Map([
37
+ ["task goals", "task_goals"],
38
+ ["decisions made", "decisions_made"],
39
+ ["open todos", "open_todos"],
40
+ ["critical error traces", "critical_error_traces"],
41
+ ])
42
+ : new Map([
43
+ ["objective", "task_goals"],
44
+ ["next move", "open_todos"],
45
+ ]);
46
+ let section;
47
+ const seen = new Set();
48
+ for (const rawLine of summary.split(/\r?\n/)) {
49
+ const heading = /^#{1,6}\s+(.+?)\s*$/.exec(rawLine)?.[1]?.toLowerCase();
50
+ if (heading) {
51
+ section = headings.get(heading);
52
+ if (section)
53
+ seen.add(section);
54
+ continue;
55
+ }
56
+ const cleaned = cleanLine(rawLine);
57
+ if (!cleaned)
58
+ continue;
59
+ const decision = stripLabel(cleaned, "Decision");
60
+ const todo = stripLabel(cleaned, "TODO");
61
+ const error = stripLabel(cleaned, "Error");
62
+ if (decision) {
63
+ result.decisions_made.push(decision);
64
+ seen.add("decisions_made");
65
+ continue;
66
+ }
67
+ if (todo) {
68
+ result.open_todos.push(todo);
69
+ seen.add("open_todos");
70
+ continue;
71
+ }
72
+ if (error) {
73
+ result.critical_error_traces.push(error);
74
+ seen.add("critical_error_traces");
75
+ continue;
76
+ }
77
+ if (section === "task_goals" || section === "open_todos")
78
+ result[section].push(cleaned);
79
+ else if (claudeTemplate && section)
80
+ result[section].push(cleaned);
81
+ }
82
+ for (const key of ["task_goals", "decisions_made", "open_todos", "critical_error_traces"]) {
83
+ result.completeness[key] = seen.has(key) ? "complete" : "missing";
84
+ }
85
+ return result;
86
+ }
87
+ function diagnostic(options) {
88
+ options.recordDiagnostic?.({ code: "unsupported_compaction_adapter", count: 1 });
89
+ return null;
90
+ }
91
+ export function normalizeClaudePostCompact(raw, options) {
92
+ if (options.harnessVersion !== "2.1.219" && options.harnessVersion !== "2.1.220")
93
+ return diagnostic(options);
94
+ const candidate = object(raw);
95
+ const expectedKeys = options.harnessVersion === "2.1.219"
96
+ ? ["compact_summary", "cwd", "hook_event_name", "permission_mode", "session_id", "transcript_path", "trigger"]
97
+ : ["compact_summary", "cwd", "hook_event_name", "prompt_id", "session_id", "transcript_path", "trigger"];
98
+ if (!candidate || Object.keys(candidate).sort().join("|") !== expectedKeys.sort().join("|"))
99
+ return diagnostic(options);
100
+ const sessionId = string(candidate.session_id);
101
+ const projectRoot = string(candidate.cwd);
102
+ const rawSummary = string(candidate.compact_summary);
103
+ const trigger = string(candidate.trigger);
104
+ const versionField = options.harnessVersion === "2.1.219"
105
+ ? string(candidate.permission_mode) : string(candidate.prompt_id);
106
+ if (!sessionId || !projectRoot || !rawSummary || !trigger || !versionField
107
+ || candidate.hook_event_name !== "PostCompact")
108
+ return diagnostic(options);
109
+ const redacted = redactLocalValue(candidate, projectRoot).value;
110
+ const summary = String(redacted.compact_summary);
111
+ return {
112
+ session_ref: `claude-code:${String(redacted.session_id)}`,
113
+ harness: "claude-code",
114
+ harness_version: options.harnessVersion,
115
+ source_event: "PostCompact",
116
+ trigger: String(redacted.trigger),
117
+ raw_summary: summary,
118
+ projection: projectCompactionSummary(summary, { template: `claude-code-${options.harnessVersion}` }),
119
+ };
120
+ }
121
+ export function selectOpenCodeCompactionSummary(eventRaw, sessionRaw, messagesRaw, options) {
122
+ if (options.harnessVersion !== "1.17.20")
123
+ throw new Error("Unsupported OpenCode compaction adapter.");
124
+ const event = object(eventRaw);
125
+ const properties = object(event?.properties);
126
+ const session = object(sessionRaw);
127
+ const sessionId = string(session?.id);
128
+ if (event?.type !== "session.compacted" || !sessionId || properties?.sessionID !== sessionId
129
+ || session?.version !== "1.17.20" || session.parentID !== undefined || !Array.isArray(messagesRaw)) {
130
+ throw new Error("OpenCode compaction shape is invalid.");
131
+ }
132
+ const compactionParents = new Set();
133
+ for (const rawMessage of messagesRaw) {
134
+ const message = object(rawMessage);
135
+ const info = object(message?.info);
136
+ if (info?.role !== "user" || info.sessionID !== sessionId || !Array.isArray(message?.parts))
137
+ continue;
138
+ if (message.parts.some((part) => object(part)?.type === "compaction")) {
139
+ const id = string(info.id);
140
+ if (id)
141
+ compactionParents.add(id);
142
+ }
143
+ }
144
+ const candidates = [];
145
+ for (const rawMessage of messagesRaw) {
146
+ const message = object(rawMessage);
147
+ const info = object(message?.info);
148
+ const parentId = string(info?.parentID);
149
+ const id = string(info?.id);
150
+ const finish = string(info?.finish);
151
+ if (info?.role !== "assistant" || info.sessionID !== sessionId || info.summary !== true || !finish
152
+ || info.error !== undefined || !id || !parentId || !compactionParents.has(parentId) || !Array.isArray(message?.parts))
153
+ continue;
154
+ const time = object(info.time);
155
+ const timestamp = typeof time?.completed === "number" ? time.completed
156
+ : typeof time?.created === "number" ? time.created : NaN;
157
+ if (!Number.isFinite(timestamp))
158
+ continue;
159
+ const texts = message.parts.flatMap((rawPart) => {
160
+ const part = object(rawPart);
161
+ return part?.type === "text" && part.ignored !== true && part.messageID === id && typeof part.text === "string"
162
+ ? [part.text] : [];
163
+ });
164
+ if (texts.length === 0)
165
+ continue;
166
+ candidates.push({ message_id: id, parent_id: parentId, completed_at: new Date(timestamp).toISOString(), time: timestamp, raw_summary: texts.join("\n") });
167
+ }
168
+ candidates.sort((left, right) => right.time - left.time || right.message_id.localeCompare(left.message_id));
169
+ const selected = candidates[0];
170
+ if (!selected)
171
+ throw new Error("OpenCode compaction did not contain a usable summary.");
172
+ return {
173
+ message_id: selected.message_id,
174
+ parent_id: selected.parent_id,
175
+ completed_at: selected.completed_at,
176
+ raw_summary: selected.raw_summary,
177
+ };
178
+ }
179
+ export function normalizeOpenCodeCompaction(eventRaw, sessionRaw, messagesRaw, options) {
180
+ if (options.harnessVersion !== "1.17.20")
181
+ return diagnostic(options);
182
+ const session = object(sessionRaw);
183
+ const projectRoot = string(session?.directory);
184
+ if (!projectRoot)
185
+ return diagnostic(options);
186
+ try {
187
+ const redacted = redactLocalValue({ event: eventRaw, session: sessionRaw, messages: messagesRaw }, projectRoot).value;
188
+ const redactedSession = object(redacted.session);
189
+ const selected = selectOpenCodeCompactionSummary(redacted.event, redactedSession, redacted.messages, options);
190
+ return {
191
+ session_ref: `opencode:${String(redactedSession?.id)}`,
192
+ harness: "opencode",
193
+ harness_version: "1.17.20",
194
+ source_event: "session.compacted",
195
+ native_id: selected.message_id,
196
+ trigger: "native",
197
+ raw_summary: selected.raw_summary,
198
+ projection: projectCompactionSummary(selected.raw_summary, { template: "opencode-1.17.20" }),
199
+ };
200
+ }
201
+ catch {
202
+ return diagnostic(options);
203
+ }
204
+ }
205
+ export function selectCompactionRecovery(recordsRaw, options = {}) {
206
+ if (!Array.isArray(recordsRaw))
207
+ return null;
208
+ const invalid = new Set(options.invalidArtifactIds ?? []);
209
+ const candidates = recordsRaw.flatMap((raw) => {
210
+ const record = object(raw);
211
+ const content = object(record?.content);
212
+ const provenance = object(record?.provenance);
213
+ const projection = content ? {
214
+ task_goals: content.task_goals,
215
+ decisions_made: content.decisions_made,
216
+ open_todos: content.open_todos,
217
+ critical_error_traces: content.critical_error_traces,
218
+ completeness: content.completeness,
219
+ } : undefined;
220
+ const id = string(record?.artifact_id);
221
+ const createdAt = string(record?.created_at);
222
+ const sessionRef = string(record?.session_ref);
223
+ if (!id || invalid.has(id) || record?.valid === false || record?.schema_version !== 1
224
+ || record?.kind !== "compaction_summary" || !createdAt || !sessionRef || !Number.isFinite(Date.parse(createdAt))
225
+ || typeof content?.revision !== "number" || !projection || !object(projection.completeness)
226
+ || !Array.isArray(projection.task_goals) || !Array.isArray(projection.decisions_made)
227
+ || !Array.isArray(projection.open_todos) || !Array.isArray(projection.critical_error_traces))
228
+ return [];
229
+ return [{ record, id, createdAt, sessionRef, content, provenance, projection: projection }];
230
+ }).sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt) || right.id.localeCompare(left.id));
231
+ const selected = candidates.find((candidate) => candidate.sessionRef === options.currentSessionRef) ?? candidates[0];
232
+ if (!selected)
233
+ return null;
234
+ const now = options.now ?? new Date();
235
+ const ageSeconds = Math.max(0, Math.floor((now.getTime() - Date.parse(selected.createdAt)) / 1000));
236
+ return {
237
+ artifact_id: selected.id,
238
+ source: selected.sessionRef === options.currentSessionRef ? "current_session" : "project_fallback",
239
+ session_ref: selected.sessionRef,
240
+ revision: Number(selected.content.revision),
241
+ captured_at: selected.createdAt,
242
+ age_seconds: ageSeconds,
243
+ harness: string(selected.provenance?.harness) ?? "unknown",
244
+ stale: ageSeconds > (options.staleAfterSeconds ?? 86400),
245
+ projection: selected.projection,
246
+ };
247
+ }
248
+ function renderItems(items) {
249
+ return items.length > 0 ? items.map((item) => `- ${item}`) : ["(none captured)"];
250
+ }
251
+ export function renderCompactionRecovery(recovery) {
252
+ const completeness = recovery.projection.completeness;
253
+ const lines = [
254
+ "## Compaction recovery",
255
+ `Source: ${recovery.source}`,
256
+ `Session: ${recovery.session_ref}`,
257
+ `Revision: ${recovery.revision}`,
258
+ `Captured: ${recovery.captured_at}`,
259
+ `Age: ${recovery.age_seconds} seconds`,
260
+ `Harness: ${recovery.harness}`,
261
+ `Completeness: goals=${completeness.task_goals}, decisions=${completeness.decisions_made}, todos=${completeness.open_todos}, errors=${completeness.critical_error_traces}`,
262
+ ];
263
+ if (recovery.stale)
264
+ lines.push("Warning: this state is stale; validate it against the current repository before relying on it.");
265
+ lines.push("", "### Task Goals", ...renderItems(recovery.projection.task_goals), "", "### Decisions Made", ...renderItems(recovery.projection.decisions_made), "", "### Open TODOs", ...renderItems(recovery.projection.open_todos), "", "### Critical Error Traces", ...renderItems(recovery.projection.critical_error_traces));
266
+ return `${lines.join("\n")}\n`;
267
+ }