@cairnkeep/cli 2.5.0 → 2.7.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 (62) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +33 -12
  3. package/bin/cairn +15 -3
  4. package/claude/commands/graphify.md +46 -55
  5. package/docs/building-an-overlay.md +2 -2
  6. package/docs/harness-compatibility.md +95 -9
  7. package/docs/learning/CURRICULUM-MAP.md +3 -1
  8. package/docs/learning/FEATURE-GUIDE.md +3 -2
  9. package/docs/learning/README.md +12 -12
  10. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -5
  11. package/docs/learning/lessons/L01-safe-trial.md +3 -7
  12. package/docs/learning/lessons/L02-installation.md +15 -9
  13. package/docs/learning/lessons/L03-first-project.md +1 -5
  14. package/docs/learning/lessons/L04-memory-fundamentals.md +0 -6
  15. package/docs/learning/lessons/L05-daily-workflow.md +0 -5
  16. package/docs/learning/lessons/L06-repository-quality.md +0 -6
  17. package/docs/learning/lessons/L07-privacy-and-storage.md +0 -5
  18. package/docs/learning/lessons/L08-multiple-machines.md +0 -5
  19. package/docs/learning/lessons/L09-domain-knowledge.md +0 -5
  20. package/docs/learning/lessons/L10-context-exploration.md +0 -5
  21. package/docs/learning/lessons/L11-containers.md +0 -5
  22. package/docs/learning/lessons/L12-managed-overlays.md +0 -6
  23. package/docs/learning/lessons/L13-session-evidence.md +0 -6
  24. package/docs/learning/lessons/L14-typed-memory.md +0 -6
  25. package/docs/learning/lessons/L15-capability-governance.md +0 -6
  26. package/docs/learning/lessons/L16-evaluation.md +0 -7
  27. package/docs/learning/lessons/L17-agent-boundary.md +0 -6
  28. package/docs/learning/templates/lesson-template.md +0 -4
  29. package/docs/operating.md +111 -9
  30. package/docs/privacy-and-data-flow.md +1 -0
  31. package/examples/eval/bundled-fake.json +1 -1
  32. package/kimi/skills/graphify/SKILL.md +33 -0
  33. package/mcp-memory-server/dist/capability-registry.js +2 -2
  34. package/mcp-memory-server/dist/graph-cli.js +386 -0
  35. package/mcp-memory-server/package.json +2 -1
  36. package/opencode/command/graphify.md +48 -58
  37. package/package.json +4 -2
  38. package/pi/prompts/graphify.md +32 -0
  39. package/scripts/bootstrap.sh +4 -1
  40. package/scripts/completion.sh +17 -6
  41. package/scripts/smoke-bash32.sh +1 -0
  42. package/scripts/sync-kimi-assets.sh +77 -0
  43. package/scripts/sync-pi-assets.sh +52 -31
  44. package/scripts/test-bootstrap-untracked.sh +23 -1
  45. package/scripts/test-cli-dispatch.sh +4 -0
  46. package/scripts/test-completion.sh +5 -0
  47. package/scripts/test-graph-harness-adapters.sh +46 -0
  48. package/scripts/test-launcher-hooks.sh +33 -3
  49. package/scripts/test-learning-docs.sh +2 -9
  50. package/scripts/test-package-install.sh +9 -0
  51. package/scripts/test-trajectory-hooks.sh +5 -1
  52. package/scripts/test-uninstall.sh +12 -2
  53. package/scripts/uninstall.sh +12 -4
  54. package/templates/graph-policy.md.template +23 -4
  55. package/templates/start-qwen.sh.template +32 -0
  56. package/docs/learning/PRODUCTION-PLAN.md +0 -153
  57. package/docs/learning/instructor-guide.md +0 -66
  58. package/docs/learning/templates/video-script-template.md +0 -50
  59. package/docs/learning/video-scripts/L00-why-cairnkeep.md +0 -80
  60. package/docs/learning/video-scripts/L01-safe-trial.md +0 -78
  61. package/docs/learning/video-scripts/L02-installation.md +0 -97
  62. package/docs/learning/video-scripts/L03-first-project.md +0 -99
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { randomBytes } from "node:crypto";
4
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { resolveCapabilityStatus } from "./capability-config.js";
7
+ import { isGraphCapabilityCompatible } from "./capability-registry.js";
8
+ const INSPECTION_TIMEOUT_MS = 30_000;
9
+ const BUILD_TIMEOUT_MS = 300_000;
10
+ const MAX_GRAPH_BYTES = 256 * 1024 * 1024;
11
+ const MAX_AUXILIARY_BYTES = 64 * 1024 * 1024;
12
+ const MAX_SUBPROCESS_OUTPUT_BYTES = 16 * 1024 * 1024;
13
+ const STALE_AFTER_MS = 24 * 60 * 60 * 1000;
14
+ function usage() {
15
+ return `cairn graph — build and inspect the optional local Graphify graph
16
+
17
+ Usage:
18
+ cairn graph build [--force]
19
+ cairn graph query <term>
20
+ cairn graph status
21
+ cairn graph diff
22
+ cairn graph explain <symbol>
23
+ cairn graph path <from> <to>
24
+ `;
25
+ }
26
+ function graphEdges(graph) {
27
+ return Array.isArray(graph.edges) ? graph.edges : Array.isArray(graph.links) ? graph.links : [];
28
+ }
29
+ function graphHyperedges(graph) {
30
+ return Array.isArray(graph.hyperedges) ? graph.hyperedges : [];
31
+ }
32
+ function plural(count, singular) {
33
+ return `${count} ${singular}${count === 1 ? "" : "s"}`;
34
+ }
35
+ async function readBoundedFile(path, maximumBytes) {
36
+ const info = await lstat(path);
37
+ if (!info.isFile() || info.isSymbolicLink() || info.size > maximumBytes) {
38
+ throw new Error("graph artifact is missing, unsafe, or too large.");
39
+ }
40
+ const bytes = await readFile(path);
41
+ if (bytes.byteLength > maximumBytes)
42
+ throw new Error("graph artifact is too large.");
43
+ return bytes;
44
+ }
45
+ function parseGraph(bytes) {
46
+ const value = JSON.parse(bytes.toString("utf8"));
47
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
48
+ throw new Error("graph.json is not a JSON object.");
49
+ }
50
+ const graph = value;
51
+ if (!Array.isArray(graph.nodes)
52
+ || (!Array.isArray(graph.edges) && !Array.isArray(graph.links))) {
53
+ throw new Error("graph.json is missing its node or edge arrays.");
54
+ }
55
+ return graph;
56
+ }
57
+ async function readGraph(path) {
58
+ try {
59
+ return parseGraph(await readBoundedFile(path, MAX_GRAPH_BYTES));
60
+ }
61
+ catch (error) {
62
+ if (error.code === "ENOENT") {
63
+ throw new Error("no published graph found; run /graphify build first.");
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+ function publishedGraphPath(projectRoot) {
69
+ return join(projectRoot, ".planning", "graphs", "graph.json");
70
+ }
71
+ async function requireGraphCapability(projectRoot) {
72
+ const status = await resolveCapabilityStatus({ projectRoot });
73
+ const graph = status.capabilities.find(({ id }) => id === "graph");
74
+ const enabled = status.contract_enabled
75
+ ? graph?.enabled === true
76
+ : await isGraphCapabilityCompatible(projectRoot);
77
+ if (!enabled) {
78
+ throw new Error("graph capability is disabled; use `cairn capabilities enable graph` with the managed contract, or enable `graphify.enabled` in .planning/config.json.");
79
+ }
80
+ }
81
+ function graphifyEnvironment() {
82
+ const environment = { PYTHONUNBUFFERED: "1" };
83
+ for (const name of [
84
+ "PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL",
85
+ "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "VIRTUAL_ENV", "PYTHONPATH",
86
+ ]) {
87
+ if (process.env[name] !== undefined)
88
+ environment[name] = process.env[name];
89
+ }
90
+ return environment;
91
+ }
92
+ function executeGraphify(projectRoot, args, timeout) {
93
+ const result = spawnSync("graphify", args, {
94
+ cwd: projectRoot,
95
+ encoding: "utf8",
96
+ shell: false,
97
+ timeout,
98
+ maxBuffer: MAX_SUBPROCESS_OUTPUT_BYTES,
99
+ env: graphifyEnvironment(),
100
+ });
101
+ if (result.error) {
102
+ const code = result.error.code;
103
+ if (code === "ENOENT") {
104
+ throw new Error("Graphify CLI not found; install it with `uv tool install graphifyy` (or `pipx install graphifyy`).");
105
+ }
106
+ if (code === "ETIMEDOUT")
107
+ throw new Error(`Graphify timed out after ${Math.round(timeout / 1000)} seconds.`);
108
+ if (code === "ENOBUFS")
109
+ throw new Error("Graphify exceeded the 16 MiB subprocess-output limit.");
110
+ throw new Error("Graphify could not start.");
111
+ }
112
+ if (result.signal)
113
+ throw new Error("Graphify terminated before completion.");
114
+ return result;
115
+ }
116
+ function forwardResult(result) {
117
+ if (result.stdout)
118
+ process.stdout.write(result.stdout);
119
+ if (result.stderr)
120
+ process.stderr.write(result.stderr);
121
+ if (result.status === 0)
122
+ return true;
123
+ process.exitCode = result.status ?? 1;
124
+ return false;
125
+ }
126
+ async function ensureSafeDirectory(path) {
127
+ await mkdir(path, { recursive: true, mode: 0o700 });
128
+ const info = await lstat(path);
129
+ if (!info.isDirectory() || info.isSymbolicLink())
130
+ throw new Error("graph directory is unsafe.");
131
+ }
132
+ async function ensureSafeGraphBoundary(projectRoot, createGraphs) {
133
+ const planningDirectory = join(projectRoot, ".planning");
134
+ let planningInfo;
135
+ try {
136
+ planningInfo = await lstat(planningDirectory);
137
+ }
138
+ catch (error) {
139
+ if (error.code === "ENOENT") {
140
+ throw new Error("planning directory not found; run `cairn bootstrap` first.");
141
+ }
142
+ throw error;
143
+ }
144
+ if (!planningInfo.isDirectory() || planningInfo.isSymbolicLink()) {
145
+ throw new Error("planning directory is unsafe.");
146
+ }
147
+ const graphsDirectory = join(planningDirectory, "graphs");
148
+ try {
149
+ const graphsInfo = await lstat(graphsDirectory);
150
+ if (!graphsInfo.isDirectory() || graphsInfo.isSymbolicLink()) {
151
+ throw new Error("graph directory is unsafe.");
152
+ }
153
+ }
154
+ catch (error) {
155
+ if (error.code !== "ENOENT")
156
+ throw error;
157
+ if (createGraphs)
158
+ await ensureSafeDirectory(graphsDirectory);
159
+ }
160
+ }
161
+ async function atomicWrite(path, bytes) {
162
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
163
+ try {
164
+ await writeFile(temporary, bytes, { mode: 0o600, flag: "wx" });
165
+ await rename(temporary, path);
166
+ }
167
+ finally {
168
+ await rm(temporary, { force: true });
169
+ }
170
+ }
171
+ async function publishOptional(source, destination) {
172
+ try {
173
+ await atomicWrite(destination, await readBoundedFile(source, MAX_AUXILIARY_BYTES));
174
+ return true;
175
+ }
176
+ catch (error) {
177
+ if (error.code !== "ENOENT")
178
+ throw error;
179
+ await rm(destination, { force: true });
180
+ return false;
181
+ }
182
+ }
183
+ async function isolatePublishedArtifacts(projectRoot) {
184
+ const directory = join(projectRoot, ".planning", "graphs");
185
+ const managedPaths = [
186
+ join(directory, "graph.json"),
187
+ join(directory, "graph.html"),
188
+ join(directory, "GRAPH_REPORT.md"),
189
+ join(directory, ".last-build-snapshot.json"),
190
+ ];
191
+ const saved = new Map();
192
+ try {
193
+ for (const [destination, maximumBytes] of [
194
+ [managedPaths[0], MAX_GRAPH_BYTES],
195
+ [managedPaths[1], MAX_AUXILIARY_BYTES],
196
+ [managedPaths[2], MAX_AUXILIARY_BYTES],
197
+ [managedPaths[3], MAX_GRAPH_BYTES],
198
+ ]) {
199
+ try {
200
+ saved.set(destination, await readBoundedFile(destination, maximumBytes));
201
+ await rm(destination);
202
+ }
203
+ catch (error) {
204
+ if (error.code !== "ENOENT")
205
+ throw error;
206
+ }
207
+ }
208
+ const previousBytes = saved.get(join(directory, "graph.json"));
209
+ const previousGraph = previousBytes ? parseGraph(previousBytes) : undefined;
210
+ return {
211
+ previousGraph,
212
+ finish: async (keepNewArtifact) => {
213
+ if (!keepNewArtifact) {
214
+ for (const path of managedPaths)
215
+ await rm(path, { force: true });
216
+ for (const [path, bytes] of saved)
217
+ await atomicWrite(path, bytes);
218
+ }
219
+ },
220
+ };
221
+ }
222
+ catch (error) {
223
+ for (const [path, bytes] of saved)
224
+ await atomicWrite(path, bytes);
225
+ throw error;
226
+ }
227
+ }
228
+ async function buildGraph(projectRoot, force) {
229
+ const outputDirectory = join(projectRoot, "graphify-out");
230
+ try {
231
+ const info = await lstat(outputDirectory);
232
+ if (!info.isDirectory() || info.isSymbolicLink())
233
+ throw new Error("Graphify output directory is unsafe.");
234
+ }
235
+ catch (error) {
236
+ if (error.code !== "ENOENT")
237
+ throw error;
238
+ }
239
+ const isolatedArtifacts = await isolatePublishedArtifacts(projectRoot);
240
+ let published = false;
241
+ try {
242
+ const result = executeGraphify(projectRoot, ["update", ".", ...(force ? ["--force"] : [])], BUILD_TIMEOUT_MS);
243
+ if (result.status !== 0) {
244
+ forwardResult(result);
245
+ return;
246
+ }
247
+ if (result.stderr)
248
+ process.stderr.write(result.stderr);
249
+ const sourceGraphPath = join(outputDirectory, "graph.json");
250
+ const sourceGraphBytes = await readBoundedFile(sourceGraphPath, MAX_GRAPH_BYTES);
251
+ const sourceGraph = parseGraph(sourceGraphBytes);
252
+ const graphsDirectory = join(projectRoot, ".planning", "graphs");
253
+ await ensureSafeDirectory(graphsDirectory);
254
+ const destinationGraphPath = publishedGraphPath(projectRoot);
255
+ if (isolatedArtifacts.previousGraph) {
256
+ const previous = isolatedArtifacts.previousGraph;
257
+ await atomicWrite(join(graphsDirectory, ".last-build-snapshot.json"), `${JSON.stringify({
258
+ version: 1,
259
+ timestamp: new Date().toISOString(),
260
+ nodes: previous.nodes,
261
+ edges: graphEdges(previous),
262
+ }, null, 2)}\n`);
263
+ }
264
+ await atomicWrite(destinationGraphPath, sourceGraphBytes);
265
+ const publishedNames = ["graph.json"];
266
+ for (const name of ["graph.html", "GRAPH_REPORT.md"]) {
267
+ if (await publishOptional(join(outputDirectory, name), join(graphsDirectory, name)))
268
+ publishedNames.push(name);
269
+ }
270
+ published = true;
271
+ process.stdout.write(`Built local code graph: ${plural(sourceGraph.nodes.length, "node")}, ${plural(graphEdges(sourceGraph).length, "edge")}, ${plural(graphHyperedges(sourceGraph).length, "hyperedge")}.\n`
272
+ + `Published: ${publishedNames.join(", ")} in .planning/graphs/.\n`);
273
+ }
274
+ finally {
275
+ await isolatedArtifacts.finish(published);
276
+ }
277
+ }
278
+ async function showStatus(projectRoot) {
279
+ const path = publishedGraphPath(projectRoot);
280
+ const [graph, info] = await Promise.all([readGraph(path), lstat(path)]);
281
+ const ageMs = Math.max(0, Date.now() - info.mtimeMs);
282
+ process.stdout.write(`Graph: ${plural(graph.nodes.length, "node")}, ${plural(graphEdges(graph).length, "edge")}, ${plural(graphHyperedges(graph).length, "hyperedge")}.\n`
283
+ + `Freshness: ${ageMs > STALE_AFTER_MS ? "STALE" : "FRESH"}; built ${new Date(info.mtimeMs).toISOString()}.\n`);
284
+ }
285
+ function keyedNodes(graph) {
286
+ return new Map(graph.nodes.flatMap((node) => {
287
+ if (!node || typeof node !== "object" || Array.isArray(node))
288
+ return [];
289
+ const id = node.id;
290
+ return typeof id === "string" ? [[id, node]] : [];
291
+ }));
292
+ }
293
+ function keyedEdges(graph) {
294
+ return new Map(graphEdges(graph).flatMap((edge) => {
295
+ if (!edge || typeof edge !== "object" || Array.isArray(edge))
296
+ return [];
297
+ const row = edge;
298
+ if (typeof row.source !== "string" || typeof row.target !== "string")
299
+ return [];
300
+ return [[`${row.source}\u0000${row.target}\u0000${String(row.relation ?? row.label ?? "")}`, edge]];
301
+ }));
302
+ }
303
+ function delta(current, previous) {
304
+ let added = 0;
305
+ let removed = 0;
306
+ let changed = 0;
307
+ for (const [key, value] of current) {
308
+ if (!previous.has(key))
309
+ added += 1;
310
+ else if (JSON.stringify(previous.get(key)) !== JSON.stringify(value))
311
+ changed += 1;
312
+ }
313
+ for (const key of previous.keys())
314
+ if (!current.has(key))
315
+ removed += 1;
316
+ return { added, removed, changed };
317
+ }
318
+ async function showDiff(projectRoot) {
319
+ const current = await readGraph(publishedGraphPath(projectRoot));
320
+ const snapshotPath = join(projectRoot, ".planning", "graphs", ".last-build-snapshot.json");
321
+ let previous;
322
+ try {
323
+ previous = await readGraph(snapshotPath);
324
+ }
325
+ catch (error) {
326
+ if (error instanceof Error && error.message.startsWith("no published graph found")) {
327
+ throw new Error("no previous graph snapshot found; run /graphify build again after a code change.");
328
+ }
329
+ throw error;
330
+ }
331
+ const nodes = delta(keyedNodes(current), keyedNodes(previous));
332
+ const edges = delta(keyedEdges(current), keyedEdges(previous));
333
+ process.stdout.write(`Nodes: ${nodes.added} added, ${nodes.removed} removed, ${nodes.changed} changed.\n`
334
+ + `Edges: ${edges.added} added, ${edges.removed} removed, ${edges.changed} changed.\n`);
335
+ }
336
+ function validInspectionArguments(command, args) {
337
+ const expected = command === "path" ? 2 : 1;
338
+ return args.length === expected
339
+ && args.every((value) => value.length > 0 && value.length <= 1024 && !value.startsWith("-"));
340
+ }
341
+ async function main() {
342
+ const [command = "help", ...args] = process.argv.slice(2);
343
+ if (command === "help" || command === "--help" || command === "-h") {
344
+ process.stdout.write(usage());
345
+ return;
346
+ }
347
+ const valid = (command === "build" && (args.length === 0 || (args.length === 1 && args[0] === "--force")))
348
+ || ((command === "status" || command === "diff") && args.length === 0)
349
+ || (["query", "explain", "path"].includes(command) && validInspectionArguments(command, args));
350
+ if (!valid) {
351
+ process.stderr.write(usage());
352
+ process.exitCode = 2;
353
+ return;
354
+ }
355
+ try {
356
+ const projectRoot = resolve(process.cwd());
357
+ await requireGraphCapability(projectRoot);
358
+ await ensureSafeGraphBoundary(projectRoot, command === "build");
359
+ if (command === "build") {
360
+ await buildGraph(projectRoot, args[0] === "--force");
361
+ return;
362
+ }
363
+ if (command === "status") {
364
+ await showStatus(projectRoot);
365
+ return;
366
+ }
367
+ if (command === "diff") {
368
+ await showDiff(projectRoot);
369
+ return;
370
+ }
371
+ await readGraph(publishedGraphPath(projectRoot));
372
+ const result = executeGraphify(projectRoot, [command, ...args, "--graph", publishedGraphPath(projectRoot)], INSPECTION_TIMEOUT_MS);
373
+ forwardResult(result);
374
+ }
375
+ catch (error) {
376
+ const message = error instanceof Error ? error.message : "operation failed.";
377
+ process.stderr.write(`cairn graph: ${message}\n`);
378
+ process.exitCode = 1;
379
+ }
380
+ }
381
+ process.stdout.on("error", (error) => {
382
+ if (error.code === "EPIPE")
383
+ process.exit(0);
384
+ throw error;
385
+ });
386
+ await main();
@@ -44,9 +44,10 @@
44
44
  "check:eval-schema": "node scripts/smoke-eval-schema.mjs",
45
45
  "check:eval-process": "node scripts/smoke-eval-process.mjs",
46
46
  "check:eval-statistics": "node scripts/smoke-eval-statistics.mjs",
47
+ "check:graph-cli": "node scripts/smoke-graph-cli.mjs",
47
48
  "check:notes": "npm run build && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment",
48
49
  "test": "npm run test:smoke",
49
- "test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard && npm run check:reviewed-memory && npm run check:trajectory-roundtrip && npm run check:trajectory-redaction && npm run check:trajectory-retention && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment && npm run check:node-compat && npm run check:typed-nodes && npm run check:memory-import && npm run check:note-mcp && npm run check:node-doctor && npm run check:artifact-store && npm run check:artifact-mcp && npm run check:compaction-capture && npm run check:capability-contract && npm run check:capability-logging && npm run check:capability-harness && npm run check:capability-mcp && npm run check:eval-schema && npm run check:eval-process && npm run check:eval-statistics",
50
+ "test:smoke": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:scope-guard && npm run check:http-guard && npm run check:remote-context && npm run check:explore-guard && npm run check:explore-cache && npm run check:explore-crossref && npm run check:route-guard && npm run check:reviewed-memory && npm run check:trajectory-roundtrip && npm run check:trajectory-redaction && npm run check:trajectory-retention && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment && npm run check:node-compat && npm run check:typed-nodes && npm run check:memory-import && npm run check:note-mcp && npm run check:node-doctor && npm run check:artifact-store && npm run check:artifact-mcp && npm run check:compaction-capture && npm run check:capability-contract && npm run check:capability-logging && npm run check:capability-harness && npm run check:capability-mcp && npm run check:eval-schema && npm run check:eval-process && npm run check:eval-statistics && npm run check:graph-cli",
50
51
  "test:search": "npm run build && npm run check:search",
51
52
  "test:memory": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:search",
52
53
  "test:all": "npm run test:memory"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Build, rebuild, query, and inspect the project knowledge graph in .planning/graphs/"
3
- argument-hint: "[build [--force]|query <term>|status|diff]"
3
+ argument-hint: "[build [--force]|query <term>|explain <symbol>|path <from> <to>|status|diff]"
4
4
  tools:
5
5
  read: true
6
6
  bash: true
@@ -9,7 +9,9 @@ tools:
9
9
 
10
10
  **STOP -- DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by OpenCode's command system. Using the Read tool on this file wastes tokens. Begin executing Step 1 immediately.**
11
11
 
12
- **graphify CLI:** the `graphify` subcommand is exposed by `gsd-tools` (on PATH). Invoke as `gsd-tools graphify …` as documented in this command and in `docs/CLI-TOOLS.md`. Other GSD queries use `gsd-tools <subcmd>` where a handler exists.
12
+ **graph workflow:** Cairnkeep owns this surface through `cairn graph`. The
13
+ operator installs only the isolated `graphify` executable; do not invoke
14
+ Graphify's harness installer.
13
15
 
14
16
  ## Output heading
15
17
 
@@ -17,32 +19,13 @@ Begin the final user-facing response with `GRAPHIFY`. Do not emit a standalone
17
19
  banner before owner I/O: in headless OpenCode, content-only output can end the
18
20
  turn before the command reaches its tools.
19
21
 
20
- ## Step 1 -- Config Gate
22
+ ## Step 1 -- Capability ownership
21
23
 
22
- **First action:** invoke the Read tool for `.planning/config.json`; do not emit
23
- text before this tool call.
24
-
25
- Check if graphify is enabled by reading `.planning/config.json` directly using the Read tool.
26
-
27
- **DO NOT use the gsd-tools config get-value command** -- it hard-exits on missing keys.
28
-
29
- 1. Read `.planning/config.json` using the Read tool
30
- 2. If the file does not exist: display the disabled message below and **STOP**
31
- 3. Parse the JSON content. Check if `config.graphify && config.graphify.enabled === true`
32
- 4. If `graphify.enabled` is NOT explicitly `true`: display the disabled message below and **STOP**
33
- 5. If `graphify.enabled` is `true`: proceed to Step 2
34
-
35
- **Disabled message:**
36
-
37
- ```
38
- GRAPHIFY
39
-
40
- Knowledge graph is disabled. To activate:
41
-
42
- gsd-tools config-set graphify.enabled true
43
-
44
- Then run /graphify build to create the initial graph.
45
- ```
24
+ Do not implement a separate configuration gate in the prompt. Every
25
+ `cairn graph` operation enforces the typed `graph` capability and its legacy
26
+ `graphify.enabled` compatibility default before Graphify or graph-file I/O.
27
+ Proceed directly to Step 2; if the owner reports a disabled capability, display
28
+ its fixed remediation message and **STOP**.
46
29
 
47
30
  ---
48
31
 
@@ -62,6 +45,8 @@ empty or unknown argument. Apply the same exact matching rule to every row:
62
45
  |----------|--------|
63
46
  | `build` or `build --force` | Run inline build (Step 3) |
64
47
  | `query <term>` | Run inline query (Step 2a) |
48
+ | `explain <symbol>` | Explain one exact symbol (Step 2d) |
49
+ | `path <from> <to>` | Find the shortest path between two exact symbols (Step 2e) |
65
50
  | `status` | Run inline status check (Step 2b) |
66
51
  | `diff` | Run inline diff check (Step 2c) |
67
52
  | No argument or unknown | Show usage message |
@@ -76,6 +61,8 @@ Usage: /graphify <mode>
76
61
  Modes:
77
62
  build [--force] Build or rebuild the knowledge graph
78
63
  query <term> Search the graph for a term
64
+ explain <symbol> Explain one exact symbol and its relationships
65
+ path <from> <to> Find the shortest path between two exact symbols
79
66
  status Show graph freshness and statistics
80
67
  diff Show changes since last build
81
68
  ```
@@ -86,29 +73,42 @@ Modes:
86
73
  the result and do not continue until the tool returns:
87
74
 
88
75
  ```bash
89
- gsd-tools graphify query <term>
76
+ cairn graph query <term>
90
77
  ```
91
78
 
92
- Parse the JSON output and display results:
93
- - If the output contains `"disabled": true`, display the disabled message from Step 1 and **STOP**
94
- - If the output contains `"error"` field, display the error message and **STOP**
95
- - If no nodes found, display: `No graph matches for '<term>'. Try /graphify build to create or rebuild the graph.`
96
- - Otherwise, display matched nodes grouped by type, with edge relationships and confidence tiers (EXTRACTED/INFERRED/AMBIGUOUS)
79
+ Pass the parsed term as exactly one argument and display the result verbatim.
80
+ On failure, display the error without retrying or changing configuration.
97
81
 
98
82
  **STOP** after displaying results. Do not spawn an agent.
99
83
 
84
+ ### Step 2d -- Explain
85
+
86
+ **Invoke the shell tool** with `cairn graph explain <symbol>`, passing the
87
+ parsed symbol as exactly one argument. Display Graphify's explanation verbatim.
88
+ On failure, display the error and suggest `/graphify build` only when the
89
+ published graph is missing.
90
+
91
+ **STOP** after displaying the result. Do not spawn an agent.
92
+
93
+ ### Step 2e -- Path
94
+
95
+ **Invoke the shell tool** with `cairn graph path <from> <to>`, passing each
96
+ parsed symbol as exactly one argument. Display Graphify's path result verbatim.
97
+ On failure, display the error and suggest `/graphify build` only when the
98
+ published graph is missing.
99
+
100
+ **STOP** after displaying the result. Do not spawn an agent.
101
+
100
102
  ### Step 2b -- Status
101
103
 
102
104
  **Invoke the shell tool** with this exact command; do not print the command as
103
105
  the result and do not continue until the tool returns:
104
106
 
105
107
  ```bash
106
- gsd-tools graphify status
108
+ cairn graph status
107
109
  ```
108
110
 
109
- Parse the JSON output and display:
110
- - If `exists: false`, display the message field
111
- - Otherwise show last build time, node/edge/hyperedge counts, and STALE or FRESH indicator
111
+ Display the local node/edge counts and freshness result verbatim.
112
112
 
113
113
  **STOP** after displaying status. Do not spawn an agent.
114
114
 
@@ -118,14 +118,11 @@ Parse the JSON output and display:
118
118
  the result and do not continue until the tool returns:
119
119
 
120
120
  ```bash
121
- gsd-tools graphify diff
121
+ cairn graph diff
122
122
  ```
123
123
 
124
- Parse the JSON output and display:
125
- - If `no_baseline: true`, display the message field
126
- - Otherwise show node and edge change counts (added/removed/changed)
127
-
128
- If no snapshot exists, suggest running `build` twice (first to create, second to generate a diff baseline).
124
+ Display the node and edge change counts verbatim. If no snapshot exists,
125
+ suggest rebuilding after the next code change.
129
126
 
130
127
  **STOP** after displaying diff. Do not spawn an agent.
131
128
 
@@ -137,20 +134,14 @@ If no snapshot exists, suggest running `build` twice (first to create, second to
137
134
  the result and do not continue until the tool returns:
138
135
 
139
136
  ```bash
140
- gsd-tools graphify build [--force]
137
+ cairn graph build [--force]
141
138
  ```
142
139
 
143
- Parse the JSON output and display:
144
- - If the output contains `"disabled": true`, display the disabled message from Step 1 and **STOP**
145
- - If the output contains an `"error"` field, display the error and the `stage` field when present, then **STOP**
146
- - Otherwise display the build summary using `status.last_build`, `status.node_count`, `status.edge_count`, `status.hyperedge_count`, whether the snapshot was saved, and whether a forced clean rebuild was used
147
-
148
- This command now performs the full pipeline directly:
149
- - runs `graphify update .` with legacy fallback to `graphify . --update`
150
- - when `--force` is present, clears `graphify-out/`, published graph artifacts, and `.planning/graphs/.last-build-snapshot.json` first
151
- - validates `graphify-out/graph.json`
152
- - copies `graph.json`, `graph.html`, and `GRAPH_REPORT.md` into `.planning/graphs/`
153
- - writes `.planning/graphs/.last-build-snapshot.json`
140
+ Display the build summary verbatim. This managed path runs Graphify's local
141
+ code-only `update`, strips provider credentials from its subprocess
142
+ environment, validates the resulting graph, atomically publishes available
143
+ artifacts into `.planning/graphs/`, and snapshots the previous graph for diff.
144
+ `--force` permits a rebuilt graph with fewer nodes after code deletion.
154
145
 
155
146
  **STOP** after displaying the build result. Do not spawn an agent.
156
147
 
@@ -158,7 +149,6 @@ This command now performs the full pipeline directly:
158
149
 
159
150
  ## Anti-Patterns
160
151
 
161
- 1. DO NOT spawn an agent for graphify operations -- build/query/status/diff are handled directly
152
+ 1. DO NOT spawn an agent for graphify operations -- build/query/explain/path/status/diff are handled directly
162
153
  2. DO NOT modify graph files directly outside the graphify CLI path
163
- 3. DO NOT skip the config gate check
164
- 4. DO NOT use gsd-tools config get-value for the config gate -- it exits on missing keys
154
+ 3. DO NOT duplicate capability parsing in the prompt -- `cairn graph` owns it
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.5.0",
4
- "description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode, Pi).",
3
+ "version": "2.7.0",
4
+ "description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -18,6 +18,7 @@
18
18
  "claude/",
19
19
  "opencode/",
20
20
  "pi/",
21
+ "kimi/",
21
22
  "examples/anythingllm/",
22
23
  "examples/overlay-distribution/",
23
24
  "examples/eval/",
@@ -63,6 +64,7 @@
63
64
  "agent",
64
65
  "claude-code",
65
66
  "opencode",
67
+ "qwen-code",
66
68
  "pi-coding-agent",
67
69
  "context",
68
70
  "rag"
@@ -0,0 +1,32 @@
1
+ ---
2
+ description: Build, query, and inspect the local project graph through Cairnkeep
3
+ argument-hint: "[build [--force]|query <term>|status|diff|explain <symbol>|path <from> <to>]"
4
+ ---
5
+
6
+ <!-- managed-by:cairnkeep -->
7
+ # Cairnkeep graph
8
+
9
+ Handle this request through Cairnkeep's public graph owner. The invocation
10
+ arguments are:
11
+
12
+ ```text
13
+ $ARGUMENTS
14
+ ```
15
+
16
+ Accept exactly one of these forms:
17
+
18
+ - `build`
19
+ - `build --force`
20
+ - `query <term>`
21
+ - `status`
22
+ - `diff`
23
+ - `explain <symbol>`
24
+ - `path <from> <to>`
25
+
26
+ Invoke the shell tool exactly once with the corresponding `cairn graph ...`
27
+ command. Pass each term or symbol as one safely quoted argument. Return the
28
+ command output verbatim and stop.
29
+
30
+ If the arguments do not match a supported form, show the supported forms and
31
+ stop without invoking a tool. Do not call the Graphify executable, edit graph
32
+ artifacts, spawn another agent, or implement graph logic in this prompt.
@@ -57,6 +57,7 @@ install_file "$TPL/start-claude.sh.template" "$target/.ai/start-claude.sh" 0
57
57
  install_file "$TPL/start-opencode.sh.template" "$target/.ai/start-opencode.sh" 0755
58
58
  install_file "$TPL/start-pi.sh.template" "$target/.ai/start-pi.sh" 0755
59
59
  install_file "$TPL/start-kimi.sh.template" "$target/.ai/start-kimi.sh" 0755
60
+ install_file "$TPL/start-qwen.sh.template" "$target/.ai/start-qwen.sh" 0755
60
61
  install_file "$TPL/env.example.template" "$target/.ai/env.example" 0644
61
62
  install_file "$TPL/trajectory-redaction.json.template" "$target/.ai/trajectory-redaction.json" 0644
62
63
  install_file "$TPL/capabilities.json.template" "$target/.ai/capabilities.json" 0600
@@ -103,7 +104,9 @@ echo "Next steps (full guide: docs/operating.md):"
103
104
  echo " 1. cp .ai/env.example .ai/.env and edit"
104
105
  echo " 2. Register the memory server: claude mcp add cairn-memory -s user -- cairn memory-server"
105
106
  echo " 3. Install the operating layer (commands/agents/hooks): cairn sync --apply"
106
- echo " For Pi trajectory capture, also run: cairn sync-pi --apply"
107
+ echo " For Pi trajectory + /graphify, also run: cairn sync-pi --apply"
108
+ echo " For Kimi /graphify, also run: cairn sync-kimi --apply"
107
109
  echo " From a source clone, use bin/cairn in place of cairn."
108
110
  echo " 4. Launch: $target/.ai/start-claude.sh"
109
111
  echo " Kimi memory-client launch: $target/.ai/start-kimi.sh"
112
+ echo " Qwen memory-client launch: $target/.ai/start-qwen.sh"