agentopology 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +53 -5
  2. package/dist/analyzer/__tests__/analyzer.test.js +36 -9
  3. package/dist/analyzer/__tests__/analyzer.test.js.map +1 -1
  4. package/dist/analyzer/__tests__/observability-gap.test.js +40 -10
  5. package/dist/analyzer/__tests__/observability-gap.test.js.map +1 -1
  6. package/dist/analyzer/index.d.ts +2 -1
  7. package/dist/analyzer/index.d.ts.map +1 -1
  8. package/dist/analyzer/index.js +1 -1
  9. package/dist/analyzer/index.js.map +1 -1
  10. package/dist/bindings/__tests__/bindings.test.js +20 -5
  11. package/dist/bindings/__tests__/bindings.test.js.map +1 -1
  12. package/dist/bindings/__tests__/claude-workflow.test.js +12 -3
  13. package/dist/bindings/__tests__/claude-workflow.test.js.map +1 -1
  14. package/dist/bindings/__tests__/defaults-agreement.test.d.ts +16 -0
  15. package/dist/bindings/__tests__/defaults-agreement.test.d.ts.map +1 -0
  16. package/dist/bindings/__tests__/defaults-agreement.test.js +80 -0
  17. package/dist/bindings/__tests__/defaults-agreement.test.js.map +1 -0
  18. package/dist/bindings/__tests__/observability.test.js +8 -2
  19. package/dist/bindings/__tests__/observability.test.js.map +1 -1
  20. package/dist/bindings/__tests__/stub-generation.test.js +8 -2
  21. package/dist/bindings/__tests__/stub-generation.test.js.map +1 -1
  22. package/dist/bindings/claude-code.d.ts.map +1 -1
  23. package/dist/bindings/claude-code.js +2 -1
  24. package/dist/bindings/claude-code.js.map +1 -1
  25. package/dist/bindings/claude-workflow.d.ts.map +1 -1
  26. package/dist/bindings/claude-workflow.js +2 -1
  27. package/dist/bindings/claude-workflow.js.map +1 -1
  28. package/dist/bindings/codex.d.ts.map +1 -1
  29. package/dist/bindings/codex.js +6 -2
  30. package/dist/bindings/codex.js.map +1 -1
  31. package/dist/bindings/gemini-cli.d.ts.map +1 -1
  32. package/dist/bindings/gemini-cli.js +2 -1
  33. package/dist/bindings/gemini-cli.js.map +1 -1
  34. package/dist/bindings/kiro.d.ts.map +1 -1
  35. package/dist/bindings/kiro.js +2 -1
  36. package/dist/bindings/kiro.js.map +1 -1
  37. package/dist/bindings/openclaw.d.ts.map +1 -1
  38. package/dist/bindings/openclaw.js +2 -1
  39. package/dist/bindings/openclaw.js.map +1 -1
  40. package/dist/cli/__tests__/cli.test.d.ts +12 -0
  41. package/dist/cli/__tests__/cli.test.d.ts.map +1 -0
  42. package/dist/cli/__tests__/cli.test.js +171 -0
  43. package/dist/cli/__tests__/cli.test.js.map +1 -0
  44. package/dist/cli/index.d.ts +1 -0
  45. package/dist/cli/index.d.ts.map +1 -1
  46. package/dist/cli/index.js +149 -3
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/docs/content.d.ts.map +1 -1
  49. package/dist/docs/content.js +112 -0
  50. package/dist/docs/content.js.map +1 -1
  51. package/dist/exporters/ascii.d.ts +13 -0
  52. package/dist/exporters/ascii.d.ts.map +1 -0
  53. package/dist/exporters/ascii.js +26 -0
  54. package/dist/exporters/ascii.js.map +1 -0
  55. package/dist/exporters/brief.d.ts +14 -0
  56. package/dist/exporters/brief.d.ts.map +1 -0
  57. package/dist/exporters/brief.js +37 -0
  58. package/dist/exporters/brief.js.map +1 -0
  59. package/dist/exporters/index.d.ts +2 -0
  60. package/dist/exporters/index.d.ts.map +1 -1
  61. package/dist/exporters/index.js +6 -0
  62. package/dist/exporters/index.js.map +1 -1
  63. package/dist/exporters/markdown.d.ts.map +1 -1
  64. package/dist/exporters/markdown.js +2 -1
  65. package/dist/exporters/markdown.js.map +1 -1
  66. package/dist/exporters/mermaid.d.ts.map +1 -1
  67. package/dist/exporters/mermaid.js +2 -1
  68. package/dist/exporters/mermaid.js.map +1 -1
  69. package/dist/import/__tests__/import.test.js +8 -2
  70. package/dist/import/__tests__/import.test.js.map +1 -1
  71. package/dist/parser/__tests__/parser.test.js +1386 -210
  72. package/dist/parser/__tests__/parser.test.js.map +1 -1
  73. package/dist/parser/__tests__/spec-sync.test.d.ts +10 -0
  74. package/dist/parser/__tests__/spec-sync.test.d.ts.map +1 -0
  75. package/dist/parser/__tests__/spec-sync.test.js +60 -0
  76. package/dist/parser/__tests__/spec-sync.test.js.map +1 -0
  77. package/dist/parser/index.d.ts +50 -6
  78. package/dist/parser/index.d.ts.map +1 -1
  79. package/dist/parser/index.js +183 -28
  80. package/dist/parser/index.js.map +1 -1
  81. package/dist/parser/validator.d.ts +7 -1
  82. package/dist/parser/validator.d.ts.map +1 -1
  83. package/dist/parser/validator.js +77 -2
  84. package/dist/parser/validator.js.map +1 -1
  85. package/dist/plan/__tests__/brief.test.d.ts +2 -0
  86. package/dist/plan/__tests__/brief.test.d.ts.map +1 -0
  87. package/dist/plan/__tests__/brief.test.js +442 -0
  88. package/dist/plan/__tests__/brief.test.js.map +1 -0
  89. package/dist/plan/brief.d.ts +207 -0
  90. package/dist/plan/brief.d.ts.map +1 -0
  91. package/dist/plan/brief.js +342 -0
  92. package/dist/plan/brief.js.map +1 -0
  93. package/dist/plan/render.d.ts +20 -0
  94. package/dist/plan/render.d.ts.map +1 -0
  95. package/dist/plan/render.js +425 -0
  96. package/dist/plan/render.js.map +1 -0
  97. package/dist/render/__tests__/ascii.test.d.ts +2 -0
  98. package/dist/render/__tests__/ascii.test.d.ts.map +1 -0
  99. package/dist/render/__tests__/ascii.test.js +187 -0
  100. package/dist/render/__tests__/ascii.test.js.map +1 -0
  101. package/dist/render/ascii.d.ts +23 -0
  102. package/dist/render/ascii.d.ts.map +1 -0
  103. package/dist/render/ascii.js +187 -0
  104. package/dist/render/ascii.js.map +1 -0
  105. package/dist/resolve/__tests__/defaults.test.d.ts +2 -0
  106. package/dist/resolve/__tests__/defaults.test.d.ts.map +1 -0
  107. package/dist/resolve/__tests__/defaults.test.js +109 -0
  108. package/dist/resolve/__tests__/defaults.test.js.map +1 -0
  109. package/dist/resolve/__tests__/order.test.d.ts +2 -0
  110. package/dist/resolve/__tests__/order.test.d.ts.map +1 -0
  111. package/dist/resolve/__tests__/order.test.js +238 -0
  112. package/dist/resolve/__tests__/order.test.js.map +1 -0
  113. package/dist/resolve/__tests__/phase.test.d.ts +12 -0
  114. package/dist/resolve/__tests__/phase.test.d.ts.map +1 -0
  115. package/dist/resolve/__tests__/phase.test.js +81 -0
  116. package/dist/resolve/__tests__/phase.test.js.map +1 -0
  117. package/dist/resolve/defaults.d.ts +51 -0
  118. package/dist/resolve/defaults.d.ts.map +1 -0
  119. package/dist/resolve/defaults.js +109 -0
  120. package/dist/resolve/defaults.js.map +1 -0
  121. package/dist/resolve/order.d.ts +73 -0
  122. package/dist/resolve/order.d.ts.map +1 -0
  123. package/dist/resolve/order.js +208 -0
  124. package/dist/resolve/order.js.map +1 -0
  125. package/dist/resolve/phase.d.ts +57 -0
  126. package/dist/resolve/phase.d.ts.map +1 -0
  127. package/dist/resolve/phase.js +60 -0
  128. package/dist/resolve/phase.js.map +1 -0
  129. package/dist/scaffold/__tests__/scaffold.test.js +8 -2
  130. package/dist/scaffold/__tests__/scaffold.test.js.map +1 -1
  131. package/dist/scaffold/__tests__/settings-merge.test.js +95 -0
  132. package/dist/scaffold/__tests__/settings-merge.test.js.map +1 -1
  133. package/dist/scaffold/incremental.d.ts.map +1 -1
  134. package/dist/scaffold/incremental.js +13 -2
  135. package/dist/scaffold/incremental.js.map +1 -1
  136. package/dist/visualizer/__tests__/topology-brain-link.test.js +19 -4
  137. package/dist/visualizer/__tests__/topology-brain-link.test.js.map +1 -1
  138. package/examples/code-review.at +32 -4
  139. package/examples/data-processing.at +33 -4
  140. package/package.json +1 -1
  141. package/spec/grammar.md +25 -1
  142. package/spec/validation.md +103 -35
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { readFileSync } from "node:fs";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
3
  import { resolve, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parse, parseSchemaType, parseSchemaFields, parseParams, parseInterfaceEndpoints, parseImports, parseIncludes } from "../index.js";
