agentopology 0.1.1 → 0.2.1

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 (64) hide show
  1. package/README.md +8 -6
  2. package/dist/analyzer/index.d.ts +1 -0
  3. package/dist/analyzer/index.d.ts.map +1 -1
  4. package/dist/analyzer/index.js +35 -0
  5. package/dist/analyzer/index.js.map +1 -1
  6. package/dist/bindings/__tests__/bindings.test.js +11 -11
  7. package/dist/bindings/__tests__/bindings.test.js.map +1 -1
  8. package/dist/bindings/claude-code.d.ts.map +1 -1
  9. package/dist/bindings/claude-code.js +339 -85
  10. package/dist/bindings/claude-code.js.map +1 -1
  11. package/dist/bindings/openclaw.d.ts.map +1 -1
  12. package/dist/bindings/openclaw.js +367 -5
  13. package/dist/bindings/openclaw.js.map +1 -1
  14. package/dist/cli/index.js +2 -2
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/docs/__tests__/docs.test.js +2 -1
  17. package/dist/docs/__tests__/docs.test.js.map +1 -1
  18. package/dist/docs/content.d.ts.map +1 -1
  19. package/dist/docs/content.js +730 -4
  20. package/dist/docs/content.js.map +1 -1
  21. package/dist/exporters/markdown.d.ts.map +1 -1
  22. package/dist/exporters/markdown.js +165 -0
  23. package/dist/exporters/markdown.js.map +1 -1
  24. package/dist/exporters/mermaid.d.ts.map +1 -1
  25. package/dist/exporters/mermaid.js +30 -0
  26. package/dist/exporters/mermaid.js.map +1 -1
  27. package/dist/import/__tests__/import.test.js +556 -2
  28. package/dist/import/__tests__/import.test.js.map +1 -1
  29. package/dist/import/claude-code.d.ts.map +1 -1
  30. package/dist/import/claude-code.js +2 -0
  31. package/dist/import/claude-code.js.map +1 -1
  32. package/dist/import/index.d.ts +1 -0
  33. package/dist/import/index.d.ts.map +1 -1
  34. package/dist/import/index.js +7 -1
  35. package/dist/import/index.js.map +1 -1
  36. package/dist/import/openclaw.d.ts +70 -0
  37. package/dist/import/openclaw.d.ts.map +1 -0
  38. package/dist/import/openclaw.js +1060 -0
  39. package/dist/import/openclaw.js.map +1 -0
  40. package/dist/import/serializer.d.ts.map +1 -1
  41. package/dist/import/serializer.js +171 -4
  42. package/dist/import/serializer.js.map +1 -1
  43. package/dist/parser/__tests__/parser.test.js +463 -0
  44. package/dist/parser/__tests__/parser.test.js.map +1 -1
  45. package/dist/parser/ast.d.ts +122 -0
  46. package/dist/parser/ast.d.ts.map +1 -1
  47. package/dist/parser/index.d.ts +14 -2
  48. package/dist/parser/index.d.ts.map +1 -1
  49. package/dist/parser/index.js +226 -5
  50. package/dist/parser/index.js.map +1 -1
  51. package/dist/parser/validator.d.ts +1 -1
  52. package/dist/parser/validator.d.ts.map +1 -1
  53. package/dist/parser/validator.js +214 -1
  54. package/dist/parser/validator.js.map +1 -1
  55. package/dist/visualizer/index.d.ts.map +1 -1
  56. package/dist/visualizer/index.js +104 -16
  57. package/dist/visualizer/index.js.map +1 -1
  58. package/examples/memory-system.at +162 -0
  59. package/examples/script-agent-pipeline.at +65 -0
  60. package/package.json +2 -1
  61. package/skill/SKILL.md +7 -7
  62. package/spec/grammar.md +345 -10
  63. package/spec/reserved-keywords.md +25 -1
  64. package/spec/validation.md +171 -2
