agentopology 0.1.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 (174) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +450 -0
  3. package/dist/analyzer/__tests__/analyzer.test.d.ts +2 -0
  4. package/dist/analyzer/__tests__/analyzer.test.d.ts.map +1 -0
  5. package/dist/analyzer/__tests__/analyzer.test.js +258 -0
  6. package/dist/analyzer/__tests__/analyzer.test.js.map +1 -0
  7. package/dist/analyzer/index.d.ts +44 -0
  8. package/dist/analyzer/index.d.ts.map +1 -0
  9. package/dist/analyzer/index.js +308 -0
  10. package/dist/analyzer/index.js.map +1 -0
  11. package/dist/bindings/__tests__/bindings.test.d.ts +2 -0
  12. package/dist/bindings/__tests__/bindings.test.d.ts.map +1 -0
  13. package/dist/bindings/__tests__/bindings.test.js +2089 -0
  14. package/dist/bindings/__tests__/bindings.test.js.map +1 -0
  15. package/dist/bindings/claude-code.d.ts +12 -0
  16. package/dist/bindings/claude-code.d.ts.map +1 -0
  17. package/dist/bindings/claude-code.js +2063 -0
  18. package/dist/bindings/claude-code.js.map +1 -0
  19. package/dist/bindings/codex.d.ts +18 -0
  20. package/dist/bindings/codex.d.ts.map +1 -0
  21. package/dist/bindings/codex.js +1633 -0
  22. package/dist/bindings/codex.js.map +1 -0
  23. package/dist/bindings/copilot-cli.d.ts +20 -0
  24. package/dist/bindings/copilot-cli.d.ts.map +1 -0
  25. package/dist/bindings/copilot-cli.js +1515 -0
  26. package/dist/bindings/copilot-cli.js.map +1 -0
  27. package/dist/bindings/cursor.d.ts +17 -0
  28. package/dist/bindings/cursor.d.ts.map +1 -0
  29. package/dist/bindings/cursor.js +1715 -0
  30. package/dist/bindings/cursor.js.map +1 -0
  31. package/dist/bindings/gemini-cli.d.ts +18 -0
  32. package/dist/bindings/gemini-cli.d.ts.map +1 -0
  33. package/dist/bindings/gemini-cli.js +1327 -0
  34. package/dist/bindings/gemini-cli.js.map +1 -0
  35. package/dist/bindings/index.d.ts +20 -0
  36. package/dist/bindings/index.d.ts.map +1 -0
  37. package/dist/bindings/index.js +33 -0
  38. package/dist/bindings/index.js.map +1 -0
  39. package/dist/bindings/kiro.d.ts +17 -0
  40. package/dist/bindings/kiro.d.ts.map +1 -0
  41. package/dist/bindings/kiro.js +1624 -0
  42. package/dist/bindings/kiro.js.map +1 -0
  43. package/dist/bindings/openclaw.d.ts +18 -0
  44. package/dist/bindings/openclaw.d.ts.map +1 -0
  45. package/dist/bindings/openclaw.js +1578 -0
  46. package/dist/bindings/openclaw.js.map +1 -0
  47. package/dist/bindings/types.d.ts +33 -0
  48. package/dist/bindings/types.d.ts.map +1 -0
  49. package/dist/bindings/types.js +23 -0
  50. package/dist/bindings/types.js.map +1 -0
  51. package/dist/cli/index.d.ts +19 -0
  52. package/dist/cli/index.d.ts.map +1 -0
  53. package/dist/cli/index.js +679 -0
  54. package/dist/cli/index.js.map +1 -0
  55. package/dist/docs/__tests__/docs.test.d.ts +2 -0
  56. package/dist/docs/__tests__/docs.test.d.ts.map +1 -0
  57. package/dist/docs/__tests__/docs.test.js +438 -0
  58. package/dist/docs/__tests__/docs.test.js.map +1 -0
  59. package/dist/docs/content.d.ts +16 -0
  60. package/dist/docs/content.d.ts.map +1 -0
  61. package/dist/docs/content.js +3273 -0
  62. package/dist/docs/content.js.map +1 -0
  63. package/dist/docs/index.d.ts +17 -0
  64. package/dist/docs/index.d.ts.map +1 -0
  65. package/dist/docs/index.js +117 -0
  66. package/dist/docs/index.js.map +1 -0
  67. package/dist/exporters/__tests__/exporters.test.d.ts +2 -0
  68. package/dist/exporters/__tests__/exporters.test.d.ts.map +1 -0
  69. package/dist/exporters/__tests__/exporters.test.js +731 -0
  70. package/dist/exporters/__tests__/exporters.test.js.map +1 -0
  71. package/dist/exporters/index.d.ts +13 -0
  72. package/dist/exporters/index.d.ts.map +1 -0
  73. package/dist/exporters/index.js +18 -0
  74. package/dist/exporters/index.js.map +1 -0
  75. package/dist/exporters/json.d.ts +8 -0
  76. package/dist/exporters/json.d.ts.map +1 -0
  77. package/dist/exporters/json.js +25 -0
  78. package/dist/exporters/json.js.map +1 -0
  79. package/dist/exporters/markdown.d.ts +9 -0
  80. package/dist/exporters/markdown.d.ts.map +1 -0
  81. package/dist/exporters/markdown.js +686 -0
  82. package/dist/exporters/markdown.js.map +1 -0
  83. package/dist/exporters/mermaid.d.ts +11 -0
  84. package/dist/exporters/mermaid.d.ts.map +1 -0
  85. package/dist/exporters/mermaid.js +161 -0
  86. package/dist/exporters/mermaid.js.map +1 -0
  87. package/dist/exporters/types.d.ts +22 -0
  88. package/dist/exporters/types.d.ts.map +1 -0
  89. package/dist/exporters/types.js +10 -0
  90. package/dist/exporters/types.js.map +1 -0
  91. package/dist/import/__tests__/import.test.d.ts +2 -0
  92. package/dist/import/__tests__/import.test.d.ts.map +1 -0
  93. package/dist/import/__tests__/import.test.js +781 -0
  94. package/dist/import/__tests__/import.test.js.map +1 -0
  95. package/dist/import/claude-code.d.ts +38 -0
  96. package/dist/import/claude-code.d.ts.map +1 -0
  97. package/dist/import/claude-code.js +897 -0
  98. package/dist/import/claude-code.js.map +1 -0
  99. package/dist/import/index.d.ts +18 -0
  100. package/dist/import/index.d.ts.map +1 -0
  101. package/dist/import/index.js +28 -0
  102. package/dist/import/index.js.map +1 -0
  103. package/dist/import/serializer.d.ts +11 -0
  104. package/dist/import/serializer.d.ts.map +1 -0
  105. package/dist/import/serializer.js +960 -0
  106. package/dist/import/serializer.js.map +1 -0
  107. package/dist/index.d.ts +26 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +21 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/parser/__tests__/parser.test.d.ts +2 -0
  112. package/dist/parser/__tests__/parser.test.d.ts.map +1 -0
  113. package/dist/parser/__tests__/parser.test.js +6514 -0
  114. package/dist/parser/__tests__/parser.test.js.map +1 -0
  115. package/dist/parser/ast.d.ts +708 -0
  116. package/dist/parser/ast.d.ts.map +1 -0
  117. package/dist/parser/ast.js +12 -0
  118. package/dist/parser/ast.js.map +1 -0
  119. package/dist/parser/index.d.ts +253 -0
  120. package/dist/parser/index.d.ts.map +1 -0
  121. package/dist/parser/index.js +1985 -0
  122. package/dist/parser/index.js.map +1 -0
  123. package/dist/parser/lexer.d.ts +100 -0
  124. package/dist/parser/lexer.d.ts.map +1 -0
  125. package/dist/parser/lexer.js +304 -0
  126. package/dist/parser/lexer.js.map +1 -0
  127. package/dist/parser/validator.d.ts +42 -0
  128. package/dist/parser/validator.d.ts.map +1 -0
  129. package/dist/parser/validator.js +2568 -0
  130. package/dist/parser/validator.js.map +1 -0
  131. package/dist/scaffold/__tests__/scaffold.test.d.ts +7 -0
  132. package/dist/scaffold/__tests__/scaffold.test.d.ts.map +1 -0
  133. package/dist/scaffold/__tests__/scaffold.test.js +295 -0
  134. package/dist/scaffold/__tests__/scaffold.test.js.map +1 -0
  135. package/dist/scaffold/incremental.d.ts +43 -0
  136. package/dist/scaffold/incremental.d.ts.map +1 -0
  137. package/dist/scaffold/incremental.js +185 -0
  138. package/dist/scaffold/incremental.js.map +1 -0
  139. package/dist/scaffold/index.d.ts +13 -0
  140. package/dist/scaffold/index.d.ts.map +1 -0
  141. package/dist/scaffold/index.js +13 -0
  142. package/dist/scaffold/index.js.map +1 -0
  143. package/dist/scaffold/manifest.d.ts +30 -0
  144. package/dist/scaffold/manifest.d.ts.map +1 -0
  145. package/dist/scaffold/manifest.js +71 -0
  146. package/dist/scaffold/manifest.js.map +1 -0
  147. package/dist/scaffold/merge.d.ts +52 -0
  148. package/dist/scaffold/merge.d.ts.map +1 -0
  149. package/dist/scaffold/merge.js +134 -0
  150. package/dist/scaffold/merge.js.map +1 -0
  151. package/dist/scaffold/types.d.ts +51 -0
  152. package/dist/scaffold/types.d.ts.map +1 -0
  153. package/dist/scaffold/types.js +10 -0
  154. package/dist/scaffold/types.js.map +1 -0
  155. package/dist/sync/index.d.ts +34 -0
  156. package/dist/sync/index.d.ts.map +1 -0
  157. package/dist/sync/index.js +392 -0
  158. package/dist/sync/index.js.map +1 -0
  159. package/dist/visualizer/index.d.ts +17 -0
  160. package/dist/visualizer/index.d.ts.map +1 -0
  161. package/dist/visualizer/index.js +1976 -0
  162. package/dist/visualizer/index.js.map +1 -0
  163. package/examples/code-review.at +167 -0
  164. package/examples/data-processing.at +296 -0
  165. package/examples/openclaw-assistant.at +142 -0
  166. package/examples/scheduled-monitor.at +193 -0
  167. package/examples/simple-pipeline.at +163 -0
  168. package/package.json +77 -0
  169. package/skill/SKILL.md +403 -0
  170. package/skill/scripts/emit.ts +853 -0
  171. package/skill/scripts/visualize.ts +89 -0
  172. package/spec/grammar.md +1466 -0
  173. package/spec/reserved-keywords.md +147 -0
  174. package/spec/validation.md +535 -0
