@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
@@ -25,6 +25,8 @@ import path from "node:path"
25
25
  // degraded outcome, not a retry loop (Pitfall 3).
26
26
 
27
27
  const SERVER_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/index.js"
28
+ const TRAJECTORY_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/trajectory-cli.js"
29
+ const ARTIFACT_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/artifact-cli.js"
28
30
  const MAX_CHARS = 12000
29
31
  const RETENTION_CAP = 5
30
32
 
@@ -43,9 +45,9 @@ type SessionMessage = {
43
45
  // phase). Async/non-blocking so a slow extraction call never freezes a live
44
46
  // interactive session; bounded by `timeoutMs` and resolves (never rejects)
45
47
  // so the caller's existing fail-open handling is unchanged.
46
- function runExtract(
47
- serverEntry: string,
48
- model: string,
48
+ function runNode(
49
+ entry: string,
50
+ args: string[],
49
51
  input: string,
50
52
  timeoutMs = 120000,
51
53
  ): Promise<{ stdout: string; stderr: string }> {
@@ -60,12 +62,12 @@ function runExtract(
60
62
  resolvePromise({ stdout, stderr })
61
63
  }
62
64
 
63
- const child = spawn("node", [serverEntry, "extract", model], {
65
+ const child = spawn("node", [entry, ...args], {
64
66
  stdio: ["pipe", "pipe", "pipe"],
65
67
  })
66
68
  const timer = setTimeout(() => {
67
69
  try {
68
- child.kill("SIGTERM")
70
+ child.kill("SIGKILL")
69
71
  } catch {
70
72
  // best-effort kill only
71
73
  }
@@ -79,12 +81,23 @@ function runExtract(
79
81
  })
80
82
  child.on("close", finish)
81
83
  child.on("error", finish)
84
+ child.stdin.on("error", () => {
85
+ // EPIPE after a failed/terminated child is part of the fail-open path.
86
+ })
82
87
 
83
88
  child.stdin.write(input)
84
89
  child.stdin.end()
85
90
  })
86
91
  }
87
92
 
93
+ function trajectoryCaptureEnabled(): boolean {
94
+ return /^(1|true|yes|on)$/i.test(process.env.CAIRN_TRAJECTORY_CAPTURE ?? "")
95
+ }
96
+
97
+ function compactionCaptureEnabled(): boolean {
98
+ return /^(1|true|yes|on)$/i.test(process.env.CAIRN_COMPACTION_CAPTURE ?? "")
99
+ }
100
+
88
101
  // Reimplements scripts/transcript-to-text.mjs's behavior (skip tool/reasoning/
89
102
  // file noise, join user/assistant text in order, cap total length keeping the
90
103
  // most recent turns) against OpenCode's { info, parts } message shape rather
@@ -113,6 +126,31 @@ export const MemoryCapturePlugin: Plugin = async ({ client, directory }) => {
113
126
  return {
114
127
  event: async ({ event }) => {
115
128
  try {
129
+ if (event.type === "session.compacted") {
130
+ if (!compactionCaptureEnabled()) return
131
+ const sessionID = event.properties?.sessionID
132
+ if (!sessionID) return
133
+
134
+ const sessionRes = await client.session.get({ path: { id: sessionID } })
135
+ const session = (sessionRes as { data?: { id?: string; parentID?: string; version?: string } })?.data
136
+ if (!session || session.parentID) return
137
+
138
+ const messagesRes = await client.session.messages({ path: { id: sessionID } })
139
+ const messages = ((messagesRes as { data?: unknown[] })?.data ?? []) as unknown[]
140
+ if (!fs.existsSync(ARTIFACT_ENTRY)) return
141
+
142
+ const capture = await runNode(
143
+ ARTIFACT_ENTRY,
144
+ ["capture-opencode", directory],
145
+ JSON.stringify({ event, session, messages, harness_version: "1.17.20" }),
146
+ 3000,
147
+ )
148
+ if (capture.stderr.trim()) {
149
+ console.warn("cairn compaction capture skipped: local capture failed")
150
+ }
151
+ return
152
+ }
153
+
116
154
  if (event.type !== "session.idle") return
117
155
  const sessionID = event.properties?.sessionID
118
156
  if (!sessionID) return
@@ -122,19 +160,24 @@ export const MemoryCapturePlugin: Plugin = async ({ client, directory }) => {
122
160
 
123
161
  const repo = directory
124
162
  const agentfsDb = path.join(repo, ".agentfs", "project.db")
125
- if (!fs.existsSync(agentfsDb)) return
126
- if (!fs.existsSync(SERVER_ENTRY)) return
163
+ const trajectoryEnabled = trajectoryCaptureEnabled()
127
164
 
165
+ // Preserve the original disabled path: all pre-existing guards run
166
+ // before any SDK request or subprocess when trajectory capture is off.
167
+ if (!trajectoryEnabled) {
168
+ if (!fs.existsSync(agentfsDb)) return
169
+ if (!fs.existsSync(SERVER_ENTRY)) return
170
+ }
128
171
  const apiKey = process.env.CAIRN_LLM_API_KEY
129
172
  const model = process.env.CAIRN_LLM_EXTRACTION_MODEL
130
- if (!apiKey || !model) return
173
+ if (!trajectoryEnabled && (!apiKey || !model)) return
131
174
 
132
175
  // session.idle's event payload only carries sessionID (no parentID) —
133
176
  // fetch the full session record to filter out subagent subsessions
134
177
  // (Pitfall 2: subagent tool calls also idle and would otherwise
135
178
  // over-stage past the 5-session retention cap's intent).
136
179
  const sessionRes = await client.session.get({ path: { id: sessionID } })
137
- const session = (sessionRes as { data?: { parentID?: string } })?.data
180
+ const session = (sessionRes as { data?: { id?: string; parentID?: string; time?: unknown } })?.data
138
181
  if (session?.parentID) return
139
182
 
140
183
  // Mark processed before doing the (possibly slow) extract call so a
@@ -144,13 +187,36 @@ export const MemoryCapturePlugin: Plugin = async ({ client, directory }) => {
144
187
 
145
188
  const messagesRes = await client.session.messages({ path: { id: sessionID } })
146
189
  const messages = ((messagesRes as { data?: SessionMessage[] })?.data ?? []) as SessionMessage[]
190
+
191
+ if (trajectoryEnabled && fs.existsSync(TRAJECTORY_ENTRY)) {
192
+ const payload = JSON.stringify({
193
+ session: { ...session, id: session?.id ?? sessionID },
194
+ messages,
195
+ })
196
+ const capture = await runNode(
197
+ TRAJECTORY_ENTRY,
198
+ ["capture-opencode", repo],
199
+ payload,
200
+ 3000,
201
+ )
202
+ if (capture.stderr.trim()) {
203
+ console.warn("cairn trajectory capture skipped: local capture failed")
204
+ }
205
+ }
206
+
207
+ // Durable-memory extraction retains its independent project/model
208
+ // gates and behavior when trajectory capture is enabled.
209
+ if (!fs.existsSync(agentfsDb)) return
210
+ if (!fs.existsSync(SERVER_ENTRY)) return
211
+ if (!apiKey || !model) return
212
+
147
213
  const text = messagesToText(messages)
148
214
  if (!text) return
149
215
 
150
216
  // Pipe the session text into the shared `extract` subcommand via
151
217
  // stdin (T-04-01 mitigation) — never string-interpolate arbitrary
152
218
  // session/message content into the shell command.
153
- const res = await runExtract(SERVER_ENTRY, model, text)
219
+ const res = await runNode(SERVER_ENTRY, ["extract", model], text)
154
220
  const candidatesJson = String(res.stdout ?? "").trim()
155
221
  if (!candidatesJson) return
156
222
 
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
+ import { spawn } from "node:child_process"
2
3
  import fs from "node:fs"
3
4
  import path from "node:path"
4
5
 
@@ -19,6 +20,47 @@ import path from "node:path"
19
20
  // session.
20
21
 
21
22
  const SERVER_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/index.js"
23
+ const ARTIFACT_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/artifact-cli.js"
24
+
25
+ function compactionCaptureEnabled(): boolean {
26
+ return /^(1|true|yes|on)$/i.test(process.env.CAIRN_COMPACTION_CAPTURE ?? "")
27
+ }
28
+
29
+ function runNode(entry: string, args: string[], timeoutMs = 3000): Promise<string> {
30
+ return new Promise((resolvePromise) => {
31
+ let stdout = ""
32
+ let settled = false
33
+ const finish = () => {
34
+ if (settled) return
35
+ settled = true
36
+ clearTimeout(timer)
37
+ resolvePromise(stdout)
38
+ }
39
+ const child = spawn("node", [entry, ...args], {
40
+ stdio: ["ignore", "pipe", "ignore"],
41
+ })
42
+ const timer = setTimeout(() => {
43
+ try {
44
+ child.kill("SIGKILL")
45
+ } catch {
46
+ // best-effort kill only
47
+ }
48
+ }, timeoutMs)
49
+ child.stdout?.on("data", (chunk: Buffer) => {
50
+ stdout += chunk.toString("utf8")
51
+ if (stdout.length > 262144) {
52
+ stdout = ""
53
+ try {
54
+ child.kill("SIGKILL")
55
+ } catch {
56
+ // best-effort kill only
57
+ }
58
+ }
59
+ })
60
+ child.on("close", finish)
61
+ child.on("error", finish)
62
+ })
63
+ }
22
64
 
23
65
  export const MemoryWakeupPlugin: Plugin = async ({ $, directory }) => {
24
66
  return {
@@ -37,7 +79,8 @@ export const MemoryWakeupPlugin: Plugin = async ({ $, directory }) => {
37
79
  const wikiIndex = path.join(repo, ".planning", "wiki", "index.md")
38
80
  const hasAgentfs = fs.existsSync(agentfsDb)
39
81
  const hasWiki = fs.existsSync(wikiIndex)
40
- if (!hasAgentfs && !hasWiki) return
82
+ const compactionEnabled = compactionCaptureEnabled()
83
+ if (!hasAgentfs && !hasWiki && !compactionEnabled) return
41
84
 
42
85
  const sections: string[] = []
43
86
 
@@ -88,12 +131,21 @@ export const MemoryWakeupPlugin: Plugin = async ({ $, directory }) => {
88
131
  }
89
132
  }
90
133
 
91
- if (sections.length === 0) return
134
+ if (sections.length > 0) {
135
+ output.system.push(
136
+ "Session-start context (auto-surfaced by the memory-wakeup plugin — use it; do not ask the user to recall anything it contains):",
137
+ )
138
+ output.system.push(sections.join("\n\n"))
139
+ }
92
140
 
93
- output.system.push(
94
- "Session-start context (auto-surfaced by the memory-wakeup plugin — use it; do not ask the user to recall anything it contains):",
95
- )
96
- output.system.push(sections.join("\n\n"))
141
+ if (compactionEnabled && fs.existsSync(ARTIFACT_ENTRY)) {
142
+ const args = ["recover", repo]
143
+ if (typeof input.sessionID === "string" && input.sessionID) {
144
+ args.push("--session-ref", `opencode:${input.sessionID}`)
145
+ }
146
+ const recovery = (await runNode(ARTIFACT_ENTRY, args, 3000)).trim()
147
+ if (recovery) output.system.push(recovery)
148
+ }
97
149
  } catch {
98
150
  // Fail open — never block a session because context surfacing failed.
99
151
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.3.1",
4
- "description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
3
+ "version": "2.5.0",
4
+ "description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode, Pi).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -17,8 +17,10 @@
17
17
  "templates/",
18
18
  "claude/",
19
19
  "opencode/",
20
+ "pi/",
20
21
  "examples/anythingllm/",
21
22
  "examples/overlay-distribution/",
23
+ "examples/eval/",
22
24
  "containers/",
23
25
  "schemas/",
24
26
  "docs/",
@@ -26,6 +28,7 @@
26
28
  "CONTRIBUTING.md",
27
29
  "SECURITY.md",
28
30
  "mcp-memory-server/dist/",
31
+ "mcp-memory-server/scripts/fixtures/compaction-*.json",
29
32
  "mcp-memory-server/package.json",
30
33
  "AGENTS.md"
31
34
  ],
@@ -33,7 +36,7 @@
33
36
  "node": ">=22"
34
37
  },
35
38
  "dependencies": {
36
- "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "@modelcontextprotocol/sdk": "^1.30.0",
37
40
  "agentfs-sdk": "^0.6.4",
38
41
  "zod": "^4.4.3"
39
42
  },
@@ -42,7 +45,7 @@
42
45
  "fix:perms": "git ls-files -s | awk '$1==\"100755\"{print $4}' | xargs chmod 755",
43
46
  "prepack": "npm run build:server && npm run fix:perms",
44
47
  "test": "for test in scripts/test-*.sh; do \"$test\" || exit 1; done",
45
- "audit:runtime": "npm audit --omit=dev --audit-level=high && npm --prefix mcp-memory-server audit --omit=dev --audit-level=high",
48
+ "audit:runtime": "node scripts/verify-dependency-security.mjs && npm audit --omit=dev --audit-level=high && npm --prefix mcp-memory-server audit --omit=dev --audit-level=high",
46
49
  "check:public": "scripts/verify-no-private-references.sh && scripts/verify-docs-parity.sh && npm run audit:runtime && npm run build:server && npm test",
47
50
  "prepublishOnly": "npm run check:public && npm --prefix mcp-memory-server test --global=false --dry-run=false"
48
51
  },
@@ -60,6 +63,7 @@
60
63
  "agent",
61
64
  "claude-code",
62
65
  "opencode",
66
+ "pi-coding-agent",
63
67
  "context",
64
68
  "rag"
65
69
  ]
@@ -0,0 +1,55 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { spawn } from "node:child_process";
3
+
4
+ const TRAJECTORY_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/trajectory-cli.js";
5
+ const CAPTURE_TIMEOUT_MS = 3000;
6
+
7
+ function captureEnabled(): boolean {
8
+ return /^(?:1|true|yes|on)$/i.test(process.env.CAIRN_TRAJECTORY_CAPTURE?.trim() ?? "");
9
+ }
10
+
11
+ function submitTrajectory(projectRoot: string, payload: string): Promise<void> {
12
+ return new Promise((resolvePromise) => {
13
+ let settled = false;
14
+ const finish = () => {
15
+ if (settled) return;
16
+ settled = true;
17
+ clearTimeout(timer);
18
+ resolvePromise();
19
+ };
20
+ const child = spawn(process.execPath, [TRAJECTORY_ENTRY, "capture-pi", projectRoot], {
21
+ stdio: ["pipe", "ignore", "ignore"],
22
+ });
23
+ const timer = setTimeout(() => {
24
+ try {
25
+ child.kill("SIGKILL");
26
+ } catch {
27
+ // Best-effort timeout; capture always fails open.
28
+ }
29
+ finish();
30
+ }, CAPTURE_TIMEOUT_MS);
31
+
32
+ child.on("close", finish);
33
+ child.on("error", finish);
34
+ child.stdin.on("error", () => {
35
+ // EPIPE is expected when validation or timeout closes the child.
36
+ });
37
+ child.stdin.end(payload);
38
+ });
39
+ }
40
+
41
+ export default function cairnTrajectoryExtension(pi: ExtensionAPI): void {
42
+ pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
43
+ if (!captureEnabled()) return;
44
+ try {
45
+ const id = ctx.sessionManager.getSessionId();
46
+ const entries = ctx.sessionManager.getBranch();
47
+ if (!id) return;
48
+ await submitTrajectory(ctx.cwd, JSON.stringify({
49
+ session: { id, version: 3, cwd: ctx.cwd, entries },
50
+ }));
51
+ } catch {
52
+ // Local trajectory capture must never block or fail a Pi shutdown.
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/cairnkeep/cairnkeep/main/schemas/artifact.schema.json",
4
+ "title": "Cairnkeep artifact contract",
5
+ "oneOf": [
6
+ { "$ref": "#/$defs/writeInput" },
7
+ { "$ref": "#/$defs/envelope" },
8
+ { "$ref": "#/$defs/listOutput" },
9
+ { "$ref": "#/$defs/doctorOutput" }
10
+ ],
11
+ "$defs": {
12
+ "kind": { "enum": ["compaction_summary", "diff", "test_output", "generated_file"] },
13
+ "artifactId": { "type": "string", "pattern": "^art_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
14
+ "sessionRef": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" },
15
+ "digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
16
+ "nodeRef": {
17
+ "type": "object", "additionalProperties": false,
18
+ "required": ["scope", "address_space", "key"],
19
+ "properties": {
20
+ "scope": { "type": "string", "minLength": 1, "maxLength": 256, "not": { "const": "all" } },
21
+ "address_space": { "enum": ["memory", "project-notes", "shared-notes"] },
22
+ "key": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))(?!.*//)(?!.*\\\\)(?!__)(?!\\.cairnkeep)(?!.*[\\u0000-\\u001f\\u007f]).+(?<!/)$" }
23
+ }
24
+ },
25
+ "provenance": {
26
+ "type": "object", "additionalProperties": false, "required": ["producer"],
27
+ "properties": {
28
+ "producer": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^\\u0000-\\u001f\\u007f]+$" },
29
+ "source_event": { "type": "string", "minLength": 1, "maxLength": 256 },
30
+ "harness": { "enum": ["claude-code", "opencode", "pi"] },
31
+ "harness_version": { "type": "string", "minLength": 1, "maxLength": 128 },
32
+ "native_id": { "type": "string", "minLength": 1, "maxLength": 256 }
33
+ }
34
+ },
35
+ "completeness": {
36
+ "type": "object", "additionalProperties": false,
37
+ "required": ["task_goals", "decisions_made", "open_todos", "critical_error_traces"],
38
+ "properties": {
39
+ "task_goals": { "enum": ["complete", "partial", "missing"] },
40
+ "decisions_made": { "enum": ["complete", "partial", "missing"] },
41
+ "open_todos": { "enum": ["complete", "partial", "missing"] },
42
+ "critical_error_traces": { "enum": ["complete", "partial", "missing"] }
43
+ }
44
+ },
45
+ "compactionInputContent": {
46
+ "type": "object", "additionalProperties": false,
47
+ "required": ["raw_summary", "task_goals", "decisions_made", "open_todos", "critical_error_traces", "completeness", "trigger"],
48
+ "properties": {
49
+ "raw_summary": { "type": "string", "maxLength": 67108864 },
50
+ "task_goals": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
51
+ "decisions_made": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
52
+ "open_todos": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
53
+ "critical_error_traces": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
54
+ "completeness": { "$ref": "#/$defs/completeness" },
55
+ "trigger": { "type": "string", "minLength": 1, "maxLength": 128 }
56
+ }
57
+ },
58
+ "compactionContent": {
59
+ "type": "object", "additionalProperties": false,
60
+ "required": ["raw_summary", "task_goals", "decisions_made", "open_todos", "critical_error_traces", "completeness", "trigger", "revision"],
61
+ "properties": {
62
+ "raw_summary": { "type": "string", "maxLength": 67108864 },
63
+ "task_goals": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
64
+ "decisions_made": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
65
+ "open_todos": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
66
+ "critical_error_traces": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
67
+ "completeness": { "$ref": "#/$defs/completeness" },
68
+ "trigger": { "type": "string", "minLength": 1, "maxLength": 128 },
69
+ "revision": { "type": "integer", "minimum": 1 }
70
+ }
71
+ },
72
+ "diffContent": { "type": "object", "additionalProperties": false, "required": ["text"], "properties": { "text": { "type": "string", "maxLength": 67108864 } } },
73
+ "testOutputContent": {
74
+ "type": "object", "additionalProperties": false, "required": ["text"],
75
+ "properties": { "text": { "type": "string", "maxLength": 67108864 }, "exit_code": { "type": "integer" }, "status": { "enum": ["passed", "failed", "unknown"] } }
76
+ },
77
+ "generatedFileInputContent": {
78
+ "type": "object", "additionalProperties": false, "required": ["path_label", "file_digest", "logical_bytes", "binary"],
79
+ "properties": {
80
+ "path_label": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))(?!.*//)(?!.*\\\\)(?!.*[\\u0000-\\u001f\\u007f]).+(?<!/)$" },
81
+ "file_digest": { "$ref": "#/$defs/digest" }, "logical_bytes": { "type": "integer", "minimum": 0 }, "binary": { "type": "boolean" }, "snapshot": { "type": "string", "maxLength": 67108864 }
82
+ }
83
+ },
84
+ "generatedFileContent": {
85
+ "type": "object", "additionalProperties": false, "required": ["path_label", "file_digest", "logical_bytes", "binary"],
86
+ "properties": {
87
+ "path_label": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))(?!.*//)(?!.*\\\\)(?!.*[\\u0000-\\u001f\\u007f]).+(?<!/)$" },
88
+ "file_digest": { "$ref": "#/$defs/digest" }, "logical_bytes": { "type": "integer", "minimum": 0 }, "binary": { "type": "boolean" }, "snapshot": { "type": "string", "maxLength": 67108864 }, "metadata_only": { "type": "boolean" }
89
+ }
90
+ },
91
+ "writeInput": {
92
+ "oneOf": [
93
+ { "$ref": "#/$defs/writeCompaction" }, { "$ref": "#/$defs/writeDiff" }, { "$ref": "#/$defs/writeTestOutput" }, { "$ref": "#/$defs/writeGeneratedFile" }
94
+ ]
95
+ },
96
+ "writeBase": {
97
+ "type": "object", "additionalProperties": false,
98
+ "required": ["kind", "session_ref", "media_type", "provenance", "content"],
99
+ "properties": {
100
+ "kind": { "$ref": "#/$defs/kind" }, "session_ref": { "$ref": "#/$defs/sessionRef" }, "node_ref": { "$ref": "#/$defs/nodeRef" },
101
+ "media_type": { "type": "string", "minLength": 1, "maxLength": 255 }, "provenance": { "$ref": "#/$defs/provenance" }, "supersedes": { "$ref": "#/$defs/artifactId" }, "content": {}
102
+ }
103
+ },
104
+ "writeCompaction": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "compaction_summary" }, "content": { "$ref": "#/$defs/compactionInputContent" } } }] },
105
+ "writeDiff": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "diff" }, "content": { "$ref": "#/$defs/diffContent" } } }] },
106
+ "writeTestOutput": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "test_output" }, "content": { "$ref": "#/$defs/testOutputContent" } } }] },
107
+ "writeGeneratedFile": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "generated_file" }, "content": { "$ref": "#/$defs/generatedFileInputContent" } } }] },
108
+ "envelope": {
109
+ "oneOf": [
110
+ { "$ref": "#/$defs/envelopeCompaction" }, { "$ref": "#/$defs/envelopeDiff" }, { "$ref": "#/$defs/envelopeTestOutput" }, { "$ref": "#/$defs/envelopeGeneratedFile" }
111
+ ]
112
+ },
113
+ "envelopeBase": {
114
+ "type": "object", "additionalProperties": false,
115
+ "required": ["schema_version", "artifact_id", "kind", "created_at", "session_ref", "media_type", "logical_bytes", "stored_bytes", "content_digest", "provenance", "redaction", "truncation", "content"],
116
+ "properties": {
117
+ "schema_version": { "const": 1 }, "artifact_id": { "$ref": "#/$defs/artifactId" }, "kind": { "$ref": "#/$defs/kind" }, "created_at": { "type": "string", "format": "date-time" },
118
+ "session_ref": { "$ref": "#/$defs/sessionRef" }, "node_ref": { "$ref": "#/$defs/nodeRef" }, "media_type": { "type": "string", "minLength": 1, "maxLength": 255 },
119
+ "logical_bytes": { "type": "integer", "minimum": 0 }, "stored_bytes": { "type": "integer", "minimum": 0 }, "content_digest": { "$ref": "#/$defs/digest" },
120
+ "provenance": { "$ref": "#/$defs/provenance" }, "redaction": { "type": "object", "additionalProperties": false, "required": ["applied", "replacement_count"], "properties": { "applied": { "type": "boolean" }, "replacement_count": { "type": "integer", "minimum": 0 } } },
121
+ "truncation": { "type": "object", "additionalProperties": false, "required": ["truncated", "original_bytes", "stored_bytes"], "properties": { "truncated": { "type": "boolean" }, "reason": { "enum": ["artifact_limit", "generated_file_metadata_only"] }, "original_bytes": { "type": "integer", "minimum": 0 }, "stored_bytes": { "type": "integer", "minimum": 0 } } },
122
+ "supersedes": { "$ref": "#/$defs/artifactId" }, "content": {}
123
+ }
124
+ },
125
+ "envelopeCompaction": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "compaction_summary" }, "content": { "$ref": "#/$defs/compactionContent" } } }] },
126
+ "envelopeDiff": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "diff" }, "content": { "$ref": "#/$defs/diffContent" } } }] },
127
+ "envelopeTestOutput": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "test_output" }, "content": { "$ref": "#/$defs/testOutputContent" } } }] },
128
+ "envelopeGeneratedFile": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "generated_file" }, "content": { "$ref": "#/$defs/generatedFileContent" } } }] },
129
+ "listOutput": { "type": "object", "additionalProperties": false, "required": ["schema_version", "artifacts", "logical_bytes"], "properties": { "schema_version": { "const": 1 }, "artifacts": { "type": "array", "items": { "$ref": "#/$defs/envelope" } }, "logical_bytes": { "type": "integer", "minimum": 0 }, "next_cursor": { "type": "string" } } },
130
+ "doctorOutput": { "type": "object", "additionalProperties": false, "required": ["schema_version", "ok", "repaired", "integrity", "issues"], "properties": { "schema_version": { "const": 1 }, "ok": { "type": "boolean" }, "repaired": { "type": "boolean" }, "integrity": { "enum": ["ok", "failed"] }, "issues": { "type": "array", "items": { "type": "string" } } }
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/cairnkeep/cairnkeep/main/schemas/capability-callback.schema.json",
4
+ "title": "Cairnkeep capability callback record",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "capability_id",
10
+ "invocation_id",
11
+ "correlation_id",
12
+ "harness",
13
+ "source",
14
+ "transport",
15
+ "started_at",
16
+ "finished_at",
17
+ "duration_ms",
18
+ "outcome",
19
+ "state_source",
20
+ "configuration_digest"
21
+ ],
22
+ "properties": {
23
+ "schema_version": { "const": 1 },
24
+ "capability_id": {
25
+ "enum": [
26
+ "memory.write",
27
+ "memory.search",
28
+ "notes.distill",
29
+ "wiki",
30
+ "graph",
31
+ "security.audit",
32
+ "route.check",
33
+ "context.explore"
34
+ ]
35
+ },
36
+ "invocation_id": {
37
+ "type": "string",
38
+ "pattern": "^cap:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
39
+ },
40
+ "correlation_id": {
41
+ "type": "string",
42
+ "minLength": 1,
43
+ "maxLength": 256,
44
+ "pattern": "^(?!unknown$)(?:cairn:)?[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$"
45
+ },
46
+ "harness": { "enum": ["claude-code", "opencode", "pi", "other"] },
47
+ "source": { "enum": ["mcp", "notes-cli", "audit-timer", "operating-command", "operating-workflow"] },
48
+ "transport": { "enum": ["stdio", "http", "local-process", "harness-command"] },
49
+ "started_at": { "type": "string", "format": "date-time" },
50
+ "finished_at": { "type": "string", "format": "date-time" },
51
+ "duration_ms": { "type": "number", "minimum": 0 },
52
+ "outcome": { "enum": ["success", "error", "timeout", "disabled"] },
53
+ "error_code": { "enum": ["callback-error", "callback-timeout", "result-error", "result-timeout", "capability-disabled"] },
54
+ "state_source": { "enum": ["environment", "project", "compatibility"] },
55
+ "configuration_digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
56
+ },
57
+ "allOf": [
58
+ {
59
+ "if": { "properties": { "outcome": { "const": "success" } }, "required": ["outcome"] },
60
+ "then": { "not": { "required": ["error_code"] } },
61
+ "else": { "required": ["error_code"] }
62
+ },
63
+ {
64
+ "if": { "properties": { "outcome": { "const": "disabled" } }, "required": ["outcome"] },
65
+ "then": { "properties": { "error_code": { "const": "capability-disabled" } } }
66
+ }
67
+ ]
68
+ }