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,781 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parse } from "../../parser/index.js";
3
+ import { validate } from "../../parser/validator.js";
4
+ import { serializeAST } from "../serializer.js";
5
+ import { importClaudeCode, parseAgentMd, parseSettingsJson, parseMcpJson, parseCommandMd, parseActionScript, parseSkillMd } from "../claude-code.js";
6
+ import { importFromPlatform } from "../index.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Test data
9
+ // ---------------------------------------------------------------------------
10
+ const SIMPLE_PIPELINE = `
11
+ topology simple-pipeline : [pipeline] {
12
+ meta {
13
+ version: "1.0.0"
14
+ description: "Test pipeline"
15
+ }
16
+ orchestrator {
17
+ model: sonnet
18
+ handles: [intake, done]
19
+ }
20
+ action intake {
21
+ kind: inline
22
+ description: "Start"
23
+ }
24
+ agent researcher {
25
+ model: gpt-4o
26
+ permissions: supervised
27
+ phase: 1
28
+ tools: [Read, Grep]
29
+ reads: ["workspace/data.md"]
30
+ writes: ["workspace/output.md"]
31
+ outputs: {
32
+ quality: high | low
33
+ }
34
+ prompt {
35
+ You are a researcher.
36
+ }
37
+ }
38
+ agent writer {
39
+ model: sonnet
40
+ phase: 2
41
+ tools: [Read, Write]
42
+ prompt {
43
+ You are a writer.
44
+ }
45
+ }
46
+ action done {
47
+ kind: report
48
+ description: "Finish"
49
+ }
50
+ flow {
51
+ intake -> researcher
52
+ researcher -> writer
53
+ writer -> done
54
+ }
55
+ settings {
56
+ allow: ["Read", "Grep"]
57
+ deny: ["Bash(rm -rf *)"]
58
+ }
59
+ }
60
+ `;
61
+ const BASIC_AGENT_MD = `---
62
+ name: researcher
63
+ description: "Gather info"
64
+ model: gpt-4o
65
+ tools:
66
+ - Read
67
+ - Grep
68
+ - Glob
69
+ mcpServers:
70
+ - supabase
71
+ permissionMode: plan
72
+ maxTurns: 25
73
+ sandbox: docker
74
+ fallback-chain: sonnet, haiku
75
+ ---
76
+
77
+ You are the Researcher agent.
78
+
79
+ ## Role
80
+ Gather information and compile research notes
81
+
82
+ ## Instructions
83
+ You are a research specialist focused on gathering comprehensive information.
84
+ Always cite your sources.
85
+
86
+ ## Reads
87
+ - workspace/data.md
88
+ - workspace/refs.md
89
+
90
+ ## Writes
91
+ - workspace/output.md
92
+
93
+ ## Outputs
94
+ - confidence: high | medium | low
95
+
96
+ You have a maximum of 5m to complete your work.
97
+
98
+ If you fail: retry-then-escalate
99
+ `;
100
+ const HUMAN_AGENT_MD = `# Binding Review (Human-in-the-Loop)
101
+
102
+ Review the generated binding code before proceeding.
103
+
104
+ Timeout: 1h
105
+ On timeout: skip
106
+ `;
107
+ const GROUP_AGENT_MD = `---
108
+ name: gap-debate
109
+ type: group
110
+ members:
111
+ - sdk-specialist
112
+ - binding-veteran
113
+ - platform-expert
114
+ description: "Three-perspective debate"
115
+ ---
116
+
117
+ # Gap Debate (Group Chat)
118
+
119
+ Three-perspective debate on gap resolution.
120
+
121
+ Speaker selection: round-robin
122
+ Max rounds: 3
123
+ Termination: consensus reached
124
+ `;
125
+ const SIMPLE_SKILL_MD = `---
126
+ name: test-pipeline
127
+ description: "Test pipeline skill"
128
+ version: "1.0.0"
129
+ topology: test-pipeline
130
+ patterns:
131
+ - pipeline
132
+ entry: commands/start.md
133
+ ---
134
+
135
+ # Test Pipeline Topology Skill
136
+
137
+ Test pipeline skill
138
+
139
+ Version: 1.0.0
140
+ Patterns: pipeline
141
+
142
+ ## Orchestrator
143
+
144
+ Model: opus
145
+ Handles: intake, done
146
+ Generates: commands/start.md
147
+
148
+ ### Outputs
149
+ - verdict: pass | fail
150
+
151
+ ## Flow
152
+
153
+ - intake -> researcher
154
+ - researcher -> writer
155
+ - writer -> done
156
+
157
+ ## Gates
158
+
159
+ ### Quality Check
160
+ After: writer
161
+ Before: done
162
+ Run: scripts/check.sh
163
+ Checks: grammar, formatting
164
+ On fail: bounce-back
165
+
166
+ ## Triggers
167
+
168
+ ### /start
169
+ Pattern: \`/start\`
170
+ `;
171
+ // ---------------------------------------------------------------------------
172
+ // Serializer tests
173
+ // ---------------------------------------------------------------------------
174
+ describe("serializeAST", () => {
175
+ it("round-trips a simple pipeline", () => {
176
+ const ast = parse(SIMPLE_PIPELINE);
177
+ const serialized = serializeAST(ast);
178
+ const reparsed = parse(serialized);
179
+ expect(reparsed.topology.name).toBe("simple-pipeline");
180
+ expect(reparsed.topology.version).toBe("1.0.0");
181
+ expect(reparsed.topology.patterns).toContain("pipeline");
182
+ });
183
+ it("preserves agents", () => {
184
+ const ast = parse(SIMPLE_PIPELINE);
185
+ const serialized = serializeAST(ast);
186
+ const reparsed = parse(serialized);
187
+ const agents = reparsed.nodes.filter((n) => n.type === "agent");
188
+ expect(agents.length).toBe(2);
189
+ const researcher = agents.find((a) => a.id === "researcher");
190
+ expect(researcher).toBeDefined();
191
+ expect(researcher.model).toBe("gpt-4o");
192
+ expect(researcher.tools).toContain("Read");
193
+ });
194
+ it("preserves actions", () => {
195
+ const ast = parse(SIMPLE_PIPELINE);
196
+ const serialized = serializeAST(ast);
197
+ const reparsed = parse(serialized);
198
+ const actions = reparsed.nodes.filter((n) => n.type === "action");
199
+ expect(actions.length).toBe(2);
200
+ });
201
+ it("preserves edges", () => {
202
+ const ast = parse(SIMPLE_PIPELINE);
203
+ const serialized = serializeAST(ast);
204
+ const reparsed = parse(serialized);
205
+ expect(reparsed.edges.length).toBe(3);
206
+ expect(reparsed.edges[0].from).toBe("intake");
207
+ expect(reparsed.edges[0].to).toBe("researcher");
208
+ });
209
+ it("preserves settings", () => {
210
+ const ast = parse(SIMPLE_PIPELINE);
211
+ const serialized = serializeAST(ast);
212
+ const reparsed = parse(serialized);
213
+ expect(reparsed.settings.allow).toContain("Read");
214
+ expect(reparsed.settings.deny).toContain("Bash(rm -rf *)");
215
+ });
216
+ it("preserves outputs", () => {
217
+ const ast = parse(SIMPLE_PIPELINE);
218
+ const serialized = serializeAST(ast);
219
+ const reparsed = parse(serialized);
220
+ const researcher = reparsed.nodes.find((n) => n.id === "researcher");
221
+ expect(researcher.outputs).toBeDefined();
222
+ expect(researcher.outputs.quality).toContain("high");
223
+ expect(researcher.outputs.quality).toContain("low");
224
+ });
225
+ it("preserves prompts", () => {
226
+ const ast = parse(SIMPLE_PIPELINE);
227
+ const serialized = serializeAST(ast);
228
+ const reparsed = parse(serialized);
229
+ const researcher = reparsed.nodes.find((n) => n.id === "researcher");
230
+ expect(researcher.prompt).toContain("researcher");
231
+ });
232
+ it("preserves reads and writes", () => {
233
+ const ast = parse(SIMPLE_PIPELINE);
234
+ const serialized = serializeAST(ast);
235
+ const reparsed = parse(serialized);
236
+ const researcher = reparsed.nodes.find((n) => n.id === "researcher");
237
+ expect(researcher.reads).toContain("workspace/data.md");
238
+ expect(researcher.writes).toContain("workspace/output.md");
239
+ });
240
+ it("preserves orchestrator", () => {
241
+ const ast = parse(SIMPLE_PIPELINE);
242
+ const serialized = serializeAST(ast);
243
+ const reparsed = parse(serialized);
244
+ const orch = reparsed.nodes.find((n) => n.type === "orchestrator");
245
+ expect(orch).toBeDefined();
246
+ expect(orch.model).toBe("sonnet");
247
+ expect(orch.handles).toContain("intake");
248
+ });
249
+ it("outputs valid .at that passes validation", () => {
250
+ const ast = parse(SIMPLE_PIPELINE);
251
+ const serialized = serializeAST(ast);
252
+ const reparsed = parse(serialized);
253
+ const results = validate(reparsed);
254
+ const errors = results.filter((r) => r.level === "error");
255
+ expect(errors).toHaveLength(0);
256
+ });
257
+ });
258
+ // ---------------------------------------------------------------------------
259
+ // AGENT.md parser tests
260
+ // ---------------------------------------------------------------------------
261
+ describe("parseAgentMd", () => {
262
+ it("parses basic agent fields", () => {
263
+ const node = parseAgentMd("researcher", BASIC_AGENT_MD);
264
+ expect(node).toBeDefined();
265
+ expect(node.type).toBe("agent");
266
+ expect(node.id).toBe("researcher");
267
+ const agent = node;
268
+ expect(agent.model).toBe("gpt-4o");
269
+ expect(agent.tools).toContain("Read");
270
+ expect(agent.tools).toContain("Grep");
271
+ expect(agent.tools).toContain("Glob");
272
+ });
273
+ it("reverse-maps permission mode", () => {
274
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
275
+ expect(agent.permissions).toBe("supervised");
276
+ });
277
+ it("parses maxTurns", () => {
278
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
279
+ expect(agent.maxTurns).toBe(25);
280
+ });
281
+ it("parses mcpServers", () => {
282
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
283
+ expect(agent.mcpServers).toContain("supabase");
284
+ });
285
+ it("parses sandbox", () => {
286
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
287
+ expect(agent.sandbox).toBe("docker");
288
+ });
289
+ it("parses fallback chain", () => {
290
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
291
+ expect(agent.fallbackChain).toContain("sonnet");
292
+ expect(agent.fallbackChain).toContain("haiku");
293
+ });
294
+ it("extracts role section", () => {
295
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
296
+ expect(agent.role).toContain("Gather information");
297
+ });
298
+ it("extracts prompt from ## Instructions", () => {
299
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
300
+ expect(agent.prompt).toContain("research specialist");
301
+ expect(agent.prompt).toContain("cite your sources");
302
+ });
303
+ it("extracts reads and writes", () => {
304
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
305
+ expect(agent.reads).toContain("workspace/data.md");
306
+ expect(agent.reads).toContain("workspace/refs.md");
307
+ expect(agent.writes).toContain("workspace/output.md");
308
+ });
309
+ it("extracts outputs", () => {
310
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
311
+ expect(agent.outputs).toBeDefined();
312
+ expect(agent.outputs.confidence).toContain("high");
313
+ expect(agent.outputs.confidence).toContain("medium");
314
+ expect(agent.outputs.confidence).toContain("low");
315
+ });
316
+ it("extracts timeout", () => {
317
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
318
+ expect(agent.timeout).toBe("5m");
319
+ });
320
+ it("extracts onFail", () => {
321
+ const agent = parseAgentMd("researcher", BASIC_AGENT_MD);
322
+ expect(agent.onFail).toBe("retry-then-escalate");
323
+ });
324
+ it("detects human nodes", () => {
325
+ const node = parseAgentMd("binding-review", HUMAN_AGENT_MD);
326
+ expect(node).toBeDefined();
327
+ expect(node.type).toBe("human");
328
+ expect(node.timeout).toBe("1h");
329
+ expect(node.onTimeout).toBe("skip");
330
+ });
331
+ it("detects group nodes", () => {
332
+ const node = parseAgentMd("gap-debate", GROUP_AGENT_MD);
333
+ expect(node).toBeDefined();
334
+ expect(node.type).toBe("group");
335
+ const group = node;
336
+ expect(group.members).toContain("sdk-specialist");
337
+ expect(group.members).toContain("binding-veteran");
338
+ expect(group.members).toContain("platform-expert");
339
+ expect(group.speakerSelection).toBe("round-robin");
340
+ expect(group.maxRounds).toBe(3);
341
+ });
342
+ it("preserves unknown frontmatter as extensions", () => {
343
+ const md = `---
344
+ name: test
345
+ model: sonnet
346
+ custom-field: custom-value
347
+ ---
348
+
349
+ ## Instructions
350
+ Test prompt.
351
+ `;
352
+ const agent = parseAgentMd("test", md);
353
+ expect(agent.extensions).toBeDefined();
354
+ expect(agent.extensions["claude-code"]["custom-field"]).toBe("custom-value");
355
+ });
356
+ });
357
+ // ---------------------------------------------------------------------------
358
+ // SKILL.md parser tests
359
+ // ---------------------------------------------------------------------------
360
+ describe("parseSkillMd", () => {
361
+ it("detects topology skill", () => {
362
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
363
+ expect(result.isTopology).toBe(true);
364
+ expect(result.name).toBe("test-pipeline");
365
+ expect(result.version).toBe("1.0.0");
366
+ });
367
+ it("extracts patterns", () => {
368
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
369
+ expect(result.patterns).toContain("pipeline");
370
+ });
371
+ it("extracts orchestrator", () => {
372
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
373
+ expect(result.orchestrator).toBeDefined();
374
+ expect(result.orchestrator.model).toBe("opus");
375
+ expect(result.orchestrator.handles).toContain("intake");
376
+ expect(result.orchestrator.handles).toContain("done");
377
+ });
378
+ it("extracts orchestrator outputs", () => {
379
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
380
+ expect(result.orchestrator.outputs).toBeDefined();
381
+ expect(result.orchestrator.outputs.verdict).toContain("pass");
382
+ });
383
+ it("extracts flow edges", () => {
384
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
385
+ expect(result.edges.length).toBe(3);
386
+ expect(result.edges[0].from).toBe("intake");
387
+ expect(result.edges[0].to).toBe("researcher");
388
+ });
389
+ it("extracts agent IDs from flow", () => {
390
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
391
+ expect(result.agentIds.has("researcher")).toBe(true);
392
+ expect(result.agentIds.has("writer")).toBe(true);
393
+ });
394
+ it("extracts gates", () => {
395
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
396
+ expect(result.gates.length).toBe(1);
397
+ expect(result.gates[0].id).toBe("quality-check");
398
+ expect(result.gates[0].after).toBe("writer");
399
+ expect(result.gates[0].onFail).toBe("bounce-back");
400
+ });
401
+ it("extracts triggers", () => {
402
+ const result = parseSkillMd(SIMPLE_SKILL_MD);
403
+ expect(result.triggers.length).toBe(1);
404
+ expect(result.triggers[0].name).toBe("start");
405
+ expect(result.triggers[0].pattern).toBe("/start");
406
+ });
407
+ it("non-topology skill is detected", () => {
408
+ const md = `---
409
+ name: helper-skill
410
+ description: "A helper"
411
+ ---
412
+
413
+ # Helper Skill
414
+ `;
415
+ const result = parseSkillMd(md);
416
+ expect(result.isTopology).toBe(false);
417
+ });
418
+ });
419
+ // ---------------------------------------------------------------------------
420
+ // settings.json parser tests
421
+ // ---------------------------------------------------------------------------
422
+ describe("parseSettingsJson", () => {
423
+ it("extracts allow/deny/ask", () => {
424
+ const { settings } = parseSettingsJson(JSON.stringify({
425
+ allow: ["Read", "Grep"],
426
+ deny: ["Bash(rm -rf *)"],
427
+ ask: ["Write"],
428
+ }));
429
+ expect(settings.allow).toContain("Read");
430
+ expect(settings.deny).toContain("Bash(rm -rf *)");
431
+ expect(settings.ask).toContain("Write");
432
+ });
433
+ it("extracts hooks", () => {
434
+ const { hooks } = parseSettingsJson(JSON.stringify({
435
+ hooks: {
436
+ PostToolUse: [{
437
+ hooks: [{ command: "bash scripts/log.sh", type: "command", timeout: 5 }],
438
+ matcher: "Bash",
439
+ }],
440
+ },
441
+ }));
442
+ expect(hooks.length).toBe(1);
443
+ expect(hooks[0].on).toBe("PostToolUse");
444
+ expect(hooks[0].run).toBe("bash scripts/log.sh");
445
+ });
446
+ it("skips gate hooks", () => {
447
+ const { hooks } = parseSettingsJson(JSON.stringify({
448
+ hooks: {
449
+ PreToolUse: [{
450
+ hooks: [{ command: "bash scripts/gate-quality.sh", type: "command" }],
451
+ matcher: "Task",
452
+ }],
453
+ },
454
+ }));
455
+ expect(hooks.length).toBe(0);
456
+ });
457
+ });
458
+ // ---------------------------------------------------------------------------
459
+ // .mcp.json parser tests
460
+ // ---------------------------------------------------------------------------
461
+ describe("parseMcpJson", () => {
462
+ it("extracts MCP servers", () => {
463
+ const servers = parseMcpJson(JSON.stringify({
464
+ mcpServers: {
465
+ supabase: { command: "npx", args: ["supabase-mcp"], env: {} },
466
+ },
467
+ }));
468
+ expect(servers.supabase).toBeDefined();
469
+ expect(servers.supabase.command).toBe("npx");
470
+ });
471
+ });
472
+ // ---------------------------------------------------------------------------
473
+ // Command parser tests
474
+ // ---------------------------------------------------------------------------
475
+ describe("parseCommandMd", () => {
476
+ it("extracts trigger from command file", () => {
477
+ const trigger = parseCommandMd("start.md", `---
478
+ description: "Start"
479
+ ---
480
+
481
+ # /start <TOPIC>
482
+
483
+ ## Arguments
484
+ - TOPIC: the topic
485
+ `);
486
+ expect(trigger).toBeDefined();
487
+ expect(trigger.name).toBe("start");
488
+ expect(trigger.pattern).toBe("/start <TOPIC>");
489
+ expect(trigger.argument).toBe("TOPIC");
490
+ });
491
+ });
492
+ // ---------------------------------------------------------------------------
493
+ // Action script parser tests
494
+ // ---------------------------------------------------------------------------
495
+ describe("parseActionScript", () => {
496
+ it("extracts action from script", () => {
497
+ const action = parseActionScript("action-intake.sh", `#!/usr/bin/env bash
498
+ # Action: intake — Parse user request
499
+ # Auto-generated by agentopology scaffold — edit as needed.
500
+ set -euo pipefail
501
+
502
+ echo "TODO: implement inline action"
503
+ `);
504
+ expect(action).toBeDefined();
505
+ expect(action.id).toBe("intake");
506
+ expect(action.description).toBe("Parse user request");
507
+ expect(action.kind).toBe("inline");
508
+ });
509
+ it("detects external actions", () => {
510
+ const action = parseActionScript("action-fetch.sh", `#!/usr/bin/env bash
511
+ # Action: fetch — Get external data
512
+ # External source: github-pr
513
+ set -euo pipefail
514
+ `);
515
+ expect(action).toBeDefined();
516
+ expect(action.kind).toBe("external");
517
+ expect(action.source).toBe("github-pr");
518
+ });
519
+ });
520
+ // ---------------------------------------------------------------------------
521
+ // Full import tests
522
+ // ---------------------------------------------------------------------------
523
+ describe("importClaudeCode", () => {
524
+ it("imports basic agent files", () => {
525
+ const files = [
526
+ {
527
+ path: "agents/worker/AGENT.md",
528
+ content: `---
529
+ name: worker
530
+ model: sonnet
531
+ tools:
532
+ - Read
533
+ - Write
534
+ ---
535
+
536
+ ## Instructions
537
+ Do work.
538
+ `,
539
+ },
540
+ ];
541
+ const ast = importClaudeCode(files, "test-import");
542
+ expect(ast.topology.name).toBe("test-import");
543
+ expect(ast.nodes.length).toBeGreaterThanOrEqual(1);
544
+ const worker = ast.nodes.find((n) => n.id === "worker");
545
+ expect(worker).toBeDefined();
546
+ expect(worker.type).toBe("agent");
547
+ expect(worker.model).toBe("sonnet");
548
+ });
549
+ it("imports with SKILL.md topology metadata", () => {
550
+ const files = [
551
+ {
552
+ path: "agents/researcher/AGENT.md",
553
+ content: `---
554
+ name: researcher
555
+ model: sonnet
556
+ ---
557
+
558
+ ## Instructions
559
+ Research things.
560
+ `,
561
+ },
562
+ {
563
+ path: "skills/my-pipeline/SKILL.md",
564
+ content: SIMPLE_SKILL_MD,
565
+ },
566
+ ];
567
+ const ast = importClaudeCode(files, "fallback-name");
568
+ expect(ast.topology.name).toBe("test-pipeline"); // from SKILL.md, not fallback
569
+ expect(ast.topology.version).toBe("1.0.0");
570
+ expect(ast.edges.length).toBe(3);
571
+ });
572
+ it("imports settings.json", () => {
573
+ const files = [
574
+ {
575
+ path: "agents/worker/AGENT.md",
576
+ content: `---
577
+ name: worker
578
+ model: sonnet
579
+ ---
580
+ `,
581
+ },
582
+ {
583
+ path: ".claude/settings.json",
584
+ content: JSON.stringify({ allow: ["Read"], deny: ["Bash"] }),
585
+ },
586
+ ];
587
+ const ast = importClaudeCode(files, "test");
588
+ expect(ast.settings.allow).toContain("Read");
589
+ });
590
+ it("imports .mcp.json", () => {
591
+ const files = [
592
+ {
593
+ path: "agents/worker/AGENT.md",
594
+ content: `---
595
+ name: worker
596
+ model: sonnet
597
+ ---
598
+ `,
599
+ },
600
+ {
601
+ path: ".mcp.json",
602
+ content: JSON.stringify({ mcpServers: { db: { command: "npx", args: ["db-mcp"] } } }),
603
+ },
604
+ ];
605
+ const ast = importClaudeCode(files, "test");
606
+ expect(ast.mcpServers.db).toBeDefined();
607
+ });
608
+ it("generates linear flow when no SKILL.md", () => {
609
+ const files = [
610
+ {
611
+ path: "agents/alpha/AGENT.md",
612
+ content: `---
613
+ name: alpha
614
+ model: sonnet
615
+ ---
616
+ `,
617
+ },
618
+ {
619
+ path: "agents/beta/AGENT.md",
620
+ content: `---
621
+ name: beta
622
+ model: sonnet
623
+ ---
624
+ `,
625
+ },
626
+ ];
627
+ const ast = importClaudeCode(files, "test");
628
+ expect(ast.edges.length).toBe(1);
629
+ expect(ast.edges[0].from).toBe("alpha");
630
+ expect(ast.edges[0].to).toBe("beta");
631
+ });
632
+ it("handles human and group nodes", () => {
633
+ const files = [
634
+ { path: "agents/review/AGENT.md", content: HUMAN_AGENT_MD },
635
+ { path: "agents/debate/AGENT.md", content: GROUP_AGENT_MD },
636
+ ];
637
+ const ast = importClaudeCode(files, "test");
638
+ const human = ast.nodes.find((n) => n.type === "human");
639
+ const group = ast.nodes.find((n) => n.type === "group");
640
+ expect(human).toBeDefined();
641
+ expect(group).toBeDefined();
642
+ });
643
+ });
644
+ // ---------------------------------------------------------------------------
645
+ // importFromPlatform dispatch tests
646
+ // ---------------------------------------------------------------------------
647
+ describe("importFromPlatform", () => {
648
+ it("dispatches to claude-code", () => {
649
+ const files = [
650
+ {
651
+ path: "agents/worker/AGENT.md",
652
+ content: `---
653
+ name: worker
654
+ model: sonnet
655
+ ---
656
+
657
+ ## Instructions
658
+ Work.
659
+ `,
660
+ },
661
+ ];
662
+ const atSource = importFromPlatform(files, "claude-code", "test");
663
+ expect(atSource).toContain("topology test");
664
+ expect(atSource).toContain("agent worker");
665
+ });
666
+ it("throws for unsupported binding", () => {
667
+ expect(() => importFromPlatform([], "unknown", "test")).toThrow("Import not supported");
668
+ });
669
+ it("generates parseable .at output", () => {
670
+ const files = [
671
+ {
672
+ path: "agents/researcher/AGENT.md",
673
+ content: BASIC_AGENT_MD,
674
+ },
675
+ ];
676
+ const atSource = importFromPlatform(files, "claude-code", "my-topology");
677
+ // Should parse without throwing
678
+ const ast = parse(atSource);
679
+ expect(ast.topology.name).toBe("my-topology");
680
+ });
681
+ });
682
+ // ---------------------------------------------------------------------------
683
+ // Round-trip test: scaffold → import
684
+ // ---------------------------------------------------------------------------
685
+ describe("round-trip: parse → serialize → parse", () => {
686
+ it("round-trips a complex topology", () => {
687
+ const source = `
688
+ topology complex : [pipeline, fan-out] {
689
+ meta {
690
+ version: "2.0.0"
691
+ description: "Complex topology"
692
+ domain: testing
693
+ }
694
+ orchestrator {
695
+ model: opus
696
+ handles: [intake, report]
697
+ }
698
+ roles {
699
+ lead: "Lead researcher"
700
+ }
701
+ action intake { kind: inline description: "Start" }
702
+ agent lead-researcher {
703
+ role: lead
704
+ model: gpt-4o
705
+ permissions: supervised
706
+ phase: 1
707
+ tools: [Read, Grep, WebSearch]
708
+ reads: ["workspace/data.md"]
709
+ writes: ["workspace/output.md"]
710
+ outputs: {
711
+ confidence: high | medium | low
712
+ }
713
+ timeout: 10m
714
+ on-fail: retry
715
+ retry: 3
716
+ prompt {
717
+ You are the lead researcher.
718
+ }
719
+ }
720
+ agent writer {
721
+ model: sonnet
722
+ phase: 2
723
+ tools: [Read, Write]
724
+ prompt {
725
+ You write documents.
726
+ }
727
+ }
728
+ action report { kind: report description: "Done" }
729
+ flow {
730
+ intake -> lead-researcher
731
+ lead-researcher -> writer
732
+ writer -> report
733
+ }
734
+ gates {
735
+ gate quality {
736
+ after: writer
737
+ run: "scripts/check.sh"
738
+ checks: [grammar]
739
+ on-fail: bounce-back
740
+ }
741
+ }
742
+ memory {
743
+ workspace {
744
+ path: "workspace/"
745
+ }
746
+ }
747
+ triggers {
748
+ command start {
749
+ pattern: "/start"
750
+ }
751
+ }
752
+ settings {
753
+ allow: ["Read", "Grep"]
754
+ }
755
+ mcp-servers {
756
+ db {
757
+ command: npx
758
+ args: ["db-mcp"]
759
+ }
760
+ }
761
+ }
762
+ `;
763
+ const ast = parse(source);
764
+ const serialized = serializeAST(ast);
765
+ const reparsed = parse(serialized);
766
+ // Verify key fields survived
767
+ expect(reparsed.topology.name).toBe("complex");
768
+ expect(reparsed.topology.version).toBe("2.0.0");
769
+ expect(reparsed.topology.domain).toBe("testing");
770
+ expect(reparsed.nodes.filter((n) => n.type === "agent").length).toBe(2);
771
+ expect(reparsed.edges.length).toBe(3);
772
+ expect(reparsed.triggers.length).toBe(1);
773
+ expect(Object.keys(reparsed.mcpServers).length).toBe(1);
774
+ const lead = reparsed.nodes.find((n) => n.id === "lead-researcher");
775
+ expect(lead.model).toBe("gpt-4o");
776
+ expect(lead.timeout).toBe("10m");
777
+ expect(lead.retry).toBe(3);
778
+ expect(lead.outputs.confidence).toContain("high");
779
+ });
780
+ });
781
+ //# sourceMappingURL=import.test.js.map