@@ -0,0 +1,2063 @@
1
+ /**
2
+ * Claude Code binding — reference implementation.
3
+ *
4
+ * Generates the `.claude/` directory structure, `.mcp.json`, and supporting
5
+ * scripts from a parsed {@link TopologyAST}.
6
+ *
7
+ * @module
8
+ */
9
+ import { deduplicateFiles } from "./types.js";
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Convert .at MCP tool notation (mcp.server.method) to Claude Code format (mcp__server__method).
15
+ * Non-MCP tool names pass through unchanged.
16
+ */
17
+ function convertMcpToolName(tool) {
18
+ if (!tool.startsWith("mcp."))
19
+ return tool;
20
+ // mcp.server-name.method_name → mcp__server-name__method_name
21
+ // Only replace the first two dots (mcp + server separator)
22
+ const parts = tool.split(".");
23
+ if (parts.length >= 3) {
24
+ return `mcp__${parts[1]}__${parts.slice(2).join(".")}`;
25
+ }
26
+ return tool;
27
+ }
28
+ /**
29
+ * Downshift all markdown headings by one level so they nest properly
30
+ * inside ## Instructions. ## → ###, ### → ####, etc.
31
+ */
32
+ function downshiftHeadings(text) {
33
+ return text.replace(/^(#{2,6})\s/gm, (_, hashes) => `#${hashes} `);
34
+ }
35
+ /** Convert a kebab-case id to Title Case. */
36
+ function toTitle(id) {
37
+ return id
38
+ .split(/[-_]/)
39
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
40
+ .join(" ");
41
+ }
42
+ /**
43
+ * Wrap content in a YAML frontmatter block.
44
+ *
45
+ * Supports scalar values, booleans, and arrays.
46
+ * Arrays are rendered as multi-line YAML lists:
47
+ * key:
48
+ * - item1
49
+ * - item2
50
+ */
51
+ function frontmatter(fields) {
52
+ const lines = ["---"];
53
+ for (const [key, value] of Object.entries(fields)) {
54
+ if (value === undefined || value === null)
55
+ continue;
56
+ if (Array.isArray(value)) {
57
+ if (value.length === 0)
58
+ continue;
59
+ lines.push(`${key}:`);
60
+ for (const item of value) {
61
+ lines.push(` - ${item}`);
62
+ }
63
+ }
64
+ else if (typeof value === "boolean") {
65
+ lines.push(`${key}: ${value}`);
66
+ }
67
+ else {
68
+ lines.push(`${key}: ${value}`);
69
+ }
70
+ }
71
+ lines.push("---");
72
+ return lines.join("\n");
73
+ }
74
+ /** Generate a shell script stub with a header comment. */
75
+ function shellStub(description) {
76
+ return [
77
+ "#!/usr/bin/env bash",
78
+ `# ${description}`,
79
+ "# Auto-generated by agentopology scaffold — edit as needed.",
80
+ 'set -euo pipefail',
81
+ "",
82
+ 'echo "TODO: implement this script"',
83
+ "",
84
+ ].join("\n");
85
+ }
86
+ /** Generate an empty directory marker (a .gitkeep file). */
87
+ function gitkeep(dirPath) {
88
+ return { path: `${dirPath}/.gitkeep`, content: "", category: "machine" };
89
+ }
90
+ /**
91
+ * Map topology permission strings to Claude Code permissionMode values.
92
+ * Returns undefined when the permission mode should be omitted (default/autonomous).
93
+ */
94
+ function mapPermissionMode(perm) {
95
+ // Claude Code native values — pass through
96
+ const nativeValues = ["plan", "auto", "confirm", "bypassPermissions"];
97
+ if (nativeValues.includes(perm)) {
98
+ // "auto" is the default, omit it
99
+ if (perm === "auto")
100
+ return undefined;
101
+ return perm;
102
+ }
103
+ // Topology-level semantic values
104
+ switch (perm) {
105
+ case "supervised":
106
+ return "plan";
107
+ case "autonomous":
108
+ return undefined; // default, omit
109
+ case "interactive":
110
+ return "default";
111
+ case "unrestricted":
112
+ return "bypassPermissions";
113
+ default:
114
+ return perm; // unknown — pass through as-is
115
+ }
116
+ }
117
+ /** Format a SchemaType to a human-readable string. Optionally resolves refs. */
118
+ function formatSchemaType(t, schemas) {
119
+ switch (t.kind) {
120
+ case "primitive":
121
+ return t.value;
122
+ case "array":
123
+ return `${formatSchemaType(t.itemType, schemas)}[]`;
124
+ case "enum":
125
+ return t.values.join(" | ");
126
+ case "ref": {
127
+ if (schemas) {
128
+ const resolved = schemas.find((s) => s.id === t.name);
129
+ if (resolved) {
130
+ const fields = resolved.fields.map((f) => `${f.name}${f.optional ? "?" : ""}: ${formatSchemaType(f.type, schemas)}`);
131
+ return `${t.name} { ${fields.join(", ")} }`;
132
+ }
133
+ }
134
+ return t.name;
135
+ }
136
+ }
137
+ }
138
+ /** Format a SchemaFieldDef to a readable line. */
139
+ function formatSchemaField(f) {
140
+ const opt = f.optional ? "?" : "";
141
+ return `- ${f.name}${opt}: ${formatSchemaType(f.type)}`;
142
+ }
143
+ // ---------------------------------------------------------------------------
144
+ // Section generators
145
+ // ---------------------------------------------------------------------------
146
+ /** Generate script stubs for each ActionNode. */
147
+ function generateActionNodes(ast) {
148
+ const files = [];
149
+ const name = ast.topology.name;
150
+ for (const node of ast.nodes) {
151
+ if (node.type !== "action")
152
+ continue;
153
+ const action = node;
154
+ const lines = ["#!/usr/bin/env bash"];
155
+ lines.push(`# Action: ${action.id} — ${action.description ?? action.label}`);
156
+ lines.push("# Auto-generated by agentopology scaffold — edit as needed.");
157
+ lines.push('set -euo pipefail');
158
+ lines.push("");
159
+ if (action.timeout) {
160
+ lines.push(`# Timeout: ${action.timeout}`);
161
+ }
162
+ if (action.onFail) {
163
+ lines.push(`# On failure: ${action.onFail}`);
164
+ }
165
+ if (action.join) {
166
+ lines.push(`# Join semantics: ${action.join}`);
167
+ }
168
+ switch (action.kind) {
169
+ case "external":
170
+ lines.push(`# External source: ${action.source ?? "unknown"}`);
171
+ lines.push("");
172
+ lines.push(`echo "TODO: implement external action — fetch from ${action.source ?? "source"}"`);
173
+ break;
174
+ case "inline":
175
+ if (action.commands && action.commands.length > 0) {
176
+ lines.push("");
177
+ for (const cmd of action.commands) {
178
+ lines.push(cmd);
179
+ }
180
+ }
181
+ else {
182
+ lines.push("");
183
+ lines.push('echo "TODO: implement inline action"');
184
+ }
185
+ break;
186
+ case "git":
187
+ lines.push("");
188
+ lines.push("# Git operation");
189
+ if (action.commands && action.commands.length > 0) {
190
+ for (const cmd of action.commands) {
191
+ lines.push(cmd);
192
+ }
193
+ }
194
+ else {
195
+ lines.push('echo "TODO: implement git action"');
196
+ }
197
+ break;
198
+ case "report":
199
+ lines.push("");
200
+ lines.push("# Report generation");
201
+ lines.push('echo "TODO: implement report action"');
202
+ break;
203
+ default:
204
+ lines.push("");
205
+ lines.push(`echo "TODO: implement ${action.kind ?? "unknown"} action"`);
206
+ break;
207
+ }
208
+ lines.push("");
209
+ files.push({
210
+ path: `.claude/skills/${name}/scripts/action-${action.id}.sh`,
211
+ content: lines.join("\n"),
212
+ category: "script",
213
+ });
214
+ }
215
+ return files;
216
+ }
217
+ /** Generate AGENT.md files for each agent node. */
218
+ function generateAgents(ast) {
219
+ const files = [];
220
+ for (const node of ast.nodes) {
221
+ if (node.type !== "agent")
222
+ continue;
223
+ const agent = node;
224
+ // Build frontmatter fields
225
+ const fm = {};
226
+ fm.name = agent.id;
227
+ // Description: prefer agent.description, then role description from roles block.
228
+ // For manual agents, omit description to prevent Claude Code auto-delegation.
229
+ if (agent.invocation !== "manual") {
230
+ const desc = agent.description ?? ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
231
+ if (desc) {
232
+ fm.description = `"${desc}"`;
233
+ }
234
+ }
235
+ if (agent.model)
236
+ fm.model = agent.model;
237
+ if (agent.maxTurns != null)
238
+ fm.maxTurns = String(agent.maxTurns);
239
+ if (agent.tools && agent.tools.length > 0)
240
+ fm.tools = agent.tools.map(convertMcpToolName);
241
+ if (agent.disallowedTools && agent.disallowedTools.length > 0)
242
+ fm.disallowedTools = agent.disallowedTools.map(convertMcpToolName);
243
+ if (agent.mcpServers && agent.mcpServers.length > 0)
244
+ fm.mcpServers = agent.mcpServers;
245
+ if (agent.background === true)
246
+ fm.background = true;
247
+ if (agent.permissions) {
248
+ const mapped = mapPermissionMode(agent.permissions);
249
+ if (mapped)
250
+ fm.permissionMode = mapped;
251
+ }
252
+ if (agent.isolation)
253
+ fm.isolation = agent.isolation;
254
+ // Note: sandbox and fallback-chain are not recognized by Claude Code frontmatter
255
+ // but are included as informational metadata for other tooling.
256
+ if (agent.sandbox != null) {
257
+ fm.sandbox = typeof agent.sandbox === "boolean" ? agent.sandbox : agent.sandbox;
258
+ }
259
+ if (agent.fallbackChain && agent.fallbackChain.length > 0) {
260
+ fm["fallback-chain"] = agent.fallbackChain.join(", ");
261
+ }
262
+ // invocation: manual — Claude Code doesn't parse this field. Instead, omit
263
+ // description to prevent auto-delegation for manual agents.
264
+ if (agent.skills && agent.skills.length > 0)
265
+ fm.skills = agent.skills;
266
+ // scale is NOT a Claude Code frontmatter field — rendered only in body as prompt text
267
+ // Merge claude-code extension fields into frontmatter
268
+ if (agent.extensions?.["claude-code"]) {
269
+ for (const [k, v] of Object.entries(agent.extensions["claude-code"])) {
270
+ if (typeof v === "string")
271
+ fm[k] = v;
272
+ else if (typeof v === "boolean")
273
+ fm[k] = v;
274
+ else if (Array.isArray(v))
275
+ fm[k] = v;
276
+ }
277
+ }
278
+ // Build frontmatter string — may include hooks YAML block
279
+ let fmStr = frontmatter(fm);
280
+ // Per-agent hooks → Claude Code AGENT.md frontmatter hooks
281
+ if (agent.hooks && agent.hooks.length > 0) {
282
+ const topologyName = ast.topology.name;
283
+ const hooksByEvent = {};
284
+ for (const hook of agent.hooks) {
285
+ if (!hooksByEvent[hook.on])
286
+ hooksByEvent[hook.on] = [];
287
+ hooksByEvent[hook.on].push(hook);
288
+ }
289
+ const hookLines = ["hooks:"];
290
+ for (const [eventName, hooks] of Object.entries(hooksByEvent)) {
291
+ hookLines.push(` ${eventName}:`);
292
+ for (const hook of hooks) {
293
+ const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
294
+ const command = `bash .claude/skills/${topologyName}/scripts/${agent.id}-${scriptName}`;
295
+ if (hook.matcher) {
296
+ hookLines.push(` - matcher: "${hook.matcher}"`);
297
+ }
298
+ else {
299
+ hookLines.push(` -`);
300
+ }
301
+ hookLines.push(` hooks:`);
302
+ hookLines.push(` - type: ${hook.type ?? "command"}`);
303
+ hookLines.push(` command: "${command}"`);
304
+ if (hook.timeout) {
305
+ hookLines.push(` timeout: ${hook.timeout}`);
306
+ }
307
+ }
308
+ }
309
+ // Insert hooks before the closing ---
310
+ fmStr = fmStr.replace(/\n---$/, "\n" + hookLines.join("\n") + "\n---");
311
+ }
312
+ // Build body
313
+ const sections = [fmStr, ""];
314
+ const displayName = agent.label || toTitle(agent.id);
315
+ // Role section — only when role text adds info beyond the prompt
316
+ const roleText = ast.roles[agent.role ?? ""] ?? ast.roles[agent.id] ?? agent.role;
317
+ if (roleText && !agent.prompt) {
318
+ sections.push("## Role");
319
+ sections.push(roleText);
320
+ sections.push("");
321
+ }
322
+ sections.push("## Instructions");
323
+ sections.push("");
324
+ if (agent.prompt) {
325
+ sections.push(downshiftHeadings(agent.prompt));
326
+ }
327
+ else if (agent.description) {
328
+ sections.push(agent.description);
329
+ if (roleText)
330
+ sections.push(`\n${roleText}`);
331
+ }
332
+ else if (roleText) {
333
+ sections.push(roleText);
334
+ }
335
+ else {
336
+ sections.push("Complete the assigned task.");
337
+ }
338
+ sections.push("");
339
+ // Reads section
340
+ if (agent.reads && agent.reads.length > 0) {
341
+ sections.push("## Reads");
342
+ for (const r of agent.reads) {
343
+ sections.push(`- ${r}`);
344
+ }
345
+ sections.push("");
346
+ }
347
+ // Writes section
348
+ if (agent.writes && agent.writes.length > 0) {
349
+ sections.push("## Writes");
350
+ for (const w of agent.writes) {
351
+ sections.push(`- ${w}`);
352
+ }
353
+ sections.push("");
354
+ }
355
+ // Outputs section
356
+ if (agent.outputs) {
357
+ sections.push("## Outputs");
358
+ for (const [field, values] of Object.entries(agent.outputs)) {
359
+ sections.push(`- ${field}: ${values.join(" | ")}`);
360
+ }
361
+ sections.push("");
362
+ }
363
+ // Timeout instruction
364
+ if (agent.timeout) {
365
+ sections.push(`You have a maximum of ${agent.timeout} to complete your work.`);
366
+ sections.push("");
367
+ }
368
+ // On-fail instruction
369
+ if (agent.onFail) {
370
+ sections.push(`If you fail: ${agent.onFail}`);
371
+ sections.push("");
372
+ }
373
+ // Retry configuration
374
+ if (agent.retry != null) {
375
+ if (typeof agent.retry === "number") {
376
+ sections.push(`Retry up to ${agent.retry} times on failure.`);
377
+ }
378
+ else {
379
+ const r = agent.retry;
380
+ const parts = [`max ${r.max} attempts`];
381
+ if (r.backoff)
382
+ parts.push(`${r.backoff} backoff`);
383
+ if (r.interval)
384
+ parts.push(`interval ${r.interval}`);
385
+ if (r.maxInterval)
386
+ parts.push(`max interval ${r.maxInterval}`);
387
+ if (r.jitter)
388
+ parts.push(`with jitter`);
389
+ if (r.nonRetryable && r.nonRetryable.length > 0) {
390
+ parts.push(`non-retryable: ${r.nonRetryable.join(", ")}`);
391
+ }
392
+ sections.push(`Retry strategy: ${parts.join(", ")}.`);
393
+ }
394
+ sections.push("");
395
+ }
396
+ // Model Configuration section
397
+ const hasModelConfig = agent.temperature != null ||
398
+ agent.maxTokens != null ||
399
+ agent.topP != null ||
400
+ agent.topK != null ||
401
+ agent.stop != null ||
402
+ agent.seed != null;
403
+ if (hasModelConfig) {
404
+ sections.push("## Model Configuration");
405
+ if (agent.temperature != null)
406
+ sections.push(`- Temperature: ${agent.temperature}`);
407
+ if (agent.maxTokens != null)
408
+ sections.push(`- Max tokens: ${agent.maxTokens}`);
409
+ if (agent.topP != null)
410
+ sections.push(`- Top-p: ${agent.topP}`);
411
+ if (agent.topK != null)
412
+ sections.push(`- Top-k: ${agent.topK}`);
413
+ if (agent.stop)
414
+ sections.push(`- Stop sequences: ${agent.stop.join(", ")}`);
415
+ if (agent.seed != null)
416
+ sections.push(`- Seed: ${agent.seed}`);
417
+ sections.push("");
418
+ }
419
+ // Thinking
420
+ if (agent.thinking) {
421
+ let thinkingLine = `Use ${agent.thinking} level reasoning.`;
422
+ if (agent.thinkingBudget != null) {
423
+ thinkingLine = `Use ${agent.thinking} level reasoning (budget: ${agent.thinkingBudget} tokens).`;
424
+ }
425
+ sections.push(thinkingLine);
426
+ sections.push("");
427
+ }
428
+ // Output format
429
+ if (agent.outputFormat) {
430
+ sections.push(`Output format: ${agent.outputFormat}`);
431
+ sections.push("");
432
+ }
433
+ // Log level
434
+ if (agent.logLevel) {
435
+ sections.push(`Log level: ${agent.logLevel}`);
436
+ sections.push("");
437
+ }
438
+ // Join semantics
439
+ if (agent.join) {
440
+ sections.push(`Wait strategy: ${agent.join} (wait for upstream agents)`);
441
+ sections.push("");
442
+ }
443
+ // Circuit breaker
444
+ if (agent.circuitBreaker) {
445
+ const cb = agent.circuitBreaker;
446
+ sections.push("## Circuit Breaker");
447
+ sections.push(`- Failure threshold: ${cb.threshold}`);
448
+ sections.push(`- Window: ${cb.window}`);
449
+ sections.push(`- Cooldown: ${cb.cooldown}`);
450
+ sections.push("");
451
+ }
452
+ // Compensates
453
+ if (agent.compensates) {
454
+ sections.push(`This agent compensates (can undo) the work of: ${agent.compensates}`);
455
+ sections.push("");
456
+ }
457
+ // Input Schema
458
+ if (agent.inputSchema && agent.inputSchema.length > 0) {
459
+ sections.push("## Input Schema");
460
+ for (const field of agent.inputSchema) {
461
+ sections.push(formatSchemaField(field));
462
+ }
463
+ sections.push("");
464
+ }
465
+ // Output Schema
466
+ if (agent.outputSchema && agent.outputSchema.length > 0) {
467
+ sections.push("## Output Schema");
468
+ for (const field of agent.outputSchema) {
469
+ sections.push(formatSchemaField(field));
470
+ }
471
+ sections.push("");
472
+ }
473
+ // Rate limit
474
+ if (agent.rateLimit) {
475
+ sections.push(`Rate limit: ${agent.rateLimit}`);
476
+ sections.push("");
477
+ }
478
+ // Artifacts: produces/consumes
479
+ if ((agent.produces && agent.produces.length > 0) || (agent.consumes && agent.consumes.length > 0)) {
480
+ sections.push("## Artifacts");
481
+ if (agent.produces && agent.produces.length > 0) {
482
+ sections.push(`Produces: ${agent.produces.join(", ")}`);
483
+ }
484
+ if (agent.consumes && agent.consumes.length > 0) {
485
+ sections.push(`Consumes: ${agent.consumes.join(", ")}`);
486
+ }
487
+ sections.push("");
488
+ }
489
+ // Variants
490
+ if (agent.variants && agent.variants.length > 0) {
491
+ sections.push("## Variants");
492
+ for (const v of agent.variants) {
493
+ const parts = [`**${v.id}** (weight: ${v.weight})`];
494
+ if (v.model)
495
+ parts.push(`model: ${v.model}`);
496
+ if (v.temperature != null)
497
+ parts.push(`temperature: ${v.temperature}`);
498
+ sections.push(`- ${parts.join(", ")}`);
499
+ if (v.prompt) {
500
+ sections.push(` Prompt: ${v.prompt}`);
501
+ }
502
+ }
503
+ sections.push("");
504
+ }
505
+ // Scale
506
+ if (agent.scale) {
507
+ const s = agent.scale;
508
+ sections.push("## Scale");
509
+ sections.push(`- Mode: ${s.mode}`);
510
+ sections.push(`- By: ${s.by}`);
511
+ sections.push(`- Min: ${s.min}`);
512
+ sections.push(`- Max: ${s.max}`);
513
+ if (s.batchSize != null)
514
+ sections.push(`- Batch size: ${s.batchSize}`);
515
+ sections.push("");
516
+ }
517
+ if (agent.behavior) {
518
+ sections.push(`Behavior: ${agent.behavior}`);
519
+ }
520
+ if (agent.skip) {
521
+ sections.push(`Skip when: ${agent.skip}`);
522
+ }
523
+ // Inject group conversation protocol if this agent is a group member
524
+ const groupProto = groupProtocols.get(agent.id);
525
+ if (groupProto) {
526
+ sections.push(groupProto);
527
+ }
528
+ // Inject group transcript note if this agent receives group output
529
+ const downstreamNote = groupDownstream.get(agent.id);
530
+ if (downstreamNote) {
531
+ sections.push(downstreamNote);
532
+ }
533
+ files.push({
534
+ path: `.claude/agents/${agent.id}/AGENT.md`,
535
+ content: sections.join("\n") + "\n",
536
+ category: "agent",
537
+ });
538
+ }
539
+ return files;
540
+ }
541
+ /** Generate AGENT.md files for each human node. */
542
+ function generateHumanNodes(ast) {
543
+ const files = [];
544
+ for (const node of ast.nodes) {
545
+ if (node.type !== "human")
546
+ continue;
547
+ const human = node;
548
+ const sections = [];
549
+ sections.push(`# ${toTitle(human.id)} (Human-in-the-Loop)`);
550
+ sections.push("");
551
+ if (human.description) {
552
+ sections.push(human.description);
553
+ sections.push("");
554
+ }
555
+ if (human.timeout) {
556
+ sections.push(`Timeout: ${human.timeout}`);
557
+ }
558
+ if (human.onTimeout) {
559
+ sections.push(`On timeout: ${human.onTimeout}`);
560
+ }
561
+ sections.push("");
562
+ files.push({
563
+ path: `.claude/agents/${human.id}/AGENT.md`,
564
+ content: sections.join("\n") + "\n",
565
+ category: "machine",
566
+ });
567
+ }
568
+ return files;
569
+ }
570
+ /**
571
+ * Generate group chat artifacts:
572
+ * - Orchestrator AGENT.md — spawns members sequentially
573
+ * - Shared transcript file — the communication channel
574
+ * - Group config JSON — metadata for reference
575
+ * - Group protocol section injected into each member's AGENT.md (via groupProtocols map)
576
+ *
577
+ * Architecture: file-based group chat.
578
+ * Instead of passing context through prompts, agents communicate via a shared
579
+ * transcript file (`.claude/groups/<id>/transcript.md`). Each agent:
580
+ * 1. Reads the transcript to see all previous messages
581
+ * 2. Appends its response under the current round heading
582
+ * The orchestrator spawns agents sequentially — execution order IS the lock.
583
+ */
584
+ /** Map of agent-id → group protocol instructions to inject into their AGENT.md. */
585
+ const groupProtocols = new Map();
586
+ /** Map of agent-id → group transcript note for downstream agents (receivers of group output). */
587
+ const groupDownstream = new Map();
588
+ function generateGroupNodes(ast) {
589
+ const files = [];
590
+ for (const node of ast.nodes) {
591
+ if (node.type !== "group")
592
+ continue;
593
+ const group = node;
594
+ const selectionMode = (group.speakerSelection ?? "sequential").replace(/^"|"$/g, "");
595
+ const rounds = group.maxRounds ?? 1;
596
+ const transcriptPath = `.claude/groups/${group.id}/transcript.md`;
597
+ // --- 1. Group config JSON ---
598
+ const config = {
599
+ id: group.id,
600
+ members: group.members,
601
+ speakerSelection: selectionMode,
602
+ maxRounds: rounds,
603
+ transcriptPath,
604
+ };
605
+ if (group.termination)
606
+ config.termination = group.termination;
607
+ if (group.timeout)
608
+ config.timeout = group.timeout;
609
+ files.push({
610
+ path: `.claude/groups/${group.id}/config.json`,
611
+ content: JSON.stringify(config, null, 2) + "\n",
612
+ category: "machine",
613
+ });
614
+ // --- 2. Transcript template ---
615
+ const transcriptLines = [];
616
+ transcriptLines.push(`# ${group.label || toTitle(group.id)}`);
617
+ transcriptLines.push("");
618
+ if (group.description) {
619
+ transcriptLines.push(group.description);
620
+ transcriptLines.push("");
621
+ }
622
+ transcriptLines.push(`**Participants:** ${group.members.join(", ")}`);
623
+ transcriptLines.push(`**Rounds:** ${rounds}`);
624
+ if (selectionMode !== "sequential") {
625
+ transcriptLines.push(`**Speaker order:** ${selectionMode}`);
626
+ }
627
+ transcriptLines.push("");
628
+ transcriptLines.push("---");
629
+ transcriptLines.push("");
630
+ transcriptLines.push("<!-- Topic will be written here by the orchestrator -->");
631
+ transcriptLines.push("");
632
+ files.push({
633
+ path: transcriptPath,
634
+ content: transcriptLines.join("\n"),
635
+ category: "agent",
636
+ });
637
+ // --- 3. Register protocol for each member agent ---
638
+ for (const member of group.members) {
639
+ const protocol = [
640
+ "",
641
+ "## Group Conversation Protocol",
642
+ "",
643
+ `You are participating in a group conversation via a shared transcript file: \`${transcriptPath}\``,
644
+ "",
645
+ "**Before responding:**",
646
+ `1. Read \`${transcriptPath}\` to see the full conversation so far`,
647
+ "2. Review what other participants have said",
648
+ "",
649
+ "**Your response:**",
650
+ `1. Append your response to \`${transcriptPath}\` under the current round heading`,
651
+ `2. Start with a \`### ${member}\` heading on its own line, then write your response below it`,
652
+ "3. Respond directly to what previous speakers said — engage with their arguments",
653
+ "4. Keep your response focused and concise",
654
+ "",
655
+ `**Do NOT** overwrite or replace the file contents. Only append to the end.`,
656
+ "",
657
+ ].join("\n");
658
+ groupProtocols.set(member, protocol);
659
+ }
660
+ // --- 3b. Register downstream agents (agents receiving group output via flow edges) ---
661
+ for (const edge of ast.edges) {
662
+ if (edge.from === group.id && !group.members.includes(edge.to)) {
663
+ const note = [
664
+ "",
665
+ "## Group Conversation Input",
666
+ "",
667
+ `This agent receives the output of a group conversation. The full debate/discussion transcript is available at: \`${transcriptPath}\``,
668
+ "",
669
+ `**Read \`${transcriptPath}\` first** to see the complete conversation before making your evaluation or response.`,
670
+ "",
671
+ ].join("\n");
672
+ groupDownstream.set(edge.to, note);
673
+ }
674
+ }
675
+ // --- 4. Build the orchestration body (shared between skill and agent) ---
676
+ const bodyLines = [];
677
+ if (group.description) {
678
+ bodyLines.push(group.description);
679
+ bodyLines.push("");
680
+ }
681
+ bodyLines.push("You orchestrate a group conversation using a shared transcript file. Each participant reads the transcript, sees what others wrote, and appends their response.");
682
+ bodyLines.push("");
683
+ bodyLines.push(`**Transcript file:** \`${transcriptPath}\``);
684
+ bodyLines.push("");
685
+ // Participant table
686
+ bodyLines.push("### Participants");
687
+ bodyLines.push("");
688
+ for (const member of group.members) {
689
+ const memberNode = ast.nodes.find((n) => n.id === member);
690
+ const memberDesc = memberNode?.type === "agent"
691
+ ? memberNode.description ?? memberNode.label
692
+ : memberNode?.label ?? member;
693
+ const memberModel = memberNode?.type === "agent"
694
+ ? memberNode.model : undefined;
695
+ const modelNote = memberModel ? ` (model: ${memberModel})` : "";
696
+ bodyLines.push(`- **${member}**${modelNote}: ${memberDesc}`);
697
+ }
698
+ bodyLines.push("");
699
+ // Step-by-step orchestration
700
+ bodyLines.push("### Step-by-step process");
701
+ bodyLines.push("");
702
+ bodyLines.push(`1. Write the topic/prompt at the top of \`${transcriptPath}\` (replace the placeholder comment)`);
703
+ bodyLines.push("");
704
+ const totalTurns = rounds * group.members.length;
705
+ let step = 2;
706
+ for (let round = 1; round <= rounds; round++) {
707
+ bodyLines.push(`**Round ${round}:**`);
708
+ bodyLines.push("");
709
+ bodyLines.push(`${step}. Append \`## Round ${round}\` to the transcript`);
710
+ step++;
711
+ for (const member of group.members) {
712
+ const memberNode = ast.nodes.find((n) => n.id === member);
713
+ const memberModel = memberNode?.type === "agent"
714
+ ? memberNode.model : undefined;
715
+ const modelParam = memberModel ? `, model: ${memberModel}` : "";
716
+ bodyLines.push(`${step}. Spawn **${member}** (subagent_type: "${member}"${modelParam}). The agent will read the transcript, see what others wrote, and append its response.`);
717
+ step++;
718
+ }
719
+ bodyLines.push("");
720
+ }
721
+ bodyLines.push(`${step}. After all ${totalTurns} turns, read the final transcript and return it as your output.`);
722
+ bodyLines.push("");
723
+ if (group.termination) {
724
+ bodyLines.push(`**Termination condition:** ${group.termination} (but always complete the configured ${rounds} round(s) unless this condition is met earlier)`);
725
+ }
726
+ if (group.timeout) {
727
+ bodyLines.push(`**Timeout:** ${group.timeout}`);
728
+ }
729
+ bodyLines.push("");
730
+ bodyLines.push("### Important");
731
+ bodyLines.push("");
732
+ bodyLines.push("- Spawn agents **one at a time, sequentially** — never in parallel");
733
+ bodyLines.push("- Each agent reads the transcript BEFORE responding, so it sees all previous messages");
734
+ bodyLines.push("- The transcript file is the shared state — do not pass conversation history in prompts");
735
+ bodyLines.push("- Wait for each agent to finish before spawning the next one");
736
+ bodyLines.push("");
737
+ const orchestrationBody = bodyLines.join("\n");
738
+ // --- 5. Skill (primary — runs in main session, has Agent tool access) ---
739
+ const skillFm = {};
740
+ skillFm.name = group.id;
741
+ if (group.description)
742
+ skillFm.description = `"${group.description}"`;
743
+ skillFm["user-invocable"] = true;
744
+ const skillSections = [];
745
+ skillSections.push(frontmatter(skillFm));
746
+ skillSections.push("");
747
+ skillSections.push(`# ${group.label || toTitle(group.id)}`);
748
+ skillSections.push("");
749
+ skillSections.push(orchestrationBody);
750
+ files.push({
751
+ path: `.claude/skills/${group.id}/SKILL.md`,
752
+ content: skillSections.join("\n") + "\n",
753
+ category: "agent",
754
+ });
755
+ // --- 6. Agent (fallback — for when spawned as subagent, may not have Agent tool) ---
756
+ const agentFm = {};
757
+ agentFm.name = group.id;
758
+ if (group.description)
759
+ agentFm.description = `"${group.description}"`;
760
+ const agentSections = [];
761
+ agentSections.push(frontmatter(agentFm));
762
+ agentSections.push("");
763
+ agentSections.push("## Instructions");
764
+ agentSections.push("");
765
+ agentSections.push(orchestrationBody);
766
+ agentSections.push("");
767
+ agentSections.push("> **Note:** If the Agent tool is not available, simulate the group conversation by writing each participant's response yourself, following the round structure above. Write all responses to the transcript file.");
768
+ agentSections.push("");
769
+ files.push({
770
+ path: `.claude/agents/${group.id}/AGENT.md`,
771
+ content: agentSections.join("\n") + "\n",
772
+ category: "agent",
773
+ });
774
+ }
775
+ return files;
776
+ }
777
+ /** Generate the main SKILL.md for the topology (orchestrator + flow + gates). */
778
+ function generateTopologySkill(ast) {
779
+ const files = [];
780
+ const name = ast.topology.name;
781
+ // Build frontmatter
782
+ const fm = {};
783
+ fm.name = name;
784
+ if (ast.topology.description) {
785
+ fm.description = `"${ast.topology.description}"`;
786
+ }
787
+ fm.version = `"${ast.topology.version}"`;
788
+ fm.topology = name;
789
+ if (ast.topology.patterns.length > 0) {
790
+ fm.patterns = ast.topology.patterns;
791
+ }
792
+ // Entry: first trigger's command file
793
+ if (ast.triggers.length > 0) {
794
+ fm.entry = `commands/${ast.triggers[0].name}.md`;
795
+ }
796
+ // Add skill-level fields from the first matching skill (if skills exist)
797
+ // For the main topology skill, look for a skill with the same name as the topology
798
+ const mainSkill = ast.skills.find((s) => s.id === name);
799
+ if (mainSkill) {
800
+ if (mainSkill.disableModelInvocation != null) {
801
+ fm["disable-model-invocation"] = mainSkill.disableModelInvocation;
802
+ }
803
+ if (mainSkill.userInvocable != null) {
804
+ fm["user-invocable"] = mainSkill.userInvocable;
805
+ }
806
+ if (mainSkill.context)
807
+ fm.context = mainSkill.context;
808
+ if (mainSkill.agent)
809
+ fm.agent = mainSkill.agent;
810
+ if (mainSkill.allowedTools && mainSkill.allowedTools.length > 0) {
811
+ fm["allowed-tools"] = mainSkill.allowedTools.map(convertMcpToolName);
812
+ }
813
+ }
814
+ const sections = [];
815
+ sections.push(frontmatter(fm));
816
+ sections.push("");
817
+ sections.push(`# ${toTitle(name)} Topology Skill`);
818
+ sections.push("");
819
+ if (ast.topology.description) {
820
+ sections.push(ast.topology.description);
821
+ sections.push("");
822
+ }
823
+ sections.push(`Version: ${ast.topology.version}`);
824
+ if (ast.topology.patterns.length > 0) {
825
+ sections.push(`Patterns: ${ast.topology.patterns.join(", ")}`);
826
+ }
827
+ sections.push("");
828
+ // Topology metadata
829
+ if (ast.topology.foundations && ast.topology.foundations.length > 0) {
830
+ sections.push(`Foundations: ${ast.topology.foundations.join(", ")}`);
831
+ }
832
+ if (ast.topology.advanced && ast.topology.advanced.length > 0) {
833
+ sections.push(`Advanced: ${ast.topology.advanced.join(", ")}`);
834
+ }
835
+ if (ast.topology.domain) {
836
+ sections.push(`Domain: ${ast.topology.domain}`);
837
+ }
838
+ if (ast.topology.timeout) {
839
+ sections.push(`Timeout: ${ast.topology.timeout}`);
840
+ }
841
+ if (ast.topology.durable) {
842
+ sections.push(`Durable: yes`);
843
+ }
844
+ if (ast.topology.foundations || ast.topology.advanced || ast.topology.domain || ast.topology.timeout || ast.topology.durable) {
845
+ sections.push("");
846
+ }
847
+ // Fragment check — skip main skill generation for fragments
848
+ if (ast.isFragment) {
849
+ sections.push("*This is a fragment topology — it is meant to be included, not run standalone.*");
850
+ sections.push("");
851
+ }
852
+ // Composition: params
853
+ if (ast.params && ast.params.length > 0) {
854
+ sections.push("## Parameters");
855
+ sections.push("");
856
+ for (const p of ast.params) {
857
+ const req = p.required ? " (required)" : "";
858
+ const def = p.default != null ? ` = ${p.default}` : "";
859
+ sections.push(`- **${p.name}**: ${p.type}${req}${def}`);
860
+ }
861
+ sections.push("");
862
+ }
863
+ // Composition: interface endpoints
864
+ if (ast.interfaceEndpoints) {
865
+ sections.push("## Interface");
866
+ sections.push("");
867
+ sections.push(`- Entry: ${ast.interfaceEndpoints.entry}`);
868
+ sections.push(`- Exit: ${ast.interfaceEndpoints.exit}`);
869
+ sections.push("");
870
+ }
871
+ // Composition: imports
872
+ if (ast.imports && ast.imports.length > 0) {
873
+ sections.push("## Imports");
874
+ sections.push("");
875
+ for (const imp of ast.imports) {
876
+ let line = `- **${imp.alias}** from \`${imp.source}\``;
877
+ if (imp.sha256)
878
+ line += ` (sha256: ${imp.sha256})`;
879
+ if (imp.registry)
880
+ line += ` [registry: ${imp.registryPackage}@${imp.registryVersion}]`;
881
+ sections.push(line);
882
+ if (Object.keys(imp.params).length > 0) {
883
+ for (const [k, v] of Object.entries(imp.params)) {
884
+ sections.push(` - ${k}: ${v}`);
885
+ }
886
+ }
887
+ }
888
+ sections.push("");
889
+ }
890
+ // Composition: includes
891
+ if (ast.includes && ast.includes.length > 0) {
892
+ sections.push("## Includes");
893
+ sections.push("");
894
+ for (const inc of ast.includes) {
895
+ sections.push(`- ${inc.source}`);
896
+ }
897
+ sections.push("");
898
+ }
899
+ // Error handler
900
+ if (ast.topology.errorHandler) {
901
+ sections.push("## Error Handling");
902
+ sections.push("");
903
+ sections.push(`Uncaught errors are handled by: **${ast.topology.errorHandler}**`);
904
+ sections.push("");
905
+ }
906
+ // Orchestrator section
907
+ const orch = ast.nodes.find((n) => n.type === "orchestrator");
908
+ if (orch) {
909
+ const orchNode = orch;
910
+ sections.push("## Orchestrator");
911
+ sections.push("");
912
+ sections.push(`Model: ${orchNode.model}`);
913
+ if (orchNode.handles?.length > 0) {
914
+ sections.push(`Handles: ${orchNode.handles.join(", ")}`);
915
+ }
916
+ if (orchNode.generates) {
917
+ sections.push(`Generates: ${orchNode.generates}`);
918
+ }
919
+ if (orchNode.outputs) {
920
+ sections.push("");
921
+ sections.push("### Outputs");
922
+ for (const [field, values] of Object.entries(orchNode.outputs)) {
923
+ sections.push(`- ${field}: ${values.join(" | ")}`);
924
+ }
925
+ }
926
+ sections.push("");
927
+ }
928
+ // Flow section (enhanced with error edges, join, weight, race, tolerance, reflection)
929
+ if (ast.edges.length > 0) {
930
+ sections.push("## Flow");
931
+ sections.push("");
932
+ for (const edge of ast.edges) {
933
+ const arrow = edge.isError ? `-x${edge.errorType ? `[${edge.errorType}]` : ""}>` : "->";
934
+ let line = `${edge.from} ${arrow} ${edge.to}`;
935
+ if (edge.condition) {
936
+ // Clean up condition: parser may include trailing "] [max N" artifacts
937
+ const cleanCond = edge.condition
938
+ .replace(/\]\s*\[max\s+\d+\s*$/i, "")
939
+ .replace(/\]\s*$/, "")
940
+ .trim();
941
+ line += ` [when ${cleanCond}]`;
942
+ }
943
+ if (edge.maxIterations)
944
+ line += ` [max ${edge.maxIterations}]`;
945
+ if (edge.per)
946
+ line += ` [per ${edge.per}]`;
947
+ if (edge.weight != null)
948
+ line += ` [weight ${edge.weight}]`;
949
+ if (edge.race)
950
+ line += ` [race]`;
951
+ if (edge.tolerance != null)
952
+ line += ` [tolerance: ${edge.tolerance}]`;
953
+ if (edge.wait)
954
+ line += ` [wait ${edge.wait}]`;
955
+ if (edge.reflection)
956
+ line += ` [reflection]`;
957
+ sections.push(`- ${line}`);
958
+ }
959
+ sections.push("");
960
+ }
961
+ // Gates section
962
+ const gates = ast.nodes.filter((n) => n.type === "gate");
963
+ if (gates.length > 0) {
964
+ sections.push("## Gates");
965
+ sections.push("");
966
+ for (const gate of gates) {
967
+ sections.push(`### ${toTitle(gate.id)}`);
968
+ if (gate.after)
969
+ sections.push(`After: ${gate.after}`);
970
+ if (gate.before)
971
+ sections.push(`Before: ${gate.before}`);
972
+ if (gate.run)
973
+ sections.push(`Run: ${gate.run}`);
974
+ if (gate.checks && gate.checks.length > 0) {
975
+ sections.push(`Checks: ${gate.checks.join(", ")}`);
976
+ }
977
+ if (gate.onFail)
978
+ sections.push(`On fail: ${gate.onFail}`);
979
+ if (gate.retry)
980
+ sections.push(`Retry: ${gate.retry}`);
981
+ if (gate.timeout)
982
+ sections.push(`Timeout: ${gate.timeout}`);
983
+ sections.push("");
984
+ }
985
+ }
986
+ // Schedules section
987
+ if (ast.schedules.length > 0) {
988
+ sections.push("## Schedules");
989
+ sections.push("");
990
+ for (const job of ast.schedules) {
991
+ const expr = job.cron ? `cron: ${job.cron}` : job.every ? `every: ${job.every}` : "";
992
+ const target = job.agent ? `agent: ${job.agent}` : job.action ? `action: ${job.action}` : "";
993
+ sections.push(`- **${job.id}**: ${expr}${target ? ` -> ${target}` : ""}${job.enabled === false ? " (disabled)" : ""}`);
994
+ }
995
+ sections.push("");
996
+ }
997
+ // Interfaces section
998
+ if (ast.interfaces.length > 0) {
999
+ sections.push("## Interfaces");
1000
+ sections.push("");
1001
+ for (const iface of ast.interfaces) {
1002
+ const typePart = iface.type ? ` (${iface.type})` : "";
1003
+ const configKeys = Object.keys(iface.config);
1004
+ const configPart = configKeys.length > 0 ? ` — ${configKeys.map((k) => `${k}: ${iface.config[k]}`).join(", ")}` : "";
1005
+ sections.push(`- **${iface.id}**${typePart}${configPart}`);
1006
+ }
1007
+ sections.push("");
1008
+ }
1009
+ // Depth levels
1010
+ if (ast.depth && ast.depth.levels && ast.depth.levels.length > 0) {
1011
+ sections.push("## Depth Levels");
1012
+ sections.push("");
1013
+ if (ast.depth.factors.length > 0) {
1014
+ sections.push(`Factors: ${ast.depth.factors.join(", ")}`);
1015
+ }
1016
+ for (const level of ast.depth.levels) {
1017
+ let line = `- **Level ${level.level}**: ${level.label}`;
1018
+ if (level.omit.length > 0)
1019
+ line += ` (omit: ${level.omit.join(", ")})`;
1020
+ sections.push(line);
1021
+ }
1022
+ sections.push("");
1023
+ }
1024
+ // Batch settings
1025
+ if (ast.batch && Object.keys(ast.batch).length > 0) {
1026
+ sections.push("## Batch");
1027
+ sections.push("");
1028
+ for (const [key, value] of Object.entries(ast.batch)) {
1029
+ sections.push(`- ${key}: ${JSON.stringify(value)}`);
1030
+ }
1031
+ sections.push("");
1032
+ }
1033
+ // Environments
1034
+ if (ast.environments && Object.keys(ast.environments).length > 0) {
1035
+ sections.push("## Environments");
1036
+ sections.push("");
1037
+ for (const [envName, envConfig] of Object.entries(ast.environments)) {
1038
+ sections.push(`### ${envName}`);
1039
+ for (const [key, value] of Object.entries(envConfig)) {
1040
+ sections.push(`- ${key}: ${JSON.stringify(value)}`);
1041
+ }
1042
+ sections.push("");
1043
+ }
1044
+ }
1045
+ // Fallback chain from settings
1046
+ const fallbackChain = ast.settings?.["fallback-chain"];
1047
+ if (fallbackChain) {
1048
+ sections.push("## Fallback Chain");
1049
+ sections.push("");
1050
+ if (Array.isArray(fallbackChain)) {
1051
+ sections.push(`Models: ${fallbackChain.join(" -> ")}`);
1052
+ }
1053
+ else {
1054
+ sections.push(`Fallback chain: ${fallbackChain}`);
1055
+ }
1056
+ sections.push("");
1057
+ }
1058
+ // Metering
1059
+ if (ast.metering) {
1060
+ sections.push("## Metering");
1061
+ sections.push("");
1062
+ sections.push(`- Track: ${ast.metering.track.join(", ")}`);
1063
+ sections.push(`- Per: ${ast.metering.per.join(", ")}`);
1064
+ sections.push(`- Output: ${ast.metering.output}`);
1065
+ sections.push(`- Format: ${ast.metering.format}`);
1066
+ sections.push(`- Pricing: ${ast.metering.pricing}`);
1067
+ sections.push("");
1068
+ }
1069
+ // Memory
1070
+ const memKeys = Object.keys(ast.memory);
1071
+ if (memKeys.length > 0) {
1072
+ sections.push("## Memory");
1073
+ sections.push("");
1074
+ for (const key of memKeys) {
1075
+ const mem = ast.memory[key];
1076
+ sections.push(`### ${key}`);
1077
+ for (const [k, v] of Object.entries(mem)) {
1078
+ if (Array.isArray(v)) {
1079
+ sections.push(`- ${k}: ${v.join(", ")}`);
1080
+ }
1081
+ else {
1082
+ sections.push(`- ${k}: ${v}`);
1083
+ }
1084
+ }
1085
+ sections.push("");
1086
+ }
1087
+ }
1088
+ files.push({
1089
+ path: `.claude/skills/${name}/SKILL.md`,
1090
+ content: sections.join("\n") + "\n",
1091
+ category: "agent",
1092
+ });
1093
+ return files;
1094
+ }
1095
+ /** Generate skill-level files from the skills block. */
1096
+ function generateSkills(ast) {
1097
+ const files = [];
1098
+ for (const skill of ast.skills) {
1099
+ const sections = [];
1100
+ // Build skill frontmatter
1101
+ const sfm = {};
1102
+ sfm.name = skill.id;
1103
+ if (skill.description)
1104
+ sfm.description = `"${skill.description}"`;
1105
+ if (skill.disableModelInvocation != null) {
1106
+ sfm["disable-model-invocation"] = skill.disableModelInvocation;
1107
+ }
1108
+ if (skill.userInvocable != null) {
1109
+ sfm["user-invocable"] = skill.userInvocable;
1110
+ }
1111
+ if (skill.context)
1112
+ sfm.context = skill.context;
1113
+ if (skill.agent)
1114
+ sfm.agent = skill.agent;
1115
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
1116
+ sfm["allowed-tools"] = skill.allowedTools.map(convertMcpToolName);
1117
+ }
1118
+ // Only emit frontmatter if there are fields beyond just the name
1119
+ if (Object.keys(sfm).length > 1) {
1120
+ sections.push(frontmatter(sfm));
1121
+ sections.push("");
1122
+ }
1123
+ sections.push(`# ${toTitle(skill.id)} Skill`);
1124
+ sections.push("");
1125
+ if (skill.description) {
1126
+ sections.push(skill.description);
1127
+ sections.push("");
1128
+ }
1129
+ if (skill.scripts && skill.scripts.length > 0) {
1130
+ sections.push("## Scripts");
1131
+ for (const s of skill.scripts) {
1132
+ sections.push(`- ${s}`);
1133
+ }
1134
+ sections.push("");
1135
+ }
1136
+ if (skill.domains && skill.domains.length > 0) {
1137
+ sections.push("## Domains");
1138
+ for (const d of skill.domains) {
1139
+ sections.push(`- ${d}`);
1140
+ }
1141
+ sections.push("");
1142
+ }
1143
+ if (skill.references && skill.references.length > 0) {
1144
+ sections.push("## References");
1145
+ for (const r of skill.references) {
1146
+ sections.push(`- ${r}`);
1147
+ }
1148
+ sections.push("");
1149
+ }
1150
+ if (skill.prompt) {
1151
+ sections.push("## Prompt");
1152
+ sections.push(`See: ${skill.prompt}`);
1153
+ sections.push("");
1154
+ }
1155
+ files.push({
1156
+ path: `.claude/skills/${skill.id}/SKILL.md`,
1157
+ content: sections.join("\n") + "\n",
1158
+ category: "agent",
1159
+ });
1160
+ // Domain and reference directory markers
1161
+ if (skill.domains && skill.domains.length > 0) {
1162
+ files.push(gitkeep(`.claude/skills/${skill.id}/domains`));
1163
+ }
1164
+ if (skill.references && skill.references.length > 0) {
1165
+ files.push(gitkeep(`.claude/skills/${skill.id}/references`));
1166
+ }
1167
+ }
1168
+ return files;
1169
+ }
1170
+ /** Generate memory structure files (domains, references, metrics, workspace). */
1171
+ function generateMemory(ast) {
1172
+ const files = [];
1173
+ const name = ast.topology.name;
1174
+ const memory = ast.memory;
1175
+ if (memory.domains) {
1176
+ files.push(gitkeep(`.claude/skills/${name}/domains`));
1177
+ }
1178
+ if (memory.references) {
1179
+ files.push(gitkeep(`.claude/skills/${name}/references`));
1180
+ }
1181
+ if (memory.metrics) {
1182
+ files.push({
1183
+ path: `.claude/skills/${name}/metrics.jsonl`,
1184
+ content: "",
1185
+ category: "machine",
1186
+ });
1187
+ }
1188
+ if (memory.workspace) {
1189
+ files.push(gitkeep(`.claude/skills/${name}/runs`));
1190
+ }
1191
+ return files;
1192
+ }
1193
+ /** Generate gate script stubs. */
1194
+ function generateGateScripts(ast) {
1195
+ const files = [];
1196
+ const name = ast.topology.name;
1197
+ for (const node of ast.nodes) {
1198
+ if (node.type !== "gate")
1199
+ continue;
1200
+ const gate = node;
1201
+ if (gate.run) {
1202
+ // Extract script name from the run command
1203
+ const scriptName = gate.run.replace(/^.*\//, "").replace(/\s.*$/, "");
1204
+ files.push({
1205
+ path: `.claude/skills/${name}/scripts/${scriptName}`,
1206
+ content: shellStub(`Gate: ${gate.id} — ${gate.run}`),
1207
+ category: "script",
1208
+ });
1209
+ }
1210
+ }
1211
+ return files;
1212
+ }
1213
+ /** Generate gate wrapper scripts that filter Task calls to the correct agent. */
1214
+ function generateGateWrapperScripts(ast) {
1215
+ const files = [];
1216
+ const name = ast.topology.name;
1217
+ for (const node of ast.nodes) {
1218
+ if (node.type !== "gate")
1219
+ continue;
1220
+ const gate = node;
1221
+ if (!gate.run)
1222
+ continue;
1223
+ if (gate.behavior === "advisory")
1224
+ continue;
1225
+ const gateScriptName = gate.run.replace(/^.*\//, "").replace(/\s.*$/, "");
1226
+ const onFailExit = gate.onFail === "halt" ? 'exit 1' : 'echo "Gate failed — bounce-back requested (advisory)"';
1227
+ const contentLines = [
1228
+ "#!/usr/bin/env bash",
1229
+ `# Gate wrapper: ${gate.id}`,
1230
+ `# Enforces gate "${gate.id}" on Task tool calls.`,
1231
+ `# Auto-generated by agentopology scaffold — edit as needed.`,
1232
+ "set -euo pipefail",
1233
+ "",
1234
+ "# Gate runs after: " + (gate.after || "any"),
1235
+ "# Gate runs before: " + (gate.before || "any"),
1236
+ `# On failure: ${gate.onFail || "halt"}`,
1237
+ ];
1238
+ if (gate.timeout) {
1239
+ contentLines.push(`# Timeout: ${gate.timeout}`);
1240
+ }
1241
+ if (gate.retry != null && gate.retry > 0) {
1242
+ contentLines.push("");
1243
+ contentLines.push(`MAX_RETRIES=${gate.retry}`);
1244
+ contentLines.push("ATTEMPT=0");
1245
+ contentLines.push("GATE_RESULT=1");
1246
+ contentLines.push("");
1247
+ contentLines.push("while [ $ATTEMPT -le $MAX_RETRIES ] && [ $GATE_RESULT -ne 0 ]; do");
1248
+ contentLines.push(" ATTEMPT=$((ATTEMPT + 1))");
1249
+ if (gate.timeout) {
1250
+ contentLines.push(` timeout ${gate.timeout} bash .claude/skills/${name}/scripts/${gateScriptName} && GATE_RESULT=0 || GATE_RESULT=$?`);
1251
+ }
1252
+ else {
1253
+ contentLines.push(` bash .claude/skills/${name}/scripts/${gateScriptName} && GATE_RESULT=0 || GATE_RESULT=$?`);
1254
+ }
1255
+ contentLines.push(' if [ $GATE_RESULT -ne 0 ] && [ $ATTEMPT -le $MAX_RETRIES ]; then');
1256
+ contentLines.push(` echo "Gate '${gate.id}' attempt $ATTEMPT failed, retrying..."`);
1257
+ contentLines.push(" sleep 1");
1258
+ contentLines.push(" fi");
1259
+ contentLines.push("done");
1260
+ }
1261
+ else {
1262
+ contentLines.push("");
1263
+ contentLines.push("# Run the gate check script");
1264
+ contentLines.push(`GATE_RESULT=0`);
1265
+ if (gate.timeout) {
1266
+ contentLines.push(`timeout ${gate.timeout} bash .claude/skills/${name}/scripts/${gateScriptName} || GATE_RESULT=$?`);
1267
+ }
1268
+ else {
1269
+ contentLines.push(`bash .claude/skills/${name}/scripts/${gateScriptName} || GATE_RESULT=$?`);
1270
+ }
1271
+ }
1272
+ contentLines.push("");
1273
+ contentLines.push(`if [ "$GATE_RESULT" -ne 0 ]; then`);
1274
+ contentLines.push(` echo "Gate '${gate.id}' FAILED (exit code $GATE_RESULT)"`);
1275
+ contentLines.push(` ${onFailExit}`);
1276
+ contentLines.push("fi");
1277
+ contentLines.push("");
1278
+ const content = contentLines.join("\n");
1279
+ files.push({
1280
+ path: `.claude/skills/${name}/scripts/gate-${gate.id}.sh`,
1281
+ content,
1282
+ category: "script",
1283
+ });
1284
+ }
1285
+ return files;
1286
+ }
1287
+ /** Generate hook script stubs for every hook's `run` field. */
1288
+ function generateHookScripts(ast) {
1289
+ const files = [];
1290
+ const name = ast.topology.name;
1291
+ for (const hook of ast.hooks) {
1292
+ if (!hook.run)
1293
+ continue;
1294
+ // Extract script filename from the run command
1295
+ const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
1296
+ files.push({
1297
+ path: `.claude/skills/${name}/scripts/${scriptName}`,
1298
+ content: [
1299
+ "#!/usr/bin/env bash",
1300
+ `# Hook: ${hook.name} — fires on ${hook.on}`,
1301
+ "# Auto-generated by agentopology scaffold — edit as needed.",
1302
+ 'set -euo pipefail',
1303
+ "",
1304
+ `echo "TODO: implement ${hook.name} hook"`,
1305
+ "",
1306
+ ].join("\n"),
1307
+ category: "script",
1308
+ });
1309
+ }
1310
+ return files;
1311
+ }
1312
+ /** Generate per-agent hook script stubs. */
1313
+ function generateAgentHookScripts(ast) {
1314
+ const files = [];
1315
+ const name = ast.topology.name;
1316
+ for (const node of ast.nodes) {
1317
+ if (node.type !== "agent")
1318
+ continue;
1319
+ const agent = node;
1320
+ if (!agent.hooks || agent.hooks.length === 0)
1321
+ continue;
1322
+ for (const hook of agent.hooks) {
1323
+ if (!hook.run)
1324
+ continue;
1325
+ const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
1326
+ files.push({
1327
+ path: `.claude/skills/${name}/scripts/${agent.id}-${scriptName}`,
1328
+ content: [
1329
+ "#!/usr/bin/env bash",
1330
+ `# Hook: ${hook.name} — fires on ${hook.on} (scoped to agent: ${agent.id})`,
1331
+ "# Auto-generated by agentopology scaffold — edit as needed.",
1332
+ 'set -euo pipefail',
1333
+ "",
1334
+ `echo "TODO: implement ${hook.name} hook for ${agent.id}"`,
1335
+ "",
1336
+ ].join("\n"),
1337
+ category: "script",
1338
+ });
1339
+ }
1340
+ }
1341
+ return files;
1342
+ }
1343
+ /**
1344
+ * Generate hook configuration in .claude/settings.json.
1345
+ *
1346
+ * Hooks are grouped by event name as an object:
1347
+ * { "hooks": { "PostToolUse": [ { hooks: [...], matcher? } ], ... } }
1348
+ */
1349
+ function generateSettings(ast) {
1350
+ const settings = {};
1351
+ const topologyName = ast.topology.name;
1352
+ // Hooks section — grouped by event name
1353
+ const hooksByEvent = {};
1354
+ for (const hook of ast.hooks) {
1355
+ const eventName = hook.on;
1356
+ if (!hooksByEvent[eventName]) {
1357
+ hooksByEvent[eventName] = [];
1358
+ }
1359
+ // Build the script command with proper path prefix
1360
+ const scriptName = hook.run.replace(/^.*\//, "").replace(/\s.*$/, "");
1361
+ const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
1362
+ const hookEntry = {
1363
+ hooks: [
1364
+ {
1365
+ type: hook.type ?? "command",
1366
+ command,
1367
+ ...(hook.timeout ? { timeout: hook.timeout } : {}),
1368
+ },
1369
+ ],
1370
+ };
1371
+ // Only add matcher when it has a value
1372
+ if (hook.matcher) {
1373
+ hookEntry.matcher = hook.matcher;
1374
+ }
1375
+ hooksByEvent[eventName].push(hookEntry);
1376
+ }
1377
+ // Per-agent hooks are now rendered in AGENT.md frontmatter (not in settings.json).
1378
+ // This is the Claude Code native approach: hooks in the agent file are scoped
1379
+ // to that agent automatically.
1380
+ // Gate enforcement — compile gates with run: to PreToolUse hooks
1381
+ const gates = ast.nodes.filter((n) => n.type === "gate");
1382
+ for (const gate of gates) {
1383
+ if (!gate.run)
1384
+ continue;
1385
+ if (gate.behavior === "advisory")
1386
+ continue; // advisory gates stay in markdown only
1387
+ const eventName = "PreToolUse";
1388
+ if (!hooksByEvent[eventName])
1389
+ hooksByEvent[eventName] = [];
1390
+ const scriptName = `gate-${gate.id}.sh`;
1391
+ const command = `bash .claude/skills/${topologyName}/scripts/${scriptName}`;
1392
+ const hookEntry = {
1393
+ hooks: [
1394
+ {
1395
+ type: "command",
1396
+ command,
1397
+ },
1398
+ ],
1399
+ matcher: "Task", // Gates fire on Task tool (agent spawning)
1400
+ };
1401
+ hooksByEvent[eventName].push(hookEntry);
1402
+ }
1403
+ // Write hooksByEvent to settings if any hooks or gates were registered
1404
+ if (Object.keys(hooksByEvent).length > 0) {
1405
+ settings.hooks = hooksByEvent;
1406
+ }
1407
+ // Environment variables
1408
+ const envSettings = generateEnvToSettings(ast);
1409
+ if (envSettings) {
1410
+ settings.env = envSettings;
1411
+ }
1412
+ // Permissions section from settings block
1413
+ const perms = ast.settings;
1414
+ if (perms) {
1415
+ const allow = perms.allow;
1416
+ const deny = perms.deny;
1417
+ const ask = perms.ask;
1418
+ if (allow && allow.length > 0)
1419
+ settings.allow = allow;
1420
+ 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;
1471
+ }
1472
+ // Top-level extensions for claude-code
1473
+ if (ast.extensions?.["claude-code"]) {
1474
+ for (const [k, v] of Object.entries(ast.extensions["claude-code"])) {
1475
+ settings[k] = v;
1476
+ }
1477
+ }
1478
+ if (Object.keys(settings).length === 0)
1479
+ return null;
1480
+ return {
1481
+ path: ".claude/settings.json",
1482
+ content: JSON.stringify(settings, null, 2) + "\n",
1483
+ category: "shared-config",
1484
+ };
1485
+ }
1486
+ /** Generate .mcp.json from mcp-servers block. Always includes env field. */
1487
+ function generateMcpJson(ast) {
1488
+ if (Object.keys(ast.mcpServers).length === 0)
1489
+ return null;
1490
+ const mcpConfig = { mcpServers: {} };
1491
+ const servers = mcpConfig.mcpServers;
1492
+ for (const [name, config] of Object.entries(ast.mcpServers)) {
1493
+ const entry = {};
1494
+ for (const [key, value] of Object.entries(config)) {
1495
+ if (key === "command" || key === "url") {
1496
+ entry[key] = value;
1497
+ }
1498
+ else if (key === "args" && Array.isArray(value)) {
1499
+ entry.args = value;
1500
+ }
1501
+ else if (key === "env" && typeof value === "object" && value !== null) {
1502
+ entry.env = value;
1503
+ }
1504
+ // Skip unknown fields — parser may leak env vars to top level
1505
+ }
1506
+ // Only include servers that have a command or url (filter parser artifacts)
1507
+ if (!("command" in entry) && !("url" in entry))
1508
+ continue;
1509
+ // Always include env field for Claude Code .mcp.json
1510
+ if (!("env" in entry)) {
1511
+ entry.env = {};
1512
+ }
1513
+ servers[name] = entry;
1514
+ }
1515
+ return {
1516
+ path: ".mcp.json",
1517
+ content: JSON.stringify(mcpConfig, null, 2) + "\n",
1518
+ category: "shared-config",
1519
+ };
1520
+ }
1521
+ /** Generate metering script stub. */
1522
+ function generateMetering(ast) {
1523
+ if (!ast.metering)
1524
+ return null;
1525
+ const m = ast.metering;
1526
+ const lines = [
1527
+ "#!/usr/bin/env bash",
1528
+ "# Metering / cost tracking script",
1529
+ "# Auto-generated by agentopology scaffold — edit as needed.",
1530
+ 'set -euo pipefail',
1531
+ "",
1532
+ `# Track: ${m.track.join(", ")}`,
1533
+ `# Per: ${m.per.join(", ")}`,
1534
+ `# Output: ${m.output} (${m.format})`,
1535
+ `# Pricing: ${m.pricing}`,
1536
+ "",
1537
+ 'echo "TODO: implement metrics collection"',
1538
+ "",
1539
+ ];
1540
+ return {
1541
+ path: "scripts/collect-metrics.sh",
1542
+ content: lines.join("\n"),
1543
+ category: "script",
1544
+ };
1545
+ }
1546
+ /** Generate tool script stubs. */
1547
+ function generateToolScripts(ast) {
1548
+ const files = [];
1549
+ const name = ast.topology.name;
1550
+ for (const tool of ast.toolDefs) {
1551
+ const ext = tool.lang === "python" ? "py" : tool.lang === "node" ? "js" : "sh";
1552
+ const shebang = tool.lang === "python"
1553
+ ? "#!/usr/bin/env python3"
1554
+ : tool.lang === "node"
1555
+ ? "#!/usr/bin/env node"
1556
+ : "#!/usr/bin/env bash";
1557
+ const lines = [
1558
+ shebang,
1559
+ `# Tool: ${tool.id} — ${tool.description}`,
1560
+ "# Auto-generated by agentopology scaffold — edit as needed.",
1561
+ "",
1562
+ ];
1563
+ if (tool.lang === "bash" || !tool.lang) {
1564
+ lines.push('set -euo pipefail');
1565
+ lines.push("");
1566
+ }
1567
+ if (tool.args && tool.args.length > 0) {
1568
+ lines.push(`# Args: ${tool.args.join(", ")}`);
1569
+ lines.push("");
1570
+ }
1571
+ if (tool.lang === "python") {
1572
+ lines.push(`print("TODO: implement ${tool.id}")`);
1573
+ }
1574
+ else if (tool.lang === "node") {
1575
+ lines.push(`console.log("TODO: implement ${tool.id}");`);
1576
+ }
1577
+ else {
1578
+ lines.push(`echo "TODO: implement ${tool.id}"`);
1579
+ }
1580
+ lines.push("");
1581
+ files.push({
1582
+ path: `.claude/skills/${name}/scripts/${tool.id}.${ext}`,
1583
+ content: lines.join("\n"),
1584
+ category: "script",
1585
+ });
1586
+ }
1587
+ return files;
1588
+ }
1589
+ /** Generate schema documentation files. */
1590
+ function generateSchemas(ast) {
1591
+ const files = [];
1592
+ const name = ast.topology.name;
1593
+ if (!ast.schemas || ast.schemas.length === 0)
1594
+ return files;
1595
+ for (const schema of ast.schemas) {
1596
+ const sections = [];
1597
+ sections.push(`# Schema: ${schema.id}`);
1598
+ sections.push("");
1599
+ sections.push("## Fields");
1600
+ sections.push("");
1601
+ for (const field of schema.fields) {
1602
+ const opt = field.optional ? " (optional)" : "";
1603
+ sections.push(`- **${field.name}**${opt}: ${formatSchemaType(field.type, ast.schemas)}`);
1604
+ }
1605
+ sections.push("");
1606
+ files.push({
1607
+ path: `.claude/skills/${name}/schemas/${schema.id}.md`,
1608
+ content: sections.join("\n") + "\n",
1609
+ category: "machine",
1610
+ });
1611
+ }
1612
+ return files;
1613
+ }
1614
+ /** Generate schedule scripts and crontab fragment. */
1615
+ function generateScheduleScripts(ast) {
1616
+ const files = [];
1617
+ if (!ast.schedules || ast.schedules.length === 0)
1618
+ return files;
1619
+ const cronLines = [
1620
+ "# Crontab fragment — auto-generated by agentopology scaffold",
1621
+ "# Install with: crontab -l | cat - crontab.fragment | crontab -",
1622
+ "",
1623
+ ];
1624
+ for (const job of ast.schedules) {
1625
+ const target = job.agent ? `agent ${job.agent}` : job.action ? `action ${job.action}` : "unknown";
1626
+ const scriptPath = `scripts/schedule-${job.id}.sh`;
1627
+ // Generate the shell script
1628
+ const lines = [
1629
+ "#!/usr/bin/env bash",
1630
+ `# Schedule: ${job.id} — invokes ${target}`,
1631
+ "# Auto-generated by agentopology scaffold — edit as needed.",
1632
+ 'set -euo pipefail',
1633
+ "",
1634
+ ];
1635
+ if (job.agent) {
1636
+ lines.push(`# Invoke agent: ${job.agent}`);
1637
+ lines.push(`claude --agent ${job.agent}`);
1638
+ }
1639
+ else if (job.action) {
1640
+ lines.push(`# Invoke action: ${job.action}`);
1641
+ lines.push(`bash scripts/action-${job.action}.sh`);
1642
+ }
1643
+ if (job.enabled === false) {
1644
+ lines.unshift("# DISABLED — this schedule is not enabled");
1645
+ }
1646
+ lines.push("");
1647
+ files.push({ path: scriptPath, content: lines.join("\n"), category: "script" });
1648
+ // Add crontab entry
1649
+ let cronExpr = job.cron ?? "# every: " + (job.every ?? "unknown");
1650
+ if (!job.cron && job.every) {
1651
+ // Convert common "every" expressions to cron
1652
+ const everyMap = {
1653
+ "1h": "0 * * * *",
1654
+ "1 hour": "0 * * * *",
1655
+ "hourly": "0 * * * *",
1656
+ "1d": "0 0 * * *",
1657
+ "1 day": "0 0 * * *",
1658
+ "daily": "0 0 * * *",
1659
+ "1w": "0 0 * * 0",
1660
+ "weekly": "0 0 * * 0",
1661
+ };
1662
+ cronExpr = everyMap[job.every.toLowerCase()] ?? `# every: ${job.every}`;
1663
+ }
1664
+ const enabledPrefix = job.enabled === false ? "# DISABLED: " : "";
1665
+ cronLines.push(`${enabledPrefix}${cronExpr} bash ${scriptPath} # ${job.id}`);
1666
+ }
1667
+ cronLines.push("");
1668
+ files.push({ path: "crontab.fragment", content: cronLines.join("\n"), category: "machine" });
1669
+ return files;
1670
+ }
1671
+ /** Generate .claude/commands/X.md for each trigger. */
1672
+ function generateCommandFiles(ast) {
1673
+ const files = [];
1674
+ for (const trigger of ast.triggers) {
1675
+ const sections = [];
1676
+ // Frontmatter
1677
+ const fm = {};
1678
+ if (ast.topology.description) {
1679
+ fm.description = `"${ast.topology.description}"`;
1680
+ }
1681
+ sections.push(frontmatter(fm));
1682
+ sections.push("");
1683
+ // Header
1684
+ sections.push(`# /${trigger.name}`);
1685
+ sections.push("");
1686
+ sections.push(trigger.pattern);
1687
+ sections.push("");
1688
+ // Arguments
1689
+ if (trigger.argument) {
1690
+ sections.push("## Arguments");
1691
+ sections.push(`- ${trigger.argument}: extracted from the command pattern`);
1692
+ sections.push("");
1693
+ }
1694
+ // Pipeline (from edges, enhanced)
1695
+ if (ast.edges.length > 0) {
1696
+ sections.push("## Pipeline");
1697
+ for (const edge of ast.edges) {
1698
+ const arrow = edge.isError ? `-x${edge.errorType ? `[${edge.errorType}]` : ""}>` : "->";
1699
+ let line = `${edge.from} ${arrow} ${edge.to}`;
1700
+ if (edge.condition) {
1701
+ const cleanCond = edge.condition
1702
+ .replace(/\]\s*\[max\s+\d+\s*$/i, "")
1703
+ .replace(/\]\s*$/, "")
1704
+ .trim();
1705
+ line += ` [when ${cleanCond}]`;
1706
+ }
1707
+ if (edge.maxIterations)
1708
+ line += ` [max ${edge.maxIterations}]`;
1709
+ if (edge.weight != null)
1710
+ line += ` [weight ${edge.weight}]`;
1711
+ if (edge.race)
1712
+ line += ` [race]`;
1713
+ if (edge.tolerance != null)
1714
+ line += ` [tolerance: ${edge.tolerance}]`;
1715
+ if (edge.wait)
1716
+ line += ` [wait ${edge.wait}]`;
1717
+ if (edge.reflection)
1718
+ line += ` [reflection]`;
1719
+ sections.push(`- ${line}`);
1720
+ }
1721
+ sections.push("");
1722
+ }
1723
+ // Agents table
1724
+ const agents = ast.nodes.filter((n) => n.type === "agent");
1725
+ if (agents.length > 0) {
1726
+ sections.push("## Agents");
1727
+ sections.push("| Agent | Phase | Model | Role |");
1728
+ sections.push("|-------|-------|-------|------|");
1729
+ for (const agent of agents) {
1730
+ const phase = agent.phase != null ? String(agent.phase) : "-";
1731
+ const model = agent.model ?? "-";
1732
+ const role = agent.role ?? "-";
1733
+ sections.push(`| ${agent.id} | ${phase} | ${model} | ${role} |`);
1734
+ }
1735
+ sections.push("");
1736
+ }
1737
+ files.push({
1738
+ path: `.claude/commands/${trigger.name}.md`,
1739
+ content: sections.join("\n") + "\n",
1740
+ category: "machine",
1741
+ });
1742
+ }
1743
+ return files;
1744
+ }
1745
+ /** Generate workspace-protocol.md if workspace memory has a protocol field. */
1746
+ function generateWorkspaceProtocol(ast) {
1747
+ const name = ast.topology.name;
1748
+ const workspace = ast.memory.workspace;
1749
+ if (!workspace || !workspace.protocol)
1750
+ return null;
1751
+ const protocol = workspace.protocol;
1752
+ const sections = [];
1753
+ sections.push("# Workspace Protocol");
1754
+ sections.push("");
1755
+ // Directory structure from workspace.structure
1756
+ const structure = workspace.structure;
1757
+ if (structure && Array.isArray(structure)) {
1758
+ sections.push("## Directory Structure");
1759
+ for (const item of structure) {
1760
+ sections.push(`- ${item}`);
1761
+ }
1762
+ sections.push("");
1763
+ }
1764
+ // Read/Write rules from agent nodes
1765
+ const agents = ast.nodes.filter((n) => n.type === "agent");
1766
+ if (agents.length > 0) {
1767
+ sections.push("## Read/Write Rules");
1768
+ for (const agent of agents) {
1769
+ const reads = agent.reads ?? [];
1770
+ const writes = agent.writes ?? [];
1771
+ if (reads.length > 0 || writes.length > 0) {
1772
+ sections.push(`### ${toTitle(agent.id)}`);
1773
+ if (reads.length > 0) {
1774
+ sections.push(`- Reads: ${reads.join(", ")}`);
1775
+ }
1776
+ if (writes.length > 0) {
1777
+ sections.push(`- Writes: ${writes.join(", ")}`);
1778
+ }
1779
+ sections.push("");
1780
+ }
1781
+ }
1782
+ }
1783
+ return {
1784
+ path: `.claude/skills/${name}/${protocol}`,
1785
+ content: sections.join("\n") + "\n",
1786
+ category: "machine",
1787
+ };
1788
+ }
1789
+ /**
1790
+ * Generate context/instructions file inside `.claude/`.
1791
+ *
1792
+ * If `ast.context.file` is set and points inside `.claude/`, that path is
1793
+ * used. Otherwise the file is written to `.claude/CONTEXT.md`.
1794
+ *
1795
+ * **Important**: We never generate a file at the project-root `CLAUDE.md`
1796
+ * because that is the user's own instructions file and overwriting it would
1797
+ * destroy their content.
1798
+ */
1799
+ function generateContextFile(ast) {
1800
+ const DEFAULT_PATH = ".claude/CONTEXT.md";
1801
+ let fileName = ast.context.file ?? DEFAULT_PATH;
1802
+ // Safety: never overwrite the user's root-level CLAUDE.md
1803
+ if (fileName === "CLAUDE.md" || fileName === "./CLAUDE.md") {
1804
+ fileName = DEFAULT_PATH;
1805
+ }
1806
+ const sections = [];
1807
+ sections.push(`# ${toTitle(ast.topology.name)}`);
1808
+ sections.push("");
1809
+ if (ast.topology.description) {
1810
+ sections.push(ast.topology.description);
1811
+ sections.push("");
1812
+ }
1813
+ sections.push(`Version: ${ast.topology.version}`);
1814
+ if (ast.topology.patterns.length > 0) {
1815
+ sections.push(`Patterns: ${ast.topology.patterns.join(", ")}`);
1816
+ }
1817
+ sections.push("");
1818
+ // Include references
1819
+ if (ast.context.includes && ast.context.includes.length > 0) {
1820
+ sections.push("## Includes");
1821
+ sections.push("");
1822
+ for (const inc of ast.context.includes) {
1823
+ sections.push(`- ${inc}`);
1824
+ }
1825
+ sections.push("");
1826
+ }
1827
+ // Defaults block
1828
+ if (ast.defaults) {
1829
+ const d = ast.defaults;
1830
+ sections.push("## Defaults");
1831
+ sections.push("");
1832
+ if (d.temperature != null)
1833
+ sections.push(`- Temperature: ${d.temperature}`);
1834
+ if (d.maxTokens != null)
1835
+ sections.push(`- Max tokens: ${d.maxTokens}`);
1836
+ if (d.topP != null)
1837
+ sections.push(`- Top-p: ${d.topP}`);
1838
+ if (d.topK != null)
1839
+ sections.push(`- Top-k: ${d.topK}`);
1840
+ if (d.stop)
1841
+ sections.push(`- Stop sequences: ${d.stop.join(", ")}`);
1842
+ if (d.seed != null)
1843
+ sections.push(`- Seed: ${d.seed}`);
1844
+ if (d.thinking)
1845
+ sections.push(`- Thinking: ${d.thinking}`);
1846
+ if (d.thinkingBudget != null)
1847
+ sections.push(`- Thinking budget: ${d.thinkingBudget}`);
1848
+ if (d.outputFormat)
1849
+ sections.push(`- Output format: ${d.outputFormat}`);
1850
+ if (d.timeout)
1851
+ sections.push(`- Timeout: ${d.timeout}`);
1852
+ if (d.logLevel)
1853
+ sections.push(`- Log level: ${d.logLevel}`);
1854
+ sections.push("");
1855
+ }
1856
+ // Providers
1857
+ if (ast.providers && ast.providers.length > 0) {
1858
+ sections.push("## Providers");
1859
+ sections.push("");
1860
+ for (const provider of ast.providers) {
1861
+ let line = `- **${provider.name}**`;
1862
+ if (provider.default)
1863
+ line += " (default)";
1864
+ if (provider.models.length > 0)
1865
+ line += ` — models: ${provider.models.join(", ")}`;
1866
+ sections.push(line);
1867
+ if (provider.baseUrl)
1868
+ sections.push(` - Base URL: ${provider.baseUrl}`);
1869
+ if (provider.apiKey)
1870
+ sections.push(` - API key: ${provider.apiKey}`);
1871
+ if (provider.auth) {
1872
+ sections.push(` - Auth: ${provider.auth.type}`);
1873
+ if (provider.auth.issuer)
1874
+ sections.push(` - Issuer: ${provider.auth.issuer}`);
1875
+ if (provider.auth.audience)
1876
+ sections.push(` - Audience: ${provider.auth.audience}`);
1877
+ if (provider.auth.scopes)
1878
+ sections.push(` - Scopes: ${provider.auth.scopes.join(", ")}`);
1879
+ }
1880
+ }
1881
+ sections.push("");
1882
+ }
1883
+ // Checkpoint config
1884
+ if (ast.checkpoint) {
1885
+ const cp = ast.checkpoint;
1886
+ sections.push("## Checkpoint");
1887
+ sections.push("");
1888
+ sections.push(`- Backend: ${cp.backend}`);
1889
+ sections.push(`- Strategy: ${cp.strategy}`);
1890
+ if (cp.connection)
1891
+ sections.push(`- Connection: ${cp.connection}`);
1892
+ if (cp.ttl)
1893
+ sections.push(`- TTL: ${cp.ttl}`);
1894
+ if (cp.replay) {
1895
+ sections.push(`- Replay: ${cp.replay.enabled ? "enabled" : "disabled"}`);
1896
+ if (cp.replay.maxHistory != null)
1897
+ sections.push(` - Max history: ${cp.replay.maxHistory}`);
1898
+ if (cp.replay.branch != null)
1899
+ sections.push(` - Branching: ${cp.replay.branch}`);
1900
+ }
1901
+ sections.push("");
1902
+ }
1903
+ // Artifacts registry
1904
+ if (ast.artifacts && ast.artifacts.length > 0) {
1905
+ sections.push("## Artifacts");
1906
+ sections.push("");
1907
+ for (const a of ast.artifacts) {
1908
+ let line = `- **${a.id}** (${a.type})`;
1909
+ if (a.path)
1910
+ line += ` — path: ${a.path}`;
1911
+ if (a.retention)
1912
+ line += `, retention: ${a.retention}`;
1913
+ if (a.dependsOn && a.dependsOn.length > 0)
1914
+ line += `, depends on: ${a.dependsOn.join(", ")}`;
1915
+ sections.push(line);
1916
+ }
1917
+ sections.push("");
1918
+ }
1919
+ return {
1920
+ path: fileName,
1921
+ content: sections.join("\n") + "\n",
1922
+ category: "machine",
1923
+ };
1924
+ }
1925
+ /**
1926
+ * Merge `ast.env` into the settings.json output under an `env` key.
1927
+ *
1928
+ * When `ast.providers` includes an "anthropic" provider with an api-key,
1929
+ * that env var is included automatically (enabling API-key mode instead
1930
+ * of subscription mode).
1931
+ *
1932
+ * Returns the env record if non-empty, otherwise null.
1933
+ */
1934
+ function generateEnvToSettings(ast) {
1935
+ const env = {};
1936
+ for (const [key, val] of Object.entries(ast.env)) {
1937
+ if (typeof val === "string") {
1938
+ env[key] = val;
1939
+ }
1940
+ else {
1941
+ const sv = val;
1942
+ if (sv.secretRef) {
1943
+ // Secret reference — emit placeholder with comment-style URI
1944
+ env[key] = `\${${key}}`; // placeholder
1945
+ env[`# ${key}_SECRET`] = sv.secretRef.uri;
1946
+ }
1947
+ else if (sv.sensitive) {
1948
+ // Sensitive value — never emit the actual value
1949
+ env[key] = `\${${key}}`;
1950
+ }
1951
+ else if (sv.encrypted) {
1952
+ // Encrypted value — emit placeholder with warning
1953
+ env[key] = `\${${key}}`;
1954
+ env[`# ${key}_ENCRYPTED`] = "WARNING: value is SOPS-encrypted, decrypt before use";
1955
+ }
1956
+ else {
1957
+ env[key] = sv.value;
1958
+ }
1959
+ }
1960
+ }
1961
+ // If an anthropic provider exists, scaffold the API key env var
1962
+ if (ast.providers && ast.providers.length > 0) {
1963
+ const anthropicProvider = ast.providers.find((p) => p.name === "anthropic");
1964
+ if (anthropicProvider?.apiKey) {
1965
+ // Extract env var name from ${ENV_VAR} format
1966
+ const envVarMatch = anthropicProvider.apiKey.match(/^\$\{(.+)\}$/);
1967
+ if (envVarMatch) {
1968
+ env[envVarMatch[1]] = anthropicProvider.apiKey;
1969
+ }
1970
+ }
1971
+ }
1972
+ if (Object.keys(env).length === 0)
1973
+ return null;
1974
+ return env;
1975
+ }
1976
+ /** Generate trigger documentation in the skill. */
1977
+ function generateTriggers(ast) {
1978
+ if (ast.triggers.length === 0)
1979
+ return "";
1980
+ const lines = ["## Triggers", ""];
1981
+ for (const trigger of ast.triggers) {
1982
+ lines.push(`### /${trigger.name}`);
1983
+ lines.push(`Pattern: \`${trigger.pattern}\``);
1984
+ if (trigger.argument)
1985
+ lines.push(`Argument: ${trigger.argument}`);
1986
+ lines.push("");
1987
+ }
1988
+ return lines.join("\n");
1989
+ }
1990
+ // ---------------------------------------------------------------------------
1991
+ // Binding export
1992
+ // ---------------------------------------------------------------------------
1993
+ /** Claude Code binding — the reference binding implementation. */
1994
+ export const claudeCodeBinding = {
1995
+ name: "claude-code",
1996
+ description: "Anthropic Claude Code CLI — generates .claude/ directory structure, .mcp.json, and scripts.",
1997
+ scaffold(ast) {
1998
+ // Fragment check — skip main skill generation for fragments
1999
+ if (ast.isFragment) {
2000
+ const files = [];
2001
+ // For fragments, only generate the topology skill doc (which notes it's a fragment)
2002
+ files.push(...generateTopologySkill(ast));
2003
+ files.push(...generateAgents(ast));
2004
+ files.push(...generateSchemas(ast));
2005
+ return deduplicateFiles(files);
2006
+ }
2007
+ const files = [];
2008
+ // Action nodes
2009
+ files.push(...generateActionNodes(ast));
2010
+ // Group nodes — MUST run before agents so group protocols are registered
2011
+ files.push(...generateGroupNodes(ast));
2012
+ // Agent files — may include group conversation protocol sections
2013
+ files.push(...generateAgents(ast));
2014
+ // Human nodes
2015
+ files.push(...generateHumanNodes(ast));
2016
+ // Main topology skill (with frontmatter)
2017
+ const skillFiles = generateTopologySkill(ast);
2018
+ // Append trigger info to the main skill
2019
+ if (ast.triggers.length > 0 && skillFiles.length > 0) {
2020
+ skillFiles[0].content += generateTriggers(ast);
2021
+ }
2022
+ files.push(...skillFiles);
2023
+ // Named skills
2024
+ files.push(...generateSkills(ast));
2025
+ // Schemas
2026
+ files.push(...generateSchemas(ast));
2027
+ // Memory directories
2028
+ files.push(...generateMemory(ast));
2029
+ // Gate scripts
2030
+ files.push(...generateGateScripts(ast));
2031
+ files.push(...generateGateWrapperScripts(ast));
2032
+ // Hook scripts (global)
2033
+ files.push(...generateHookScripts(ast));
2034
+ // Hook scripts (per-agent)
2035
+ files.push(...generateAgentHookScripts(ast));
2036
+ // Tool scripts
2037
+ files.push(...generateToolScripts(ast));
2038
+ // Schedule scripts
2039
+ files.push(...generateScheduleScripts(ast));
2040
+ // Command files
2041
+ files.push(...generateCommandFiles(ast));
2042
+ // Workspace protocol
2043
+ const wsProtocol = generateWorkspaceProtocol(ast);
2044
+ if (wsProtocol)
2045
+ files.push(wsProtocol);
2046
+ // Settings (hooks + permissions + extensions)
2047
+ const settingsFile = generateSettings(ast);
2048
+ if (settingsFile)
2049
+ files.push(settingsFile);
2050
+ // MCP servers (with env field)
2051
+ const mcpFile = generateMcpJson(ast);
2052
+ if (mcpFile)
2053
+ files.push(mcpFile);
2054
+ // Metering
2055
+ const meteringFile = generateMetering(ast);
2056
+ if (meteringFile)
2057
+ files.push(meteringFile);
2058
+ // Context file (.claude/CONTEXT.md or custom — never root CLAUDE.md)
2059
+ files.push(generateContextFile(ast));
2060
+ return deduplicateFiles(files);
2061
+ },
2062
+ };
2063
+ //# sourceMappingURL=claude-code.js.map