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,1985 @@
1
+ /**
2
+ * AgenTopology parser.
3
+ *
4
+ * Takes the raw text of an `.at` file and produces a fully-typed
5
+ * {@link TopologyAST}. Individual section parsers are also exported for
6
+ * advanced / incremental use.
7
+ *
8
+ * ```ts
9
+ * import { parse } from "./parser";
10
+ * const ast = parse(source);
11
+ * ```
12
+ *
13
+ * @module
14
+ */
15
+ import { stripComments, extractBlock, extractAllBlocks, parseKV, parseList, parseMultilineList, unquote, parseFields, parseOutputsBlock, dedentBlock, } from "./lexer.js";
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+ /** Convert a kebab-case or snake_case identifier to a Title Case label. */
20
+ function toLabel(id) {
21
+ return id
22
+ .split(/[-_]/)
23
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
24
+ .join(" ");
25
+ }
26
+ /**
27
+ * Parse an `extensions { binding-name { key: value } }` sub-block.
28
+ *
29
+ * Each named sub-block inside `extensions` becomes a key in the returned
30
+ * record, with its key-value pairs parsed via {@link parseFields}.
31
+ */
32
+ function parseExtensionsBlock(block) {
33
+ const extBlock = extractBlock(block, "extensions");
34
+ if (!extBlock)
35
+ return undefined;
36
+ const result = {};
37
+ // Extract each named sub-block inside extensions
38
+ const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
39
+ let m;
40
+ while ((m = re.exec(extBlock.body)) !== null) {
41
+ const name = m[1];
42
+ const startIdx = m.index + m[0].length;
43
+ let depth = 1;
44
+ let i = startIdx;
45
+ while (i < extBlock.body.length && depth > 0) {
46
+ if (extBlock.body[i] === "{")
47
+ depth++;
48
+ else if (extBlock.body[i] === "}")
49
+ depth--;
50
+ i++;
51
+ }
52
+ if (depth === 0) {
53
+ const innerBody = extBlock.body.slice(startIdx, i - 1);
54
+ const fields = parseFields(innerBody);
55
+ const entry = {};
56
+ for (const [k, v] of Object.entries(fields)) {
57
+ // Try to parse booleans and numbers
58
+ if (v === "true")
59
+ entry[k] = true;
60
+ else if (v === "false")
61
+ entry[k] = false;
62
+ else if (/^\d+$/.test(v))
63
+ entry[k] = parseInt(v, 10);
64
+ else
65
+ entry[k] = unquote(v);
66
+ }
67
+ result[name] = entry;
68
+ }
69
+ }
70
+ return Object.keys(result).length > 0 ? result : undefined;
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Schema parsing
74
+ // ---------------------------------------------------------------------------
75
+ /** Known primitive type names for the schema type system. */
76
+ const SCHEMA_PRIMITIVES = new Set(["string", "number", "integer", "boolean", "object"]);
77
+ /**
78
+ * Parse a type expression string into a {@link SchemaType}.
79
+ *
80
+ * Supports:
81
+ * - Primitives: `"string"`, `"number"`, `"integer"`, `"boolean"`, `"object"`
82
+ * - Arrays: `"array of string"`, `"array of finding"`
83
+ * - Enums: `"low | medium | high"`
84
+ * - References: any non-primitive identifier like `"finding"`
85
+ */
86
+ export function parseSchemaType(typeStr) {
87
+ const trimmed = typeStr.trim();
88
+ // Enum: contains ` | ` separator
89
+ if (trimmed.includes("|")) {
90
+ const values = trimmed.split("|").map((v) => v.trim()).filter(Boolean);
91
+ return { kind: "enum", values };
92
+ }
93
+ // Array: "array of <type>"
94
+ const arrayMatch = trimmed.match(/^array\s+of\s+(.+)$/i);
95
+ if (arrayMatch) {
96
+ return { kind: "array", itemType: parseSchemaType(arrayMatch[1]) };
97
+ }
98
+ // Primitive
99
+ if (SCHEMA_PRIMITIVES.has(trimmed)) {
100
+ return { kind: "primitive", value: trimmed };
101
+ }
102
+ // Reference to a named schema
103
+ return { kind: "ref", name: trimmed };
104
+ }
105
+ /**
106
+ * Parse a block body of `name: type-expression` lines into schema field definitions.
107
+ *
108
+ * Each line is parsed as a KV pair. A leading `?` on the type expression marks
109
+ * the field as optional.
110
+ */
111
+ export function parseSchemaFields(body) {
112
+ const fields = [];
113
+ for (const line of body.split("\n")) {
114
+ const kv = parseKV(line);
115
+ if (!kv)
116
+ continue;
117
+ const [name, rawType] = kv;
118
+ let typeStr = rawType.trim();
119
+ let optional = false;
120
+ if (typeStr.startsWith("?")) {
121
+ optional = true;
122
+ typeStr = typeStr.slice(1).trim();
123
+ }
124
+ fields.push({
125
+ name,
126
+ type: parseSchemaType(typeStr),
127
+ optional,
128
+ });
129
+ }
130
+ return fields;
131
+ }
132
+ /**
133
+ * Parse the top-level `schemas { schema <id> { ... } ... }` block.
134
+ *
135
+ * Returns an array of named schema definitions.
136
+ */
137
+ export function parseSchemas(topBody) {
138
+ const schemasBlock = extractBlock(topBody, "schemas");
139
+ if (!schemasBlock)
140
+ return [];
141
+ const schemaBlocks = extractAllBlocks(schemasBlock.body, "schema");
142
+ const schemas = [];
143
+ for (const block of schemaBlocks) {
144
+ if (!block.id)
145
+ continue;
146
+ schemas.push({
147
+ id: block.id,
148
+ fields: parseSchemaFields(block.body),
149
+ });
150
+ }
151
+ return schemas;
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // Section parsers
155
+ // ---------------------------------------------------------------------------
156
+ /**
157
+ * Parse the topology header line.
158
+ *
159
+ * Expects `topology <name> : [pattern, ...] {` somewhere in the source.
160
+ *
161
+ * @throws If no topology header is found.
162
+ */
163
+ export function parseTopologyHeader(src) {
164
+ const m = src.match(/topology\s+(\S+)\s*:\s*\[([^\]]*)\]\s*\{/);
165
+ if (!m)
166
+ throw new Error("Could not find topology header");
167
+ return {
168
+ name: m[1],
169
+ patterns: m[2]
170
+ .split(",")
171
+ .map((s) => s.trim())
172
+ .filter(Boolean),
173
+ };
174
+ }
175
+ /**
176
+ * Parse the `meta { ... }` block.
177
+ *
178
+ * Extracts version, description, foundations, and advanced fields.
179
+ */
180
+ export function parseMeta(body) {
181
+ const fields = parseFields(body);
182
+ const result = {};
183
+ if (fields.version)
184
+ result.version = unquote(fields.version);
185
+ if (fields.description)
186
+ result.description = unquote(fields.description);
187
+ if (fields.domain)
188
+ result.domain = fields.domain;
189
+ const foundations = parseMultilineList(body, "foundations");
190
+ if (foundations.length)
191
+ result.foundations = foundations;
192
+ const advanced = parseMultilineList(body, "advanced");
193
+ if (advanced.length)
194
+ result.advanced = advanced;
195
+ if (fields.timeout)
196
+ result.timeout = fields.timeout;
197
+ if (fields["error-handler"])
198
+ result.errorHandler = fields["error-handler"];
199
+ if (fields.durable)
200
+ result.durable = fields.durable === "true";
201
+ return result;
202
+ }
203
+ /**
204
+ * Parse the `orchestrator { ... }` block into an {@link OrchestratorNode}.
205
+ */
206
+ export function parseOrchestrator(body) {
207
+ const fields = parseFields(body);
208
+ const handles = parseMultilineList(body, "handles");
209
+ const outputs = parseOutputsBlock(body);
210
+ const node = {
211
+ id: "orchestrator",
212
+ type: "orchestrator",
213
+ label: "Orchestrator",
214
+ model: fields.model ?? "unknown",
215
+ handles,
216
+ };
217
+ if (fields.generates)
218
+ node.generates = unquote(fields.generates);
219
+ if (outputs)
220
+ node.outputs = outputs;
221
+ return node;
222
+ }
223
+ /**
224
+ * Parse the `roles { ... }` block into a map of role name to description.
225
+ */
226
+ export function parseRoles(body) {
227
+ const roles = {};
228
+ for (const line of body.split("\n")) {
229
+ const m = line.match(/^\s*([a-zA-Z_-]+)\s*:\s*"(.+?)"\s*$/);
230
+ if (m)
231
+ roles[m[1]] = m[2];
232
+ }
233
+ return roles;
234
+ }
235
+ /**
236
+ * Parse an `action <id> { ... }` block into an {@link ActionNode}.
237
+ */
238
+ export function parseAction(id, body) {
239
+ const fields = parseFields(body);
240
+ const commands = parseMultilineList(body, "commands");
241
+ const node = {
242
+ id,
243
+ type: "action",
244
+ label: toLabel(id),
245
+ };
246
+ if (fields.kind)
247
+ node.kind = fields.kind;
248
+ if (fields.source)
249
+ node.source = unquote(fields.source);
250
+ if (fields.description)
251
+ node.description = unquote(fields.description);
252
+ if (commands.length)
253
+ node.commands = commands;
254
+ if (fields.timeout)
255
+ node.timeout = fields.timeout;
256
+ if (fields["on-fail"])
257
+ node.onFail = fields["on-fail"];
258
+ if (fields.join)
259
+ node.join = fields.join;
260
+ return node;
261
+ }
262
+ /**
263
+ * Parse an `agent <id> { ... }` block into an {@link AgentNode}.
264
+ *
265
+ * @param id - The agent identifier from the block header.
266
+ * @param body - The block body (between braces).
267
+ * @param roles - Role descriptions from the `roles` block, used to attach a
268
+ * role description to matching agents.
269
+ */
270
+ export function parseAgent(id, body, roles) {
271
+ const fields = parseFields(body);
272
+ const tools = parseMultilineList(body, "tools");
273
+ const skills = parseMultilineList(body, "skills");
274
+ const reads = parseMultilineList(body, "reads");
275
+ const writes = parseMultilineList(body, "writes");
276
+ const disallowedTools = parseMultilineList(body, "disallowed-tools");
277
+ const outputs = parseOutputsBlock(body);
278
+ const node = {
279
+ id,
280
+ type: "agent",
281
+ label: toLabel(id),
282
+ };
283
+ if (fields.phase)
284
+ node.phase = parseFloat(fields.phase);
285
+ if (fields.model)
286
+ node.model = fields.model;
287
+ if (fields.permissions)
288
+ node.permissions = fields.permissions;
289
+ // Extract prompt from prompt {} block (not KV pair)
290
+ const promptBlock = extractBlock(body, "prompt");
291
+ if (promptBlock) {
292
+ node.prompt = dedentBlock(promptBlock.body);
293
+ }
294
+ if (tools.length)
295
+ node.tools = tools;
296
+ if (skills.length)
297
+ node.skills = skills;
298
+ if (reads.length)
299
+ node.reads = reads;
300
+ if (writes.length)
301
+ node.writes = writes;
302
+ if (disallowedTools.length)
303
+ node.disallowedTools = disallowedTools;
304
+ if (fields.skip)
305
+ node.skip = fields.skip;
306
+ if (fields.behavior)
307
+ node.behavior = fields.behavior;
308
+ if (fields.invocation)
309
+ node.invocation = fields.invocation;
310
+ // Retry: support both simple number and structured block form
311
+ const retryBlock = extractBlock(body, "retry");
312
+ if (retryBlock) {
313
+ const rFields = parseFields(retryBlock.body);
314
+ const retryConfig = {
315
+ max: rFields.max ? parseInt(rFields.max, 10) : 1,
316
+ };
317
+ if (rFields.backoff)
318
+ retryConfig.backoff = rFields.backoff;
319
+ if (rFields.interval)
320
+ retryConfig.interval = rFields.interval;
321
+ if (rFields["max-interval"])
322
+ retryConfig.maxInterval = rFields["max-interval"];
323
+ if (rFields.jitter)
324
+ retryConfig.jitter = rFields.jitter === "true";
325
+ const nonRetryable = parseMultilineList(retryBlock.body, "non-retryable");
326
+ if (nonRetryable.length)
327
+ retryConfig.nonRetryable = nonRetryable;
328
+ node.retry = retryConfig;
329
+ }
330
+ else if (fields.retry) {
331
+ node.retry = parseInt(fields.retry, 10);
332
+ }
333
+ // Timeout
334
+ if (fields.timeout)
335
+ node.timeout = fields.timeout;
336
+ // On-fail
337
+ if (fields["on-fail"])
338
+ node.onFail = fields["on-fail"];
339
+ // Sampling parameters
340
+ if (fields.temperature)
341
+ node.temperature = parseFloat(fields.temperature);
342
+ if (fields["max-tokens"])
343
+ node.maxTokens = parseInt(fields["max-tokens"], 10);
344
+ if (fields["top-p"])
345
+ node.topP = parseFloat(fields["top-p"]);
346
+ if (fields["top-k"])
347
+ node.topK = parseInt(fields["top-k"], 10);
348
+ const stop = parseMultilineList(body, "stop");
349
+ if (stop.length)
350
+ node.stop = stop;
351
+ if (fields.seed)
352
+ node.seed = parseInt(fields.seed, 10);
353
+ // Thinking
354
+ if (fields.thinking)
355
+ node.thinking = fields.thinking;
356
+ if (fields["thinking-budget"])
357
+ node.thinkingBudget = parseInt(fields["thinking-budget"], 10);
358
+ // Output format
359
+ if (fields["output-format"])
360
+ node.outputFormat = fields["output-format"];
361
+ // Log level
362
+ if (fields["log-level"])
363
+ node.logLevel = fields["log-level"];
364
+ // Join semantics
365
+ if (fields.join)
366
+ node.join = fields.join;
367
+ // Circuit breaker
368
+ const cbBlock = extractBlock(body, "circuit-breaker");
369
+ if (cbBlock) {
370
+ const cbFields = parseFields(cbBlock.body);
371
+ const cb = {
372
+ threshold: cbFields.threshold ? parseInt(cbFields.threshold, 10) : 1,
373
+ window: cbFields.window ?? "1m",
374
+ cooldown: cbFields.cooldown ?? "30s",
375
+ };
376
+ node.circuitBreaker = cb;
377
+ }
378
+ // Compensation / saga
379
+ if (fields.compensates)
380
+ node.compensates = fields.compensates;
381
+ if (fields.isolation)
382
+ node.isolation = fields.isolation;
383
+ if (fields.background)
384
+ node.background = fields.background === "true";
385
+ const mcpServers = parseMultilineList(body, "mcp-servers");
386
+ if (mcpServers.length)
387
+ node.mcpServers = mcpServers;
388
+ if (outputs)
389
+ node.outputs = outputs;
390
+ // Parse scale sub-block
391
+ const scaleBlock = extractBlock(body, "scale");
392
+ if (scaleBlock) {
393
+ const sFields = parseFields(scaleBlock.body);
394
+ const scale = {
395
+ mode: sFields.mode ?? "auto",
396
+ by: sFields.by ?? "",
397
+ min: sFields.min ? parseInt(sFields.min, 10) : 1,
398
+ max: sFields.max ? parseInt(sFields.max, 10) : 1,
399
+ batchSize: sFields["batch-size"]
400
+ ? parseInt(sFields["batch-size"], 10)
401
+ : null,
402
+ };
403
+ node.scale = scale;
404
+ }
405
+ // Parse per-agent hooks block
406
+ const agentHooksBlock = extractBlock(body, "hooks");
407
+ if (agentHooksBlock) {
408
+ const agentHooks = [];
409
+ const hookBlocks = extractAllBlocks(agentHooksBlock.body, "hook");
410
+ for (const hBlock of hookBlocks) {
411
+ if (!hBlock.id)
412
+ continue;
413
+ const hFields = parseFields(hBlock.body);
414
+ const agentHook = {
415
+ name: hBlock.id,
416
+ on: hFields.on ?? "",
417
+ matcher: hFields.matcher ? unquote(hFields.matcher) : "",
418
+ run: hFields.run ? unquote(hFields.run) : "",
419
+ ...(hFields.type ? { type: hFields.type } : {}),
420
+ ...(hFields.timeout
421
+ ? { timeout: parseInt(hFields.timeout, 10) }
422
+ : {}),
423
+ };
424
+ // Parse extensions sub-block inside per-agent hook
425
+ const hookExtensions = parseExtensionsBlock(hBlock.body);
426
+ if (hookExtensions)
427
+ agentHook.extensions = hookExtensions;
428
+ agentHooks.push(agentHook);
429
+ }
430
+ if (agentHooks.length > 0)
431
+ node.hooks = agentHooks;
432
+ }
433
+ // Sandbox override
434
+ if (fields.sandbox) {
435
+ const sv = fields.sandbox;
436
+ if (sv === "true")
437
+ node.sandbox = true;
438
+ else if (sv === "false")
439
+ node.sandbox = false;
440
+ else
441
+ node.sandbox = sv;
442
+ }
443
+ // Fallback chain
444
+ const fallbackChain = parseMultilineList(body, "fallback-chain");
445
+ if (fallbackChain.length)
446
+ node.fallbackChain = fallbackChain;
447
+ // Input/output schema blocks
448
+ const inputSchemaBlock = extractBlock(body, "input-schema");
449
+ if (inputSchemaBlock) {
450
+ node.inputSchema = parseSchemaFields(inputSchemaBlock.body);
451
+ }
452
+ const outputSchemaBlock = extractBlock(body, "output-schema");
453
+ if (outputSchemaBlock) {
454
+ node.outputSchema = parseSchemaFields(outputSchemaBlock.body);
455
+ }
456
+ // Artifact lineage: produces and consumes
457
+ const produces = parseMultilineList(body, "produces");
458
+ if (produces.length)
459
+ node.produces = produces;
460
+ const consumes = parseMultilineList(body, "consumes");
461
+ if (consumes.length)
462
+ node.consumes = consumes;
463
+ // Prompt variants (A/B testing)
464
+ const variantsBlock = extractBlock(body, "variants");
465
+ if (variantsBlock) {
466
+ const variants = [];
467
+ const variantBlocks = extractAllBlocks(variantsBlock.body, "variant");
468
+ for (const vBlock of variantBlocks) {
469
+ if (!vBlock.id)
470
+ continue;
471
+ const vFields = parseFields(vBlock.body);
472
+ const variant = {
473
+ id: vBlock.id,
474
+ weight: vFields.weight ? parseFloat(vFields.weight) : 0,
475
+ };
476
+ const vPromptBlock = extractBlock(vBlock.body, "prompt");
477
+ if (vPromptBlock) {
478
+ variant.prompt = dedentBlock(vPromptBlock.body);
479
+ }
480
+ if (vFields.temperature)
481
+ variant.temperature = parseFloat(vFields.temperature);
482
+ if (vFields.model)
483
+ variant.model = vFields.model;
484
+ variants.push(variant);
485
+ }
486
+ if (variants.length > 0)
487
+ node.variants = variants;
488
+ }
489
+ // Rate limit
490
+ if (fields["rate-limit"])
491
+ node.rateLimit = fields["rate-limit"];
492
+ // New fields: description, max-turns, extensions
493
+ if (fields.description)
494
+ node.description = unquote(fields.description);
495
+ if (fields["max-turns"])
496
+ node.maxTurns = parseInt(fields["max-turns"], 10);
497
+ const extensions = parseExtensionsBlock(body);
498
+ if (extensions)
499
+ node.extensions = extensions;
500
+ // Attach role description if available.
501
+ // Prefer exact match, then longest prefix match.
502
+ const exactRole = roles[id];
503
+ if (exactRole) {
504
+ node.role = exactRole;
505
+ }
506
+ else {
507
+ const prefixMatches = Object.keys(roles)
508
+ .filter((r) => id.startsWith(r))
509
+ .sort((a, b) => b.length - a.length);
510
+ if (prefixMatches.length > 0)
511
+ node.role = roles[prefixMatches[0]];
512
+ }
513
+ return node;
514
+ }
515
+ /**
516
+ * Parse a `gate <id> { ... }` block into a {@link GateNode}.
517
+ */
518
+ export function parseGate(id, body) {
519
+ const fields = parseFields(body);
520
+ const checks = parseMultilineList(body, "checks");
521
+ const node = {
522
+ id,
523
+ type: "gate",
524
+ label: toLabel(id),
525
+ };
526
+ if (fields.after)
527
+ node.after = fields.after;
528
+ if (fields.before)
529
+ node.before = fields.before;
530
+ if (fields.run)
531
+ node.run = unquote(fields.run);
532
+ if (checks.length)
533
+ node.checks = checks;
534
+ if (fields.retry)
535
+ node.retry = parseInt(fields.retry, 10);
536
+ if (fields["on-fail"])
537
+ node.onFail = fields["on-fail"];
538
+ if (fields.behavior)
539
+ node.behavior = fields.behavior;
540
+ if (fields.timeout)
541
+ node.timeout = fields.timeout;
542
+ const extensions = parseExtensionsBlock(body);
543
+ if (extensions)
544
+ node.extensions = extensions;
545
+ return node;
546
+ }
547
+ /**
548
+ * Parse a `human <id> { ... }` block into a {@link HumanNode}.
549
+ */
550
+ export function parseHuman(id, body) {
551
+ const fields = parseFields(body);
552
+ const node = {
553
+ id,
554
+ type: "human",
555
+ label: toLabel(id),
556
+ };
557
+ if (fields.description)
558
+ node.description = unquote(fields.description);
559
+ if (fields.timeout)
560
+ node.timeout = fields.timeout;
561
+ if (fields["on-timeout"])
562
+ node.onTimeout = fields["on-timeout"];
563
+ return node;
564
+ }
565
+ /**
566
+ * Parse a `group <id> { ... }` block into a {@link GroupNode}.
567
+ */
568
+ export function parseGroup(id, body) {
569
+ const fields = parseFields(body);
570
+ const members = parseList(fields.members ?? "");
571
+ const node = {
572
+ id,
573
+ type: "group",
574
+ label: toLabel(id),
575
+ members,
576
+ };
577
+ if (fields["speaker-selection"])
578
+ node.speakerSelection = fields["speaker-selection"];
579
+ if (fields["max-rounds"])
580
+ node.maxRounds = parseInt(fields["max-rounds"], 10);
581
+ if (fields.termination)
582
+ node.termination = unquote(fields.termination);
583
+ if (fields.description)
584
+ node.description = unquote(fields.description);
585
+ if (fields.timeout)
586
+ node.timeout = fields.timeout;
587
+ return node;
588
+ }
589
+ /**
590
+ * Parse the `flow { ... }` block into an array of {@link EdgeDef}.
591
+ *
592
+ * Supports chain syntax (`a -> b -> c`), fan-out (`a -> [b, c]`),
593
+ * error edges (`-x->`, `-x[type]->`), and edge annotations
594
+ * (`[when condition, max N, tolerance: N, race, wait 30s]`).
595
+ */
596
+ export function parseFlow(body, _edgeAttributeErrors) {
597
+ const edges = [];
598
+ for (const line of body.split("\n")) {
599
+ const trimmed = line.trim();
600
+ if (!trimmed)
601
+ continue;
602
+ let condition = null;
603
+ let maxIterations = null;
604
+ let per = null;
605
+ let tolerance;
606
+ let race;
607
+ let wait;
608
+ let weight;
609
+ let reflection;
610
+ let flowPart = trimmed;
611
+ // Match annotation at end of line: [when ..., max N, per agent-id, tolerance: N, race, wait 30s, reflection]
612
+ // Must start with "when", "max", "per", "tolerance", "race", "wait", "join", "weight", or "reflection" to avoid matching fan-out lists.
613
+ const bracketMatch = trimmed.match(/\[(when\s+.+?|max\s+\d+.*?|per\s+\S+.*?|tolerance\s*:.+?|race.*?|wait\s+\S+.*?|join\s+\S+.*?|weight\s+\S+.*?|reflection.*?)\]\s*$/);
614
+ if (bracketMatch) {
615
+ flowPart = trimmed.slice(0, bracketMatch.index).trim();
616
+ const annotation = bracketMatch[1];
617
+ // V12: Detect wrong attribute order — "max" before "when"
618
+ const maxIdx = annotation.search(/\bmax\s+\d+/);
619
+ const whenIdx = annotation.search(/\bwhen\s+/);
620
+ if (maxIdx !== -1 && whenIdx !== -1 && maxIdx < whenIdx && _edgeAttributeErrors) {
621
+ _edgeAttributeErrors.push({
622
+ rule: "V12",
623
+ level: "error",
624
+ message: `Edge annotation has wrong attribute order: "max" before "when" — expected [when ..., max N] (line: ${trimmed})`,
625
+ });
626
+ }
627
+ const whenMatch = annotation.match(/when\s+(.+?)(?:,\s*(?:max|per|tolerance|race|wait|weight|reflection)|$)/);
628
+ if (whenMatch)
629
+ condition = whenMatch[1].trim();
630
+ const maxMatch = annotation.match(/max\s+(\d+)/);
631
+ if (maxMatch)
632
+ maxIterations = parseInt(maxMatch[1], 10);
633
+ const perMatch = annotation.match(/per\s+(\S+)/);
634
+ if (perMatch)
635
+ per = perMatch[1].replace(/,\s*$/, "");
636
+ // tolerance: N or tolerance: 33%
637
+ const toleranceMatch = annotation.match(/tolerance\s*:\s*(\d+%?)/);
638
+ if (toleranceMatch) {
639
+ const tolVal = toleranceMatch[1];
640
+ tolerance = tolVal.endsWith("%") ? tolVal : parseInt(tolVal, 10);
641
+ }
642
+ // race
643
+ if (/\brace\b/.test(annotation)) {
644
+ race = true;
645
+ }
646
+ // wait 30s
647
+ const waitMatch = annotation.match(/wait\s+(\d+[smhd])/);
648
+ if (waitMatch) {
649
+ wait = waitMatch[1];
650
+ }
651
+ // weight 0.7
652
+ const weightMatch = annotation.match(/weight\s+(\d+(?:\.\d+)?)/);
653
+ if (weightMatch) {
654
+ weight = parseFloat(weightMatch[1]);
655
+ }
656
+ // reflection
657
+ if (/\breflection\b/.test(annotation)) {
658
+ reflection = true;
659
+ }
660
+ }
661
+ // Split the flow part into segments, handling error edge arrows.
662
+ // We need to split on both -> and -x-> / -x[type]->.
663
+ // Strategy: use regex to split on arrow patterns, tracking which type each is.
664
+ const segments = [];
665
+ const arrowPattern = /-x\[([^\]]+)\]->|-x->|->/g;
666
+ let lastIndex = 0;
667
+ let arrowMatch;
668
+ const arrowTypes = [];
669
+ // Reset lastIndex
670
+ arrowPattern.lastIndex = 0;
671
+ while ((arrowMatch = arrowPattern.exec(flowPart)) !== null) {
672
+ const before = flowPart.slice(lastIndex, arrowMatch.index).trim();
673
+ if (before) {
674
+ segments.push({ text: before, isError: false });
675
+ }
676
+ if (arrowMatch[0].startsWith("-x")) {
677
+ // Error edge
678
+ if (arrowMatch[1]) {
679
+ // -x[type]->
680
+ arrowTypes.push({ isError: true, errorType: arrowMatch[1] });
681
+ }
682
+ else {
683
+ // -x->
684
+ arrowTypes.push({ isError: true });
685
+ }
686
+ }
687
+ else {
688
+ // Normal ->
689
+ arrowTypes.push({ isError: false });
690
+ }
691
+ lastIndex = arrowMatch.index + arrowMatch[0].length;
692
+ }
693
+ // Remaining text after last arrow
694
+ const remaining = flowPart.slice(lastIndex).trim();
695
+ if (remaining) {
696
+ segments.push({ text: remaining, isError: false });
697
+ }
698
+ // Now segments[i] and arrowTypes[i] give us: segments[i] --arrowTypes[i]--> segments[i+1]
699
+ for (let i = 0; i < segments.length - 1; i++) {
700
+ const from = segments[i].text;
701
+ const to = segments[i + 1].text;
702
+ const arrow = arrowTypes[i];
703
+ const isLastSegment = i === segments.length - 2;
704
+ // Check for fan-out: [a, b, c]
705
+ const fanOutMatch = to.match(/^\[([^\]]+)\]$/);
706
+ // Build base edge properties
707
+ const baseEdge = {
708
+ condition: isLastSegment ? condition : null,
709
+ maxIterations: isLastSegment ? maxIterations : null,
710
+ per: isLastSegment ? per : null,
711
+ };
712
+ if (arrow.isError) {
713
+ baseEdge.isError = true;
714
+ if (arrow.errorType)
715
+ baseEdge.errorType = arrow.errorType;
716
+ }
717
+ if (isLastSegment && tolerance !== undefined)
718
+ baseEdge.tolerance = tolerance;
719
+ if (isLastSegment && race)
720
+ baseEdge.race = true;
721
+ if (isLastSegment && wait)
722
+ baseEdge.wait = wait;
723
+ if (isLastSegment && weight !== undefined)
724
+ baseEdge.weight = weight;
725
+ if (isLastSegment && reflection)
726
+ baseEdge.reflection = true;
727
+ if (fanOutMatch) {
728
+ const targets = fanOutMatch[1].split(",").map((s) => s.trim());
729
+ for (const target of targets) {
730
+ edges.push({
731
+ from,
732
+ to: target,
733
+ condition: baseEdge.condition ?? null,
734
+ maxIterations: baseEdge.maxIterations ?? null,
735
+ per: baseEdge.per ?? null,
736
+ ...(baseEdge.isError ? { isError: true } : {}),
737
+ ...(baseEdge.errorType ? { errorType: baseEdge.errorType } : {}),
738
+ ...(baseEdge.tolerance !== undefined ? { tolerance: baseEdge.tolerance } : {}),
739
+ ...(baseEdge.race ? { race: true } : {}),
740
+ ...(baseEdge.wait ? { wait: baseEdge.wait } : {}),
741
+ ...(baseEdge.weight !== undefined ? { weight: baseEdge.weight } : {}),
742
+ ...(baseEdge.reflection ? { reflection: true } : {}),
743
+ });
744
+ }
745
+ }
746
+ else {
747
+ edges.push({
748
+ from,
749
+ to,
750
+ condition: baseEdge.condition ?? null,
751
+ maxIterations: baseEdge.maxIterations ?? null,
752
+ per: baseEdge.per ?? null,
753
+ ...(baseEdge.isError ? { isError: true } : {}),
754
+ ...(baseEdge.errorType ? { errorType: baseEdge.errorType } : {}),
755
+ ...(baseEdge.tolerance !== undefined ? { tolerance: baseEdge.tolerance } : {}),
756
+ ...(baseEdge.race ? { race: true } : {}),
757
+ ...(baseEdge.wait ? { wait: baseEdge.wait } : {}),
758
+ ...(baseEdge.weight !== undefined ? { weight: baseEdge.weight } : {}),
759
+ ...(baseEdge.reflection ? { reflection: true } : {}),
760
+ });
761
+ }
762
+ }
763
+ }
764
+ return edges;
765
+ }
766
+ /**
767
+ * Parse the `depth { ... }` block.
768
+ */
769
+ export function parseDepth(body) {
770
+ const factors = parseMultilineList(body, "factors");
771
+ const levels = [];
772
+ const levelRe = /level\s+(\d+)\s+"([^"]+)"\s*\{([^}]*)\}/g;
773
+ let m;
774
+ while ((m = levelRe.exec(body)) !== null) {
775
+ const omit = parseMultilineList(m[3], "omit");
776
+ levels.push({
777
+ level: parseInt(m[1], 10),
778
+ label: m[2],
779
+ omit,
780
+ });
781
+ }
782
+ return { factors, levels };
783
+ }
784
+ /**
785
+ * Parse the `memory { ... }` block.
786
+ *
787
+ * Extracts known sub-blocks (domains, references, external-docs, metrics,
788
+ * workspace) and returns them as a nested record.
789
+ */
790
+ export function parseMemory(body, _unknownSubBlockWarnings) {
791
+ const memory = {};
792
+ const knownSubs = [
793
+ "domains",
794
+ "references",
795
+ "external-docs",
796
+ "metrics",
797
+ "workspace",
798
+ ];
799
+ const knownSubsSet = new Set(knownSubs);
800
+ for (const name of knownSubs) {
801
+ const block = extractBlock(body, name);
802
+ if (!block)
803
+ continue;
804
+ const fields = parseFields(block.body);
805
+ const entry = {};
806
+ for (const [k, v] of Object.entries(fields)) {
807
+ entry[k] = v.startsWith("[") ? parseList(v) : unquote(v);
808
+ }
809
+ // Check for multiline lists
810
+ for (const key of ["blueprints", "files", "structure"]) {
811
+ const list = parseMultilineList(block.body, key);
812
+ if (list.length)
813
+ entry[key] = list;
814
+ }
815
+ memory[name] = entry;
816
+ }
817
+ // Detect unknown sub-blocks
818
+ if (_unknownSubBlockWarnings) {
819
+ const subBlockRe = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
820
+ let m;
821
+ while ((m = subBlockRe.exec(body)) !== null) {
822
+ const name = m[1];
823
+ if (!knownSubsSet.has(name)) {
824
+ _unknownSubBlockWarnings.push({
825
+ rule: "V24",
826
+ level: "warning",
827
+ message: `Unknown sub-block "${name}" in memory block — known sub-blocks are: ${knownSubs.join(", ")}`,
828
+ });
829
+ }
830
+ }
831
+ }
832
+ return memory;
833
+ }
834
+ /**
835
+ * Parse the `batch { ... }` block.
836
+ */
837
+ export function parseBatch(body) {
838
+ const fields = parseFields(body);
839
+ const result = {};
840
+ if (fields.parallel)
841
+ result.parallel = fields.parallel === "true";
842
+ if (fields.per)
843
+ result.per = fields.per;
844
+ if (fields.workspace)
845
+ result.workspace = unquote(fields.workspace);
846
+ // Parse conflicts sub-block
847
+ const conflictsBlock = extractBlock(body, "conflicts");
848
+ if (conflictsBlock) {
849
+ const cFields = parseFields(conflictsBlock.body);
850
+ const detect = parseMultilineList(conflictsBlock.body, "detect");
851
+ result.conflicts = {
852
+ detect,
853
+ resolve: cFields.resolve ?? null,
854
+ };
855
+ }
856
+ return result;
857
+ }
858
+ /**
859
+ * Parse the `environments { ... }` block.
860
+ *
861
+ * Extracts named environment sub-blocks. Handles any environment name,
862
+ * not just "staging" and "production".
863
+ */
864
+ export function parseEnvironments(body) {
865
+ const envs = {};
866
+ // Extract all named sub-blocks generically
867
+ const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
868
+ let m;
869
+ while ((m = re.exec(body)) !== null) {
870
+ const envName = m[1];
871
+ const startIdx = m.index + m[0].length;
872
+ let depth = 1;
873
+ let i = startIdx;
874
+ while (i < body.length && depth > 0) {
875
+ if (body[i] === "{")
876
+ depth++;
877
+ else if (body[i] === "}")
878
+ depth--;
879
+ i++;
880
+ }
881
+ if (depth === 0) {
882
+ const innerBody = body.slice(startIdx, i - 1);
883
+ const fields = parseFields(innerBody);
884
+ const env = {};
885
+ for (const [k, v] of Object.entries(fields)) {
886
+ env[k] = unquote(v);
887
+ }
888
+ envs[envName] = env;
889
+ }
890
+ }
891
+ return envs;
892
+ }
893
+ /**
894
+ * Parse the `triggers { ... }` block.
895
+ */
896
+ export function parseTriggers(body) {
897
+ const triggers = [];
898
+ const cmdBlocks = extractAllBlocks(body, "command");
899
+ for (const block of cmdBlocks) {
900
+ if (!block.id)
901
+ continue;
902
+ const fields = parseFields(block.body);
903
+ const trigger = {
904
+ name: block.id,
905
+ pattern: fields.pattern ? unquote(fields.pattern) : "",
906
+ };
907
+ if (fields.argument)
908
+ trigger.argument = fields.argument;
909
+ triggers.push(trigger);
910
+ }
911
+ return triggers;
912
+ }
913
+ /**
914
+ * Parse the global `hooks { ... }` block.
915
+ */
916
+ export function parseHooks(body) {
917
+ const hooks = [];
918
+ const hookBlocks = extractAllBlocks(body, "hook");
919
+ for (const block of hookBlocks) {
920
+ if (!block.id)
921
+ continue;
922
+ const fields = parseFields(block.body);
923
+ const hook = {
924
+ name: block.id,
925
+ on: fields.on ?? "",
926
+ matcher: fields.matcher ? unquote(fields.matcher) : "",
927
+ run: fields.run ? unquote(fields.run) : "",
928
+ ...(fields.type ? { type: fields.type } : {}),
929
+ ...(fields.timeout
930
+ ? { timeout: parseInt(fields.timeout, 10) }
931
+ : {}),
932
+ };
933
+ // Parse extensions sub-block inside hook
934
+ const extensions = parseExtensionsBlock(block.body);
935
+ if (extensions)
936
+ hook.extensions = extensions;
937
+ hooks.push(hook);
938
+ }
939
+ return hooks;
940
+ }
941
+ /**
942
+ * Parse the `settings { ... }` block.
943
+ */
944
+ export function parseSettings(body) {
945
+ const result = {};
946
+ for (const key of ["allow", "deny", "ask"]) {
947
+ const list = parseMultilineList(body, key);
948
+ result[key] = list;
949
+ }
950
+ // Sandbox setting
951
+ const fields = parseFields(body);
952
+ if (fields.sandbox) {
953
+ const sv = fields.sandbox;
954
+ if (sv === "true")
955
+ result.sandbox = true;
956
+ else if (sv === "false")
957
+ result.sandbox = false;
958
+ else
959
+ result.sandbox = sv;
960
+ }
961
+ // Fallback chain
962
+ const fallbackChain = parseMultilineList(body, "fallback-chain");
963
+ if (fallbackChain.length)
964
+ result.fallbackChain = fallbackChain;
965
+ return result;
966
+ }
967
+ /**
968
+ * Parse the `metering { ... }` block.
969
+ */
970
+ export function parseMetering(body) {
971
+ const fields = parseFields(body);
972
+ const track = parseMultilineList(body, "track");
973
+ const per = parseMultilineList(body, "per");
974
+ if (track.length === 0 && !fields.output)
975
+ return null;
976
+ return {
977
+ track,
978
+ per,
979
+ output: fields.output ? unquote(fields.output) : "",
980
+ format: fields.format ?? "",
981
+ pricing: fields.pricing ?? "",
982
+ };
983
+ }
984
+ /**
985
+ * Parse the `mcp-servers { ... }` block.
986
+ */
987
+ export function parseMcpServers(body) {
988
+ const servers = {};
989
+ const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
990
+ let m;
991
+ while ((m = re.exec(body)) !== null) {
992
+ const name = m[1];
993
+ const startIdx = m.index + m[0].length;
994
+ let depth = 1;
995
+ let i = startIdx;
996
+ while (i < body.length && depth > 0) {
997
+ if (body[i] === "{")
998
+ depth++;
999
+ else if (body[i] === "}")
1000
+ depth--;
1001
+ i++;
1002
+ }
1003
+ if (depth === 0) {
1004
+ const innerBody = body.slice(startIdx, i - 1);
1005
+ const fields = parseFields(innerBody);
1006
+ const server = {};
1007
+ for (const [k, v] of Object.entries(fields)) {
1008
+ server[k] = unquote(v);
1009
+ }
1010
+ const args = parseMultilineList(innerBody, "args");
1011
+ if (args.length)
1012
+ server.args = args;
1013
+ // Parse env sub-block (key-value pairs for environment variables)
1014
+ const envBlock = extractBlock(innerBody, "env");
1015
+ if (envBlock) {
1016
+ const envVars = {};
1017
+ for (const line of envBlock.body.split("\n")) {
1018
+ const kv = parseKV(line);
1019
+ if (kv) {
1020
+ envVars[kv[0]] = unquote(kv[1]);
1021
+ }
1022
+ }
1023
+ if (Object.keys(envVars).length > 0) {
1024
+ server.env = envVars;
1025
+ }
1026
+ }
1027
+ servers[name] = server;
1028
+ }
1029
+ }
1030
+ return servers;
1031
+ }
1032
+ /**
1033
+ * Parse `skill <id> { ... }` blocks.
1034
+ */
1035
+ export function parseSkillBlocks(topBody) {
1036
+ const skillBlocks = extractAllBlocks(topBody, "skill");
1037
+ const skillDefs = [];
1038
+ for (const block of skillBlocks) {
1039
+ if (!block.id)
1040
+ continue;
1041
+ const sFields = parseFields(block.body);
1042
+ const sd = {
1043
+ id: block.id,
1044
+ description: sFields.description ? unquote(sFields.description) : "",
1045
+ };
1046
+ const scripts = parseMultilineList(block.body, "scripts");
1047
+ const domains = parseMultilineList(block.body, "domains");
1048
+ const references = parseMultilineList(block.body, "references");
1049
+ if (scripts.length)
1050
+ sd.scripts = scripts;
1051
+ if (domains.length)
1052
+ sd.domains = domains;
1053
+ if (references.length)
1054
+ sd.references = references;
1055
+ if (sFields.prompt)
1056
+ sd.prompt = unquote(sFields.prompt);
1057
+ // New skill fields
1058
+ if (sFields["disable-model-invocation"]) {
1059
+ sd.disableModelInvocation = sFields["disable-model-invocation"] === "true";
1060
+ }
1061
+ if (sFields["user-invocable"]) {
1062
+ sd.userInvocable = sFields["user-invocable"] === "true";
1063
+ }
1064
+ if (sFields.context)
1065
+ sd.context = sFields.context;
1066
+ if (sFields.agent)
1067
+ sd.agent = sFields.agent;
1068
+ const allowedTools = parseMultilineList(block.body, "allowed-tools");
1069
+ if (allowedTools.length)
1070
+ sd.allowedTools = allowedTools;
1071
+ const skillExtensions = parseExtensionsBlock(block.body);
1072
+ if (skillExtensions)
1073
+ sd.extensions = skillExtensions;
1074
+ skillDefs.push(sd);
1075
+ }
1076
+ return skillDefs;
1077
+ }
1078
+ /**
1079
+ * Parse the `context { ... }` block.
1080
+ *
1081
+ * Returns context file configuration with optional file path and includes list.
1082
+ */
1083
+ export function parseContext(body) {
1084
+ const fields = parseFields(body);
1085
+ const result = {};
1086
+ if (fields.file)
1087
+ result.file = unquote(fields.file);
1088
+ const includes = parseMultilineList(body, "includes");
1089
+ if (includes.length)
1090
+ result.includes = includes;
1091
+ return result;
1092
+ }
1093
+ /**
1094
+ * Parse the `env { ... }` block.
1095
+ *
1096
+ * Returns a record of environment variable key-value pairs. Values may be
1097
+ * plain strings (backward compatible) or {@link SensitiveValue} objects when
1098
+ * the `sensitive` or `secret` modifier is used.
1099
+ */
1100
+ export function parseEnv(body) {
1101
+ const result = {};
1102
+ for (const line of body.split("\n")) {
1103
+ const kv = parseKV(line);
1104
+ if (kv) {
1105
+ const rawValue = kv[1].trim();
1106
+ // Check for `secret "uri"` modifier
1107
+ if (rawValue.startsWith("secret ")) {
1108
+ const uriPart = rawValue.slice("secret ".length).trim();
1109
+ const uri = unquote(uriPart);
1110
+ const schemeMatch = uri.match(/^([a-zA-Z][a-zA-Z0-9]*):\/\//);
1111
+ const scheme = schemeMatch ? schemeMatch[1] : "";
1112
+ const secretRef = { scheme, uri };
1113
+ result[kv[0]] = {
1114
+ value: uri,
1115
+ sensitive: true,
1116
+ secretRef,
1117
+ };
1118
+ }
1119
+ // Check for `encrypted` modifier
1120
+ else if (rawValue.startsWith("encrypted ")) {
1121
+ const innerValue = rawValue.slice("encrypted ".length).trim();
1122
+ const unquoted = unquote(innerValue);
1123
+ const sv = {
1124
+ value: unquoted,
1125
+ sensitive: true,
1126
+ encrypted: true,
1127
+ };
1128
+ // Parse SOPS envelope: ENC[METHOD,data:...]
1129
+ const sopsMatch = unquoted.match(/^ENC\[([A-Z0-9_]+),data:/);
1130
+ if (sopsMatch) {
1131
+ sv.sopsMethod = sopsMatch[1];
1132
+ }
1133
+ result[kv[0]] = sv;
1134
+ }
1135
+ // Check for `sensitive` modifier
1136
+ else if (rawValue.startsWith("sensitive ")) {
1137
+ const innerValue = rawValue.slice("sensitive ".length).trim();
1138
+ result[kv[0]] = {
1139
+ value: unquote(innerValue),
1140
+ sensitive: true,
1141
+ };
1142
+ }
1143
+ // Plain string (backward compatible)
1144
+ else {
1145
+ result[kv[0]] = unquote(rawValue);
1146
+ }
1147
+ }
1148
+ }
1149
+ return result;
1150
+ }
1151
+ /**
1152
+ * Parse the `providers { ... }` block.
1153
+ *
1154
+ * Each named sub-block declares a provider with api-key, base-url, models,
1155
+ * default, and any extra fields.
1156
+ */
1157
+ export function parseProviders(body) {
1158
+ const providers = [];
1159
+ // Extract each named sub-block inside providers
1160
+ const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
1161
+ let m;
1162
+ while ((m = re.exec(body)) !== null) {
1163
+ const name = m[1];
1164
+ const startIdx = m.index + m[0].length;
1165
+ let depth = 1;
1166
+ let i = startIdx;
1167
+ while (i < body.length && depth > 0) {
1168
+ if (body[i] === "{")
1169
+ depth++;
1170
+ else if (body[i] === "}")
1171
+ depth--;
1172
+ i++;
1173
+ }
1174
+ // Advance regex past the closing brace so we don't match sub-blocks
1175
+ re.lastIndex = i;
1176
+ if (depth === 0) {
1177
+ const innerBody = body.slice(startIdx, i - 1);
1178
+ const fields = parseFields(innerBody);
1179
+ // Parse models list
1180
+ const models = parseMultilineList(innerBody, "models");
1181
+ // Collect known fields
1182
+ const knownKeys = new Set(["api-key", "base-url", "models", "default"]);
1183
+ const extra = {};
1184
+ for (const [k, v] of Object.entries(fields)) {
1185
+ if (!knownKeys.has(k)) {
1186
+ if (v === "true")
1187
+ extra[k] = true;
1188
+ else if (v === "false")
1189
+ extra[k] = false;
1190
+ else if (/^\d+$/.test(v))
1191
+ extra[k] = parseInt(v, 10);
1192
+ else
1193
+ extra[k] = unquote(v);
1194
+ }
1195
+ }
1196
+ const provider = {
1197
+ name,
1198
+ models,
1199
+ extra,
1200
+ };
1201
+ if (fields["api-key"])
1202
+ provider.apiKey = unquote(fields["api-key"]);
1203
+ if (fields["base-url"])
1204
+ provider.baseUrl = unquote(fields["base-url"]);
1205
+ if (fields["default"])
1206
+ provider.default = fields["default"] === "true";
1207
+ // Parse auth sub-block
1208
+ const authBlock = extractBlock(innerBody, "auth");
1209
+ if (authBlock) {
1210
+ const aFields = parseFields(authBlock.body);
1211
+ const auth = {
1212
+ type: aFields.type ?? "api-key",
1213
+ };
1214
+ if (aFields.issuer)
1215
+ auth.issuer = unquote(aFields.issuer);
1216
+ if (aFields.audience)
1217
+ auth.audience = unquote(aFields.audience);
1218
+ if (aFields["token-url"])
1219
+ auth.tokenUrl = unquote(aFields["token-url"]);
1220
+ if (aFields["client-id"])
1221
+ auth.clientId = unquote(aFields["client-id"]);
1222
+ const scopes = parseMultilineList(authBlock.body, "scopes");
1223
+ if (scopes.length)
1224
+ auth.scopes = scopes;
1225
+ provider.auth = auth;
1226
+ }
1227
+ providers.push(provider);
1228
+ }
1229
+ }
1230
+ return providers;
1231
+ }
1232
+ /**
1233
+ * Parse the `schedule { ... }` block.
1234
+ *
1235
+ * Extracts `job <id> { ... }` sub-blocks within the schedule section.
1236
+ */
1237
+ export function parseSchedule(body) {
1238
+ const jobs = [];
1239
+ const jobBlocks = extractAllBlocks(body, "job");
1240
+ for (const block of jobBlocks) {
1241
+ if (!block.id)
1242
+ continue;
1243
+ const fields = parseFields(block.body);
1244
+ const job = {
1245
+ id: block.id,
1246
+ enabled: true,
1247
+ };
1248
+ if (fields.cron)
1249
+ job.cron = unquote(fields.cron);
1250
+ if (fields.every)
1251
+ job.every = unquote(fields.every);
1252
+ if (fields.agent)
1253
+ job.agent = fields.agent;
1254
+ if (fields.action)
1255
+ job.action = fields.action;
1256
+ if (fields.enabled)
1257
+ job.enabled = fields.enabled !== "false";
1258
+ jobs.push(job);
1259
+ }
1260
+ return jobs;
1261
+ }
1262
+ /**
1263
+ * Parse the `interfaces { ... }` block.
1264
+ *
1265
+ * Each named sub-block declares an interface with a `type` field and arbitrary
1266
+ * configuration. Follows the same named-sub-block pattern as {@link parseMcpServers}.
1267
+ */
1268
+ export function parseInterfaces(body) {
1269
+ const interfaces = [];
1270
+ const re = /(?:^|\n)\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*\{/gm;
1271
+ let m;
1272
+ while ((m = re.exec(body)) !== null) {
1273
+ const name = m[1];
1274
+ const startIdx = m.index + m[0].length;
1275
+ let depth = 1;
1276
+ let i = startIdx;
1277
+ while (i < body.length && depth > 0) {
1278
+ if (body[i] === "{")
1279
+ depth++;
1280
+ else if (body[i] === "}")
1281
+ depth--;
1282
+ i++;
1283
+ }
1284
+ if (depth === 0) {
1285
+ const innerBody = body.slice(startIdx, i - 1);
1286
+ const fields = parseFields(innerBody);
1287
+ const config = {};
1288
+ for (const [k, v] of Object.entries(fields)) {
1289
+ if (k === "type")
1290
+ continue;
1291
+ if (v === "true")
1292
+ config[k] = true;
1293
+ else if (v === "false")
1294
+ config[k] = false;
1295
+ else if (/^\d+$/.test(v))
1296
+ config[k] = parseInt(v, 10);
1297
+ else
1298
+ config[k] = unquote(v);
1299
+ }
1300
+ const iface = {
1301
+ id: name,
1302
+ config,
1303
+ };
1304
+ if (fields.type)
1305
+ iface.type = fields.type;
1306
+ interfaces.push(iface);
1307
+ }
1308
+ }
1309
+ return interfaces;
1310
+ }
1311
+ /**
1312
+ * Parse the top-level `tools { ... }` block containing `tool <id> { ... }` sub-blocks.
1313
+ */
1314
+ export function parseToolsBlock(topBody) {
1315
+ const toolsBlock = extractBlock(topBody, "tools");
1316
+ const toolDefs = [];
1317
+ if (!toolsBlock)
1318
+ return toolDefs;
1319
+ const toolBlocks = extractAllBlocks(toolsBlock.body, "tool");
1320
+ for (const block of toolBlocks) {
1321
+ if (!block.id)
1322
+ continue;
1323
+ const tFields = parseFields(block.body);
1324
+ const td = {
1325
+ id: block.id,
1326
+ script: tFields.script ? unquote(tFields.script) : "",
1327
+ description: tFields.description ? unquote(tFields.description) : "",
1328
+ };
1329
+ const args = parseMultilineList(block.body, "args");
1330
+ if (args.length)
1331
+ td.args = args;
1332
+ if (tFields.lang)
1333
+ td.lang = tFields.lang;
1334
+ toolDefs.push(td);
1335
+ }
1336
+ return toolDefs;
1337
+ }
1338
+ /**
1339
+ * Parse the `defaults { ... }` block.
1340
+ *
1341
+ * Extracts topology-level default values for sampling parameters and
1342
+ * shared agent configuration.
1343
+ */
1344
+ export function parseDefaults(body) {
1345
+ const fields = parseFields(body);
1346
+ const defaults = {};
1347
+ let hasFields = false;
1348
+ if (fields.temperature) {
1349
+ defaults.temperature = parseFloat(fields.temperature);
1350
+ hasFields = true;
1351
+ }
1352
+ if (fields["max-tokens"]) {
1353
+ defaults.maxTokens = parseInt(fields["max-tokens"], 10);
1354
+ hasFields = true;
1355
+ }
1356
+ if (fields["top-p"]) {
1357
+ defaults.topP = parseFloat(fields["top-p"]);
1358
+ hasFields = true;
1359
+ }
1360
+ if (fields["top-k"]) {
1361
+ defaults.topK = parseInt(fields["top-k"], 10);
1362
+ hasFields = true;
1363
+ }
1364
+ const stop = parseMultilineList(body, "stop");
1365
+ if (stop.length) {
1366
+ defaults.stop = stop;
1367
+ hasFields = true;
1368
+ }
1369
+ if (fields.seed) {
1370
+ defaults.seed = parseInt(fields.seed, 10);
1371
+ hasFields = true;
1372
+ }
1373
+ if (fields.thinking) {
1374
+ defaults.thinking = fields.thinking;
1375
+ hasFields = true;
1376
+ }
1377
+ if (fields["thinking-budget"]) {
1378
+ defaults.thinkingBudget = parseInt(fields["thinking-budget"], 10);
1379
+ hasFields = true;
1380
+ }
1381
+ if (fields["output-format"]) {
1382
+ defaults.outputFormat = fields["output-format"];
1383
+ hasFields = true;
1384
+ }
1385
+ if (fields.timeout) {
1386
+ defaults.timeout = fields.timeout;
1387
+ hasFields = true;
1388
+ }
1389
+ if (fields["log-level"]) {
1390
+ defaults.logLevel = fields["log-level"];
1391
+ hasFields = true;
1392
+ }
1393
+ return hasFields ? defaults : null;
1394
+ }
1395
+ /**
1396
+ * Parse the `observability { ... }` block.
1397
+ *
1398
+ * Extracts simple KV fields plus `capture { ... }` and `spans { ... }`
1399
+ * sub-blocks. Returns an {@link ObservabilityDef} with defaults applied
1400
+ * for any omitted fields.
1401
+ */
1402
+ export function parseObservability(body) {
1403
+ const fields = parseFields(body);
1404
+ // Parse simple KV fields with defaults
1405
+ const enabled = fields.enabled !== undefined ? fields.enabled === "true" : true;
1406
+ const level = fields.level ?? "info";
1407
+ const exporter = fields.exporter ?? "otlp";
1408
+ const endpoint = fields.endpoint ? unquote(fields.endpoint) : undefined;
1409
+ const service = fields.service ? unquote(fields.service) : undefined;
1410
+ const sampleRate = fields["sample-rate"] !== undefined
1411
+ ? parseFloat(fields["sample-rate"])
1412
+ : 1.0;
1413
+ // Parse capture sub-block with defaults (all false)
1414
+ const captureBlock = extractBlock(body, "capture");
1415
+ const captureFields = captureBlock ? parseFields(captureBlock.body) : {};
1416
+ const capture = {
1417
+ prompts: captureFields.prompts === "true",
1418
+ completions: captureFields.completions === "true",
1419
+ toolArgs: (captureFields["tool-args"] ?? captureFields.toolArgs) === "true",
1420
+ toolResults: (captureFields["tool-results"] ?? captureFields.toolResults) === "true",
1421
+ };
1422
+ // Parse spans sub-block with defaults (agents/tools/gates: true, memory: false)
1423
+ const spansBlock = extractBlock(body, "spans");
1424
+ const spansFields = spansBlock ? parseFields(spansBlock.body) : {};
1425
+ const spans = {
1426
+ agents: spansFields.agents !== undefined ? spansFields.agents === "true" : true,
1427
+ tools: spansFields.tools !== undefined ? spansFields.tools === "true" : true,
1428
+ gates: spansFields.gates !== undefined ? spansFields.gates === "true" : true,
1429
+ memory: spansFields.memory !== undefined ? spansFields.memory === "true" : false,
1430
+ };
1431
+ const def = {
1432
+ enabled,
1433
+ level,
1434
+ exporter,
1435
+ sampleRate,
1436
+ capture,
1437
+ spans,
1438
+ };
1439
+ if (endpoint !== undefined)
1440
+ def.endpoint = endpoint;
1441
+ if (service !== undefined)
1442
+ def.service = service;
1443
+ return def;
1444
+ }
1445
+ /**
1446
+ * Parse the `checkpoint { ... }` block.
1447
+ *
1448
+ * Extracts KV fields plus an optional `replay { ... }` sub-block.
1449
+ * Returns a {@link CheckpointDef} with parsed connection supporting
1450
+ * the `secret "uri"` modifier.
1451
+ */
1452
+ export function parseCheckpoint(body) {
1453
+ const fields = parseFields(body);
1454
+ const def = {
1455
+ backend: fields.backend ?? "memory",
1456
+ strategy: fields.strategy ?? "every-node",
1457
+ };
1458
+ // Connection: may use `secret "uri"` modifier
1459
+ if (fields.connection) {
1460
+ const rawConn = fields.connection.trim();
1461
+ if (rawConn.startsWith("secret ")) {
1462
+ const uri = unquote(rawConn.slice("secret ".length).trim());
1463
+ def.connection = uri;
1464
+ }
1465
+ else {
1466
+ def.connection = unquote(rawConn);
1467
+ }
1468
+ }
1469
+ if (fields.ttl)
1470
+ def.ttl = fields.ttl;
1471
+ // Parse replay sub-block
1472
+ const replayBlock = extractBlock(body, "replay");
1473
+ if (replayBlock) {
1474
+ const rFields = parseFields(replayBlock.body);
1475
+ const replay = {
1476
+ enabled: rFields.enabled === "true",
1477
+ };
1478
+ if (rFields["max-history"]) {
1479
+ replay.maxHistory = parseInt(rFields["max-history"], 10);
1480
+ }
1481
+ if (rFields.branch) {
1482
+ replay.branch = rFields.branch === "true";
1483
+ }
1484
+ def.replay = replay;
1485
+ }
1486
+ return def;
1487
+ }
1488
+ /**
1489
+ * Parse the top-level `artifacts { artifact <id> { ... } ... }` block.
1490
+ *
1491
+ * Returns an array of artifact definitions with their dependencies.
1492
+ */
1493
+ export function parseArtifacts(topBody) {
1494
+ const artifactsBlock = extractBlock(topBody, "artifacts");
1495
+ if (!artifactsBlock)
1496
+ return [];
1497
+ const artifactBlocks = extractAllBlocks(artifactsBlock.body, "artifact");
1498
+ const artifacts = [];
1499
+ for (const block of artifactBlocks) {
1500
+ if (!block.id)
1501
+ continue;
1502
+ const fields = parseFields(block.body);
1503
+ const artifact = {
1504
+ id: block.id,
1505
+ type: fields.type ?? "unknown",
1506
+ };
1507
+ if (fields.path)
1508
+ artifact.path = unquote(fields.path);
1509
+ if (fields.retention)
1510
+ artifact.retention = fields.retention;
1511
+ const dependsOn = parseMultilineList(block.body, "depends-on");
1512
+ if (dependsOn.length)
1513
+ artifact.dependsOn = dependsOn;
1514
+ artifacts.push(artifact);
1515
+ }
1516
+ return artifacts;
1517
+ }
1518
+ // ---------------------------------------------------------------------------
1519
+ // Main parser
1520
+ // ---------------------------------------------------------------------------
1521
+ /**
1522
+ * Parse an AgenTopology `.at` source string into a complete {@link TopologyAST}.
1523
+ *
1524
+ * @param source - The raw `.at` file content.
1525
+ * @returns The fully-parsed AST.
1526
+ * @throws If the topology header or body cannot be extracted.
1527
+ */
1528
+ /**
1529
+ * Build a source map that records the line number (1-based) where each named
1530
+ * block starts in the raw source. Scans for `agent <id> {`, `gate <id> {`,
1531
+ * `action <id> {`, and `orchestrator {` patterns.
1532
+ */
1533
+ function buildSourceMap(rawSource) {
1534
+ const sourceMap = {};
1535
+ const lines = rawSource.split("\n");
1536
+ for (let i = 0; i < lines.length; i++) {
1537
+ const line = lines[i];
1538
+ // Match: agent <id> {, action <id> {, gate <id> {, human <id> {
1539
+ const namedMatch = line.match(/^\s*(?:agent|action|gate|human|group)\s+([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:\{|[^{]*\{)/);
1540
+ if (namedMatch) {
1541
+ sourceMap[namedMatch[1]] = i + 1; // 1-based line number
1542
+ continue;
1543
+ }
1544
+ // Match: orchestrator {
1545
+ const orchMatch = line.match(/^\s*orchestrator\s*(?:\{|[^{]*\{)/);
1546
+ if (orchMatch) {
1547
+ sourceMap["orchestrator"] = i + 1;
1548
+ }
1549
+ }
1550
+ return sourceMap;
1551
+ }
1552
+ // ---------------------------------------------------------------------------
1553
+ // Composition parsing: params, interface, import, include, fragment
1554
+ // ---------------------------------------------------------------------------
1555
+ /**
1556
+ * Parse a `params { ... }` block into typed parameter definitions.
1557
+ *
1558
+ * Each line has the form `name: type` or `name: type = default`.
1559
+ * If a default is provided, the parameter is optional; otherwise required.
1560
+ */
1561
+ export function parseParams(body) {
1562
+ const params = [];
1563
+ const VALID_TYPES = new Set(["string", "number", "boolean"]);
1564
+ for (const line of body.split("\n")) {
1565
+ const trimmed = line.trim();
1566
+ if (!trimmed)
1567
+ continue;
1568
+ // Match: name: type = default OR name: type
1569
+ const m = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(string|number|boolean)(?:\s*=\s*(.+))?$/);
1570
+ if (!m)
1571
+ continue;
1572
+ const [, name, type, rawDefault] = m;
1573
+ const paramType = type;
1574
+ let defaultValue;
1575
+ let required = true;
1576
+ if (rawDefault !== undefined) {
1577
+ required = false;
1578
+ const dv = rawDefault.trim();
1579
+ if (paramType === "number") {
1580
+ defaultValue = parseFloat(dv);
1581
+ }
1582
+ else if (paramType === "boolean") {
1583
+ defaultValue = dv === "true";
1584
+ }
1585
+ else {
1586
+ // string — strip quotes if present
1587
+ defaultValue = unquote(dv);
1588
+ }
1589
+ }
1590
+ params.push({ name, type: paramType, required, ...(defaultValue !== undefined ? { default: defaultValue } : {}) });
1591
+ }
1592
+ return params;
1593
+ }
1594
+ /**
1595
+ * Parse an `interface { entry: <id> exit: <id> }` block body.
1596
+ */
1597
+ export function parseInterfaceEndpoints(body) {
1598
+ const fields = parseFields(body);
1599
+ const entry = fields["entry"];
1600
+ const exit = fields["exit"];
1601
+ if (!entry || !exit)
1602
+ return null;
1603
+ return { entry: unquote(entry), exit: unquote(exit) };
1604
+ }
1605
+ /**
1606
+ * Extract and parse the `interface { ... }` block from a topology body,
1607
+ * carefully avoiding collision with `interfaces { ... }`.
1608
+ */
1609
+ function parseInterfaceBlock(topBody) {
1610
+ // Match "interface" followed by whitespace+"{" but NOT "interfaces"
1611
+ const re = /(?:^|\n)\s*interface\s*\{/m;
1612
+ const m = re.exec(topBody);
1613
+ if (!m)
1614
+ return null;
1615
+ // Make sure we did not match "interfaces" — check what precedes the `{`
1616
+ const matchText = m[0];
1617
+ if (/interfaces/.test(matchText))
1618
+ return null;
1619
+ // Find the opening brace and count braces to extract the body
1620
+ const braceIdx = m.index + m[0].length;
1621
+ let depth = 1;
1622
+ let i = braceIdx;
1623
+ while (i < topBody.length && depth > 0) {
1624
+ if (topBody[i] === "{")
1625
+ depth++;
1626
+ else if (topBody[i] === "}")
1627
+ depth--;
1628
+ i++;
1629
+ }
1630
+ if (depth !== 0)
1631
+ return null;
1632
+ const body = topBody.slice(braceIdx, i - 1);
1633
+ return parseInterfaceEndpoints(body);
1634
+ }
1635
+ /**
1636
+ * Parse import statements from a topology body.
1637
+ *
1638
+ * Syntax:
1639
+ * import "./path.at" as alias
1640
+ * import "./path.at" as alias with { key: value ... }
1641
+ *
1642
+ * Import statements are parsed from the raw body text since they are
1643
+ * not brace-delimited blocks in the usual sense.
1644
+ */
1645
+ export function parseImports(body) {
1646
+ const imports = [];
1647
+ // Regex: import "source" as alias (captures everything after for optional with block)
1648
+ const importRe = /(?:^|\n)\s*import\s+"([^"]+)"\s+as\s+([a-zA-Z][a-zA-Z0-9_-]*)/g;
1649
+ let m;
1650
+ while ((m = importRe.exec(body)) !== null) {
1651
+ const source = m[1];
1652
+ const alias = m[2];
1653
+ const params = {};
1654
+ // Check for `with { ... }` block following the import
1655
+ const afterImport = body.slice(m.index + m[0].length);
1656
+ const withMatch = afterImport.match(/^\s*with\s*\{([^}]*)\}/);
1657
+ if (withMatch) {
1658
+ const withBody = withMatch[1];
1659
+ for (const line of withBody.split("\n")) {
1660
+ const kv = parseKV(line);
1661
+ if (!kv)
1662
+ continue;
1663
+ const [key, rawVal] = kv;
1664
+ const val = unquote(rawVal);
1665
+ // Try number
1666
+ if (/^-?\d+(\.\d+)?$/.test(val)) {
1667
+ params[key] = parseFloat(val);
1668
+ }
1669
+ else if (val === "true" || val === "false") {
1670
+ params[key] = val === "true";
1671
+ }
1672
+ else {
1673
+ params[key] = val;
1674
+ }
1675
+ }
1676
+ }
1677
+ const importDef = { source, alias, params };
1678
+ // Detect registry imports (not starting with ./ or ../)
1679
+ const isRegistry = !source.startsWith("./") && !source.startsWith("../");
1680
+ if (isRegistry) {
1681
+ importDef.registry = true;
1682
+ // Parse package@version format
1683
+ const atIdx = source.lastIndexOf("@");
1684
+ if (atIdx > 0) {
1685
+ importDef.registryPackage = source.slice(0, atIdx);
1686
+ importDef.registryVersion = source.slice(atIdx + 1);
1687
+ }
1688
+ else {
1689
+ importDef.registryPackage = source;
1690
+ }
1691
+ }
1692
+ // Check for sha256 after the with block (or after alias if no with block)
1693
+ const afterWithOrAlias = withMatch
1694
+ ? afterImport.slice(withMatch[0].length)
1695
+ : afterImport;
1696
+ const sha256Match = afterWithOrAlias.match(/^\s*sha256\s*:\s*"([^"]+)"/);
1697
+ if (sha256Match) {
1698
+ importDef.sha256 = sha256Match[1];
1699
+ }
1700
+ imports.push(importDef);
1701
+ }
1702
+ return imports;
1703
+ }
1704
+ /**
1705
+ * Parse include statements from a topology body.
1706
+ *
1707
+ * Syntax: include "./path.at"
1708
+ */
1709
+ export function parseIncludes(body) {
1710
+ const includes = [];
1711
+ const re = /(?:^|\n)\s*include\s+"([^"]+)"/g;
1712
+ let m;
1713
+ while ((m = re.exec(body)) !== null) {
1714
+ includes.push({ source: m[1] });
1715
+ }
1716
+ return includes;
1717
+ }
1718
+ export function parse(source) {
1719
+ const src = stripComments(source);
1720
+ // --- Source map (line numbers for nodes) ---
1721
+ const sourceMap = buildSourceMap(source);
1722
+ // --- Detect fragment vs topology root ---
1723
+ const isFragment = /(?:^|\n)\s*fragment\s+\S+\s*\{/.test(source);
1724
+ // --- Topology header ---
1725
+ let header;
1726
+ if (isFragment) {
1727
+ // Fragment header: fragment <name> { ... }
1728
+ const fm = source.match(/fragment\s+(\S+)\s*\{/);
1729
+ if (!fm)
1730
+ throw new Error("Could not find fragment header");
1731
+ header = { name: fm[1], patterns: [] };
1732
+ }
1733
+ else {
1734
+ // Use raw source to preserve the topology line (comments already on other lines)
1735
+ header = parseTopologyHeader(source);
1736
+ }
1737
+ // --- Extract the full topology/fragment body ---
1738
+ const topoBlock = extractBlock(source, isFragment ? "fragment" : "topology");
1739
+ if (!topoBlock) {
1740
+ throw new Error(isFragment ? "Could not extract fragment body" : "Could not extract topology body");
1741
+ }
1742
+ const topBody = stripComments(topoBlock.body);
1743
+ // --- Meta ---
1744
+ const metaBlock = extractBlock(topBody, "meta");
1745
+ const metaFields = metaBlock ? parseMeta(metaBlock.body) : {};
1746
+ const topology = {
1747
+ name: header.name,
1748
+ version: metaFields.version ?? "0.0.0",
1749
+ description: metaFields.description ?? "",
1750
+ patterns: header.patterns,
1751
+ ...(metaFields.foundations ? { foundations: metaFields.foundations } : {}),
1752
+ ...(metaFields.advanced ? { advanced: metaFields.advanced } : {}),
1753
+ ...(metaFields.domain ? { domain: metaFields.domain } : {}),
1754
+ ...(metaFields.timeout ? { timeout: metaFields.timeout } : {}),
1755
+ ...(metaFields.errorHandler ? { errorHandler: metaFields.errorHandler } : {}),
1756
+ ...(metaFields.durable !== undefined ? { durable: metaFields.durable } : {}),
1757
+ };
1758
+ // --- Roles ---
1759
+ const rolesBlock = extractBlock(topBody, "roles");
1760
+ const roles = rolesBlock ? parseRoles(rolesBlock.body) : {};
1761
+ // --- Nodes ---
1762
+ const nodes = [];
1763
+ // Orchestrator
1764
+ const orchBlock = extractBlock(topBody, "orchestrator");
1765
+ if (orchBlock) {
1766
+ nodes.push(parseOrchestrator(orchBlock.body));
1767
+ }
1768
+ // Actions
1769
+ const actionBlocks = extractAllBlocks(topBody, "action");
1770
+ for (const block of actionBlocks) {
1771
+ if (block.id) {
1772
+ nodes.push(parseAction(block.id, block.body));
1773
+ }
1774
+ }
1775
+ // Agents
1776
+ const agentBlocks = extractAllBlocks(topBody, "agent");
1777
+ for (const block of agentBlocks) {
1778
+ if (block.id) {
1779
+ nodes.push(parseAgent(block.id, block.body, roles));
1780
+ }
1781
+ }
1782
+ // Gates
1783
+ const gatesBlock = extractBlock(topBody, "gates");
1784
+ if (gatesBlock) {
1785
+ const gateBlocks = extractAllBlocks(gatesBlock.body, "gate");
1786
+ for (const block of gateBlocks) {
1787
+ if (block.id) {
1788
+ nodes.push(parseGate(block.id, block.body));
1789
+ }
1790
+ }
1791
+ }
1792
+ // Human nodes
1793
+ const humanBlocks = extractAllBlocks(topBody, "human");
1794
+ for (const block of humanBlocks) {
1795
+ if (block.id) {
1796
+ nodes.push(parseHuman(block.id, block.body));
1797
+ }
1798
+ }
1799
+ // Group chat nodes
1800
+ const groupBlocks = extractAllBlocks(topBody, "group");
1801
+ for (const block of groupBlocks) {
1802
+ if (block.id) {
1803
+ nodes.push(parseGroup(block.id, block.body));
1804
+ }
1805
+ }
1806
+ // --- Skills ---
1807
+ const skillDefs = parseSkillBlocks(topBody);
1808
+ // --- Tools ---
1809
+ const toolDefs = parseToolsBlock(topBody);
1810
+ // --- Edges (flow) ---
1811
+ const flowBlock = extractBlock(topBody, "flow");
1812
+ const edgeAttributeErrors = [];
1813
+ const edges = flowBlock ? parseFlow(flowBlock.body, edgeAttributeErrors) : [];
1814
+ // --- Depth ---
1815
+ const depthBlock = extractBlock(topBody, "depth");
1816
+ const depth = depthBlock
1817
+ ? parseDepth(depthBlock.body)
1818
+ : { factors: [], levels: [] };
1819
+ // --- Memory ---
1820
+ const memoryBlock = extractBlock(topBody, "memory");
1821
+ const unknownMemorySubBlockWarnings = [];
1822
+ const memory = memoryBlock ? parseMemory(memoryBlock.body, unknownMemorySubBlockWarnings) : {};
1823
+ // --- Batch ---
1824
+ const batchBlock = extractBlock(topBody, "batch");
1825
+ const batch = batchBlock ? parseBatch(batchBlock.body) : {};
1826
+ // --- Environments ---
1827
+ const envsBlock = extractBlock(topBody, "environments");
1828
+ const environments = envsBlock
1829
+ ? parseEnvironments(envsBlock.body)
1830
+ : {};
1831
+ // --- Triggers ---
1832
+ const triggersBlock = extractBlock(topBody, "triggers");
1833
+ const triggers = triggersBlock
1834
+ ? parseTriggers(triggersBlock.body)
1835
+ : [];
1836
+ // --- Hooks (global) ---
1837
+ // Collect per-agent hook names to avoid double-counting them as global.
1838
+ const perAgentHookNames = new Set();
1839
+ for (const n of nodes) {
1840
+ if (n.type === "agent" && n.hooks) {
1841
+ for (const h of n.hooks) {
1842
+ perAgentHookNames.add(h.name);
1843
+ }
1844
+ }
1845
+ }
1846
+ const allHooksBlocks = extractAllBlocks(topBody, "hooks");
1847
+ let hooks = [];
1848
+ for (const hb of allHooksBlocks) {
1849
+ const parsed = parseHooks(hb.body);
1850
+ const globalOnly = parsed.filter((h) => !perAgentHookNames.has(h.name));
1851
+ if (globalOnly.length > 0) {
1852
+ hooks = globalOnly;
1853
+ break;
1854
+ }
1855
+ }
1856
+ if (hooks.length === 0 && allHooksBlocks.length > 1) {
1857
+ const lastBlock = allHooksBlocks[allHooksBlocks.length - 1];
1858
+ hooks = parseHooks(lastBlock.body);
1859
+ }
1860
+ // --- Settings ---
1861
+ const settingsBlock = extractBlock(topBody, "settings");
1862
+ const settings = settingsBlock
1863
+ ? parseSettings(settingsBlock.body)
1864
+ : {};
1865
+ // --- MCP Servers ---
1866
+ const mcpBlock = extractBlock(topBody, "mcp-servers");
1867
+ const mcpServers = mcpBlock ? parseMcpServers(mcpBlock.body) : {};
1868
+ // --- Metering ---
1869
+ const meteringBlock = extractBlock(topBody, "metering");
1870
+ const metering = meteringBlock
1871
+ ? parseMetering(meteringBlock.body)
1872
+ : null;
1873
+ // --- Context ---
1874
+ const contextBlock = extractBlock(topBody, "context");
1875
+ const context = contextBlock ? parseContext(contextBlock.body) : {};
1876
+ // --- Env ---
1877
+ const envBlock = extractBlock(topBody, "env");
1878
+ const env = envBlock ? parseEnv(envBlock.body) : {};
1879
+ // --- Providers ---
1880
+ const providersBlock = extractBlock(topBody, "providers");
1881
+ const providers = providersBlock ? parseProviders(providersBlock.body) : [];
1882
+ // --- Schedule ---
1883
+ const scheduleBlock = extractBlock(topBody, "schedule");
1884
+ const schedules = scheduleBlock ? parseSchedule(scheduleBlock.body) : [];
1885
+ // --- Interfaces ---
1886
+ const interfacesBlock = extractBlock(topBody, "interfaces");
1887
+ const interfaces = interfacesBlock ? parseInterfaces(interfacesBlock.body) : [];
1888
+ // --- Defaults ---
1889
+ const defaultsBlock = extractBlock(topBody, "defaults");
1890
+ const defaults = defaultsBlock ? parseDefaults(defaultsBlock.body) : null;
1891
+ // --- Schemas ---
1892
+ const schemas = parseSchemas(topBody);
1893
+ // --- Observability ---
1894
+ const observabilityBlock = extractBlock(topBody, "observability");
1895
+ const observability = observabilityBlock ? parseObservability(observabilityBlock.body) : null;
1896
+ // --- Checkpoint ---
1897
+ const checkpointBlock = extractBlock(topBody, "checkpoint");
1898
+ const checkpoint = checkpointBlock ? parseCheckpoint(checkpointBlock.body) : null;
1899
+ // --- Artifacts ---
1900
+ const artifacts = parseArtifacts(topBody);
1901
+ // --- Top-level Extensions ---
1902
+ const topLevelExtensions = parseExtensionsBlock(topBody);
1903
+ // --- Params ---
1904
+ const paramsBlock = extractBlock(topBody, "params");
1905
+ const params = paramsBlock ? parseParams(paramsBlock.body) : [];
1906
+ // --- Interface (entry/exit endpoints) ---
1907
+ // Note: "interface" block is distinct from "interfaces" (external interface definitions).
1908
+ // We use a targeted regex to avoid matching "interfaces" when looking for "interface".
1909
+ const interfaceEndpoints = parseInterfaceBlock(topBody);
1910
+ // --- Imports ---
1911
+ const imports = parseImports(topBody);
1912
+ // --- Includes ---
1913
+ const includes = parseIncludes(topBody);
1914
+ // --- Duplicate section detection ---
1915
+ const singletonKeywords = [
1916
+ "meta", "orchestrator", "flow", "memory", "batch", "environments",
1917
+ "triggers", "settings", "mcp-servers", "metering", "context", "env",
1918
+ "providers", "schedule", "interfaces", "depth", "gates", "roles", "tools",
1919
+ "defaults", "schemas", "observability", "params", "interface",
1920
+ "checkpoint", "artifacts",
1921
+ ];
1922
+ const duplicateSectionWarnings = [];
1923
+ for (const keyword of singletonKeywords) {
1924
+ const allBlocks = extractAllBlocks(topBody, keyword);
1925
+ if (allBlocks.length > 1) {
1926
+ duplicateSectionWarnings.push({
1927
+ rule: "V23",
1928
+ level: "warning",
1929
+ message: `Duplicate "${keyword}" block detected (found ${allBlocks.length}) — only the first occurrence is used`,
1930
+ });
1931
+ }
1932
+ }
1933
+ // --- Assemble AST ---
1934
+ const ast = {
1935
+ topology,
1936
+ nodes,
1937
+ edges,
1938
+ depth,
1939
+ memory,
1940
+ batch,
1941
+ environments,
1942
+ triggers,
1943
+ hooks,
1944
+ settings,
1945
+ mcpServers,
1946
+ metering,
1947
+ skills: skillDefs,
1948
+ toolDefs,
1949
+ roles,
1950
+ context,
1951
+ env,
1952
+ providers,
1953
+ schedules,
1954
+ interfaces,
1955
+ defaults,
1956
+ schemas,
1957
+ ...(topLevelExtensions ? { extensions: topLevelExtensions } : {}),
1958
+ observability,
1959
+ params,
1960
+ interfaceEndpoints,
1961
+ imports,
1962
+ includes,
1963
+ checkpoint,
1964
+ artifacts,
1965
+ ...(isFragment ? { isFragment: true } : {}),
1966
+ };
1967
+ // Attach V12 parse-time errors for the validator to consume.
1968
+ if (edgeAttributeErrors.length > 0) {
1969
+ ast._edgeAttributeErrors = edgeAttributeErrors;
1970
+ }
1971
+ // Attach V23 duplicate section warnings for the validator to consume.
1972
+ if (duplicateSectionWarnings.length > 0) {
1973
+ ast._duplicateSectionWarnings = duplicateSectionWarnings;
1974
+ }
1975
+ // Attach V24 unknown memory sub-block warnings for the validator to consume.
1976
+ if (unknownMemorySubBlockWarnings.length > 0) {
1977
+ ast._unknownMemorySubBlockWarnings = unknownMemorySubBlockWarnings;
1978
+ }
1979
+ // Attach source map for line number tracking in validation results.
1980
+ if (Object.keys(sourceMap).length > 0) {
1981
+ ast._sourceMap = sourceMap;
1982
+ }
1983
+ return ast;
1984
+ }
1985
+ //# sourceMappingURL=index.js.map