@@ -223,7 +223,14 @@ describe("Lexer", () => {
223
223
  describe("Parser sections", () => {
224
224
  describe("meta", () => {
225
225
  it("parses version, description, and patterns from header", () => {
226
- const src = `topology test : [pipeline] {\n meta {\n version: "2.0.0"\n description: "A test topology"\n domain: legal\n }\n orchestrator {\n model: opus\n handles: [intake]\n }\n action intake {\n kind: inline\n }\n flow {\n intake -> intake\n }\n}`;
226
+ const src = `topology test : [pipeline] {\n meta {
227
+ version: "2.0.0"\n
228
+ description: "A test topology"\n
229
+ domain: legal\n
230
+ }\n orchestrator {
231
+ model: opus\n
232
+ handles: [intake]\n
233
+ }\n action intake {\n kind: inline\n }\n flow {\n intake -> intake\n }\n}`;
227
234
  const ast = parse(src);
228
235
  expect(ast.topology.name).toBe("test");
229
236
  expect(ast.topology.version).toBe("2.0.0");
@@ -232,7 +239,14 @@ describe("Parser sections", () => {
232
239
  expect(ast.topology.domain).toBe("legal");
233
240
  });
234
241
  it("parses foundations and advanced lists", () => {
235
- const src = `topology t : [pipeline] {\n meta {\n version: "1.0.0"\n foundations: [pipeline, fan-out]\n advanced: [consensus]\n }\n orchestrator {\n model: opus\n handles: [a]\n }\n action a { kind: inline }\n flow { a -> a }\n}`;
242
+ const src = `topology t : [pipeline] {\n meta {
243
+ version: "1.0.0"\n
244
+ foundations: [pipeline, fan-out]\n
245
+ advanced: [consensus]\n
246
+ }\n orchestrator {
247
+ model: opus\n
248
+ handles: [a]\n
249
+ }\n action a { kind: inline }\n flow { a -> a }\n}`;
236
250
  const ast = parse(src);
237
251
  expect(ast.topology.foundations).toEqual(["pipeline", "fan-out"]);
238
252
  expect(ast.topology.advanced).toEqual(["consensus"]);
@@ -252,7 +266,10 @@ describe("Parser sections", () => {
252
266
  });
253
267
  describe("roles", () => {
254
268
  it("parses multiple role definitions", () => {
255
- const src = `topology t : [pipeline] {\n roles {\n writer: "Writes drafts"\n reviewer: "Reviews content"\n }\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n}`;
269
+ const src = `topology t : [pipeline] {\n roles {\n writer: "Writes drafts"\n reviewer: "Reviews content"\n }\n orchestrator {
270
+ model: opus\n
271
+ handles: [a]
272
+ }\n action a { kind: inline }\n flow { a -> a }\n}`;
256
273
  const ast = parse(src);
257
274
  expect(ast.roles).toEqual({
258
275
  writer: "Writes drafts",
@@ -262,7 +279,13 @@ describe("Parser sections", () => {
262
279
  });
263
280
  describe("action", () => {
264
281
  it("parses inline action", () => {
265
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a {\n kind: inline\n description: "Parse request"\n }\n flow { a -> a }\n}`;
282
+ const src = `topology t : [pipeline] {\n orchestrator {
283
+ model: opus\n
284
+ handles: [a]
285
+ }\n action a {
286
+ kind: inline\n
287
+ description: "Parse request"\n
288
+ }\n flow { a -> a }\n}`;
266
289
  const ast = parse(src);
267
290
  const action = ast.nodes.find((n) => n.id === "a");
268
291
  expect(action).toBeDefined();
@@ -273,7 +296,13 @@ describe("Parser sections", () => {
273
296
  }
274
297
  });
275
298
  it("parses external action with source", () => {
276
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [fetch] }\n action fetch {\n kind: external\n source: "github-pr"\n }\n flow { fetch -> fetch }\n}`;
299
+ const src = `topology t : [pipeline] {\n orchestrator {
300
+ model: opus\n
301
+ handles: [fetch]
302
+ }\n action fetch {
303
+ kind: external\n
304
+ source: "github-pr"\n
305
+ }\n flow { fetch -> fetch }\n}`;
277
306
  const ast = parse(src);
278
307
  const action = ast.nodes.find((n) => n.id === "fetch");
279
308
  if (action.type === "action") {
@@ -282,7 +311,13 @@ describe("Parser sections", () => {
282
311
  }
283
312
  });
284
313
  it("parses action with commands list", () => {
285
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [build] }\n action build {\n kind: inline\n commands: [npm install, npm test]\n }\n flow { build -> build }\n}`;
314
+ const src = `topology t : [pipeline] {\n orchestrator {
315
+ model: opus\n
316
+ handles: [build]
317
+ }\n action build {
318
+ kind: inline\n
319
+ commands: [npm install, npm test]\n
320
+ }\n flow { build -> build }\n}`;
286
321
  const ast = parse(src);
287
322
  const action = ast.nodes.find((n) => n.id === "build");
288
323
  if (action.type === "action") {
@@ -292,7 +327,25 @@ describe("Parser sections", () => {
292
327
  });
293
328
  describe("agent", () => {
294
329
  it("parses all basic fields", () => {
295
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent my-agent {\n model: sonnet\n phase: 2\n tools: [Read, Write]\n reads: ["workspace/input"]\n writes: ["workspace/output"]\n permissions: auto\n behavior: advisory\n invocation: manual\n isolation: worktree\n background: true\n retry: 3\n skip: "depth < 2"\n description: "Test agent"\n max-turns: 10\n }\n flow { a -> a }\n}`;
330
+ const src = `topology t : [pipeline] {\n orchestrator {
331
+ model: opus\n
332
+ handles: [a]
333
+ }\n action a { kind: inline }\n agent my-agent {
334
+ model: sonnet\n
335
+ phase: 2\n
336
+ tools: [Read, Write]\n
337
+ reads: ["workspace/input"]\n
338
+ writes: ["workspace/output"]\n
339
+ permissions: auto\n
340
+ behavior: advisory\n
341
+ invocation: manual\n
342
+ isolation: worktree\n
343
+ background: true\n
344
+ retry: 3\n
345
+ skip: "depth < 2"\n
346
+ description: "Test agent"\n
347
+ max-turns: 10\n
348
+ }\n flow { a -> a }\n}`;
296
349
  const ast = parse(src);
297
350
  const agent = ast.nodes.find((n) => n.id === "my-agent");
298
351
  expect(agent).toBeDefined();
@@ -312,7 +365,10 @@ describe("Parser sections", () => {
312
365
  expect(agent.maxTurns).toBe(10);
313
366
  });
314
367
  it("parses inline prompt block", () => {
315
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent writer {\n model: opus\n prompt {\n You are a writer.\n Write good content.\n }\n }\n flow { a -> writer }\n}`;
368
+ const src = `topology t : [pipeline] {\n orchestrator {
369
+ model: opus\n
370
+ handles: [a]
371
+ }\n action a { kind: inline }\n agent writer {\n model: opus\n prompt {\n You are a writer.\n Write good content.\n }\n }\n flow { a -> writer }\n}`;
316
372
  const ast = parse(src);
317
373
  const agent = ast.nodes.find((n) => n.id === "writer");
318
374
  expect(agent.prompt).toBeDefined();
@@ -320,19 +376,31 @@ describe("Parser sections", () => {
320
376
  expect(agent.prompt).toContain("Write good content.");
321
377
  });
322
378
  it("parses disallowed-tools", () => {
323
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent safe {\n model: opus\n disallowed-tools: [Bash, Write]\n }\n flow { a -> safe }\n}`;
379
+ const src = `topology t : [pipeline] {\n orchestrator {
380
+ model: opus\n
381
+ handles: [a]
382
+ }\n action a { kind: inline }\n agent safe {
383
+ model: opus\n
384
+ disallowed-tools: [Bash, Write]\n
385
+ }\n flow { a -> safe }\n}`;
324
386
  const ast = parse(src);
325
387
  const agent = ast.nodes.find((n) => n.id === "safe");
326
388
  expect(agent.disallowedTools).toEqual(["Bash", "Write"]);
327
389
  });
328
390
  it("parses outputs block", () => {
329
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent scorer {\n model: opus\n outputs {\n quality: high | medium | low\n }\n }\n flow { a -> scorer }\n}`;
391
+ const src = `topology t : [pipeline] {\n orchestrator {
392
+ model: opus\n
393
+ handles: [a]
394
+ }\n action a { kind: inline }\n agent scorer {\n model: opus\n outputs {\n quality: high | medium | low\n }\n }\n flow { a -> scorer }\n}`;
330
395
  const ast = parse(src);
331
396
  const agent = ast.nodes.find((n) => n.id === "scorer");
332
397
  expect(agent.outputs).toEqual({ quality: ["high", "medium", "low"] });
333
398
  });
334
399
  it("parses scale block", () => {
335
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent worker {\n model: opus\n scale {\n mode: auto\n by: doc-count\n min: 2\n max: 8\n batch-size: 50\n }\n }\n flow { a -> worker }\n}`;
400
+ const src = `topology t : [pipeline] {\n orchestrator {
401
+ model: opus\n
402
+ handles: [a]
403
+ }\n action a { kind: inline }\n agent worker {\n model: opus\n scale {\n mode: auto\n by: doc-count\n min: 2\n max: 8\n batch-size: 50\n }\n }\n flow { a -> worker }\n}`;
336
404
  const ast = parse(src);
337
405
  const agent = ast.nodes.find((n) => n.id === "worker");
338
406
  expect(agent.scale).toEqual({
@@ -344,7 +412,16 @@ describe("Parser sections", () => {
344
412
  });
345
413
  });
346
414
  it("parses per-agent hooks block", () => {
347
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent hooked {\n model: opus\n hooks {\n hook pre-tool {\n on: PreToolUse\n matcher: "Bash"\n run: "scripts/check.sh"\n type: command\n timeout: 5000\n }\n }\n }\n flow { a -> hooked }\n}`;
415
+ const src = `topology t : [pipeline] {\n orchestrator {
416
+ model: opus\n
417
+ handles: [a]
418
+ }\n action a { kind: inline }\n agent hooked {\n model: opus\n hooks {\n hook pre-tool {
419
+ on: PreToolUse\n
420
+ matcher: "Bash"\n
421
+ run: "scripts/check.sh"\n
422
+ type: command\n
423
+ timeout: 5000\n
424
+ }\n }\n }\n flow { a -> hooked }\n}`;
348
425
  const ast = parse(src);
349
426
  const agent = ast.nodes.find((n) => n.id === "hooked");
350
427
  expect(agent.hooks).toBeDefined();
@@ -357,7 +434,10 @@ describe("Parser sections", () => {
357
434
  expect(agent.hooks[0].timeout).toBe(5000);
358
435
  });
359
436
  it("parses extensions block", () => {
360
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent extended {\n model: opus\n extensions {\n openclaw {\n calendar-integration: true\n port: 8080\n }\n }\n }\n flow { a -> extended }\n}`;
437
+ const src = `topology t : [pipeline] {\n orchestrator {
438
+ model: opus\n
439
+ handles: [a]
440
+ }\n action a { kind: inline }\n agent extended {\n model: opus\n extensions {\n openclaw {\n calendar-integration: true\n port: 8080\n }\n }\n }\n flow { a -> extended }\n}`;
361
441
  const ast = parse(src);
362
442
  const agent = ast.nodes.find((n) => n.id === "extended");
363
443
  expect(agent.extensions).toBeDefined();
@@ -367,19 +447,34 @@ describe("Parser sections", () => {
367
447
  });
368
448
  });
369
449
  it("parses mcp-servers list", () => {
370
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent mcp-user {\n model: opus\n mcp-servers: [storage, monitoring]\n }\n flow { a -> mcp-user }\n}`;
450
+ const src = `topology t : [pipeline] {\n orchestrator {
451
+ model: opus\n
452
+ handles: [a]
453
+ }\n action a { kind: inline }\n agent mcp-user {
454
+ model: opus\n
455
+ mcp-servers: [storage, monitoring]\n
456
+ }\n flow { a -> mcp-user }\n}`;
371
457
  const ast = parse(src);
372
458
  const agent = ast.nodes.find((n) => n.id === "mcp-user");
373
459
  expect(agent.mcpServers).toEqual(["storage", "monitoring"]);
374
460
  });
375
461
  it("parses skills list", () => {
376
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent skilled {\n model: opus\n skills: [extraction, classification]\n }\n flow { a -> skilled }\n}`;
462
+ const src = `topology t : [pipeline] {\n orchestrator {
463
+ model: opus\n
464
+ handles: [a]
465
+ }\n action a { kind: inline }\n agent skilled {
466
+ model: opus\n
467
+ skills: [extraction, classification]\n
468
+ }\n flow { a -> skilled }\n}`;
377
469
  const ast = parse(src);
378
470
  const agent = ast.nodes.find((n) => n.id === "skilled");
379
471
  expect(agent.skills).toEqual(["extraction", "classification"]);
380
472
  });
381
473
  it("attaches role from roles block", () => {
382
- const src = `topology t : [pipeline] {\n roles {\n writer: "Writes things"\n }\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent writer {\n model: opus\n }\n flow { a -> writer }\n}`;
474
+ const src = `topology t : [pipeline] {\n roles {\n writer: "Writes things"\n }\n orchestrator {
475
+ model: opus\n
476
+ handles: [a]
477
+ }\n action a { kind: inline }\n agent writer {\n model: opus\n }\n flow { a -> writer }\n}`;
383
478
  const ast = parse(src);
384
479
  const agent = ast.nodes.find((n) => n.id === "writer");
385
480
  expect(agent.role).toBe("Writes things");
@@ -387,14 +482,20 @@ describe("Parser sections", () => {
387
482
  });
388
483
  describe("flow", () => {
389
484
  it("parses simple chain", () => {
390
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a, c] }\n action a { kind: inline }\n agent b { model: opus }\n action c { kind: report }\n flow {\n a -> b -> c\n }\n}`;
485
+ const src = `topology t : [pipeline] {\n orchestrator {
486
+ model: opus\n
487
+ handles: [a, c]
488
+ }\n action a { kind: inline }\n agent b { model: opus }\n action c { kind: report }\n flow {\n a -> b -> c\n }\n}`;
391
489
  const ast = parse(src);
392
490
  expect(ast.edges).toHaveLength(2);
393
491
  expect(ast.edges[0]).toEqual({ from: "a", to: "b", condition: null, maxIterations: null, per: null });
394
492
  expect(ast.edges[1]).toEqual({ from: "b", to: "c", condition: null, maxIterations: null, per: null });
395
493
  });
396
494
  it("parses fan-out [a, b]", () => {
397
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [start] }\n action start { kind: inline }\n agent x { model: opus }\n agent y { model: opus }\n flow {\n start -> [x, y]\n }\n}`;
495
+ const src = `topology t : [pipeline] {\n orchestrator {
496
+ model: opus\n
497
+ handles: [start]
498
+ }\n action start { kind: inline }\n agent x { model: opus }\n agent y { model: opus }\n flow {\n start -> [x, y]\n }\n}`;
398
499
  const ast = parse(src);
399
500
  expect(ast.edges).toHaveLength(2);
400
501
  expect(ast.edges[0].from).toBe("start");
@@ -403,7 +504,10 @@ describe("Parser sections", () => {
403
504
  expect(ast.edges[1].to).toBe("y");
404
505
  });
405
506
  it("parses conditions [when x.y == z]", () => {
406
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent b { model: opus\n outputs { verdict: yes | no } }\n agent c { model: opus }\n flow {\n b -> c [when b.verdict == yes]\n }\n}`;
507
+ const src = `topology t : [pipeline] {\n orchestrator {
508
+ model: opus\n
509
+ handles: [a]
510
+ }\n action a { kind: inline }\n agent b { model: opus\n outputs { verdict: yes | no } }\n agent c { model: opus }\n flow {\n b -> c [when b.verdict == yes]\n }\n}`;
407
511
  const ast = parse(src);
408
512
  const edge = ast.edges.find((e) => e.from === "b" && e.to === "c");
409
513
  expect(edge).toBeDefined();
@@ -411,7 +515,10 @@ describe("Parser sections", () => {
411
515
  expect(edge.maxIterations).toBeNull();
412
516
  });
413
517
  it("parses bounded loops [max N]", () => {
414
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent b { model: opus }\n agent c { model: opus }\n flow {\n c -> b [max 3]\n }\n}`;
518
+ const src = `topology t : [pipeline] {\n orchestrator {
519
+ model: opus\n
520
+ handles: [a]
521
+ }\n action a { kind: inline }\n agent b { model: opus }\n agent c { model: opus }\n flow {\n c -> b [max 3]\n }\n}`;
415
522
  const ast = parse(src);
416
523
  const edge = ast.edges.find((e) => e.from === "c" && e.to === "b");
417
524
  expect(edge).toBeDefined();
@@ -419,7 +526,10 @@ describe("Parser sections", () => {
419
526
  expect(edge.condition).toBeNull();
420
527
  });
421
528
  it("parses combined [when x.y == z, max 3]", () => {
422
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent b { model: opus\n outputs { verdict: yes | no } }\n agent c { model: opus }\n flow {\n c -> b [when c.verdict == no, max 3]\n }\n}`;
529
+ const src = `topology t : [pipeline] {\n orchestrator {
530
+ model: opus\n
531
+ handles: [a]
532
+ }\n action a { kind: inline }\n agent b { model: opus\n outputs { verdict: yes | no } }\n agent c { model: opus }\n flow {\n c -> b [when c.verdict == no, max 3]\n }\n}`;
423
533
  const ast = parse(src);
424
534
  const edge = ast.edges.find((e) => e.from === "c" && e.to === "b");
425
535
  expect(edge).toBeDefined();
@@ -429,7 +539,18 @@ describe("Parser sections", () => {
429
539
  });
430
540
  describe("gates", () => {
431
541
  it("parses gate with all fields", () => {
432
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent b { model: opus }\n agent c { model: opus }\n gates {\n gate quality {\n after: b\n before: c\n run: "scripts/check.sh"\n checks: [grammar, formatting]\n on-fail: bounce-back\n retry: 2\n behavior: blocking\n }\n }\n flow { a -> b -> c }\n}`;
542
+ const src = `topology t : [pipeline] {\n orchestrator {
543
+ model: opus\n
544
+ handles: [a]
545
+ }\n action a { kind: inline }\n agent b { model: opus }\n agent c { model: opus }\n gates {\n gate quality {
546
+ after: b\n
547
+ before: c\n
548
+ run: "scripts/check.sh"\n
549
+ checks: [grammar, formatting]\n
550
+ on-fail: bounce-back\n
551
+ retry: 2\n
552
+ behavior: blocking\n
553
+ }\n }\n flow { a -> b -> c }\n}`;
433
554
  const ast = parse(src);
434
555
  const gate = ast.nodes.find((n) => n.id === "quality");
435
556
  expect(gate).toBeDefined();
@@ -443,7 +564,10 @@ describe("Parser sections", () => {
443
564
  expect(gate.behavior).toBe("blocking");
444
565
  });
445
566
  it("parses gate with extensions", () => {
446
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent b { model: opus }\n gates {\n gate ext-gate {\n after: b\n run: "check.sh"\n extensions {\n custom {\n enabled: true\n }\n }\n }\n }\n flow { a -> b }\n}`;
567
+ const src = `topology t : [pipeline] {\n orchestrator {
568
+ model: opus\n
569
+ handles: [a]
570
+ }\n action a { kind: inline }\n agent b { model: opus }\n gates {\n gate ext-gate {\n after: b\n run: "check.sh"\n extensions {\n custom {\n enabled: true\n }\n }\n }\n }\n flow { a -> b }\n}`;
447
571
  const ast = parse(src);
448
572
  const gate = ast.nodes.find((n) => n.id === "ext-gate");
449
573
  expect(gate.extensions).toBeDefined();
@@ -452,7 +576,10 @@ describe("Parser sections", () => {
452
576
  });
453
577
  describe("depth", () => {
454
578
  it("parses levels and factors", () => {
455
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n depth {\n factors: [batch-count, token-volume]\n level 1 "Quick" {\n omit: [reviewer]\n }\n level 2 "Full" {\n omit: []\n }\n }\n}`;
579
+ const src = `topology t : [pipeline] {\n orchestrator {
580
+ model: opus\n
581
+ handles: [a]
582
+ }\n action a { kind: inline }\n flow { a -> a }\n depth {\n factors: [batch-count, token-volume]\n level 1 "Quick" {\n omit: [reviewer]\n }\n level 2 "Full" {\n omit: []\n }\n }\n}`;
456
583
  const ast = parse(src);
457
584
  expect(ast.depth.factors).toEqual(["batch-count", "token-volume"]);
458
585
  expect(ast.depth.levels).toHaveLength(2);
@@ -462,7 +589,10 @@ describe("Parser sections", () => {
462
589
  });
463
590
  describe("memory", () => {
464
591
  it("parses workspace, domains, and metrics", () => {
465
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n memory {\n workspace {\n path: "workspace/"\n protocol: "proto.md"\n structure: [raw, processed]\n }\n domains {\n path: "domains/"\n routing: "FILE_MAP.md"\n }\n metrics {\n path: "metrics.jsonl"\n mode: append-only\n }\n }\n}`;
592
+ const src = `topology t : [pipeline] {\n orchestrator {
593
+ model: opus\n
594
+ handles: [a]
595
+ }\n action a { kind: inline }\n flow { a -> a }\n memory {\n workspace {\n path: "workspace/"\n protocol: "proto.md"\n structure: [raw, processed]\n }\n domains {\n path: "domains/"\n routing: "FILE_MAP.md"\n }\n metrics {\n path: "metrics.jsonl"\n mode: append-only\n }\n }\n}`;
466
596
  const ast = parse(src);
467
597
  const ws = ast.memory["workspace"];
468
598
  expect(ws["path"]).toBe("workspace/");
@@ -478,7 +608,10 @@ describe("Parser sections", () => {
478
608
  });
479
609
  describe("batch", () => {
480
610
  it("parses parallel, per, workspace, and conflicts", () => {
481
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n batch {\n parallel: true\n per: ticket\n workspace: "runs/{BATCH_ID}/"\n conflicts {\n detect: ["workspace/out.json"]\n resolve: sequential-rebase\n }\n }\n}`;
611
+ const src = `topology t : [pipeline] {\n orchestrator {
612
+ model: opus\n
613
+ handles: [a]
614
+ }\n action a { kind: inline }\n flow { a -> a }\n batch {\n parallel: true\n per: ticket\n workspace: "runs/{BATCH_ID}/"\n conflicts {\n detect: ["workspace/out.json"]\n resolve: sequential-rebase\n }\n }\n}`;
482
615
  const ast = parse(src);
483
616
  expect(ast.batch["parallel"]).toBe(true);
484
617
  expect(ast.batch["per"]).toBe("ticket");
@@ -490,7 +623,10 @@ describe("Parser sections", () => {
490
623
  });
491
624
  describe("environments", () => {
492
625
  it("parses multiple named environment blocks", () => {
493
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n environments {\n staging {\n bucket: "stage-bucket"\n }\n production {\n bucket: "prod-bucket"\n }\n }\n}`;
626
+ const src = `topology t : [pipeline] {\n orchestrator {
627
+ model: opus\n
628
+ handles: [a]
629
+ }\n action a { kind: inline }\n flow { a -> a }\n environments {\n staging {\n bucket: "stage-bucket"\n }\n production {\n bucket: "prod-bucket"\n }\n }\n}`;
494
630
  const ast = parse(src);
495
631
  expect(ast.environments["staging"]).toEqual({ bucket: "stage-bucket" });
496
632
  expect(ast.environments["production"]).toEqual({ bucket: "prod-bucket" });
@@ -498,7 +634,10 @@ describe("Parser sections", () => {
498
634
  });
499
635
  describe("triggers", () => {
500
636
  it("parses command with pattern", () => {
501
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n triggers {\n command audit {\n pattern: "/audit"\n }\n }\n}`;
637
+ const src = `topology t : [pipeline] {\n orchestrator {
638
+ model: opus\n
639
+ handles: [a]
640
+ }\n action a { kind: inline }\n flow { a -> a }\n triggers {\n command audit {\n pattern: "/audit"\n }\n }\n}`;
502
641
  const ast = parse(src);
503
642
  expect(ast.triggers).toHaveLength(1);
504
643
  expect(ast.triggers[0].name).toBe("audit");
@@ -506,7 +645,10 @@ describe("Parser sections", () => {
506
645
  expect(ast.triggers[0].argument).toBeUndefined();
507
646
  });
508
647
  it("parses command with pattern and argument", () => {
509
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n triggers {\n command create {\n pattern: "/create <TOPIC>"\n argument: TOPIC\n }\n }\n}`;
648
+ const src = `topology t : [pipeline] {\n orchestrator {
649
+ model: opus\n
650
+ handles: [a]
651
+ }\n action a { kind: inline }\n flow { a -> a }\n triggers {\n command create {\n pattern: "/create <TOPIC>"\n argument: TOPIC\n }\n }\n}`;
510
652
  const ast = parse(src);
511
653
  expect(ast.triggers[0].name).toBe("create");
512
654
  expect(ast.triggers[0].pattern).toBe("/create <TOPIC>");
@@ -515,7 +657,16 @@ describe("Parser sections", () => {
515
657
  });
516
658
  describe("hooks", () => {
517
659
  it("parses hooks with on, run, type, matcher, timeout", () => {
518
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n hooks {\n hook my-hook {\n on: PreToolUse\n matcher: "Bash"\n run: "scripts/guard.sh"\n type: command\n timeout: 3000\n }\n }\n}`;
660
+ const src = `topology t : [pipeline] {\n orchestrator {
661
+ model: opus\n
662
+ handles: [a]
663
+ }\n action a { kind: inline }\n flow { a -> a }\n hooks {\n hook my-hook {
664
+ on: PreToolUse\n
665
+ matcher: "Bash"\n
666
+ run: "scripts/guard.sh"\n
667
+ type: command\n
668
+ timeout: 3000\n
669
+ }\n }\n}`;
519
670
  const ast = parse(src);
520
671
  expect(ast.hooks).toHaveLength(1);
521
672
  expect(ast.hooks[0].name).toBe("my-hook");
@@ -528,7 +679,10 @@ describe("Parser sections", () => {
528
679
  });
529
680
  describe("settings", () => {
530
681
  it("parses allow, deny, ask lists", () => {
531
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n settings {\n allow: [Read, Write]\n deny: [Bash]\n ask: [Edit]\n }\n}`;
682
+ const src = `topology t : [pipeline] {\n orchestrator {
683
+ model: opus\n
684
+ handles: [a]
685
+ }\n action a { kind: inline }\n flow { a -> a }\n settings {\n allow: [Read, Write]\n deny: [Bash]\n ask: [Edit]\n }\n}`;
532
686
  const ast = parse(src);
533
687
  expect(ast.settings["allow"]).toEqual(["Read", "Write"]);
534
688
  expect(ast.settings["deny"]).toEqual(["Bash"]);
@@ -537,7 +691,10 @@ describe("Parser sections", () => {
537
691
  });
538
692
  describe("mcp-servers", () => {
539
693
  it("parses stdio and http server types", () => {
540
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n mcp-servers {\n storage {\n type: stdio\n command: "npx"\n args: ["-y", "storage-server"]\n }\n monitor {\n type: http\n url: "https://mcp.example.com"\n }\n }\n}`;
694
+ const src = `topology t : [pipeline] {\n orchestrator {
695
+ model: opus\n
696
+ handles: [a]
697
+ }\n action a { kind: inline }\n flow { a -> a }\n mcp-servers {\n storage {\n type: stdio\n command: "npx"\n args: ["-y", "storage-server"]\n }\n monitor {\n type: http\n url: "https://mcp.example.com"\n }\n }\n}`;
541
698
  const ast = parse(src);
542
699
  expect(ast.mcpServers["storage"]).toBeDefined();
543
700
  expect(ast.mcpServers["storage"]["type"]).toBe("stdio");
@@ -549,7 +706,10 @@ describe("Parser sections", () => {
549
706
  });
550
707
  describe("metering", () => {
551
708
  it("parses track, per, output, format, pricing", () => {
552
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n metering {\n track: [tokens-in, tokens-out, cost]\n per: [agent, run]\n output: "metrics/"\n format: jsonl\n pricing: anthropic-current\n }\n}`;
709
+ const src = `topology t : [pipeline] {\n orchestrator {
710
+ model: opus\n
711
+ handles: [a]
712
+ }\n action a { kind: inline }\n flow { a -> a }\n metering {\n track: [tokens-in, tokens-out, cost]\n per: [agent, run]\n output: "metrics/"\n format: jsonl\n pricing: anthropic-current\n }\n}`;
553
713
  const ast = parse(src);
554
714
  expect(ast.metering).not.toBeNull();
555
715
  expect(ast.metering.track).toEqual(["tokens-in", "tokens-out", "cost"]);
@@ -561,7 +721,10 @@ describe("Parser sections", () => {
561
721
  });
562
722
  describe("tools", () => {
563
723
  it("parses tool with script, args, lang, description", () => {
564
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n tools {\n tool extract-pdf {\n script: "scripts/extract.py"\n args: [input, output]\n lang: python\n description: "Extract text from PDF"\n }\n }\n}`;
724
+ const src = `topology t : [pipeline] {\n orchestrator {
725
+ model: opus\n
726
+ handles: [a]
727
+ }\n action a { kind: inline }\n flow { a -> a }\n tools {\n tool extract-pdf {\n script: "scripts/extract.py"\n args: [input, output]\n lang: python\n description: "Extract text from PDF"\n }\n }\n}`;
565
728
  const ast = parse(src);
566
729
  expect(ast.toolDefs).toHaveLength(1);
567
730
  expect(ast.toolDefs[0].id).toBe("extract-pdf");
@@ -573,7 +736,10 @@ describe("Parser sections", () => {
573
736
  });
574
737
  describe("skill", () => {
575
738
  it("parses all skill fields", () => {
576
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n skill my-skill {\n description: "A test skill"\n scripts: [script-a, script-b]\n domains: ["domains/rubric.md"]\n references: ["ref/guide.md"]\n prompt: "prompts/skill.md"\n disable-model-invocation: true\n user-invocable: false\n context: fork\n agent: task-agent\n allowed-tools: [Read, Write]\n extensions {\n custom {\n key: value\n }\n }\n }\n}`;
739
+ const src = `topology t : [pipeline] {\n orchestrator {
740
+ model: opus\n
741
+ handles: [a]
742
+ }\n action a { kind: inline }\n flow { a -> a }\n skill my-skill {\n description: "A test skill"\n scripts: [script-a, script-b]\n domains: ["domains/rubric.md"]\n references: ["ref/guide.md"]\n prompt: "prompts/skill.md"\n disable-model-invocation: true\n user-invocable: false\n context: fork\n agent: task-agent\n allowed-tools: [Read, Write]\n extensions {\n custom {\n key: value\n }\n }\n }\n}`;
577
743
  const ast = parse(src);
578
744
  expect(ast.skills).toHaveLength(1);
579
745
  const skill = ast.skills[0];
@@ -594,7 +760,10 @@ describe("Parser sections", () => {
594
760
  });
595
761
  describe("context", () => {
596
762
  it("parses file and includes", () => {
597
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n context {\n file: "CONTEXT.md"\n includes: [README.md, CONTRIBUTING.md]\n }\n}`;
763
+ const src = `topology t : [pipeline] {\n orchestrator {
764
+ model: opus\n
765
+ handles: [a]
766
+ }\n action a { kind: inline }\n flow { a -> a }\n context {\n file: "CONTEXT.md"\n includes: [README.md, CONTRIBUTING.md]\n }\n}`;
598
767
  const ast = parse(src);
599
768
  expect(ast.context.file).toBe("CONTEXT.md");
600
769
  expect(ast.context.includes).toEqual(["README.md", "CONTRIBUTING.md"]);
@@ -602,7 +771,10 @@ describe("Parser sections", () => {
602
771
  });
603
772
  describe("env", () => {
604
773
  it("parses key-value pairs", () => {
605
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n env {\n API_KEY: "sk-123"\n REGION: "us-east-1"\n }\n}`;
774
+ const src = `topology t : [pipeline] {\n orchestrator {
775
+ model: opus\n
776
+ handles: [a]
777
+ }\n action a { kind: inline }\n flow { a -> a }\n env {\n API_KEY: "sk-123"\n REGION: "us-east-1"\n }\n}`;
606
778
  const ast = parse(src);
607
779
  expect(ast.env["API_KEY"]).toBe("sk-123");
608
780
  expect(ast.env["REGION"]).toBe("us-east-1");
@@ -610,7 +782,10 @@ describe("Parser sections", () => {
610
782
  });
611
783
  describe("providers", () => {
612
784
  it("parses provider with all fields", () => {
613
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n providers {\n anthropic {\n api-key: "\${ANTHROPIC_API_KEY}"\n base-url: "https://api.anthropic.com"\n models: [opus, sonnet, haiku]\n default: true\n }\n openai {\n api-key: "\${OPENAI_API_KEY}"\n models: [gpt-4o]\n }\n }\n}`;
785
+ const src = `topology t : [pipeline] {\n orchestrator {
786
+ model: opus\n
787
+ handles: [a]
788
+ }\n action a { kind: inline }\n flow { a -> a }\n providers {\n anthropic {\n api-key: "\${ANTHROPIC_API_KEY}"\n base-url: "https://api.anthropic.com"\n models: [opus, sonnet, haiku]\n default: true\n }\n openai {\n api-key: "\${OPENAI_API_KEY}"\n models: [gpt-4o]\n }\n }\n}`;
614
789
  const ast = parse(src);
615
790
  expect(ast.providers).toHaveLength(2);
616
791
  expect(ast.providers[0].name).toBe("anthropic");
@@ -625,7 +800,13 @@ describe("Parser sections", () => {
625
800
  });
626
801
  describe("schedule", () => {
627
802
  it("parses job with cron", () => {
628
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job nightly {\n cron: "0 0 * * *"\n agent: worker\n }\n }\n}`;
803
+ const src = `topology t : [pipeline] {\n orchestrator {
804
+ model: opus\n
805
+ handles: [a]
806
+ }\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job nightly {
807
+ cron: "0 0 * * *"\n
808
+ agent: worker\n
809
+ }\n }\n}`;
629
810
  const ast = parse(src);
630
811
  expect(ast.schedules).toHaveLength(1);
631
812
  expect(ast.schedules[0].id).toBe("nightly");
@@ -634,7 +815,14 @@ describe("Parser sections", () => {
634
815
  expect(ast.schedules[0].enabled).toBe(true);
635
816
  });
636
817
  it("parses job with every", () => {
637
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job weekly {\n every: "monday 9:00"\n agent: worker\n enabled: true\n }\n }\n}`;
818
+ const src = `topology t : [pipeline] {\n orchestrator {
819
+ model: opus\n
820
+ handles: [a]
821
+ }\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job weekly {
822
+ every: "monday 9:00"\n
823
+ agent: worker\n
824
+ enabled: true\n
825
+ }\n }\n}`;
638
826
  const ast = parse(src);
639
827
  expect(ast.schedules).toHaveLength(1);
640
828
  expect(ast.schedules[0].id).toBe("weekly");
@@ -643,14 +831,24 @@ describe("Parser sections", () => {
643
831
  expect(ast.schedules[0].enabled).toBe(true);
644
832
  });
645
833
  it("parses job with enabled: false", () => {
646
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job disabled-job {\n cron: "0 0 * * *"\n agent: worker\n enabled: false\n }\n }\n}`;
834
+ const src = `topology t : [pipeline] {\n orchestrator {
835
+ model: opus\n
836
+ handles: [a]
837
+ }\n action a { kind: inline }\n agent worker { model: opus }\n flow { a -> worker }\n schedule {\n job disabled-job {
838
+ cron: "0 0 * * *"\n
839
+ agent: worker\n
840
+ enabled: false\n
841
+ }\n }\n}`;
647
842
  const ast = parse(src);
648
843
  expect(ast.schedules[0].enabled).toBe(false);
649
844
  });
650
845
  });
651
846
  describe("interfaces", () => {
652
847
  it("parses multiple interfaces with type and config", () => {
653
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n interfaces {\n slack {\n type: webhook\n webhook: "\${SLACK_URL}"\n channel: "alerts"\n }\n dashboard {\n type: http\n port: 8080\n }\n }\n}`;
848
+ const src = `topology t : [pipeline] {\n orchestrator {
849
+ model: opus\n
850
+ handles: [a]
851
+ }\n action a { kind: inline }\n flow { a -> a }\n interfaces {\n slack {\n type: webhook\n webhook: "\${SLACK_URL}"\n channel: "alerts"\n }\n dashboard {\n type: http\n port: 8080\n }\n }\n}`;
654
852
  const ast = parse(src);
655
853
  expect(ast.interfaces).toHaveLength(2);
656
854
  const slack = ast.interfaces.find((i) => i.id === "slack");
@@ -666,32 +864,56 @@ describe("Parser sections", () => {
666
864
  });
667
865
  describe("sandbox", () => {
668
866
  it("parses sandbox field on agent (string value)", () => {
669
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent sandboxed { model: opus\n sandbox: docker }\n flow { a -> sandboxed }\n}`;
867
+ const src = `topology t : [pipeline] {\n orchestrator {
868
+ model: opus\n
869
+ handles: [a]
870
+ }\n action a { kind: inline }\n agent sandboxed {
871
+ model: opus\n
872
+ sandbox: docker
873
+ }\n flow { a -> sandboxed }\n}`;
670
874
  const ast = parse(src);
671
875
  const agent = ast.nodes.find((n) => n.id === "sandboxed");
672
876
  expect(agent.sandbox).toBe("docker");
673
877
  });
674
878
  it("parses sandbox: true on agent", () => {
675
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent sandboxed { model: opus\n sandbox: true }\n flow { a -> sandboxed }\n}`;
879
+ const src = `topology t : [pipeline] {\n orchestrator {
880
+ model: opus\n
881
+ handles: [a]
882
+ }\n action a { kind: inline }\n agent sandboxed {
883
+ model: opus\n
884
+ sandbox: true
885
+ }\n flow { a -> sandboxed }\n}`;
676
886
  const ast = parse(src);
677
887
  const agent = ast.nodes.find((n) => n.id === "sandboxed");
678
888
  expect(agent.sandbox).toBe(true);
679
889
  });
680
890
  it("parses sandbox in settings", () => {
681
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n settings {\n sandbox: network-only\n }\n}`;
891
+ const src = `topology t : [pipeline] {\n orchestrator {
892
+ model: opus\n
893
+ handles: [a]
894
+ }\n action a { kind: inline }\n flow { a -> a }\n settings {\n sandbox: network-only\n }\n}`;
682
895
  const ast = parse(src);
683
896
  expect(ast.settings["sandbox"]).toBe("network-only");
684
897
  });
685
898
  });
686
899
  describe("fallback-chain", () => {
687
900
  it("parses fallback-chain on agent", () => {
688
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent resilient { model: opus\n fallback-chain: [sonnet, haiku] }\n flow { a -> resilient }\n}`;
901
+ const src = `topology t : [pipeline] {\n orchestrator {
902
+ model: opus\n
903
+ handles: [a]
904
+ }\n action a { kind: inline }\n agent resilient {
905
+ model: opus\n
906
+ fallback-chain: [sonnet, haiku]
907
+ }\n flow { a -> resilient }\n}`;
689
908
  const ast = parse(src);
690
909
  const agent = ast.nodes.find((n) => n.id === "resilient");
691
910
  expect(agent.fallbackChain).toEqual(["sonnet", "haiku"]);
692
911
  });
693
912
  it("parses fallback-chain in settings", () => {
694
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n settings {\n fallback-chain: [opus, sonnet, haiku]\n }\n}`;
913
+ const src = `topology t : [pipeline] {\n orchestrator {
914
+ model: opus\n
915
+ handles: [a]
916
+ }\n action a { kind: inline }\n flow { a -> a }\n settings {\n fallback-chain: [opus, sonnet, haiku]\n }\n}`;
695
917
  const ast = parse(src);
696
918
  expect(ast.settings["fallbackChain"]).toEqual(["opus", "sonnet", "haiku"]);
697
919
  });
@@ -1366,33 +1588,60 @@ describe("Integration: example files", () => {
1366
1588
  // =========================================================================
1367
1589
  describe("Edge cases", () => {
1368
1590
  it("inline comments are stripped from agent model", () => {
1369
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent worker {\n model: opus # main model\n tools: [Read, Write] # core tools\n }\n flow { a -> worker }\n}`;
1591
+ const src = `topology t : [pipeline] {\n orchestrator {
1592
+ model: opus\n
1593
+ handles: [a]
1594
+ }\n action a { kind: inline }\n agent worker {
1595
+ model: opus # main model\n
1596
+ tools: [Read, Write] # core tools\n
1597
+ }\n flow { a -> worker }\n}`;
1370
1598
  const ast = parse(src);
1371
1599
  const agent = ast.nodes.find((n) => n.id === "worker");
1372
1600
  expect(agent.model).toBe("opus");
1373
1601
  expect(agent.tools).toEqual(["Read", "Write"]);
1374
1602
  });
1375
1603
  it("prompt blocks with # characters inside are preserved", () => {
1376
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent writer {\n model: opus\n prompt {\n # Step 1: Research\n Do research.\n # Step 2: Write\n Write content.\n }\n }\n flow { a -> writer }\n}`;
1604
+ const src = `topology t : [pipeline] {\n orchestrator {
1605
+ model: opus\n
1606
+ handles: [a]
1607
+ }\n action a { kind: inline }\n agent writer {\n model: opus\n prompt {\n # Step 1: Research\n Do research.\n # Step 2: Write\n Write content.\n }\n }\n flow { a -> writer }\n}`;
1377
1608
  const ast = parse(src);
1378
1609
  const agent = ast.nodes.find((n) => n.id === "writer");
1379
1610
  expect(agent.prompt).toContain("# Step 1: Research");
1380
1611
  expect(agent.prompt).toContain("# Step 2: Write");
1381
1612
  });
1382
1613
  it("nested braces in prompt blocks", () => {
1383
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent coder {\n model: opus\n prompt {\n Output JSON like:\n { "key": { "nested": true } }\n }\n }\n flow { a -> coder }\n}`;
1614
+ const src = `topology t : [pipeline] {\n orchestrator {
1615
+ model: opus\n
1616
+ handles: [a]
1617
+ }\n action a { kind: inline }\n agent coder {\n model: opus\n prompt {\n Output JSON like:\n { "key": { "nested": true } }\n }\n }\n flow { a -> coder }\n}`;
1384
1618
  const ast = parse(src);
1385
1619
  const agent = ast.nodes.find((n) => n.id === "coder");
1386
1620
  expect(agent.prompt).toContain('"key"');
1387
1621
  });
1388
1622
  it("agent with decimal phase numbers", () => {
1389
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent precise {\n model: opus\n phase: 4.5\n }\n flow { a -> precise }\n}`;
1623
+ const src = `topology t : [pipeline] {\n orchestrator {
1624
+ model: opus\n
1625
+ handles: [a]
1626
+ }\n action a { kind: inline }\n agent precise {
1627
+ model: opus\n
1628
+ phase: 4.5\n
1629
+ }\n flow { a -> precise }\n}`;
1390
1630
  const ast = parse(src);
1391
1631
  const agent = ast.nodes.find((n) => n.id === "precise");
1392
1632
  expect(agent.phase).toBe(4.5);
1393
1633
  });
1394
1634
  it("multiple agents with same phase number", () => {
1395
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent p1 {\n model: opus\n phase: 1\n }\n agent p2 {\n model: opus\n phase: 1\n }\n flow { a -> [p1, p2] }\n}`;
1635
+ const src = `topology t : [pipeline] {\n orchestrator {
1636
+ model: opus\n
1637
+ handles: [a]
1638
+ }\n action a { kind: inline }\n agent p1 {
1639
+ model: opus\n
1640
+ phase: 1\n
1641
+ }\n agent p2 {
1642
+ model: opus\n
1643
+ phase: 1\n
1644
+ }\n flow { a -> [p1, p2] }\n}`;
1396
1645
  const ast = parse(src);
1397
1646
  const p1 = ast.nodes.find((n) => n.id === "p1");
1398
1647
  const p2 = ast.nodes.find((n) => n.id === "p2");
@@ -1400,14 +1649,26 @@ describe("Edge cases", () => {
1400
1649
  expect(p2.phase).toBe(1);
1401
1650
  });
1402
1651
  it("gate with only after (no before)", () => {
1403
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent b { model: opus }\n gates {\n gate after-only {\n after: b\n run: "check.sh"\n }\n }\n flow { a -> b }\n}`;
1652
+ const src = `topology t : [pipeline] {\n orchestrator {
1653
+ model: opus\n
1654
+ handles: [a]
1655
+ }\n action a { kind: inline }\n agent b { model: opus }\n gates {\n gate after-only {
1656
+ after: b\n
1657
+ run: "check.sh"\n
1658
+ }\n }\n flow { a -> b }\n}`;
1404
1659
  const ast = parse(src);
1405
1660
  const gate = ast.nodes.find((n) => n.id === "after-only");
1406
1661
  expect(gate.after).toBe("b");
1407
1662
  expect(gate.before).toBeUndefined();
1408
1663
  });
1409
1664
  it("hook with only on and run (minimal)", () => {
1410
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n hooks {\n hook minimal {\n on: Stop\n run: "cleanup.sh"\n }\n }\n}`;
1665
+ const src = `topology t : [pipeline] {\n orchestrator {
1666
+ model: opus\n
1667
+ handles: [a]
1668
+ }\n action a { kind: inline }\n flow { a -> a }\n hooks {\n hook minimal {
1669
+ on: Stop\n
1670
+ run: "cleanup.sh"\n
1671
+ }\n }\n}`;
1411
1672
  const ast = parse(src);
1412
1673
  expect(ast.hooks).toHaveLength(1);
1413
1674
  expect(ast.hooks[0].name).toBe("minimal");
@@ -1417,21 +1678,30 @@ describe("Edge cases", () => {
1417
1678
  expect(ast.hooks[0].timeout).toBeUndefined();
1418
1679
  });
1419
1680
  it("empty sections produce reasonable defaults", () => {
1420
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n memory {\n }\n settings {\n }\n}`;
1681
+ const src = `topology t : [pipeline] {\n orchestrator {
1682
+ model: opus\n
1683
+ handles: [a]
1684
+ }\n action a { kind: inline }\n flow { a -> a }\n memory {\n }\n settings {\n }\n}`;
1421
1685
  const ast = parse(src);
1422
1686
  expect(ast.memory).toEqual({});
1423
1687
  expect(ast.settings).toEqual({ allow: [], deny: [], ask: [] });
1424
1688
  });
1425
1689
  it("extremely long prompt block", () => {
1426
1690
  const longContent = "This is a very long line. ".repeat(500);
1427
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent verbose {\n model: opus\n prompt {\n ${longContent}\n }\n }\n flow { a -> verbose }\n}`;
1691
+ const src = `topology t : [pipeline] {\n orchestrator {
1692
+ model: opus\n
1693
+ handles: [a]
1694
+ }\n action a { kind: inline }\n agent verbose {\n model: opus\n prompt {\n ${longContent}\n }\n }\n flow { a -> verbose }\n}`;
1428
1695
  const ast = parse(src);
1429
1696
  const agent = ast.nodes.find((n) => n.id === "verbose");
1430
1697
  expect(agent.prompt).toBeDefined();
1431
1698
  expect(agent.prompt.length).toBeGreaterThan(1000);
1432
1699
  });
1433
1700
  it("flow with combined [when x.y == z, max 3]", () => {
1434
- const src = `topology t : [pipeline] {\n orchestrator { model: opus\n handles: [a] }\n action a { kind: inline }\n agent b { model: opus\n outputs { status: pass | fail } }\n agent c { model: opus }\n flow {\n c -> b [when b.status == fail, max 3]\n }\n}`;
1701
+ const src = `topology t : [pipeline] {\n orchestrator {
1702
+ model: opus\n
1703
+ handles: [a]
1704
+ }\n action a { kind: inline }\n agent b { model: opus\n outputs { status: pass | fail } }\n agent c { model: opus }\n flow {\n c -> b [when b.status == fail, max 3]\n }\n}`;
1435
1705
  const ast = parse(src);
1436
1706
  const edge = ast.edges.find((e) => e.from === "c" && e.to === "b");
1437
1707
  expect(edge).toBeDefined();
@@ -1465,7 +1735,10 @@ describe("Parse errors (should throw)", () => {
1465
1735
  describe("Validation errors", () => {
1466
1736
  it("V1 UniqueNames: two agents with the same id", () => {
1467
1737
  const src = `topology t : [pipeline] {
1468
- orchestrator { model: opus handles: [a] }
1738
+ orchestrator {
1739
+ model: opus
1740
+ handles: [a]
1741
+ }
1469
1742
  action a { kind: inline }
1470
1743
  agent dup { model: opus }
1471
1744
  agent dup { model: sonnet }
@@ -1479,7 +1752,10 @@ describe("Validation errors", () => {
1479
1752
  });
1480
1753
  it("V3 FlowReferences: flow edge referencing a non-existent node", () => {
1481
1754
  const src = `topology t : [pipeline] {
1482
- orchestrator { model: opus handles: [a] }
1755
+ orchestrator {
1756
+ model: opus
1757
+ handles: [a]
1758
+ }
1483
1759
  action a { kind: inline }
1484
1760
  agent real { model: opus }
1485
1761
  flow { a -> ghost }
@@ -1492,7 +1768,10 @@ describe("Validation errors", () => {
1492
1768
  });
1493
1769
  it("V14 ToolExclusivity: agent with both tools and disallowed-tools", () => {
1494
1770
  const src = `topology t : [pipeline] {
1495
- orchestrator { model: opus handles: [a] }
1771
+ orchestrator {
1772
+ model: opus
1773
+ handles: [a]
1774
+ }
1496
1775
  action a { kind: inline }
1497
1776
  agent confused {
1498
1777
  model: opus
@@ -1509,7 +1788,10 @@ describe("Validation errors", () => {
1509
1788
  });
1510
1789
  it("V6 LoopBound: back-edge without max N annotation", () => {
1511
1790
  const src = `topology t : [pipeline] {
1512
- orchestrator { model: opus handles: [a] }
1791
+ orchestrator {
1792
+ model: opus
1793
+ handles: [a]
1794
+ }
1513
1795
  action a { kind: inline }
1514
1796
  agent step1 { model: opus }
1515
1797
  agent step2 { model: opus }
@@ -1527,7 +1809,10 @@ describe("Validation errors", () => {
1527
1809
  });
1528
1810
  it("V6 NoSelfEdges: self-referencing edge without max bound", () => {
1529
1811
  const src = `topology t : [pipeline] {
1530
- orchestrator { model: opus handles: [a] }
1812
+ orchestrator {
1813
+ model: opus
1814
+ handles: [a]
1815
+ }
1531
1816
  action a { kind: inline }
1532
1817
  agent analyzer { model: opus }
1533
1818
  flow {
@@ -1543,7 +1828,10 @@ describe("Validation errors", () => {
1543
1828
  });
1544
1829
  it("V13 GateAnchors: gate with after referencing non-existent node", () => {
1545
1830
  const src = `topology t : [pipeline] {
1546
- orchestrator { model: opus handles: [a] }
1831
+ orchestrator {
1832
+ model: opus
1833
+ handles: [a]
1834
+ }
1547
1835
  action a { kind: inline }
1548
1836
  agent worker { model: opus }
1549
1837
  gates {
@@ -1567,7 +1855,10 @@ describe("Validation errors", () => {
1567
1855
  describe("Validation warnings", () => {
1568
1856
  it("V18 ModelNotInProvider: agent uses model not listed in any provider", () => {
1569
1857
  const src = `topology t : [pipeline] {
1570
- orchestrator { model: opus handles: [a] }
1858
+ orchestrator {
1859
+ model: opus
1860
+ handles: [a]
1861
+ }
1571
1862
  action a { kind: inline }
1572
1863
  agent worker {
1573
1864
  model: mystery-model
@@ -1588,7 +1879,10 @@ describe("Validation warnings", () => {
1588
1879
  });
1589
1880
  it("V22 FallbackNotInProvider: fallback chain model not in provider", () => {
1590
1881
  const src = `topology t : [pipeline] {
1591
- orchestrator { model: opus handles: [a] }
1882
+ orchestrator {
1883
+ model: opus
1884
+ handles: [a]
1885
+ }
1592
1886
  action a { kind: inline }
1593
1887
  agent worker {
1594
1888
  model: opus
@@ -1615,7 +1909,10 @@ describe("Validation warnings", () => {
1615
1909
  describe("Edge cases that should NOT throw", () => {
1616
1910
  it("agent with no tools parses fine", () => {
1617
1911
  const src = `topology t : [pipeline] {
1618
- orchestrator { model: opus handles: [a] }
1912
+ orchestrator {
1913
+ model: opus
1914
+ handles: [a]
1915
+ }
1619
1916
  action a { kind: inline }
1620
1917
  agent bare { model: opus }
1621
1918
  flow { a -> bare }
@@ -1627,7 +1924,10 @@ describe("Edge cases that should NOT throw", () => {
1627
1924
  });
1628
1925
  it("agent with empty prompt block", () => {
1629
1926
  const src = `topology t : [pipeline] {
1630
- orchestrator { model: opus handles: [a] }
1927
+ orchestrator {
1928
+ model: opus
1929
+ handles: [a]
1930
+ }
1631
1931
  action a { kind: inline }
1632
1932
  agent empty-prompt {
1633
1933
  model: opus
@@ -1642,7 +1942,10 @@ describe("Edge cases that should NOT throw", () => {
1642
1942
  });
1643
1943
  it("flow with no conditions (all unconditional edges)", () => {
1644
1944
  const src = `topology t : [pipeline] {
1645
- orchestrator { model: opus handles: [a] }
1945
+ orchestrator {
1946
+ model: opus
1947
+ handles: [a]
1948
+ }
1646
1949
  action a { kind: inline }
1647
1950
  agent s1 { model: opus }
1648
1951
  agent s2 { model: opus }
@@ -1661,7 +1964,10 @@ describe("Edge cases that should NOT throw", () => {
1661
1964
  });
1662
1965
  it("topology with no agents (just an orchestrator)", () => {
1663
1966
  const src = `topology t : [pipeline] {
1664
- orchestrator { model: opus handles: [a] }
1967
+ orchestrator {
1968
+ model: opus
1969
+ handles: [a]
1970
+ }
1665
1971
  action a { kind: inline }
1666
1972
  flow { a -> a }
1667
1973
  }`;
@@ -1741,7 +2047,10 @@ topology test-v12-invalid : [pipeline] {
1741
2047
  it("[max 3] alone (no when) should produce no V12 error", () => {
1742
2048
  const src = `
1743
2049
  topology test-v12-maxonly : [pipeline] {
1744
- orchestrator { model: sonnet handles: [intake] }
2050
+ orchestrator {
2051
+ model: sonnet
2052
+ handles: [intake]
2053
+ }
1745
2054
  action intake {}
1746
2055
  agent worker { model: sonnet }
1747
2056
  flow {
@@ -1776,7 +2085,10 @@ topology test-v12-whenonly : [pipeline] {
1776
2085
  describe("Flow parsing edge cases (Phase 2)", () => {
1777
2086
  it("chain with 3+ nodes: a -> b -> c -> d produces 3 edges", () => {
1778
2087
  const src = `topology t : [pipeline] {
1779
- orchestrator { model: opus handles: [a] }
2088
+ orchestrator {
2089
+ model: opus
2090
+ handles: [a]
2091
+ }
1780
2092
  action a { kind: inline }
1781
2093
  agent b { model: opus }
1782
2094
  agent c { model: opus }
@@ -1815,7 +2127,10 @@ describe("Flow parsing edge cases (Phase 2)", () => {
1815
2127
  });
1816
2128
  it("empty flow block produces empty edges array", () => {
1817
2129
  const src = `topology t : [pipeline] {
1818
- orchestrator { model: opus handles: [a] }
2130
+ orchestrator {
2131
+ model: opus
2132
+ handles: [a]
2133
+ }
1819
2134
  action a { kind: inline }
1820
2135
  flow {
1821
2136
  }
@@ -1825,9 +2140,15 @@ describe("Flow parsing edge cases (Phase 2)", () => {
1825
2140
  });
1826
2141
  it("single node (no arrow) produces no edges", () => {
1827
2142
  const src = `topology t : [pipeline] {
1828
- orchestrator { model: opus handles: [a] }
2143
+ orchestrator {
2144
+ model: opus
2145
+ handles: [a]
2146
+ }
1829
2147
  action a { kind: inline }
1830
- agent lonely { model: opus invocation: manual }
2148
+ agent lonely {
2149
+ model: opus
2150
+ invocation: manual
2151
+ }
1831
2152
  flow {
1832
2153
  }
1833
2154
  }`;
@@ -1841,7 +2162,10 @@ describe("Role prefix matching (Phase 2)", () => {
1841
2162
  roles {
1842
2163
  review: "Performs reviews"
1843
2164
  }
1844
- orchestrator { model: opus handles: [a] }
2165
+ orchestrator {
2166
+ model: opus
2167
+ handles: [a]
2168
+ }
1845
2169
  action a { kind: inline }
1846
2170
  agent review-security {
1847
2171
  model: opus
@@ -1859,7 +2183,10 @@ describe("Role prefix matching (Phase 2)", () => {
1859
2183
  review: "Generic reviewer"
1860
2184
  review-security: "Security reviewer"
1861
2185
  }
1862
- orchestrator { model: opus handles: [a] }
2186
+ orchestrator {
2187
+ model: opus
2188
+ handles: [a]
2189
+ }
1863
2190
  action a { kind: inline }
1864
2191
  agent review-security {
1865
2192
  model: opus
@@ -1876,7 +2203,10 @@ describe("Role prefix matching (Phase 2)", () => {
1876
2203
  roles {
1877
2204
  writer: "Writes content"
1878
2205
  }
1879
- orchestrator { model: opus handles: [a] }
2206
+ orchestrator {
2207
+ model: opus
2208
+ handles: [a]
2209
+ }
1880
2210
  action a { kind: inline }
1881
2211
  agent analyzer {
1882
2212
  model: opus
@@ -1961,7 +2291,10 @@ describe("Line number tracking", () => {
1961
2291
  // Gate after: a human node — cannot be wired as a SubagentStop hook
1962
2292
  // because human nodes do not register as subagent_types.
1963
2293
  const ast = parse(`topology t : [pipeline] {
1964
- orchestrator { model: opus handles: [a] }
2294
+ orchestrator {
2295
+ model: opus
2296
+ handles: [a]
2297
+ }
1965
2298
  action a { kind: inline }
1966
2299
  agent b { model: opus }
1967
2300
  human review-step {
@@ -1989,7 +2322,10 @@ describe("Line number tracking", () => {
1989
2322
  // Gate after: an agent node — compiles to a SubagentStop hook that
1990
2323
  // blocks (exit 2 prevents subagent from stopping). No warning needed.
1991
2324
  const ast = parse(`topology t : [pipeline] {
1992
- orchestrator { model: opus handles: [a] }
2325
+ orchestrator {
2326
+ model: opus
2327
+ handles: [a]
2328
+ }
1993
2329
  action a { kind: inline }
1994
2330
  agent b { model: opus }
1995
2331
  agent c { model: opus }
@@ -2056,7 +2392,10 @@ describe("Line number tracking", () => {
2056
2392
  });
2057
2393
  it("V87: orchestrator with no delegation field is accepted (defaults to subagent)", () => {
2058
2394
  const ast = parse(`topology t : [pipeline] {
2059
- orchestrator { model: opus handles: [a] }
2395
+ orchestrator {
2396
+ model: opus
2397
+ handles: [a]
2398
+ }
2060
2399
  action a { kind: inline }
2061
2400
  flow { a -> a }
2062
2401
  }`);
@@ -2071,7 +2410,10 @@ describe("Line number tracking", () => {
2071
2410
  describe("C12: env sub-map inside mcp-servers entries", () => {
2072
2411
  it("parses env block inside an mcp-server entry", () => {
2073
2412
  const src = `topology t : [pipeline] {
2074
- orchestrator { model: opus handles: [a] }
2413
+ orchestrator {
2414
+ model: opus
2415
+ handles: [a]
2416
+ }
2075
2417
  action a { kind: inline }
2076
2418
  agent w { model: opus }
2077
2419
  mcp-servers {
@@ -2097,7 +2439,10 @@ describe("C12: env sub-map inside mcp-servers entries", () => {
2097
2439
  });
2098
2440
  it("mcp-server without env block still works", () => {
2099
2441
  const src = `topology t : [pipeline] {
2100
- orchestrator { model: opus handles: [a] }
2442
+ orchestrator {
2443
+ model: opus
2444
+ handles: [a]
2445
+ }
2101
2446
  action a { kind: inline }
2102
2447
  agent w { model: opus }
2103
2448
  mcp-servers {
@@ -2115,7 +2460,10 @@ describe("C12: env sub-map inside mcp-servers entries", () => {
2115
2460
  });
2116
2461
  it("parses multiple mcp-servers with env blocks", () => {
2117
2462
  const src = `topology t : [pipeline] {
2118
- orchestrator { model: opus handles: [a] }
2463
+ orchestrator {
2464
+ model: opus
2465
+ handles: [a]
2466
+ }
2119
2467
  action a { kind: inline }
2120
2468
  agent w { model: opus }
2121
2469
  mcp-servers {
@@ -2142,7 +2490,10 @@ describe("C12: env sub-map inside mcp-servers entries", () => {
2142
2490
  describe("C13: extensions block inside hook bodies", () => {
2143
2491
  it("parses extensions inside a global hook", () => {
2144
2492
  const src = `topology t : [pipeline] {
2145
- orchestrator { model: opus handles: [a] }
2493
+ orchestrator {
2494
+ model: opus
2495
+ handles: [a]
2496
+ }
2146
2497
  action a { kind: inline }
2147
2498
  agent w { model: opus }
2148
2499
  hooks {
@@ -2168,7 +2519,10 @@ describe("C13: extensions block inside hook bodies", () => {
2168
2519
  });
2169
2520
  it("parses extensions inside a per-agent hook", () => {
2170
2521
  const src = `topology t : [pipeline] {
2171
- orchestrator { model: opus handles: [a] }
2522
+ orchestrator {
2523
+ model: opus
2524
+ handles: [a]
2525
+ }
2172
2526
  action a { kind: inline }
2173
2527
  agent w {
2174
2528
  model: opus
@@ -2195,7 +2549,10 @@ describe("C13: extensions block inside hook bodies", () => {
2195
2549
  });
2196
2550
  it("hook without extensions still works", () => {
2197
2551
  const src = `topology t : [pipeline] {
2198
- orchestrator { model: opus handles: [a] }
2552
+ orchestrator {
2553
+ model: opus
2554
+ handles: [a]
2555
+ }
2199
2556
  action a { kind: inline }
2200
2557
  agent w { model: opus }
2201
2558
  hooks {
@@ -2215,7 +2572,10 @@ describe("C13: extensions block inside hook bodies", () => {
2215
2572
  describe("C14: top-level extensions block in topology body", () => {
2216
2573
  it("parses a top-level extensions block", () => {
2217
2574
  const src = `topology t : [pipeline] {
2218
- orchestrator { model: opus handles: [a] }
2575
+ orchestrator {
2576
+ model: opus
2577
+ handles: [a]
2578
+ }
2219
2579
  action a { kind: inline }
2220
2580
  agent w { model: opus }
2221
2581
  extensions {
@@ -2241,7 +2601,10 @@ describe("C14: top-level extensions block in topology body", () => {
2241
2601
  });
2242
2602
  it("topology without extensions block has no extensions field", () => {
2243
2603
  const src = `topology t : [pipeline] {
2244
- orchestrator { model: opus handles: [a] }
2604
+ orchestrator {
2605
+ model: opus
2606
+ handles: [a]
2607
+ }
2245
2608
  action a { kind: inline }
2246
2609
  agent w { model: opus }
2247
2610
  flow { a -> w }
@@ -2257,7 +2620,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2257
2620
  describe("Parsing", () => {
2258
2621
  it("agent with timeout parses correctly", () => {
2259
2622
  const src = `topology t : [pipeline] {
2260
- orchestrator { model: opus handles: [a] }
2623
+ orchestrator {
2624
+ model: opus
2625
+ handles: [a]
2626
+ }
2261
2627
  action a { kind: inline }
2262
2628
  agent w {
2263
2629
  model: sonnet
@@ -2271,7 +2637,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2271
2637
  });
2272
2638
  it("agent with on-fail: halt parses correctly", () => {
2273
2639
  const src = `topology t : [pipeline] {
2274
- orchestrator { model: opus handles: [a] }
2640
+ orchestrator {
2641
+ model: opus
2642
+ handles: [a]
2643
+ }
2275
2644
  action a { kind: inline }
2276
2645
  agent w {
2277
2646
  model: sonnet
@@ -2285,7 +2654,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2285
2654
  });
2286
2655
  it("agent with on-fail: retry parses correctly", () => {
2287
2656
  const src = `topology t : [pipeline] {
2288
- orchestrator { model: opus handles: [a] }
2657
+ orchestrator {
2658
+ model: opus
2659
+ handles: [a]
2660
+ }
2289
2661
  action a { kind: inline }
2290
2662
  agent w {
2291
2663
  model: sonnet
@@ -2299,7 +2671,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2299
2671
  });
2300
2672
  it("agent with on-fail: skip parses correctly", () => {
2301
2673
  const src = `topology t : [pipeline] {
2302
- orchestrator { model: opus handles: [a] }
2674
+ orchestrator {
2675
+ model: opus
2676
+ handles: [a]
2677
+ }
2303
2678
  action a { kind: inline }
2304
2679
  agent w {
2305
2680
  model: sonnet
@@ -2313,7 +2688,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2313
2688
  });
2314
2689
  it("agent with on-fail: fallback parses correctly", () => {
2315
2690
  const src = `topology t : [pipeline] {
2316
- orchestrator { model: opus handles: [a] }
2691
+ orchestrator {
2692
+ model: opus
2693
+ handles: [a]
2694
+ }
2317
2695
  action a { kind: inline }
2318
2696
  agent w {
2319
2697
  model: sonnet
@@ -2330,7 +2708,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2330
2708
  });
2331
2709
  it("simple retry: 3 still works (backward compat)", () => {
2332
2710
  const src = `topology t : [pipeline] {
2333
- orchestrator { model: opus handles: [a] }
2711
+ orchestrator {
2712
+ model: opus
2713
+ handles: [a]
2714
+ }
2334
2715
  action a { kind: inline }
2335
2716
  agent w {
2336
2717
  model: sonnet
@@ -2344,7 +2725,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2344
2725
  });
2345
2726
  it("retry block parses all fields", () => {
2346
2727
  const src = `topology t : [pipeline] {
2347
- orchestrator { model: opus handles: [a] }
2728
+ orchestrator {
2729
+ model: opus
2730
+ handles: [a]
2731
+ }
2348
2732
  action a { kind: inline }
2349
2733
  agent w {
2350
2734
  model: sonnet
@@ -2372,7 +2756,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2372
2756
  });
2373
2757
  it("action with timeout and on-fail parses correctly", () => {
2374
2758
  const src = `topology t : [pipeline] {
2375
- orchestrator { model: opus handles: [a] }
2759
+ orchestrator {
2760
+ model: opus
2761
+ handles: [a]
2762
+ }
2376
2763
  action a {
2377
2764
  kind: inline
2378
2765
  timeout: 30s
@@ -2389,7 +2776,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2389
2776
  });
2390
2777
  it("gate with timeout parses correctly", () => {
2391
2778
  const src = `topology t : [pipeline] {
2392
- orchestrator { model: opus handles: [a] }
2779
+ orchestrator {
2780
+ model: opus
2781
+ handles: [a]
2782
+ }
2393
2783
  action a { kind: inline }
2394
2784
  agent w { model: sonnet }
2395
2785
  gates {
@@ -2560,7 +2950,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2560
2950
  nodes: [
2561
2951
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
2562
2952
  { id: "intake", type: "action", label: "I" },
2563
- { id: "worker", type: "agent", label: "W", model: "sonnet", retry: { max: 3, backoff: "random" } },
2953
+ { id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
2954
+ max: 3,
2955
+ backoff: "random"
2956
+ } },
2564
2957
  ],
2565
2958
  });
2566
2959
  const results = validate(ast);
@@ -2573,7 +2966,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2573
2966
  nodes: [
2574
2967
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
2575
2968
  { id: "intake", type: "action", label: "I" },
2576
- { id: "worker", type: "agent", label: "W", model: "sonnet", retry: { max: 3, interval: "forever" } },
2969
+ { id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
2970
+ max: 3,
2971
+ interval: "forever"
2972
+ } },
2577
2973
  ],
2578
2974
  });
2579
2975
  const results = validate(ast);
@@ -2586,7 +2982,10 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2586
2982
  nodes: [
2587
2983
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
2588
2984
  { id: "intake", type: "action", label: "I" },
2589
- { id: "worker", type: "agent", label: "W", model: "sonnet", retry: { max: 3, maxInterval: "nope" } },
2985
+ { id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
2986
+ max: 3,
2987
+ maxInterval: "nope"
2988
+ } },
2590
2989
  ],
2591
2990
  });
2592
2991
  const results = validate(ast);
@@ -2599,7 +2998,14 @@ describe("Error Handling (timeout, on-fail, retry block)", () => {
2599
2998
  nodes: [
2600
2999
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
2601
3000
  { id: "intake", type: "action", label: "I" },
2602
- { id: "worker", type: "agent", label: "W", model: "sonnet", retry: { max: 3, backoff: "exponential", interval: "1s", maxInterval: "60s", jitter: true, nonRetryable: ["auth-error"] } },
3001
+ { id: "worker", type: "agent", label: "W", model: "sonnet", retry: {
3002
+ max: 3,
3003
+ backoff: "exponential",
3004
+ interval: "1s",
3005
+ maxInterval: "60s",
3006
+ jitter: true,
3007
+ nonRetryable: ["auth-error"]
3008
+ } },
2603
3009
  ],
2604
3010
  });
2605
3011
  const results = validate(ast);
@@ -2904,7 +3310,10 @@ topology code-review : [pipeline] {
2904
3310
  describe("Wave 2: Error edges (-x->)", () => {
2905
3311
  it("parses -x-> as an error edge with isError: true", () => {
2906
3312
  const src = `topology t : [pipeline] {
2907
- orchestrator { model: opus handles: [a] }
3313
+ orchestrator {
3314
+ model: opus
3315
+ handles: [a]
3316
+ }
2908
3317
  action a { kind: inline }
2909
3318
  agent analyzer { model: opus }
2910
3319
  agent error-handler { model: opus }
@@ -2921,7 +3330,10 @@ describe("Wave 2: Error edges (-x->)", () => {
2921
3330
  });
2922
3331
  it("parses -x[timeout]-> as a typed error edge", () => {
2923
3332
  const src = `topology t : [pipeline] {
2924
- orchestrator { model: opus handles: [a] }
3333
+ orchestrator {
3334
+ model: opus
3335
+ handles: [a]
3336
+ }
2925
3337
  action a { kind: inline }
2926
3338
  agent analyzer { model: opus }
2927
3339
  agent fallback { model: opus }
@@ -2938,7 +3350,10 @@ describe("Wave 2: Error edges (-x->)", () => {
2938
3350
  });
2939
3351
  it("parses -x[auth-error]-> with hyphenated error type", () => {
2940
3352
  const src = `topology t : [pipeline] {
2941
- orchestrator { model: opus handles: [a] }
3353
+ orchestrator {
3354
+ model: opus
3355
+ handles: [a]
3356
+ }
2942
3357
  action a { kind: inline }
2943
3358
  agent caller { model: opus }
2944
3359
  agent auth-fallback { model: opus }
@@ -2955,7 +3370,10 @@ describe("Wave 2: Error edges (-x->)", () => {
2955
3370
  });
2956
3371
  it("normal edges do not have isError set", () => {
2957
3372
  const src = `topology t : [pipeline] {
2958
- orchestrator { model: opus handles: [a] }
3373
+ orchestrator {
3374
+ model: opus
3375
+ handles: [a]
3376
+ }
2959
3377
  action a { kind: inline }
2960
3378
  agent b { model: opus }
2961
3379
  flow {
@@ -2969,7 +3387,10 @@ describe("Wave 2: Error edges (-x->)", () => {
2969
3387
  });
2970
3388
  it("mixed normal and error edges on same node", () => {
2971
3389
  const src = `topology t : [pipeline] {
2972
- orchestrator { model: opus handles: [a] }
3390
+ orchestrator {
3391
+ model: opus
3392
+ handles: [a]
3393
+ }
2973
3394
  action a { kind: inline }
2974
3395
  agent analyzer { model: opus }
2975
3396
  agent reviewer { model: opus }
@@ -2992,7 +3413,10 @@ describe("Wave 2: Error edges (-x->)", () => {
2992
3413
  describe("Wave 2: [tolerance] edge attribute", () => {
2993
3414
  it("parses [tolerance: 2] as integer tolerance", () => {
2994
3415
  const src = `topology t : [pipeline] {
2995
- orchestrator { model: opus handles: [a] }
3416
+ orchestrator {
3417
+ model: opus
3418
+ handles: [a]
3419
+ }
2996
3420
  action a { kind: inline }
2997
3421
  agent b { model: opus }
2998
3422
  agent c { model: opus }
@@ -3010,7 +3434,10 @@ describe("Wave 2: [tolerance] edge attribute", () => {
3010
3434
  });
3011
3435
  it("parses [tolerance: 33%] as percentage string", () => {
3012
3436
  const src = `topology t : [pipeline] {
3013
- orchestrator { model: opus handles: [a] }
3437
+ orchestrator {
3438
+ model: opus
3439
+ handles: [a]
3440
+ }
3014
3441
  action a { kind: inline }
3015
3442
  agent b { model: opus }
3016
3443
  agent c { model: opus }
@@ -3029,7 +3456,10 @@ describe("Wave 2: [tolerance] edge attribute", () => {
3029
3456
  describe("Wave 2: [race] edge attribute", () => {
3030
3457
  it("parses [race] as boolean edge attribute", () => {
3031
3458
  const src = `topology t : [pipeline] {
3032
- orchestrator { model: opus handles: [a] }
3459
+ orchestrator {
3460
+ model: opus
3461
+ handles: [a]
3462
+ }
3033
3463
  action a { kind: inline }
3034
3464
  agent fast { model: opus }
3035
3465
  agent slow { model: opus }
@@ -3048,7 +3478,10 @@ describe("Wave 2: [race] edge attribute", () => {
3048
3478
  describe("Wave 2: [wait N] edge attribute", () => {
3049
3479
  it("parses [wait 30s] as inline timer", () => {
3050
3480
  const src = `topology t : [pipeline] {
3051
- orchestrator { model: opus handles: [a] }
3481
+ orchestrator {
3482
+ model: opus
3483
+ handles: [a]
3484
+ }
3052
3485
  action a { kind: inline }
3053
3486
  agent notifier { model: opus }
3054
3487
  flow {
@@ -3062,7 +3495,10 @@ describe("Wave 2: [wait N] edge attribute", () => {
3062
3495
  });
3063
3496
  it("parses [wait 5m] with minute duration", () => {
3064
3497
  const src = `topology t : [pipeline] {
3065
- orchestrator { model: opus handles: [a] }
3498
+ orchestrator {
3499
+ model: opus
3500
+ handles: [a]
3501
+ }
3066
3502
  action a { kind: inline }
3067
3503
  agent notifier { model: opus }
3068
3504
  flow {
@@ -3078,7 +3514,10 @@ describe("Wave 2: [wait N] edge attribute", () => {
3078
3514
  describe("Wave 2: join field on agent/action", () => {
3079
3515
  it("parses join: all on agent", () => {
3080
3516
  const src = `topology t : [pipeline] {
3081
- orchestrator { model: opus handles: [a] }
3517
+ orchestrator {
3518
+ model: opus
3519
+ handles: [a]
3520
+ }
3082
3521
  action a { kind: inline }
3083
3522
  agent synthesizer {
3084
3523
  model: opus
@@ -3092,7 +3531,10 @@ describe("Wave 2: join field on agent/action", () => {
3092
3531
  });
3093
3532
  it("parses join: any on agent", () => {
3094
3533
  const src = `topology t : [pipeline] {
3095
- orchestrator { model: opus handles: [a] }
3534
+ orchestrator {
3535
+ model: opus
3536
+ handles: [a]
3537
+ }
3096
3538
  action a { kind: inline }
3097
3539
  agent fast-consumer {
3098
3540
  model: opus
@@ -3106,7 +3548,10 @@ describe("Wave 2: join field on agent/action", () => {
3106
3548
  });
3107
3549
  it("parses join: all-done on agent", () => {
3108
3550
  const src = `topology t : [pipeline] {
3109
- orchestrator { model: opus handles: [a] }
3551
+ orchestrator {
3552
+ model: opus
3553
+ handles: [a]
3554
+ }
3110
3555
  action a { kind: inline }
3111
3556
  agent collector {
3112
3557
  model: opus
@@ -3120,7 +3565,10 @@ describe("Wave 2: join field on agent/action", () => {
3120
3565
  });
3121
3566
  it("parses join on action", () => {
3122
3567
  const src = `topology t : [pipeline] {
3123
- orchestrator { model: opus handles: [merge-step] }
3568
+ orchestrator {
3569
+ model: opus
3570
+ handles: [merge-step]
3571
+ }
3124
3572
  action merge-step {
3125
3573
  kind: inline
3126
3574
  join: none-failed
@@ -3140,7 +3588,10 @@ describe("Wave 2: Topology meta timeout and error-handler", () => {
3140
3588
  version: "1.0.0"
3141
3589
  timeout: 30m
3142
3590
  }
3143
- orchestrator { model: opus handles: [a] }
3591
+ orchestrator {
3592
+ model: opus
3593
+ handles: [a]
3594
+ }
3144
3595
  action a { kind: inline }
3145
3596
  flow { a -> a }
3146
3597
  }`;
@@ -3153,7 +3604,10 @@ describe("Wave 2: Topology meta timeout and error-handler", () => {
3153
3604
  version: "1.0.0"
3154
3605
  error-handler: global-error-agent
3155
3606
  }
3156
- orchestrator { model: opus handles: [a] }
3607
+ orchestrator {
3608
+ model: opus
3609
+ handles: [a]
3610
+ }
3157
3611
  action a { kind: inline }
3158
3612
  agent global-error-agent { model: opus }
3159
3613
  flow { a -> global-error-agent }
@@ -3168,7 +3622,10 @@ describe("Wave 2: Topology meta timeout and error-handler", () => {
3168
3622
  timeout: 2h
3169
3623
  error-handler: catch-all
3170
3624
  }
3171
- orchestrator { model: opus handles: [a] }
3625
+ orchestrator {
3626
+ model: opus
3627
+ handles: [a]
3628
+ }
3172
3629
  action a { kind: inline }
3173
3630
  agent catch-all { model: opus }
3174
3631
  flow { a -> catch-all }
@@ -3409,7 +3866,10 @@ describe("Wave 2: Validator rules V39-V45", () => {
3409
3866
  describe("Wave 2: Backward compatibility", () => {
3410
3867
  it("existing flow syntax with [when] and [max] still works", () => {
3411
3868
  const src = `topology t : [pipeline] {
3412
- orchestrator { model: opus handles: [a] }
3869
+ orchestrator {
3870
+ model: opus
3871
+ handles: [a]
3872
+ }
3413
3873
  action a { kind: inline }
3414
3874
  agent b { model: opus
3415
3875
  outputs { verdict: yes | no } }
@@ -3429,7 +3889,10 @@ describe("Wave 2: Backward compatibility", () => {
3429
3889
  });
3430
3890
  it("existing fan-out syntax still works", () => {
3431
3891
  const src = `topology t : [pipeline] {
3432
- orchestrator { model: opus handles: [start] }
3892
+ orchestrator {
3893
+ model: opus
3894
+ handles: [start]
3895
+ }
3433
3896
  action start { kind: inline }
3434
3897
  agent x { model: opus }
3435
3898
  agent y { model: opus }
@@ -3446,7 +3909,10 @@ describe("Wave 2: Backward compatibility", () => {
3446
3909
  });
3447
3910
  it("existing chain syntax a -> b -> c still works", () => {
3448
3911
  const src = `topology t : [pipeline] {
3449
- orchestrator { model: opus handles: [a] }
3912
+ orchestrator {
3913
+ model: opus
3914
+ handles: [a]
3915
+ }
3450
3916
  action a { kind: inline }
3451
3917
  agent b { model: opus }
3452
3918
  agent c { model: opus }
@@ -3461,7 +3927,10 @@ describe("Wave 2: Backward compatibility", () => {
3461
3927
  });
3462
3928
  it("existing [per id] syntax still works", () => {
3463
3929
  const src = `topology t : [pipeline] {
3464
- orchestrator { model: opus handles: [a] }
3930
+ orchestrator {
3931
+ model: opus
3932
+ handles: [a]
3933
+ }
3465
3934
  action a { kind: inline }
3466
3935
  agent b { model: opus }
3467
3936
  flow {
@@ -3475,7 +3944,10 @@ describe("Wave 2: Backward compatibility", () => {
3475
3944
  });
3476
3945
  it("existing [when ..., max N] combined syntax still works", () => {
3477
3946
  const src = `topology t : [pipeline] {
3478
- orchestrator { model: opus handles: [a] }
3947
+ orchestrator {
3948
+ model: opus
3949
+ handles: [a]
3950
+ }
3479
3951
  action a { kind: inline }
3480
3952
  agent b { model: opus
3481
3953
  outputs { status: pass | fail } }
@@ -3881,10 +4353,13 @@ topology test : [pipeline] {
3881
4353
  // =========================================================================
3882
4354
  describe("Sensitive and Secret env values", () => {
3883
4355
  const minimalPrefix = `topology t : [pipeline] {
3884
- orchestrator { model: opus handles: [a] }
3885
- action a { kind: inline }
3886
- flow { a -> a }`;
3887
- it("parses sensitive env var reference as SensitiveValue", () => {
4356
+ orchestrator {
4357
+ model: opus
4358
+ handles: [a]
4359
+ }
4360
+ action a { kind: inline }
4361
+ flow { a -> a }`;
4362
+ it("parses sensitive env var reference as SensitiveValue", () => {
3888
4363
  const src = `${minimalPrefix}
3889
4364
  env {
3890
4365
  API_KEY: sensitive "\${API_KEY}"
@@ -3958,7 +4433,10 @@ describe("Sensitive and Secret env values", () => {
3958
4433
  });
3959
4434
  describe("V51 - Sensitive literal warning", () => {
3960
4435
  const minimalPrefix = `topology t : [pipeline] {
3961
- orchestrator { model: opus handles: [a] }
4436
+ orchestrator {
4437
+ model: opus
4438
+ handles: [a]
4439
+ }
3962
4440
  action a { kind: inline }
3963
4441
  flow { a -> a }`;
3964
4442
  it("warns when sensitive is used with a literal string", () => {
@@ -4010,7 +4488,10 @@ describe("V51 - Sensitive literal warning", () => {
4010
4488
  });
4011
4489
  describe("V52 - Secret URI scheme validation", () => {
4012
4490
  const minimalPrefix = `topology t : [pipeline] {
4013
- orchestrator { model: opus handles: [a] }
4491
+ orchestrator {
4492
+ model: opus
4493
+ handles: [a]
4494
+ }
4014
4495
  action a { kind: inline }
4015
4496
  flow { a -> a }`;
4016
4497
  it("errors on unknown secret URI scheme", () => {
@@ -4058,7 +4539,10 @@ describe("V52 - Secret URI scheme validation", () => {
4058
4539
  describe("Wave 3: Observability block parsing", () => {
4059
4540
  it("full observability block parses correctly", () => {
4060
4541
  const src = `topology t : [pipeline] {
4061
- orchestrator { model: opus handles: [a] }
4542
+ orchestrator {
4543
+ model: opus
4544
+ handles: [a]
4545
+ }
4062
4546
  action a { kind: inline }
4063
4547
  flow { a -> a }
4064
4548
  observability {
@@ -4101,7 +4585,10 @@ describe("Wave 3: Observability block parsing", () => {
4101
4585
  });
4102
4586
  it("observability with only capture sub-block", () => {
4103
4587
  const src = `topology t : [pipeline] {
4104
- orchestrator { model: opus handles: [a] }
4588
+ orchestrator {
4589
+ model: opus
4590
+ handles: [a]
4591
+ }
4105
4592
  action a { kind: inline }
4106
4593
  flow { a -> a }
4107
4594
  observability {
@@ -4129,7 +4616,10 @@ describe("Wave 3: Observability block parsing", () => {
4129
4616
  });
4130
4617
  it("observability with only spans sub-block", () => {
4131
4618
  const src = `topology t : [pipeline] {
4132
- orchestrator { model: opus handles: [a] }
4619
+ orchestrator {
4620
+ model: opus
4621
+ handles: [a]
4622
+ }
4133
4623
  action a { kind: inline }
4134
4624
  flow { a -> a }
4135
4625
  observability {
@@ -4154,7 +4644,10 @@ describe("Wave 3: Observability block parsing", () => {
4154
4644
  });
4155
4645
  it("defaults are applied when fields omitted", () => {
4156
4646
  const src = `topology t : [pipeline] {
4157
- orchestrator { model: opus handles: [a] }
4647
+ orchestrator {
4648
+ model: opus
4649
+ handles: [a]
4650
+ }
4158
4651
  action a { kind: inline }
4159
4652
  flow { a -> a }
4160
4653
  observability {
@@ -4179,7 +4672,10 @@ describe("Wave 3: Observability block parsing", () => {
4179
4672
  });
4180
4673
  it("backward compat: topology without observability -> null", () => {
4181
4674
  const src = `topology t : [pipeline] {
4182
- orchestrator { model: opus handles: [a] }
4675
+ orchestrator {
4676
+ model: opus
4677
+ handles: [a]
4678
+ }
4183
4679
  action a { kind: inline }
4184
4680
  flow { a -> a }
4185
4681
  }`;
@@ -4188,7 +4684,10 @@ describe("Wave 3: Observability block parsing", () => {
4188
4684
  });
4189
4685
  it("observability with enabled: false", () => {
4190
4686
  const src = `topology t : [pipeline] {
4191
- orchestrator { model: opus handles: [a] }
4687
+ orchestrator {
4688
+ model: opus
4689
+ handles: [a]
4690
+ }
4192
4691
  action a { kind: inline }
4193
4692
  flow { a -> a }
4194
4693
  observability {
@@ -4849,7 +5348,10 @@ topology simple : [pipeline] {
4849
5348
  describe("Wave 5 — Compensation / Saga (F27)", () => {
4850
5349
  it("parses agent with compensates field", () => {
4851
5350
  const src = `topology t : [pipeline] {
4852
- orchestrator { model: opus handles: [a] }
5351
+ orchestrator {
5352
+ model: opus
5353
+ handles: [a]
5354
+ }
4853
5355
  action a { kind: inline }
4854
5356
  agent deployer {
4855
5357
  model: sonnet
@@ -4867,7 +5369,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
4867
5369
  });
4868
5370
  it("agent without compensates has no compensates field", () => {
4869
5371
  const src = `topology t : [pipeline] {
4870
- orchestrator { model: opus handles: [a] }
5372
+ orchestrator {
5373
+ model: opus
5374
+ handles: [a]
5375
+ }
4871
5376
  action a { kind: inline }
4872
5377
  agent worker {
4873
5378
  model: sonnet
@@ -4881,7 +5386,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
4881
5386
  });
4882
5387
  it("V58: compensates referencing non-existent agent -> error", () => {
4883
5388
  const src = `topology t : [pipeline] {
4884
- orchestrator { model: opus handles: [a] }
5389
+ orchestrator {
5390
+ model: opus
5391
+ handles: [a]
5392
+ }
4885
5393
  action a { kind: inline }
4886
5394
  agent rollback {
4887
5395
  model: sonnet
@@ -4899,7 +5407,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
4899
5407
  });
4900
5408
  it("V58: compensates referencing existing agent -> no error", () => {
4901
5409
  const src = `topology t : [pipeline] {
4902
- orchestrator { model: opus handles: [a] }
5410
+ orchestrator {
5411
+ model: opus
5412
+ handles: [a]
5413
+ }
4903
5414
  action a { kind: inline }
4904
5415
  agent deployer {
4905
5416
  model: sonnet
@@ -4917,7 +5428,10 @@ describe("Wave 5 — Compensation / Saga (F27)", () => {
4917
5428
  });
4918
5429
  it("backward compat: existing topologies without compensates still work", () => {
4919
5430
  const src = `topology t : [pipeline] {
4920
- orchestrator { model: opus handles: [a] }
5431
+ orchestrator {
5432
+ model: opus
5433
+ handles: [a]
5434
+ }
4921
5435
  action a { kind: inline }
4922
5436
  agent w1 { model: sonnet }
4923
5437
  agent w2 { model: opus }
@@ -4942,7 +5456,10 @@ describe("Circuit Breaker (F26)", () => {
4942
5456
  describe("Parsing", () => {
4943
5457
  it("agent with circuit-breaker block parses correctly", () => {
4944
5458
  const src = `topology t : [pipeline] {
4945
- orchestrator { model: opus handles: [a] }
5459
+ orchestrator {
5460
+ model: opus
5461
+ handles: [a]
5462
+ }
4946
5463
  action a { kind: inline }
4947
5464
  agent w {
4948
5465
  model: sonnet
@@ -4963,7 +5480,10 @@ describe("Circuit Breaker (F26)", () => {
4963
5480
  });
4964
5481
  it("agent without circuit-breaker has no circuitBreaker field", () => {
4965
5482
  const src = `topology t : [pipeline] {
4966
- orchestrator { model: opus handles: [a] }
5483
+ orchestrator {
5484
+ model: opus
5485
+ handles: [a]
5486
+ }
4967
5487
  action a { kind: inline }
4968
5488
  agent w { model: sonnet }
4969
5489
  flow { a -> w }
@@ -5021,7 +5541,11 @@ describe("Circuit Breaker (F26)", () => {
5021
5541
  nodes: [
5022
5542
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
5023
5543
  { id: "intake", type: "action", label: "Intake" },
5024
- { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: { threshold: 5, window: "5m", cooldown: "30s" } },
5544
+ { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
5545
+ threshold: 5,
5546
+ window: "5m",
5547
+ cooldown: "30s"
5548
+ } },
5025
5549
  ],
5026
5550
  });
5027
5551
  const results = validate(ast);
@@ -5033,7 +5557,11 @@ describe("Circuit Breaker (F26)", () => {
5033
5557
  nodes: [
5034
5558
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
5035
5559
  { id: "intake", type: "action", label: "Intake" },
5036
- { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: { threshold: 0, window: "5m", cooldown: "30s" } },
5560
+ { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
5561
+ threshold: 0,
5562
+ window: "5m",
5563
+ cooldown: "30s"
5564
+ } },
5037
5565
  ],
5038
5566
  });
5039
5567
  const results = validate(ast);
@@ -5046,7 +5574,11 @@ describe("Circuit Breaker (F26)", () => {
5046
5574
  nodes: [
5047
5575
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
5048
5576
  { id: "intake", type: "action", label: "Intake" },
5049
- { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: { threshold: 3, window: "five-min", cooldown: "30s" } },
5577
+ { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
5578
+ threshold: 3,
5579
+ window: "five-min",
5580
+ cooldown: "30s"
5581
+ } },
5050
5582
  ],
5051
5583
  });
5052
5584
  const results = validate(ast);
@@ -5059,7 +5591,11 @@ describe("Circuit Breaker (F26)", () => {
5059
5591
  nodes: [
5060
5592
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
5061
5593
  { id: "intake", type: "action", label: "Intake" },
5062
- { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: { threshold: 3, window: "5m", cooldown: "invalid" } },
5594
+ { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
5595
+ threshold: 3,
5596
+ window: "5m",
5597
+ cooldown: "invalid"
5598
+ } },
5063
5599
  ],
5064
5600
  });
5065
5601
  const results = validate(ast);
@@ -5072,7 +5608,11 @@ describe("Circuit Breaker (F26)", () => {
5072
5608
  nodes: [
5073
5609
  { id: "orchestrator", type: "orchestrator", label: "O", model: "opus", handles: ["intake"] },
5074
5610
  { id: "intake", type: "action", label: "Intake" },
5075
- { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: { threshold: -1, window: "bad", cooldown: "worse" } },
5611
+ { id: "worker", type: "agent", label: "Worker", model: "sonnet", circuitBreaker: {
5612
+ threshold: -1,
5613
+ window: "bad",
5614
+ cooldown: "worse"
5615
+ } },
5076
5616
  ],
5077
5617
  });
5078
5618
  const results = validate(ast);
@@ -5093,7 +5633,10 @@ describe("Circuit Breaker (F26)", () => {
5093
5633
  describe("Human node type (F32)", () => {
5094
5634
  it("parses human node with all fields", () => {
5095
5635
  const src = `topology t : [pipeline] {
5096
- orchestrator { model: opus handles: [intake] }
5636
+ orchestrator {
5637
+ model: opus
5638
+ handles: [intake]
5639
+ }
5097
5640
  action intake { kind: inline }
5098
5641
  human approval {
5099
5642
  description: "Human reviews and approves the deployment plan"
@@ -5113,7 +5656,10 @@ describe("Human node type (F32)", () => {
5113
5656
  });
5114
5657
  it("human node appears in flow edges", () => {
5115
5658
  const src = `topology t : [pipeline] {
5116
- orchestrator { model: opus handles: [intake] }
5659
+ orchestrator {
5660
+ model: opus
5661
+ handles: [intake]
5662
+ }
5117
5663
  action intake { kind: inline }
5118
5664
  human review {
5119
5665
  description: "Review step"
@@ -5128,7 +5674,10 @@ describe("Human node type (F32)", () => {
5128
5674
  });
5129
5675
  it("V59: invalid on-timeout -> error", () => {
5130
5676
  const src = `topology t : [pipeline] {
5131
- orchestrator { model: opus handles: [intake] }
5677
+ orchestrator {
5678
+ model: opus
5679
+ handles: [intake]
5680
+ }
5132
5681
  action intake { kind: inline }
5133
5682
  human approval {
5134
5683
  timeout: 1h
@@ -5147,7 +5696,10 @@ describe("Human node type (F32)", () => {
5147
5696
  it("V59: valid on-timeout values pass", () => {
5148
5697
  for (const value of ["halt", "skip", "fallback backup-agent"]) {
5149
5698
  const src = `topology t : [pipeline] {
5150
- orchestrator { model: opus handles: [intake] }
5699
+ orchestrator {
5700
+ model: opus
5701
+ handles: [intake]
5702
+ }
5151
5703
  action intake { kind: inline }
5152
5704
  human approval {
5153
5705
  timeout: 1h
@@ -5163,7 +5715,10 @@ describe("Human node type (F32)", () => {
5163
5715
  });
5164
5716
  it("V30: invalid timeout on human node -> error", () => {
5165
5717
  const src = `topology t : [pipeline] {
5166
- orchestrator { model: opus handles: [intake] }
5718
+ orchestrator {
5719
+ model: opus
5720
+ handles: [intake]
5721
+ }
5167
5722
  action intake { kind: inline }
5168
5723
  human approval {
5169
5724
  timeout: forever
@@ -5179,7 +5734,10 @@ describe("Human node type (F32)", () => {
5179
5734
  });
5180
5735
  it("backward compat: existing topologies without human nodes still work", () => {
5181
5736
  const src = `topology t : [pipeline] {
5182
- orchestrator { model: opus handles: [a] }
5737
+ orchestrator {
5738
+ model: opus
5739
+ handles: [a]
5740
+ }
5183
5741
  action a { kind: inline }
5184
5742
  agent w1 { model: sonnet }
5185
5743
  agent w2 { model: opus }
@@ -5200,7 +5758,10 @@ describe("Human node type (F32)", () => {
5200
5758
  describe("F31: Quorum join (N-of-M)", () => {
5201
5759
  it("parses join: 2-of-3 on agent", () => {
5202
5760
  const src = `topology t : [pipeline] {
5203
- orchestrator { model: opus handles: [a] }
5761
+ orchestrator {
5762
+ model: opus
5763
+ handles: [a]
5764
+ }
5204
5765
  action a { kind: inline }
5205
5766
  agent synthesizer {
5206
5767
  model: opus
@@ -5214,7 +5775,10 @@ describe("F31: Quorum join (N-of-M)", () => {
5214
5775
  });
5215
5776
  it("parses join: 3-of-5 on agent", () => {
5216
5777
  const src = `topology t : [pipeline] {
5217
- orchestrator { model: opus handles: [a] }
5778
+ orchestrator {
5779
+ model: opus
5780
+ handles: [a]
5781
+ }
5218
5782
  action a { kind: inline }
5219
5783
  agent voter {
5220
5784
  model: opus
@@ -5285,7 +5849,10 @@ describe("F31: Quorum join (N-of-M)", () => {
5285
5849
  describe("F35: Weighted routing [weight N]", () => {
5286
5850
  it("parses [weight 0.7] as edge attribute", () => {
5287
5851
  const src = `topology t : [pipeline] {
5288
- orchestrator { model: opus handles: [a] }
5852
+ orchestrator {
5853
+ model: opus
5854
+ handles: [a]
5855
+ }
5289
5856
  action a { kind: inline }
5290
5857
  agent b { model: opus }
5291
5858
  flow {
@@ -5299,7 +5866,10 @@ describe("F35: Weighted routing [weight N]", () => {
5299
5866
  });
5300
5867
  it("parses [weight 1.0] as edge attribute", () => {
5301
5868
  const src = `topology t : [pipeline] {
5302
- orchestrator { model: opus handles: [a] }
5869
+ orchestrator {
5870
+ model: opus
5871
+ handles: [a]
5872
+ }
5303
5873
  action a { kind: inline }
5304
5874
  agent b { model: opus }
5305
5875
  flow {
@@ -5313,7 +5883,10 @@ describe("F35: Weighted routing [weight N]", () => {
5313
5883
  });
5314
5884
  it("parses [weight N] on fan-out edges", () => {
5315
5885
  const src = `topology t : [pipeline] {
5316
- orchestrator { model: opus handles: [router] }
5886
+ orchestrator {
5887
+ model: opus
5888
+ handles: [router]
5889
+ }
5317
5890
  action router { kind: inline }
5318
5891
  agent a { model: opus }
5319
5892
  agent b { model: opus }
@@ -5447,7 +6020,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5447
6020
  ttl: 7d
5448
6021
  }
5449
6022
 
5450
- orchestrator { model: opus handles: [intake] }
6023
+ orchestrator {
6024
+ model: opus
6025
+ handles: [intake]
6026
+ }
5451
6027
  action intake { kind: inline }
5452
6028
  agent worker { model: sonnet }
5453
6029
  flow { intake -> worker }
@@ -5466,7 +6042,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5466
6042
  version: "1.0.0"
5467
6043
  durable: true
5468
6044
  }
5469
- orchestrator { model: opus handles: [intake] }
6045
+ orchestrator {
6046
+ model: opus
6047
+ handles: [intake]
6048
+ }
5470
6049
  action intake { kind: inline }
5471
6050
  agent worker { model: sonnet }
5472
6051
  flow { intake -> worker }
@@ -5477,7 +6056,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5477
6056
  it("durable defaults to undefined when not set", () => {
5478
6057
  const src = `topology t : [pipeline] {
5479
6058
  meta { version: "1.0.0" }
5480
- orchestrator { model: opus handles: [intake] }
6059
+ orchestrator {
6060
+ model: opus
6061
+ handles: [intake]
6062
+ }
5481
6063
  action intake { kind: inline }
5482
6064
  agent worker { model: sonnet }
5483
6065
  flow { intake -> worker }
@@ -5488,7 +6070,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5488
6070
  it("checkpoint defaults to null when not present", () => {
5489
6071
  const src = `topology t : [pipeline] {
5490
6072
  meta { version: "1.0.0" }
5491
- orchestrator { model: opus handles: [intake] }
6073
+ orchestrator {
6074
+ model: opus
6075
+ handles: [intake]
6076
+ }
5492
6077
  action intake { kind: inline }
5493
6078
  agent worker { model: sonnet }
5494
6079
  flow { intake -> worker }
@@ -5502,7 +6087,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5502
6087
  backend: mongodb
5503
6088
  strategy: every-node
5504
6089
  }
5505
- orchestrator { model: opus handles: [intake] }
6090
+ orchestrator {
6091
+ model: opus
6092
+ handles: [intake]
6093
+ }
5506
6094
  action intake { kind: inline }
5507
6095
  agent worker { model: sonnet }
5508
6096
  flow { intake -> worker }
@@ -5521,7 +6109,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5521
6109
  backend: ${backend}
5522
6110
  strategy: every-node
5523
6111
  }
5524
- orchestrator { model: opus handles: [intake] }
6112
+ orchestrator {
6113
+ model: opus
6114
+ handles: [intake]
6115
+ }
5525
6116
  action intake { kind: inline }
5526
6117
  agent worker { model: sonnet }
5527
6118
  flow { intake -> worker }
@@ -5538,7 +6129,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5538
6129
  backend: postgres
5539
6130
  strategy: periodic
5540
6131
  }
5541
- orchestrator { model: opus handles: [intake] }
6132
+ orchestrator {
6133
+ model: opus
6134
+ handles: [intake]
6135
+ }
5542
6136
  action intake { kind: inline }
5543
6137
  agent worker { model: sonnet }
5544
6138
  flow { intake -> worker }
@@ -5557,7 +6151,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5557
6151
  strategy: every-node
5558
6152
  ttl: forever
5559
6153
  }
5560
- orchestrator { model: opus handles: [intake] }
6154
+ orchestrator {
6155
+ model: opus
6156
+ handles: [intake]
6157
+ }
5561
6158
  action intake { kind: inline }
5562
6159
  agent worker { model: sonnet }
5563
6160
  flow { intake -> worker }
@@ -5575,7 +6172,10 @@ describe("Checkpoint / Durable Execution (F34)", () => {
5575
6172
  strategy: every-node
5576
6173
  ttl: 7d
5577
6174
  }
5578
- orchestrator { model: opus handles: [intake] }
6175
+ orchestrator {
6176
+ model: opus
6177
+ handles: [intake]
6178
+ }
5579
6179
  action intake { kind: inline }
5580
6180
  agent worker { model: sonnet }
5581
6181
  flow { intake -> worker }
@@ -5601,7 +6201,10 @@ describe("Time-Travel / Replay (F37)", () => {
5601
6201
  branch: true
5602
6202
  }
5603
6203
  }
5604
- orchestrator { model: opus handles: [intake] }
6204
+ orchestrator {
6205
+ model: opus
6206
+ handles: [intake]
6207
+ }
5605
6208
  action intake { kind: inline }
5606
6209
  agent worker { model: sonnet }
5607
6210
  flow { intake -> worker }
@@ -5622,7 +6225,10 @@ describe("Time-Travel / Replay (F37)", () => {
5622
6225
  enabled: true
5623
6226
  }
5624
6227
  }
5625
- orchestrator { model: opus handles: [intake] }
6228
+ orchestrator {
6229
+ model: opus
6230
+ handles: [intake]
6231
+ }
5626
6232
  action intake { kind: inline }
5627
6233
  agent worker { model: sonnet }
5628
6234
  flow { intake -> worker }
@@ -5641,7 +6247,10 @@ describe("Time-Travel / Replay (F37)", () => {
5641
6247
  enabled: true
5642
6248
  }
5643
6249
  }
5644
- orchestrator { model: opus handles: [intake] }
6250
+ orchestrator {
6251
+ model: opus
6252
+ handles: [intake]
6253
+ }
5645
6254
  action intake { kind: inline }
5646
6255
  agent worker { model: sonnet }
5647
6256
  flow { intake -> worker }
@@ -5661,7 +6270,10 @@ describe("Time-Travel / Replay (F37)", () => {
5661
6270
  max-history: -5
5662
6271
  }
5663
6272
  }
5664
- orchestrator { model: opus handles: [intake] }
6273
+ orchestrator {
6274
+ model: opus
6275
+ handles: [intake]
6276
+ }
5665
6277
  action intake { kind: inline }
5666
6278
  agent worker { model: sonnet }
5667
6279
  flow { intake -> worker }
@@ -5680,7 +6292,10 @@ describe("Time-Travel / Replay (F37)", () => {
5680
6292
  enabled: true
5681
6293
  }
5682
6294
  }
5683
- orchestrator { model: opus handles: [intake] }
6295
+ orchestrator {
6296
+ model: opus
6297
+ handles: [intake]
6298
+ }
5684
6299
  action intake { kind: inline }
5685
6300
  agent worker { model: sonnet }
5686
6301
  flow { intake -> worker }
@@ -5710,7 +6325,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5710
6325
  depends-on: [research-notes]
5711
6326
  }
5712
6327
  }
5713
- orchestrator { model: opus handles: [intake] }
6328
+ orchestrator {
6329
+ model: opus
6330
+ handles: [intake]
6331
+ }
5714
6332
  action intake { kind: inline }
5715
6333
  agent researcher {
5716
6334
  model: sonnet
@@ -5740,7 +6358,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5740
6358
  });
5741
6359
  it("empty artifacts block returns empty array", () => {
5742
6360
  const src = `topology t : [pipeline] {
5743
- orchestrator { model: opus handles: [intake] }
6361
+ orchestrator {
6362
+ model: opus
6363
+ handles: [intake]
6364
+ }
5744
6365
  action intake { kind: inline }
5745
6366
  agent worker { model: sonnet }
5746
6367
  flow { intake -> worker }
@@ -5758,7 +6379,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5758
6379
  type: json
5759
6380
  }
5760
6381
  }
5761
- orchestrator { model: opus handles: [intake] }
6382
+ orchestrator {
6383
+ model: opus
6384
+ handles: [intake]
6385
+ }
5762
6386
  action intake { kind: inline }
5763
6387
  agent worker { model: sonnet }
5764
6388
  flow { intake -> worker }
@@ -5777,7 +6401,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5777
6401
  type: markdown
5778
6402
  }
5779
6403
  }
5780
- orchestrator { model: opus handles: [intake] }
6404
+ orchestrator {
6405
+ model: opus
6406
+ handles: [intake]
6407
+ }
5781
6408
  action intake { kind: inline }
5782
6409
  agent worker {
5783
6410
  model: sonnet
@@ -5799,7 +6426,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5799
6426
  type: markdown
5800
6427
  }
5801
6428
  }
5802
- orchestrator { model: opus handles: [intake] }
6429
+ orchestrator {
6430
+ model: opus
6431
+ handles: [intake]
6432
+ }
5803
6433
  action intake { kind: inline }
5804
6434
  agent worker {
5805
6435
  model: sonnet
@@ -5821,7 +6451,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5821
6451
  depends-on: [phantom]
5822
6452
  }
5823
6453
  }
5824
- orchestrator { model: opus handles: [intake] }
6454
+ orchestrator {
6455
+ model: opus
6456
+ handles: [intake]
6457
+ }
5825
6458
  action intake { kind: inline }
5826
6459
  agent worker { model: sonnet }
5827
6460
  flow { intake -> worker }
@@ -5843,7 +6476,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5843
6476
  depends-on: [notes]
5844
6477
  }
5845
6478
  }
5846
- orchestrator { model: opus handles: [intake] }
6479
+ orchestrator {
6480
+ model: opus
6481
+ handles: [intake]
6482
+ }
5847
6483
  action intake { kind: inline }
5848
6484
  agent writer {
5849
6485
  model: sonnet
@@ -5869,7 +6505,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5869
6505
  depends-on: [a]
5870
6506
  }
5871
6507
  }
5872
- orchestrator { model: opus handles: [intake] }
6508
+ orchestrator {
6509
+ model: opus
6510
+ handles: [intake]
6511
+ }
5873
6512
  action intake { kind: inline }
5874
6513
  agent worker { model: sonnet }
5875
6514
  flow { intake -> worker }
@@ -5896,7 +6535,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5896
6535
  depends-on: [clean]
5897
6536
  }
5898
6537
  }
5899
- orchestrator { model: opus handles: [intake] }
6538
+ orchestrator {
6539
+ model: opus
6540
+ handles: [intake]
6541
+ }
5900
6542
  action intake { kind: inline }
5901
6543
  agent worker { model: sonnet }
5902
6544
  flow { intake -> worker }
@@ -5913,7 +6555,10 @@ describe("Asset / Artifact Lineage (F38)", () => {
5913
6555
  describe("Reflection edge attribute (F33)", () => {
5914
6556
  it("parses [reflection] attribute — sets reflection to true", () => {
5915
6557
  const src = `topology t : [pipeline] {
5916
- orchestrator { model: opus handles: [intake] }
6558
+ orchestrator {
6559
+ model: opus
6560
+ handles: [intake]
6561
+ }
5917
6562
  action intake { kind: inline }
5918
6563
  agent writer { model: sonnet }
5919
6564
  agent evaluator { model: opus }
@@ -5931,7 +6576,10 @@ describe("Reflection edge attribute (F33)", () => {
5931
6576
  });
5932
6577
  it("V62: [reflection] without [max N] — error", () => {
5933
6578
  const src = `topology t : [pipeline] {
5934
- orchestrator { model: opus handles: [intake] }
6579
+ orchestrator {
6580
+ model: opus
6581
+ handles: [intake]
6582
+ }
5935
6583
  action intake { kind: inline }
5936
6584
  agent writer { model: sonnet }
5937
6585
  agent evaluator { model: opus }
@@ -5951,7 +6599,10 @@ describe("Reflection edge attribute (F33)", () => {
5951
6599
  });
5952
6600
  it("V62: [reflection] on non-back-edge — warning", () => {
5953
6601
  const src = `topology t : [pipeline] {
5954
- orchestrator { model: opus handles: [intake] }
6602
+ orchestrator {
6603
+ model: opus
6604
+ handles: [intake]
6605
+ }
5955
6606
  action intake { kind: inline }
5956
6607
  agent writer { model: sonnet }
5957
6608
  agent evaluator { model: opus }
@@ -5972,7 +6623,10 @@ describe("Reflection edge attribute (F33)", () => {
5972
6623
  describe("Group chat node (F36)", () => {
5973
6624
  it("parses group node with all fields", () => {
5974
6625
  const src = `topology t : [pipeline] {
5975
- orchestrator { model: opus handles: [intake] }
6626
+ orchestrator {
6627
+ model: opus
6628
+ handles: [intake]
6629
+ }
5976
6630
  action intake { kind: inline }
5977
6631
  agent architect { model: opus }
5978
6632
  agent critic { model: sonnet }
@@ -6000,7 +6654,10 @@ describe("Group chat node (F36)", () => {
6000
6654
  });
6001
6655
  it("group node appears in flow edges", () => {
6002
6656
  const src = `topology t : [pipeline] {
6003
- orchestrator { model: opus handles: [intake] }
6657
+ orchestrator {
6658
+ model: opus
6659
+ handles: [intake]
6660
+ }
6004
6661
  action intake { kind: inline }
6005
6662
  agent a1 { model: sonnet }
6006
6663
  agent a2 { model: sonnet }
@@ -6017,7 +6674,10 @@ describe("Group chat node (F36)", () => {
6017
6674
  });
6018
6675
  it("V63: members reference non-existent agents — error", () => {
6019
6676
  const src = `topology t : [pipeline] {
6020
- orchestrator { model: opus handles: [intake] }
6677
+ orchestrator {
6678
+ model: opus
6679
+ handles: [intake]
6680
+ }
6021
6681
  action intake { kind: inline }
6022
6682
  agent a1 { model: sonnet }
6023
6683
  group chat {
@@ -6034,7 +6694,10 @@ describe("Group chat node (F36)", () => {
6034
6694
  });
6035
6695
  it("V64: invalid speaker-selection — error", () => {
6036
6696
  const src = `topology t : [pipeline] {
6037
- orchestrator { model: opus handles: [intake] }
6697
+ orchestrator {
6698
+ model: opus
6699
+ handles: [intake]
6700
+ }
6038
6701
  action intake { kind: inline }
6039
6702
  agent a1 { model: sonnet }
6040
6703
  group chat {
@@ -6052,7 +6715,10 @@ describe("Group chat node (F36)", () => {
6052
6715
  });
6053
6716
  it("V65: max-rounds is 0 — error", () => {
6054
6717
  const src = `topology t : [pipeline] {
6055
- orchestrator { model: opus handles: [intake] }
6718
+ orchestrator {
6719
+ model: opus
6720
+ handles: [intake]
6721
+ }
6056
6722
  action intake { kind: inline }
6057
6723
  agent a1 { model: sonnet }
6058
6724
  group chat {
@@ -6069,7 +6735,10 @@ describe("Group chat node (F36)", () => {
6069
6735
  });
6070
6736
  it("group node with minimal fields (just members) — succeeds", () => {
6071
6737
  const src = `topology t : [pipeline] {
6072
- orchestrator { model: opus handles: [intake] }
6738
+ orchestrator {
6739
+ model: opus
6740
+ handles: [intake]
6741
+ }
6073
6742
  action intake { kind: inline }
6074
6743
  agent a1 { model: sonnet }
6075
6744
  agent a2 { model: sonnet }
@@ -6096,7 +6765,10 @@ describe("Group chat node (F36)", () => {
6096
6765
  describe("Rate limiting (F39)", () => {
6097
6766
  it("parses rate-limit: 60/min", () => {
6098
6767
  const src = `topology t : [pipeline] {
6099
- orchestrator { model: opus handles: [intake] }
6768
+ orchestrator {
6769
+ model: opus
6770
+ handles: [intake]
6771
+ }
6100
6772
  action intake { kind: inline }
6101
6773
  agent api-caller {
6102
6774
  model: sonnet
@@ -6111,7 +6783,10 @@ describe("Rate limiting (F39)", () => {
6111
6783
  });
6112
6784
  it("parses rate-limit: 1000/hour", () => {
6113
6785
  const src = `topology t : [pipeline] {
6114
- orchestrator { model: opus handles: [intake] }
6786
+ orchestrator {
6787
+ model: opus
6788
+ handles: [intake]
6789
+ }
6115
6790
  action intake { kind: inline }
6116
6791
  agent api-caller {
6117
6792
  model: sonnet
@@ -6125,7 +6800,10 @@ describe("Rate limiting (F39)", () => {
6125
6800
  });
6126
6801
  it("V66: invalid format rate-limit: fast — error", () => {
6127
6802
  const src = `topology t : [pipeline] {
6128
- orchestrator { model: opus handles: [intake] }
6803
+ orchestrator {
6804
+ model: opus
6805
+ handles: [intake]
6806
+ }
6129
6807
  action intake { kind: inline }
6130
6808
  agent api-caller {
6131
6809
  model: sonnet
@@ -6142,7 +6820,10 @@ describe("Rate limiting (F39)", () => {
6142
6820
  });
6143
6821
  it("V66: invalid unit rate-limit: 60/week — error", () => {
6144
6822
  const src = `topology t : [pipeline] {
6145
- orchestrator { model: opus handles: [intake] }
6823
+ orchestrator {
6824
+ model: opus
6825
+ handles: [intake]
6826
+ }
6146
6827
  action intake { kind: inline }
6147
6828
  agent api-caller {
6148
6829
  model: sonnet
@@ -6158,7 +6839,10 @@ describe("Rate limiting (F39)", () => {
6158
6839
  });
6159
6840
  it("V66: zero rate rate-limit: 0/min — error", () => {
6160
6841
  const src = `topology t : [pipeline] {
6161
- orchestrator { model: opus handles: [intake] }
6842
+ orchestrator {
6843
+ model: opus
6844
+ handles: [intake]
6845
+ }
6162
6846
  action intake { kind: inline }
6163
6847
  agent api-caller {
6164
6848
  model: sonnet
@@ -6175,7 +6859,10 @@ describe("Rate limiting (F39)", () => {
6175
6859
  it("valid rate-limit values pass validation", () => {
6176
6860
  for (const rl of ["1/sec", "60/min", "1000/hour", "10000/day"]) {
6177
6861
  const src = `topology t : [pipeline] {
6178
- orchestrator { model: opus handles: [intake] }
6862
+ orchestrator {
6863
+ model: opus
6864
+ handles: [intake]
6865
+ }
6179
6866
  action intake { kind: inline }
6180
6867
  agent api-caller {
6181
6868
  model: sonnet
@@ -6324,7 +7011,10 @@ describe("Registry imports (F28-F30)", () => {
6324
7011
  describe("Prompt variants (F40)", () => {
6325
7012
  it("parses variants block in agent", () => {
6326
7013
  const src = `topology t : [pipeline] {
6327
- orchestrator { model: opus handles: [intake] }
7014
+ orchestrator {
7015
+ model: opus
7016
+ handles: [intake]
7017
+ }
6328
7018
  action intake { kind: inline }
6329
7019
  agent reviewer {
6330
7020
  model: sonnet
@@ -6357,7 +7047,10 @@ describe("Prompt variants (F40)", () => {
6357
7047
  });
6358
7048
  it("parses variant with model override", () => {
6359
7049
  const src = `topology t : [pipeline] {
6360
- orchestrator { model: opus handles: [intake] }
7050
+ orchestrator {
7051
+ model: opus
7052
+ handles: [intake]
7053
+ }
6361
7054
  action intake { kind: inline }
6362
7055
  agent reviewer {
6363
7056
  model: sonnet
@@ -6449,7 +7142,10 @@ describe("Prompt variants (F40)", () => {
6449
7142
  describe("Encrypted values (F41)", () => {
6450
7143
  it("parses encrypted value in env block", () => {
6451
7144
  const src = `topology t : [pipeline] {
6452
- orchestrator { model: opus handles: [intake] }
7145
+ orchestrator {
7146
+ model: opus
7147
+ handles: [intake]
7148
+ }
6453
7149
  action intake { kind: inline }
6454
7150
  agent w { model: sonnet }
6455
7151
  env {
@@ -6466,7 +7162,10 @@ describe("Encrypted values (F41)", () => {
6466
7162
  });
6467
7163
  it("encrypted implies sensitive", () => {
6468
7164
  const src = `topology t : [pipeline] {
6469
- orchestrator { model: opus handles: [intake] }
7165
+ orchestrator {
7166
+ model: opus
7167
+ handles: [intake]
7168
+ }
6470
7169
  action intake { kind: inline }
6471
7170
  agent w { model: sonnet }
6472
7171
  env {
@@ -6518,7 +7217,10 @@ describe("Encrypted values (F41)", () => {
6518
7217
  describe("Auth block in providers (F42)", () => {
6519
7218
  it("parses auth block in provider", () => {
6520
7219
  const src = `topology t : [pipeline] {
6521
- orchestrator { model: opus handles: [intake] }
7220
+ orchestrator {
7221
+ model: opus
7222
+ handles: [intake]
7223
+ }
6522
7224
  action intake { kind: inline }
6523
7225
  agent w { model: sonnet }
6524
7226
  providers {
@@ -6543,7 +7245,10 @@ describe("Auth block in providers (F42)", () => {
6543
7245
  });
6544
7246
  it("provider without auth block (backward compat) passes", () => {
6545
7247
  const src = `topology t : [pipeline] {
6546
- orchestrator { model: opus handles: [intake] }
7248
+ orchestrator {
7249
+ model: opus
7250
+ handles: [intake]
7251
+ }
6547
7252
  action intake { kind: inline }
6548
7253
  agent w { model: sonnet }
6549
7254
  providers {
@@ -6618,7 +7323,14 @@ describe("Auth block in providers (F42)", () => {
6618
7323
  // =========================================================================
6619
7324
  describe("store and retrieval blocks", () => {
6620
7325
  it("parses a basic store with type, backend, and path", () => {
6621
- const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator { model: opus handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n memory {\n store my-store {\n type: semantic\n backend: lancedb\n path: ".memory/"\n }\n }\n}`;
7326
+ const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
7327
+ model: opus
7328
+ handles: [a]
7329
+ }\n action a { kind: inline }\n flow { a -> a }\n memory {\n store my-store {
7330
+ type: semantic\n
7331
+ backend: lancedb\n
7332
+ path: ".memory/"\n
7333
+ }\n }\n}`;
6622
7334
  const ast = parse(src);
6623
7335
  expect(ast.stores).toHaveLength(1);
6624
7336
  expect(ast.stores[0].id).toBe("my-store");
@@ -6627,7 +7339,19 @@ describe("Auth block in providers (F42)", () => {
6627
7339
  expect(ast.stores[0].path).toBe(".memory/");
6628
7340
  });
6629
7341
  it("parses multiple stores in one memory block", () => {
6630
- const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator { model: opus handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n memory {\n store kb {\n type: semantic\n backend: lancedb\n }\n store relations {\n type: graph\n backend: kuzu\n }\n store history {\n type: episodic\n backend: sqlite-vec\n }\n }\n}`;
7342
+ const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
7343
+ model: opus
7344
+ handles: [a]
7345
+ }\n action a { kind: inline }\n flow { a -> a }\n memory {\n store kb {
7346
+ type: semantic\n
7347
+ backend: lancedb\n
7348
+ }\n store relations {
7349
+ type: graph\n
7350
+ backend: kuzu\n
7351
+ }\n store history {
7352
+ type: episodic\n
7353
+ backend: sqlite-vec\n
7354
+ }\n }\n}`;
6631
7355
  const ast = parse(src);
6632
7356
  expect(ast.stores).toHaveLength(3);
6633
7357
  expect(ast.stores[0].id).toBe("kb");
@@ -6727,7 +7451,14 @@ describe("Auth block in providers (F42)", () => {
6727
7451
  expect(store.backendConfig["replicas"]).toBe(3);
6728
7452
  });
6729
7453
  it("parses store with connection secret", () => {
6730
- const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator { model: opus handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n memory {\n store remote-db {\n type: semantic\n backend: pinecone\n connection: secret "DB_URL"\n }\n }\n}`;
7454
+ const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
7455
+ model: opus
7456
+ handles: [a]
7457
+ }\n action a { kind: inline }\n flow { a -> a }\n memory {\n store remote-db {
7458
+ type: semantic\n
7459
+ backend: pinecone\n
7460
+ connection: secret "DB_URL"\n
7461
+ }\n }\n}`;
6731
7462
  const ast = parse(src);
6732
7463
  expect(ast.stores).toHaveLength(1);
6733
7464
  expect(ast.stores[0].connection).toBe("DB_URL");
@@ -6818,7 +7549,10 @@ describe("Auth block in providers (F42)", () => {
6818
7549
  expect(agent.retrieval).toBe("planner");
6819
7550
  });
6820
7551
  it("parses empty memory block with no stores", () => {
6821
- const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator { model: opus handles: [a] }\n action a { kind: inline }\n flow { a -> a }\n memory {}\n}`;
7552
+ const src = `topology t : [pipeline] {\n meta { version: "1.0.0" }\n orchestrator {
7553
+ model: opus
7554
+ handles: [a]
7555
+ }\n action a { kind: inline }\n flow { a -> a }\n memory {}\n}`;
6822
7556
  const ast = parse(src);
6823
7557
  expect(ast.stores).toEqual([]);
6824
7558
  expect(ast.retrievals).toEqual([]);
@@ -7061,7 +7795,10 @@ describe("Auth block in providers (F42)", () => {
7061
7795
  });
7062
7796
  describe("Company brain — pattern, brain store type, custody primitive", () => {
7063
7797
  const brainSrc = `topology cb : [brain] {
7064
- meta { version: "1.0.0" description: "x" }
7798
+ meta {
7799
+ version: "1.0.0"
7800
+ description: "x"
7801
+ }
7065
7802
  memory {
7066
7803
  store brain {
7067
7804
  type: brain
@@ -7172,7 +7909,10 @@ describe("Company brain — pattern, brain store type, custody primitive", () =>
7172
7909
  });
7173
7910
  it("parses a brain store's presentation-only `sources` block", () => {
7174
7911
  const src = `topology cb : [brain] {
7175
- meta { version: "1.0.0" description: "x" }
7912
+ meta {
7913
+ version: "1.0.0"
7914
+ description: "x"
7915
+ }
7176
7916
  memory {
7177
7917
  store brain {
7178
7918
  type: brain
@@ -7188,7 +7928,12 @@ describe("Company brain — pattern, brain store type, custody primitive", () =>
7188
7928
  }
7189
7929
  }
7190
7930
  }
7191
- agent lib { model: sonnet description: "k" tools: [Read, Write] custodian-of: [brain] }
7931
+ agent lib {
7932
+ model: sonnet
7933
+ description: "k"
7934
+ tools: [Read, Write]
7935
+ custodian-of: [brain]
7936
+ }
7192
7937
  action intake {
7193
7938
  kind: inline
7194
7939
  description: "in"
@@ -7204,6 +7949,437 @@ describe("Company brain — pattern, brain store type, custody primitive", () =>
7204
7949
  // It is presentation-only — purely data on the AST, no validation error.
7205
7950
  expect(validate(ast).filter((r) => r.level === "error")).toHaveLength(0);
7206
7951
  });
7952
+ it("V24: a store's `sources` sub-block does not trigger memory sub-block warnings", () => {
7953
+ // Regression: the V24 scanner used to regex the whole memory body flatly,
7954
+ // flagging blocks NESTED in a store (sources/gmail/slack) as unknown memory
7955
+ // sub-blocks. The scanner is now depth-aware — only top-level memory blocks
7956
+ // are checked, and `store <id> {` is recognized by its leading keyword.
7957
+ const src = `topology cb : [brain] {
7958
+ meta {
7959
+ version: "1.0.0"
7960
+ description: "x"
7961
+ }
7962
+ memory {
7963
+ store brain {
7964
+ type: brain
7965
+ path: "brain/"
7966
+ scope: org
7967
+ sources {
7968
+ gmail { color: "#EA4335" }
7969
+ slack { color: "#4A154B" }
7970
+ }
7971
+ }
7972
+ }
7973
+ agent lib {
7974
+ model: sonnet
7975
+ description: "k"
7976
+ tools: [Read, Write]
7977
+ custodian-of: [brain]
7978
+ }
7979
+ action intake {
7980
+ kind: inline
7981
+ description: "in"
7982
+ }
7983
+ flow { intake -> lib }
7984
+ }`;
7985
+ const v24 = validate(parse(src)).filter((r) => r.rule === "V24");
7986
+ expect(v24).toHaveLength(0);
7987
+ });
7988
+ it("V89: a KV `prompt:` on an agent is an error, not a silent drop", () => {
7989
+ // Regression: spec/grammar.md:350 defines agent.prompt as a BLOCK. The
7990
+ // string form belongs to `skill` (grammar.md:1377). Writing
7991
+ // `prompt: "path.md"` on an agent parsed to nothing and validate() said
7992
+ // PASS — so the agent shipped with no instructions and the binding fell
7993
+ // back to repeating its role sentence. examples/code-review.at did this on
7994
+ // four agents, pointing at a prompts/ directory that never existed.
7995
+ const src = `topology t : [pipeline] {
7996
+ meta {
7997
+ version: "1.0.0"
7998
+ description: "x"
7999
+ }
8000
+ agent worker {
8001
+ model: sonnet
8002
+ description: "w"
8003
+ prompt: "prompts/worker.md"
8004
+ }
8005
+ action intake {
8006
+ kind: inline
8007
+ description: "in"
8008
+ }
8009
+ flow { intake -> worker }
8010
+ }`;
8011
+ const ast = parse(src);
8012
+ expect(ast.nodes.find((n) => n.id === "worker")).not.toHaveProperty("prompt");
8013
+ const v89 = validate(ast).filter((r) => r.rule === "V89");
8014
+ expect(v89).toHaveLength(1);
8015
+ expect(v89[0].level).toBe("error");
8016
+ expect(v89[0].node).toBe("worker");
8017
+ });
8018
+ it("V89: the `prompt { }` block form is accepted and reaches the AST", () => {
8019
+ const src = `topology t : [pipeline] {
8020
+ meta {
8021
+ version: "1.0.0"
8022
+ description: "x"
8023
+ }
8024
+ agent worker {
8025
+ model: sonnet
8026
+ description: "w"
8027
+ prompt {
8028
+ Do the thing.
8029
+ }
8030
+ }
8031
+ action intake {
8032
+ kind: inline
8033
+ description: "in"
8034
+ }
8035
+ flow { intake -> worker }
8036
+ }`;
8037
+ const ast = parse(src);
8038
+ const worker = ast.nodes.find((n) => n.id === "worker");
8039
+ expect(worker.prompt).toBe("Do the thing.");
8040
+ expect(validate(ast).filter((r) => r.rule === "V89")).toHaveLength(0);
8041
+ });
8042
+ it("V90: a collapsed meta block is caught — it corrupts the version", () => {
8043
+ // The damaging case. `version` silently became `1.0.0" description: "x`,
8044
+ // which then printed in every binding's output and in the plan brief.
8045
+ const src = [
8046
+ "topology t : [pipeline] {",
8047
+ ' meta { version: "1.0.0" description: "x" }',
8048
+ " agent a {",
8049
+ " model: sonnet",
8050
+ ' description: "a"',
8051
+ " }",
8052
+ " action i {",
8053
+ " kind: inline",
8054
+ ' description: "in"',
8055
+ " }",
8056
+ " flow { i -> a }",
8057
+ "}",
8058
+ ].join("\n");
8059
+ const v90 = validate(parse(src)).filter((r) => r.rule === "V90");
8060
+ expect(v90).toHaveLength(1);
8061
+ expect(v90[0].message).toContain("version");
8062
+ });
8063
+ it("V90: a collapsed agent block is caught", () => {
8064
+ const src = [
8065
+ "topology t : [pipeline] {",
8066
+ " meta {",
8067
+ ' version: "1.0.0"',
8068
+ ' description: "x"',
8069
+ " }",
8070
+ " agent a { model: sonnet retry: 3 }",
8071
+ " action i {",
8072
+ " kind: inline",
8073
+ ' description: "in"',
8074
+ " }",
8075
+ " flow { i -> a }",
8076
+ "}",
8077
+ ].join("\n");
8078
+ const v90 = validate(parse(src)).filter((r) => r.rule === "V90");
8079
+ expect(v90.some((r) => r.message.includes("model"))).toBe(true);
8080
+ });
8081
+ it("V90: a colon inside a prose description is NOT flagged", () => {
8082
+ // `parseMeta` strips quotes, so a description arrives as bare prose. A
8083
+ // colon in English is ordinary — "Demo: agent pipeline with DuckDB".
8084
+ // Two shipped examples do exactly this and must stay clean.
8085
+ const src = [
8086
+ "topology t : [pipeline] {",
8087
+ " meta {",
8088
+ ' version: "1.0.0"',
8089
+ ' description: "Demo: agent pipeline with DuckDB storage"',
8090
+ " }",
8091
+ " agent a {",
8092
+ " model: sonnet",
8093
+ ' description: "Static analysis: complexity and blast radius"',
8094
+ " }",
8095
+ " action i {",
8096
+ " kind: inline",
8097
+ ' description: "in"',
8098
+ " }",
8099
+ " flow { i -> a }",
8100
+ "}",
8101
+ ].join("\n");
8102
+ expect(validate(parse(src)).filter((r) => r.rule === "V90")).toHaveLength(0);
8103
+ });
8104
+ it("V90: a quoted value FOLLOWED by more content is still a swallow", () => {
8105
+ // Where quotes survive parsing, the check is sharper: one well-formed
8106
+ // quoted token then more content means a field was eaten.
8107
+ const src = [
8108
+ "topology t : [pipeline] {",
8109
+ " meta {",
8110
+ ' version: "1.0.0"',
8111
+ ' description: "x"',
8112
+ " }",
8113
+ " agent a {",
8114
+ ' description: "k" model: sonnet',
8115
+ " }",
8116
+ " action i {",
8117
+ " kind: inline",
8118
+ ' description: "in"',
8119
+ " }",
8120
+ " flow { i -> a }",
8121
+ "}",
8122
+ ].join("\n");
8123
+ expect(validate(parse(src)).filter((r) => r.rule === "V90").length).toBeGreaterThan(0);
8124
+ });
8125
+ it("V90: every shipped example is free of swallowed fields", () => {
8126
+ const dir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples");
8127
+ for (const f of readdirSync(dir).filter((x) => x.endsWith(".at"))) {
8128
+ const v90 = validate(parse(readFileSync(resolve(dir, f), "utf8"))).filter((r) => r.rule === "V90");
8129
+ expect(v90, `${f}: ${v90.map((r) => r.message).join("; ")}`).toHaveLength(0);
8130
+ }
8131
+ });
8132
+ it("V90: a spec-legal single-line nested object is NOT a swallow", () => {
8133
+ // `spec/grammar.md:117` gives `outputs: { depth: 1 | 2 | 3 }` verbatim as
8134
+ // legal, and it parses correctly. V90 flagged it, which rejected valid
8135
+ // topologies — a false positive is worse than a miss.
8136
+ const src = [
8137
+ "topology t : [pipeline] {",
8138
+ " meta {",
8139
+ ' version: "1.0.0"',
8140
+ ' description: "x"',
8141
+ " }",
8142
+ " agent a {",
8143
+ " model: sonnet",
8144
+ ' description: "a"',
8145
+ " outputs: { verdict: approve | reject }",
8146
+ " tools: [Read, Write]",
8147
+ " }",
8148
+ " action i {",
8149
+ " kind: inline",
8150
+ ' description: "in"',
8151
+ " }",
8152
+ " flow { i -> a }",
8153
+ "}",
8154
+ ].join("\n");
8155
+ const ast = parse(src);
8156
+ expect(validate(ast).filter((r) => r.rule === "V90")).toHaveLength(0);
8157
+ // and it really did parse — the exemption must not hide a broken parse
8158
+ const a = ast.nodes.find((n) => n.id === "a");
8159
+ expect(a.outputs?.verdict).toEqual(["approve", "reject"]);
8160
+ });
8161
+ it("V90: a swallow with no space after the colon is still caught", () => {
8162
+ const src = [
8163
+ "topology t : [pipeline] {",
8164
+ " meta {",
8165
+ ' version: "1.0.0"',
8166
+ ' description: "x"',
8167
+ " }",
8168
+ " agent a { model: sonnet retry:3 }",
8169
+ " action i {",
8170
+ " kind: inline",
8171
+ ' description: "in"',
8172
+ " }",
8173
+ " flow { i -> a }",
8174
+ "}",
8175
+ ].join("\n");
8176
+ expect(validate(parse(src)).filter((r) => r.rule === "V90").length).toBeGreaterThan(0);
8177
+ });
8178
+ it("V90: orchestrator, human and group blocks are checked too", () => {
8179
+ const src = [
8180
+ "topology t : [pipeline] {",
8181
+ " meta {",
8182
+ ' version: "1.0.0"',
8183
+ ' description: "x"',
8184
+ " }",
8185
+ " orchestrator { model: opus handles: [i] }",
8186
+ " agent a {",
8187
+ " model: sonnet",
8188
+ ' description: "a"',
8189
+ " }",
8190
+ " action i {",
8191
+ " kind: inline",
8192
+ ' description: "in"',
8193
+ " }",
8194
+ " flow { i -> a }",
8195
+ "}",
8196
+ ].join("\n");
8197
+ const v90 = validate(parse(src)).filter((r) => r.rule === "V90");
8198
+ expect(v90.length).toBeGreaterThan(0);
8199
+ expect(v90[0].node).toBe("orchestrator");
8200
+ });
8201
+ it("V92: two bracket groups on an edge are rejected, not silently mangled", () => {
8202
+ // The annotation matcher spanned from the first `[` to the last `]`, so
8203
+ // `[when a.verdict == fail] [max 3]` produced the condition string
8204
+ // `a.verdict == fail] [max 3` — brackets and all — with no error.
8205
+ const src = [
8206
+ "topology t : [pipeline] {",
8207
+ " meta {",
8208
+ ' version: "1.0.0"',
8209
+ ' description: "x"',
8210
+ " }",
8211
+ " agent a {",
8212
+ " model: sonnet",
8213
+ ' description: "a"',
8214
+ " outputs: {",
8215
+ " verdict: pass | fail",
8216
+ " }",
8217
+ " }",
8218
+ " agent b {",
8219
+ " model: sonnet",
8220
+ ' description: "b"',
8221
+ " }",
8222
+ " action i {",
8223
+ " kind: inline",
8224
+ ' description: "in"',
8225
+ " }",
8226
+ " flow { i -> a",
8227
+ " a -> b [when a.verdict == fail] [max 3] }",
8228
+ "}",
8229
+ ].join("\n");
8230
+ const v92 = validate(parse(src)).filter((r) => r.rule === "V92");
8231
+ expect(v92).toHaveLength(1);
8232
+ expect(v92[0].message).toContain("ONE bracket");
8233
+ });
8234
+ it("V92: a fan-out list plus an annotation is legal, not two annotations", () => {
8235
+ // `analyzer -> [reporter, alerter] [when ...]` ships in
8236
+ // examples/scheduled-monitor.at. Testing for `] [` alone flagged it.
8237
+ const src = [
8238
+ "topology t : [pipeline, fan-out] {",
8239
+ " meta {",
8240
+ ' version: "1.0.0"',
8241
+ ' description: "x"',
8242
+ " }",
8243
+ " agent a {",
8244
+ " model: sonnet",
8245
+ ' description: "a"',
8246
+ " outputs: {",
8247
+ " verdict: pass | fail",
8248
+ " }",
8249
+ " }",
8250
+ " agent b {",
8251
+ " model: sonnet",
8252
+ ' description: "b"',
8253
+ " }",
8254
+ " agent c {",
8255
+ " model: sonnet",
8256
+ ' description: "c"',
8257
+ " }",
8258
+ " action i {",
8259
+ " kind: inline",
8260
+ ' description: "in"',
8261
+ " }",
8262
+ " flow { i -> a",
8263
+ " a -> [b, c] [when a.verdict == fail] }",
8264
+ "}",
8265
+ ].join("\n");
8266
+ expect(validate(parse(src)).filter((r) => r.rule === "V92")).toHaveLength(0);
8267
+ });
8268
+ it("V92: the legal single-bracket form is unaffected", () => {
8269
+ const src = [
8270
+ "topology t : [pipeline] {",
8271
+ " meta {",
8272
+ ' version: "1.0.0"',
8273
+ ' description: "x"',
8274
+ " }",
8275
+ " agent a {",
8276
+ " model: sonnet",
8277
+ ' description: "a"',
8278
+ " outputs: {",
8279
+ " verdict: pass | fail",
8280
+ " }",
8281
+ " }",
8282
+ " agent b {",
8283
+ " model: sonnet",
8284
+ ' description: "b"',
8285
+ " }",
8286
+ " action i {",
8287
+ " kind: inline",
8288
+ ' description: "in"',
8289
+ " }",
8290
+ " flow { i -> a",
8291
+ " a -> b [when a.verdict == fail, max 3] }",
8292
+ "}",
8293
+ ].join("\n");
8294
+ const ast = parse(src);
8295
+ expect(validate(ast).filter((r) => r.rule === "V92")).toHaveLength(0);
8296
+ const e = ast.edges.find((x) => x.from === "a" && x.to === "b");
8297
+ expect(e.condition).toBe("a.verdict == fail");
8298
+ expect(e.maxIterations).toBe(3);
8299
+ });
8300
+ it("V91: a required field satisfied by an injected placeholder is still missing", () => {
8301
+ // The parser substitutes a sentinel because the AST types are non-optional,
8302
+ // and every downstream "is it present?" check then passed. V7 (model
8303
+ // required) saw "unknown" and was satisfied; codex emitted it verbatim into
8304
+ // config.toml as a model that does not exist.
8305
+ const noVersion = [
8306
+ "topology t : [pipeline] {",
8307
+ " meta {",
8308
+ ' description: "no version declared"',
8309
+ " }",
8310
+ " agent a {",
8311
+ " model: sonnet",
8312
+ ' description: "a"',
8313
+ " }",
8314
+ " action i {",
8315
+ " kind: inline",
8316
+ ' description: "in"',
8317
+ " }",
8318
+ " flow { i -> a }",
8319
+ "}",
8320
+ ].join("\n");
8321
+ const ast = parse(noVersion);
8322
+ expect(ast.topology.version).toBe("0.0.0"); // the sentinel is still injected
8323
+ const v91 = validate(ast).filter((r) => r.rule === "V91");
8324
+ expect(v91).toHaveLength(1);
8325
+ expect(v91[0].message).toContain("version");
8326
+ });
8327
+ it("V91: an orchestrator with no model is caught, where V7 was satisfied", () => {
8328
+ const src = [
8329
+ "topology t : [pipeline] {",
8330
+ " meta {",
8331
+ ' version: "1.0.0"',
8332
+ ' description: "x"',
8333
+ " }",
8334
+ " orchestrator {",
8335
+ " handles: [i]",
8336
+ " }",
8337
+ " agent a {",
8338
+ " model: sonnet",
8339
+ ' description: "a"',
8340
+ " }",
8341
+ " action i {",
8342
+ " kind: inline",
8343
+ ' description: "in"',
8344
+ " }",
8345
+ " flow { i -> a }",
8346
+ "}",
8347
+ ].join("\n");
8348
+ const results = validate(parse(src));
8349
+ expect(results.filter((r) => r.rule === "V7")).toHaveLength(0); // V7 still passes
8350
+ expect(results.filter((r) => r.rule === "V91")).toHaveLength(1); // V91 does not
8351
+ });
8352
+ it("V91: a topology declaring both required fields is clean", () => {
8353
+ const src = [
8354
+ "topology t : [pipeline] {",
8355
+ " meta {",
8356
+ ' version: "1.0.0"',
8357
+ ' description: "x"',
8358
+ " }",
8359
+ " orchestrator {",
8360
+ " model: opus",
8361
+ " handles: [i]",
8362
+ " }",
8363
+ " agent a {",
8364
+ " model: sonnet",
8365
+ ' description: "a"',
8366
+ " }",
8367
+ " action i {",
8368
+ " kind: inline",
8369
+ ' description: "in"',
8370
+ " }",
8371
+ " flow { i -> a }",
8372
+ "}",
8373
+ ].join("\n");
8374
+ expect(validate(parse(src)).filter((r) => r.rule === "V91")).toHaveLength(0);
8375
+ });
8376
+ it("V91: every shipped example declares its required fields", () => {
8377
+ const dir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples");
8378
+ for (const f of readdirSync(dir).filter((x) => x.endsWith(".at"))) {
8379
+ const v91 = validate(parse(readFileSync(resolve(dir, f), "utf8"))).filter((r) => r.rule === "V91");
8380
+ expect(v91, `${f}: ${v91.map((r) => r.message).join("; ")}`).toHaveLength(0);
8381
+ }
8382
+ });
7207
8383
  it("round-trips the company-brain.at example with no validation errors", () => {
7208
8384
  const src = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../../examples/company-brain.at"), "utf8");
7209
8385
  const ast = parse(src);