@@ -48,6 +48,8 @@ function toTitle(id) {
48
48
  * - item1
49
49
  * - item2
50
50
  */
51
+ /** Characters that require quoting in YAML scalar values. */
52
+ const YAML_SPECIAL = /[:#\[\]{}&*!|>'"%@`]/;
51
53
  function frontmatter(fields) {
52
54
  const lines = ["---"];
53
55
  for (const [key, value] of Object.entries(fields)) {
@@ -65,7 +67,10 @@ function frontmatter(fields) {
65
67
  lines.push(`${key}: ${value}`);
66
68
  }
67
69
  else {
68
- lines.push(`${key}: ${value}`);
70
+ // Auto-quote strings containing YAML-special characters
71
+ const str = String(value);
72
+ const needsQuotes = YAML_SPECIAL.test(str) || str.trim() !== str;
73
+ lines.push(`${key}: ${needsQuotes ? `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : str}`);
69
74
  }
70
75
  }
71
76
  lines.push("---");
@@ -93,10 +98,10 @@ function gitkeep(dirPath) {
93
98
  */
94
99
  function mapPermissionMode(perm) {
95
100
  // Claude Code native values — pass through
96
- const nativeValues = ["plan", "auto", "confirm", "bypassPermissions"];
101
+ const nativeValues = ["plan", "default", "acceptEdits", "dontAsk", "bypassPermissions"];
97
102
  if (nativeValues.includes(perm)) {
98
- // "auto" is the default, omit it
99
- if (perm === "auto")
103
+ // "default" is the default, omit it
104
+ if (perm === "default")
100
105
  return undefined;
101
106
  return perm;
102
107
  }
@@ -106,8 +111,10 @@ function mapPermissionMode(perm) {
106
111
  return "plan";
107
112
  case "autonomous":
108
113
  return undefined; // default, omit
114
+ case "auto":
115
+ return undefined; // default, omit
109
116
  case "interactive":
110
- return "default";
117
+ return "acceptEdits";
111
118
  case "unrestricted":
112
119
  return "bypassPermissions";
113
120
  default:
@@ -229,7 +236,7 @@ function generateAgents(ast) {
229
236
  if (agent.invocation !== "manual") {
230
237
  const desc = agent.description ?? ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
231
238
  if (desc) {
232
- fm.description = `"${desc}"`;
239
+ fm.description = desc;
233
240
  }
234
241
  }
235
242
  if (agent.model)
@@ -237,9 +244,9 @@ function generateAgents(ast) {
237
244
  if (agent.maxTurns != null)
238
245
  fm.maxTurns = String(agent.maxTurns);
239
246
  if (agent.tools && agent.tools.length > 0)
240
- fm.tools = agent.tools.map(convertMcpToolName);
247
+ fm.tools = agent.tools.map(convertMcpToolName).join(", ");
241
248
  if (agent.disallowedTools && agent.disallowedTools.length > 0)
242
- fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName);
249
+ fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName).join(", ");
243
250
  if (agent.mcpServers && agent.mcpServers.length > 0)
244
251
  fm.mcpServers = agent.mcpServers;
245
252
  if (agent.background === true)
@@ -352,6 +359,103 @@ function generateAgents(ast) {
352
359
  }
353
360
  sections.push("");
354
361
  }
362
+ // Memory stores — rich instructions with store details and access methods
363
+ if (agent.memory && agent.memory.length > 0) {
364
+ const storeMap = new Map((ast.stores ?? []).map(s => [s.id, s]));
365
+ sections.push("## Memory Stores");
366
+ sections.push("");
367
+ sections.push("You have access to the following memory stores. Use them to persist and retrieve knowledge across conversations.");
368
+ sections.push("");
369
+ for (const storeId of agent.memory) {
370
+ const store = storeMap.get(storeId);
371
+ if (!store) {
372
+ sections.push(`### ${storeId}`);
373
+ sections.push(`- Store not found in topology`);
374
+ sections.push("");
375
+ continue;
376
+ }
377
+ sections.push(`### ${storeId}`);
378
+ if (store.description)
379
+ sections.push(store.description);
380
+ sections.push("");
381
+ sections.push(`- **Type**: ${store.type}`);
382
+ sections.push(`- **Backend**: ${store.backend}`);
383
+ if (store.path)
384
+ sections.push(`- **Path**: \`${store.path}\``);
385
+ if (store.scope)
386
+ sections.push(`- **Scope**: ${store.scope}`);
387
+ // Access method — MCP or skill-based
388
+ const hasMcp = STORE_MCP_COMMANDS[store.backend] != null;
389
+ if (hasMcp) {
390
+ sections.push(`- **Access**: MCP server \`${storeId}\` (auto-configured in .mcp.json)`);
391
+ }
392
+ else if (store.path) {
393
+ sections.push(`- **Access**: Direct file access at \`${store.path}\``);
394
+ }
395
+ // Embedding config
396
+ if (store.embedding) {
397
+ sections.push(`- **Embedding**: ${store.embedding.model} (${store.embedding.provider}, ${store.embedding.dimensions}d)`);
398
+ }
399
+ // Search strategy
400
+ if (store.search) {
401
+ const parts = [`strategy: ${store.search.strategy}`];
402
+ if (store.search.topK)
403
+ parts.push(`top-k: ${store.search.topK}`);
404
+ if (store.search.rerank)
405
+ parts.push(`rerank: yes`);
406
+ sections.push(`- **Search**: ${parts.join(", ")}`);
407
+ }
408
+ // Ingestion sources
409
+ if (store.ingestion?.sources) {
410
+ sections.push(`- **Ingestion sources**: ${store.ingestion.sources.join(", ")}`);
411
+ }
412
+ // Lifecycle
413
+ if (store.lifecycle?.retention) {
414
+ sections.push(`- **Retention**: ${store.lifecycle.retention}`);
415
+ }
416
+ sections.push("");
417
+ }
418
+ }
419
+ // Retrieval strategy — rich details
420
+ if (agent.retrieval) {
421
+ const retrieval = (ast.retrievals ?? []).find(r => r.id === agent.retrieval);
422
+ if (retrieval) {
423
+ sections.push("## Retrieval Strategy");
424
+ sections.push("");
425
+ sections.push(`**${retrieval.id}** — When you need to recall information, use this strategy:`);
426
+ sections.push("");
427
+ if (retrieval.sources)
428
+ sections.push(`- **Sources**: ${retrieval.sources.join(", ")}`);
429
+ if (retrieval.budget)
430
+ sections.push(`- **Token budget**: ${retrieval.budget} tokens max for retrieved context`);
431
+ if (retrieval.paths)
432
+ sections.push(`- **Search paths**: ${retrieval.paths.join(", ")}`);
433
+ if (retrieval.rerank)
434
+ sections.push(`- **Reranking**: enabled — results are cross-ranked for relevance`);
435
+ if (retrieval.diversity)
436
+ sections.push(`- **Diversity**: enabled — results span different sources`);
437
+ if (retrieval.cacheHitThreshold != null) {
438
+ sections.push(`- **Cache hit**: threshold ${retrieval.cacheHitThreshold} → ${retrieval.cacheHitAction ?? "short-circuit"}`);
439
+ }
440
+ if (retrieval.scoring) {
441
+ const s = retrieval.scoring;
442
+ const weights = [];
443
+ if (s.recencyWeight != null)
444
+ weights.push(`recency: ${s.recencyWeight}`);
445
+ if (s.semanticWeight != null)
446
+ weights.push(`semantic: ${s.semanticWeight}`);
447
+ if (s.importanceWeight != null)
448
+ weights.push(`importance: ${s.importanceWeight}`);
449
+ if (weights.length)
450
+ sections.push(`- **Scoring weights**: ${weights.join(", ")}`);
451
+ }
452
+ sections.push("");
453
+ }
454
+ else {
455
+ sections.push(`Retrieval strategy: ${agent.retrieval}`);
456
+ sections.push("");
457
+ }
458
+ }
355
459
  // Outputs section
356
460
  if (agent.outputs) {
357
461
  sections.push("## Outputs");
@@ -739,7 +843,7 @@ function generateGroupNodes(ast) {
739
843
  const skillFm = {};
740
844
  skillFm.name = group.id;
741
845
  if (group.description)
742
- skillFm.description = `"${group.description}"`;
846
+ skillFm.description = group.description;
743
847
  skillFm["user-invocable"] = true;
744
848
  const skillSections = [];
745
849
  skillSections.push(frontmatter(skillFm));
@@ -756,7 +860,7 @@ function generateGroupNodes(ast) {
756
860
  const agentFm = {};
757
861
  agentFm.name = group.id;
758
862
  if (group.description)
759
- agentFm.description = `"${group.description}"`;
863
+ agentFm.description = group.description;
760
864
  const agentSections = [];
761
865
  agentSections.push(frontmatter(agentFm));
762
866
  agentSections.push("");
@@ -782,9 +886,9 @@ function generateTopologySkill(ast) {
782
886
  const fm = {};
783
887
  fm.name = name;
784
888
  if (ast.topology.description) {
785
- fm.description = `"${ast.topology.description}"`;
889
+ fm.description = ast.topology.description;
786
890
  }
787
- fm.version = `"${ast.topology.version}"`;
891
+ fm.version = ast.topology.version;
788
892
  fm.topology = name;
789
893
  if (ast.topology.patterns.length > 0) {
790
894
  fm.patterns = ast.topology.patterns;
@@ -808,7 +912,7 @@ function generateTopologySkill(ast) {
808
912
  if (mainSkill.agent)
809
913
  fm.agent = mainSkill.agent;
810
914
  if (mainSkill.allowedTools && mainSkill.allowedTools.length > 0) {
811
- fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName);
915
+ fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName).join(", ");
812
916
  }
813
917
  }
814
918
  const sections = [];
@@ -1085,6 +1189,30 @@ function generateTopologySkill(ast) {
1085
1189
  sections.push("");
1086
1190
  }
1087
1191
  }
1192
+ // Memory Stores
1193
+ const stores = ast.stores ?? [];
1194
+ if (stores.length > 0) {
1195
+ sections.push("## Memory Stores");
1196
+ sections.push("");
1197
+ for (const store of stores) {
1198
+ sections.push(`### ${store.id}`);
1199
+ sections.push(`- Type: ${store.type}`);
1200
+ sections.push(`- Backend: ${store.backend}`);
1201
+ if (store.path)
1202
+ sections.push(`- Path: ${store.path}`);
1203
+ if (store.connection)
1204
+ sections.push(`- Connection: ${store.connection}`);
1205
+ if (store.scope)
1206
+ sections.push(`- Scope: ${store.scope}`);
1207
+ if (store.search?.strategy)
1208
+ sections.push(`- Search: ${store.search.strategy}`);
1209
+ if (store.embedding?.model)
1210
+ sections.push(`- Embedding: ${store.embedding.model}`);
1211
+ if (store.lifecycle?.retention)
1212
+ sections.push(`- Retention: ${store.lifecycle.retention}`);
1213
+ sections.push("");
1214
+ }
1215
+ }
1088
1216
  files.push({
1089
1217
  path: `.claude/skills/${name}/SKILL.md`,
1090
1218
  content: sections.join("\n") + "\n",
@@ -1101,7 +1229,7 @@ function generateSkills(ast) {
1101
1229
  const sfm = {};
1102
1230
  sfm.name = skill.id;
1103
1231
  if (skill.description)
1104
- sfm.description = `"${skill.description}"`;
1232
+ sfm.description = skill.description;
1105
1233
  if (skill.disableModelInvocation != null) {
1106
1234
  sfm["disable-model-invocation"] = skill.disableModelInvocation;
1107
1235
  }
@@ -1113,7 +1241,7 @@ function generateSkills(ast) {
1113
1241
  if (skill.agent)
1114
1242
  sfm.agent = skill.agent;
1115
1243
  if (skill.allowedTools && skill.allowedTools.length > 0) {
1116
- sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName);
1244
+ sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName).join(", ");
1117
1245
  }
1118
1246
  // Only emit frontmatter if there are fields beyond just the name
1119
1247
  if (Object.keys(sfm).length > 1) {
@@ -1188,6 +1316,109 @@ function generateMemory(ast) {
1188
1316
  if (memory.workspace) {
1189
1317
  files.push(gitkeep(`.claude/skills/${name}/runs`));
1190
1318
  }
1319
+ // Store directory markers
1320
+ const stores = ast.stores ?? [];
1321
+ for (const store of stores) {
1322
+ files.push(gitkeep(`.claude/stores/${store.id}`));
1323
+ }
1324
+ return files;
1325
+ }
1326
+ /**
1327
+ * Generate memory command files for each store.
1328
+ *
1329
+ * These are Claude Code slash-commands (`.claude/commands/`) that give agents
1330
+ * a concrete way to interact with memory stores — search, ingest, and inspect.
1331
+ * This works alongside (or instead of) MCP servers.
1332
+ */
1333
+ function generateMemoryCommands(ast) {
1334
+ const files = [];
1335
+ const stores = ast.stores ?? [];
1336
+ for (const store of stores) {
1337
+ // Search command — for stores with search config or semantic/episodic type
1338
+ if (store.search || store.type === "semantic" || store.type === "episodic") {
1339
+ const hasMcp = STORE_MCP_COMMANDS[store.backend] != null;
1340
+ const searchLines = [
1341
+ "---",
1342
+ `description: "Search the ${store.id} memory store"`,
1343
+ "---",
1344
+ "",
1345
+ `# /memory-search-${store.id}`,
1346
+ "",
1347
+ `Search the **${store.id}** store for relevant information.`,
1348
+ "",
1349
+ `## Store Details`,
1350
+ `- **Type**: ${store.type}`,
1351
+ `- **Backend**: ${store.backend}`,
1352
+ ];
1353
+ if (store.path)
1354
+ searchLines.push(`- **Path**: \`${store.path}\``);
1355
+ if (store.search) {
1356
+ searchLines.push(`- **Strategy**: ${store.search.strategy}`);
1357
+ if (store.search.topK)
1358
+ searchLines.push(`- **Top-K**: ${store.search.topK}`);
1359
+ if (store.search.rerank)
1360
+ searchLines.push(`- **Reranking**: enabled`);
1361
+ }
1362
+ if (store.embedding) {
1363
+ searchLines.push(`- **Embedding**: ${store.embedding.model} (${store.embedding.provider}, ${store.embedding.dimensions}d)`);
1364
+ }
1365
+ searchLines.push("");
1366
+ searchLines.push("## Instructions");
1367
+ searchLines.push("");
1368
+ if (hasMcp) {
1369
+ searchLines.push(`Use the \`${store.id}\` MCP server to search this store.`);
1370
+ searchLines.push(`The MCP server is configured in \`.mcp.json\` and provides tools for querying the ${store.backend} database.`);
1371
+ }
1372
+ else {
1373
+ searchLines.push(`This store uses ${store.backend} at \`${store.path ?? ".claude/stores/" + store.id}\`.`);
1374
+ searchLines.push(`Read the database directly or use an appropriate library to query it.`);
1375
+ }
1376
+ searchLines.push("");
1377
+ searchLines.push("Search for: $ARGUMENTS");
1378
+ searchLines.push("");
1379
+ files.push({
1380
+ path: `.claude/commands/memory-search-${store.id}.md`,
1381
+ content: searchLines.join("\n"),
1382
+ category: "machine",
1383
+ });
1384
+ }
1385
+ // Ingest command — for stores with ingestion config
1386
+ if (store.ingestion) {
1387
+ const ingestLines = [
1388
+ "---",
1389
+ `description: "Ingest documents into the ${store.id} memory store"`,
1390
+ "---",
1391
+ "",
1392
+ `# /memory-ingest-${store.id}`,
1393
+ "",
1394
+ `Ingest documents into the **${store.id}** store.`,
1395
+ "",
1396
+ "## Ingestion Config",
1397
+ `- **Sources**: ${store.ingestion.sources?.join(", ") ?? "not specified"}`,
1398
+ `- **Chunking**: ${store.ingestion.chunking ?? "recursive"}`,
1399
+ ];
1400
+ if (store.ingestion.chunkSize)
1401
+ ingestLines.push(`- **Chunk size**: ${store.ingestion.chunkSize}`);
1402
+ if (store.ingestion.overlap)
1403
+ ingestLines.push(`- **Overlap**: ${store.ingestion.overlap}`);
1404
+ if (store.embedding) {
1405
+ ingestLines.push(`- **Embedding**: ${store.embedding.model} (${store.embedding.provider}, ${store.embedding.dimensions}d)`);
1406
+ }
1407
+ ingestLines.push("");
1408
+ ingestLines.push("## Instructions");
1409
+ ingestLines.push("");
1410
+ ingestLines.push(`1. Read files from the configured sources: ${store.ingestion.sources?.join(", ") ?? "docs/"}`);
1411
+ ingestLines.push(`2. Split content into chunks (${store.ingestion.chunking ?? "recursive"}, size: ${store.ingestion.chunkSize ?? 512})`);
1412
+ ingestLines.push(`3. Generate embeddings using ${store.embedding?.model ?? "the configured model"}`);
1413
+ ingestLines.push(`4. Insert into ${store.backend} at \`${store.path ?? ".claude/stores/" + store.id}\``);
1414
+ ingestLines.push("");
1415
+ files.push({
1416
+ path: `.claude/commands/memory-ingest-${store.id}.md`,
1417
+ content: ingestLines.join("\n"),
1418
+ category: "machine",
1419
+ });
1420
+ }
1421
+ }
1191
1422
  return files;
1192
1423
  }
1193
1424
  /** Generate gate script stubs. */
@@ -1210,7 +1441,7 @@ function generateGateScripts(ast) {
1210
1441
  }
1211
1442
  return files;
1212
1443
  }
1213
- /** Generate gate wrapper scripts that filter Task calls to the correct agent. */
1444
+ /** Generate gate wrapper scripts that run validation when agents complete. */
1214
1445
  function generateGateWrapperScripts(ast) {
1215
1446
  const files = [];
1216
1447
  const name = ast.topology.name;
@@ -1223,11 +1454,13 @@ function generateGateWrapperScripts(ast) {
1223
1454
  if (gate.behavior === "advisory")
1224
1455
  continue;
1225
1456
  const gateScriptName = gate.run.replace(/^.*\//, "").replace(/\s.*$/, "");
1226
- const onFailExit = gate.onFail === "halt" ? 'exit 1' : 'echo "Gate failed — bounce-back requested (advisory)"';
1457
+ const onFailExit = gate.onFail === "halt"
1458
+ ? `echo "Gate '${gate.id}' validation failed. Fix issues before proceeding." >&2\n exit 2`
1459
+ : `echo "Gate '${gate.id}' failed — bounce back and fix the issues." >&2\n exit 2`;
1227
1460
  const contentLines = [
1228
1461
  "#!/usr/bin/env bash",
1229
1462
  `# Gate wrapper: ${gate.id}`,
1230
- `# Enforces gate "${gate.id}" on Task tool calls.`,
1463
+ `# Enforces gate "${gate.id}" on SubagentStop (after: ${gate.after || "any"}).`,
1231
1464
  `# Auto-generated by agentopology scaffold — edit as needed.`,
1232
1465
  "set -euo pipefail",
1233
1466
  "",
@@ -1377,14 +1610,17 @@ function generateSettings(ast) {
1377
1610
  // Per-agent hooks are now rendered in AGENT.md frontmatter (not in settings.json).
1378
1611
  // This is the Claude Code native approach: hooks in the agent file are scoped
1379
1612
  // to that agent automatically.
1380
- // Gate enforcement — compile gates with run: to PreToolUse hooks
1613
+ // Gate enforcement — compile gates with run: to SubagentStop hooks
1614
+ // Gates fire AFTER an agent finishes (SubagentStop), not before a tool runs.
1615
+ // The matcher is the gate's `after:` agent name so the hook fires when that
1616
+ // specific agent completes.
1381
1617
  const gates = ast.nodes.filter((n) => n.type === "gate");
1382
1618
  for (const gate of gates) {
1383
1619
  if (!gate.run)
1384
1620
  continue;
1385
1621
  if (gate.behavior === "advisory")
1386
1622
  continue; // advisory gates stay in markdown only
1387
- const eventName = "PreToolUse";
1623
+ const eventName = "SubagentStop";
1388
1624
  if (!hooksByEvent[eventName])
1389
1625
  hooksByEvent[eventName] = [];
1390
1626
  const scriptName = `gate-${gate.id}.sh`;
@@ -1396,8 +1632,12 @@ function generateSettings(ast) {
1396
1632
  command,
1397
1633
  },
1398
1634
  ],
1399
- matcher: "Task", // Gates fire on Task tool (agent spawning)
1400
1635
  };
1636
+ // Use the gate's after: agent as matcher so the hook only fires
1637
+ // when that specific agent completes. Without after:, match all agents.
1638
+ if (gate.after) {
1639
+ hookEntry.matcher = gate.after;
1640
+ }
1401
1641
  hooksByEvent[eventName].push(hookEntry);
1402
1642
  }
1403
1643
  // Write hooksByEvent to settings if any hooks or gates were registered
@@ -1409,66 +1649,21 @@ function generateSettings(ast) {
1409
1649
  if (envSettings) {
1410
1650
  settings.env = envSettings;
1411
1651
  }
1412
- // Permissions section from settings block
1652
+ // Permissions section from settings block — nested under "permissions" key
1413
1653
  const perms = ast.settings;
1414
1654
  if (perms) {
1415
1655
  const allow = perms.allow;
1416
1656
  const deny = perms.deny;
1417
- const ask = perms.ask;
1657
+ const permObj = {};
1418
1658
  if (allow && allow.length > 0)
1419
- settings.allow = allow;
1659
+ permObj.allow = allow;
1420
1660
  if (deny && deny.length > 0)
1421
- settings.deny = deny;
1422
- if (ask && ask.length > 0)
1423
- settings.ask = ask;
1424
- }
1425
- // Defaults documentation
1426
- if (ast.defaults) {
1427
- const d = ast.defaults;
1428
- const defaultsDoc = {};
1429
- if (d.temperature != null)
1430
- defaultsDoc.temperature = d.temperature;
1431
- if (d.maxTokens != null)
1432
- defaultsDoc.maxTokens = d.maxTokens;
1433
- if (d.topP != null)
1434
- defaultsDoc.topP = d.topP;
1435
- if (d.topK != null)
1436
- defaultsDoc.topK = d.topK;
1437
- if (d.stop)
1438
- defaultsDoc.stop = d.stop;
1439
- if (d.seed != null)
1440
- defaultsDoc.seed = d.seed;
1441
- if (d.thinking)
1442
- defaultsDoc.thinking = d.thinking;
1443
- if (d.thinkingBudget != null)
1444
- defaultsDoc.thinkingBudget = d.thinkingBudget;
1445
- if (d.outputFormat)
1446
- defaultsDoc.outputFormat = d.outputFormat;
1447
- if (d.timeout)
1448
- defaultsDoc.timeout = d.timeout;
1449
- if (d.logLevel)
1450
- defaultsDoc.logLevel = d.logLevel;
1451
- if (Object.keys(defaultsDoc).length > 0) {
1452
- settings._defaults = defaultsDoc;
1453
- }
1454
- }
1455
- // Observability documentation
1456
- if (ast.observability) {
1457
- const o = ast.observability;
1458
- const obsDoc = {
1459
- enabled: o.enabled,
1460
- level: o.level,
1461
- exporter: o.exporter,
1462
- sampleRate: o.sampleRate,
1463
- };
1464
- if (o.endpoint)
1465
- obsDoc.endpoint = o.endpoint;
1466
- if (o.service)
1467
- obsDoc.service = o.service;
1468
- obsDoc.capture = o.capture;
1469
- obsDoc.spans = o.spans;
1470
- settings._observability = obsDoc;
1661
+ permObj.deny = deny;
1662
+ if (Object.keys(permObj).length > 0)
1663
+ settings.permissions = permObj;
1471
1664
  }
1665
+ // Defaults and observability are documented in the CLAUDE.md context file,
1666
+ // not in settings.json (Claude Code doesn't recognize these keys).
1472
1667
  // Top-level extensions for claude-code
1473
1668
  if (ast.extensions?.["claude-code"]) {
1474
1669
  for (const [k, v] of Object.entries(ast.extensions["claude-code"])) {
@@ -1483,9 +1678,34 @@ function generateSettings(ast) {
1483
1678
  category: "shared-config",
1484
1679
  };
1485
1680
  }
1486
- /** Generate .mcp.json from mcp-servers block. Always includes env field. */
1681
+ /**
1682
+ * Backend → MCP server mapping.
1683
+ *
1684
+ * Only packages verified to exist on npm or PyPI are included.
1685
+ * npm packages use `npx -y`, Python packages use `uvx`.
1686
+ * `pathFlag` controls how the DB path is passed (positional, --db, --db-path, etc.).
1687
+ *
1688
+ * Verified 2026-03-21:
1689
+ * npm: kuzu-mcp-server@0.1.0, mongodb-mcp-server@1.8.1
1690
+ * pip: mcp-server-sqlite@2025.4.25, mcp-server-qdrant@0.8.1,
1691
+ * mcp-server-neo4j@0.1.8, chroma-mcp@0.2.6
1692
+ */
1693
+ const STORE_MCP_COMMANDS = {
1694
+ // npm packages
1695
+ "kuzu": { command: "npx", args: ["-y", "kuzu-mcp-server"] }, // positional
1696
+ "mongodb": { command: "npx", args: ["-y", "mongodb-mcp-server"], pathFlag: "--uri" },
1697
+ // Python MCP servers (via uvx)
1698
+ "sqlite": { command: "uvx", args: ["mcp-server-sqlite"], pathFlag: "--db-path" },
1699
+ "sqlite-vec": { command: "uvx", args: ["mcp-server-sqlite"], pathFlag: "--db-path" },
1700
+ "qdrant": { command: "uvx", args: ["mcp-server-qdrant"], pathFlag: "--qdrant-url" },
1701
+ "neo4j": { command: "uvx", args: ["mcp-server-neo4j"], pathFlag: "--db-uri" },
1702
+ "chroma": { command: "uvx", args: ["chroma-mcp"], pathFlag: "--host" },
1703
+ // No verified MCP servers: lancedb, pinecone, pgvector, falkordb
1704
+ // These backends use skill-based access (slash commands) instead
1705
+ };
1487
1706
  function generateMcpJson(ast) {
1488
- if (Object.keys(ast.mcpServers).length === 0)
1707
+ const stores = ast.stores ?? [];
1708
+ if (Object.keys(ast.mcpServers).length === 0 && stores.length === 0)
1489
1709
  return null;
1490
1710
  const mcpConfig = { mcpServers: {} };
1491
1711
  const servers = mcpConfig.mcpServers;
@@ -1512,6 +1732,36 @@ function generateMcpJson(ast) {
1512
1732
  }
1513
1733
  servers[name] = entry;
1514
1734
  }
1735
+ // Merge memory store MCP server entries
1736
+ for (const store of stores) {
1737
+ const mcpCmd = STORE_MCP_COMMANDS[store.backend];
1738
+ if (!mcpCmd) {
1739
+ // Backend not in the known MCP map — skip (skill-based access only)
1740
+ continue;
1741
+ }
1742
+ const args = [...mcpCmd.args];
1743
+ const storePath = store.path ?? `.claude/stores/${store.id}`;
1744
+ // Each server has its own CLI convention for the DB path
1745
+ if (mcpCmd.pathFlag) {
1746
+ args.push(mcpCmd.pathFlag, storePath);
1747
+ }
1748
+ else {
1749
+ // No pathFlag = positional argument (e.g., kuzu-mcp-server ./path)
1750
+ args.push(storePath);
1751
+ }
1752
+ const env = {};
1753
+ if (store.connection) {
1754
+ env.CONNECTION_STRING = store.connection;
1755
+ }
1756
+ servers[store.id] = {
1757
+ command: mcpCmd.command,
1758
+ args,
1759
+ env,
1760
+ };
1761
+ }
1762
+ // If no servers were added (all stores had unsupported backends), return null
1763
+ if (Object.keys(servers).length === 0)
1764
+ return null;
1515
1765
  return {
1516
1766
  path: ".mcp.json",
1517
1767
  content: JSON.stringify(mcpConfig, null, 2) + "\n",
@@ -1676,21 +1926,23 @@ function generateCommandFiles(ast) {
1676
1926
  // Frontmatter
1677
1927
  const fm = {};
1678
1928
  if (ast.topology.description) {
1679
- fm.description = `"${ast.topology.description}"`;
1929
+ fm.description = ast.topology.description;
1930
+ }
1931
+ if (trigger.argument) {
1932
+ fm["argument-hint"] = `[${trigger.argument}]`;
1680
1933
  }
1681
1934
  sections.push(frontmatter(fm));
1682
1935
  sections.push("");
1683
- // Header
1936
+ // Header — use $ARGUMENTS for Claude Code command argument substitution
1684
1937
  sections.push(`# /${trigger.name}`);
1685
1938
  sections.push("");
1686
- sections.push(trigger.pattern);
1687
- sections.push("");
1688
- // Arguments
1689
1939
  if (trigger.argument) {
1690
- sections.push("## Arguments");
1691
- sections.push(`- ${trigger.argument}: extracted from the command pattern`);
1692
- sections.push("");
1940
+ sections.push(`/${trigger.name} $ARGUMENTS`);
1693
1941
  }
1942
+ else {
1943
+ sections.push(trigger.pattern);
1944
+ }
1945
+ sections.push("");
1694
1946
  // Pipeline (from edges, enhanced)
1695
1947
  if (ast.edges.length > 0) {
1696
1948
  sections.push("## Pipeline");
@@ -1797,7 +2049,7 @@ function generateWorkspaceProtocol(ast) {
1797
2049
  * destroy their content.
1798
2050
  */
1799
2051
  function generateContextFile(ast) {
1800
- const DEFAULT_PATH = ".claude/CONTEXT.md";
2052
+ const DEFAULT_PATH = ".claude/CLAUDE.md";
1801
2053
  let fileName = ast.context.file ?? DEFAULT_PATH;
1802
2054
  // Safety: never overwrite the user's root-level CLAUDE.md
1803
2055
  if (fileName === "CLAUDE.md" || fileName === "./CLAUDE.md") {
@@ -2026,6 +2278,8 @@ export const claudeCodeBinding = {
2026
2278
  files.push(...generateSchemas(ast));
2027
2279
  // Memory directories
2028
2280
  files.push(...generateMemory(ast));
2281
+ // Memory store command files (search/ingest per store)
2282
+ files.push(...generateMemoryCommands(ast));
2029
2283
  // Gate scripts
2030
2284
  files.push(...generateGateScripts(ast));
2031
2285
  files.push(...generateGateWrapperScripts(ast));