@mhingston5/lasso 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +707 -0
  2. package/docs/agent-wrangling.png +0 -0
  3. package/package.json +26 -0
  4. package/src/capabilities/matcher.ts +25 -0
  5. package/src/capabilities/registry.ts +103 -0
  6. package/src/capabilities/types.ts +15 -0
  7. package/src/cir/lower.ts +253 -0
  8. package/src/cir/optimize.ts +251 -0
  9. package/src/cir/types.ts +131 -0
  10. package/src/cir/validate.ts +265 -0
  11. package/src/compiler/compile.ts +601 -0
  12. package/src/compiler/feedback.ts +471 -0
  13. package/src/compiler/runtime-helpers.ts +455 -0
  14. package/src/composition/chain.ts +58 -0
  15. package/src/composition/conditional.ts +76 -0
  16. package/src/composition/parallel.ts +75 -0
  17. package/src/composition/types.ts +105 -0
  18. package/src/environment/analyzer.ts +56 -0
  19. package/src/environment/discovery.ts +179 -0
  20. package/src/environment/types.ts +68 -0
  21. package/src/failures/classifiers.ts +134 -0
  22. package/src/failures/generator.ts +421 -0
  23. package/src/failures/map-reference-failures.ts +23 -0
  24. package/src/failures/ontology.ts +210 -0
  25. package/src/failures/recovery.ts +214 -0
  26. package/src/failures/types.ts +14 -0
  27. package/src/index.ts +67 -0
  28. package/src/memory/advisor.ts +132 -0
  29. package/src/memory/extractor.ts +166 -0
  30. package/src/memory/store.ts +107 -0
  31. package/src/memory/types.ts +53 -0
  32. package/src/metaharness/engine.ts +256 -0
  33. package/src/metaharness/predictor.ts +168 -0
  34. package/src/metaharness/types.ts +40 -0
  35. package/src/mutation/derive.ts +308 -0
  36. package/src/mutation/diff.ts +52 -0
  37. package/src/mutation/engine.ts +256 -0
  38. package/src/mutation/types.ts +84 -0
  39. package/src/pi/command-input.ts +209 -0
  40. package/src/pi/commands.ts +351 -0
  41. package/src/pi/extension.ts +16 -0
  42. package/src/planner/synthesize.ts +83 -0
  43. package/src/planner/template-rules.ts +183 -0
  44. package/src/planner/types.ts +42 -0
  45. package/src/reference/catalog.ts +128 -0
  46. package/src/reference/patch-validation-strategies.ts +170 -0
  47. package/src/reference/patch-validation.ts +174 -0
  48. package/src/reference/pr-review-merge.ts +155 -0
  49. package/src/reference/strategies.ts +126 -0
  50. package/src/reference/types.ts +33 -0
  51. package/src/replanner/risk-rules.ts +161 -0
  52. package/src/replanner/runtime.ts +308 -0
  53. package/src/replanner/synthesize.ts +619 -0
  54. package/src/replanner/types.ts +73 -0
  55. package/src/spec/schema.ts +254 -0
  56. package/src/spec/types.ts +319 -0
  57. package/src/spec/validate.ts +296 -0
  58. package/src/state/snapshots.ts +43 -0
  59. package/src/state/types.ts +12 -0
  60. package/src/synthesis/graph-builder.ts +267 -0
  61. package/src/synthesis/harness-builder.ts +113 -0
  62. package/src/synthesis/intent-ir.ts +63 -0
  63. package/src/synthesis/policy-builder.ts +320 -0
  64. package/src/synthesis/risk-analyzer.ts +182 -0
  65. package/src/synthesis/skill-parser.ts +441 -0
  66. package/src/verification/engine.ts +230 -0
  67. package/src/versioning/file-store.ts +103 -0
  68. package/src/versioning/history.ts +43 -0
  69. package/src/versioning/store.ts +16 -0
  70. package/src/versioning/types.ts +31 -0
  71. package/test/capabilities/matcher.test.ts +67 -0
  72. package/test/capabilities/registry.test.ts +136 -0
  73. package/test/capabilities/synthesis.test.ts +264 -0
  74. package/test/cir/lower.test.ts +417 -0
  75. package/test/cir/optimize.test.ts +266 -0
  76. package/test/cir/validate.test.ts +368 -0
  77. package/test/compiler/adaptive-runtime.test.ts +157 -0
  78. package/test/compiler/compile.test.ts +1198 -0
  79. package/test/compiler/feedback.test.ts +784 -0
  80. package/test/compiler/guardrails.test.ts +191 -0
  81. package/test/compiler/trace.test.ts +404 -0
  82. package/test/composition/chain.test.ts +328 -0
  83. package/test/composition/conditional.test.ts +241 -0
  84. package/test/composition/parallel.test.ts +215 -0
  85. package/test/environment/analyzer.test.ts +204 -0
  86. package/test/environment/discovery.test.ts +149 -0
  87. package/test/failures/classifiers.test.ts +287 -0
  88. package/test/failures/generator.test.ts +203 -0
  89. package/test/failures/ontology.test.ts +439 -0
  90. package/test/failures/recovery.test.ts +300 -0
  91. package/test/helpers/createFixtureRepo.ts +84 -0
  92. package/test/helpers/createPatchValidationFixture.ts +144 -0
  93. package/test/helpers/runCompiledWorkflow.ts +208 -0
  94. package/test/memory/advisor.test.ts +332 -0
  95. package/test/memory/extractor.test.ts +295 -0
  96. package/test/memory/store.test.ts +244 -0
  97. package/test/metaharness/engine.test.ts +575 -0
  98. package/test/metaharness/predictor.test.ts +436 -0
  99. package/test/mutation/derive-failure.test.ts +209 -0
  100. package/test/mutation/engine.test.ts +622 -0
  101. package/test/package-smoke.test.ts +29 -0
  102. package/test/pi/command-input.test.ts +153 -0
  103. package/test/pi/commands.test.ts +623 -0
  104. package/test/planner/classify-template.test.ts +32 -0
  105. package/test/planner/synthesize.test.ts +901 -0
  106. package/test/reference/PatchValidation.failures.test.ts +137 -0
  107. package/test/reference/PatchValidation.test.ts +326 -0
  108. package/test/reference/PrReviewMerge.failures.test.ts +121 -0
  109. package/test/reference/PrReviewMerge.test.ts +55 -0
  110. package/test/reference/catalog-open.test.ts +70 -0
  111. package/test/replanner/runtime.test.ts +207 -0
  112. package/test/replanner/synthesize.test.ts +303 -0
  113. package/test/spec/validate.test.ts +1056 -0
  114. package/test/state/snapshots.test.ts +264 -0
  115. package/test/synthesis/custom-workflow.test.ts +264 -0
  116. package/test/synthesis/graph-builder.test.ts +370 -0
  117. package/test/synthesis/harness-builder.test.ts +128 -0
  118. package/test/synthesis/policy-builder.test.ts +149 -0
  119. package/test/synthesis/risk-analyzer.test.ts +230 -0
  120. package/test/synthesis/skill-parser.test.ts +796 -0
  121. package/test/verification/engine.test.ts +509 -0
  122. package/test/versioning/history.test.ts +144 -0
  123. package/test/versioning/store.test.ts +254 -0
  124. package/vitest.config.ts +9 -0
@@ -0,0 +1,575 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { DefaultMetaHarness } from "../../src/metaharness/engine.js";
3
+ import type {
4
+ MetaHarnessConfig,
5
+ MetaHarnessResult,
6
+ } from "../../src/metaharness/types.js";
7
+ import type { HarnessSpec } from "../../src/spec/types.js";
8
+ import type { EnvironmentModel } from "../../src/environment/types.js";
9
+ import type { FailureSignature } from "../../src/failures/ontology.js";
10
+ import type { MemoryStore, MemoryAdvice } from "../../src/memory/types.js";
11
+ import type { CapabilityRegistry } from "../../src/capabilities/types.js";
12
+ import type { MutationPolicy } from "../../src/mutation/types.js";
13
+ import type { HarnessStage } from "../../src/composition/types.js";
14
+
15
+ function makeMinimalSpec(overrides?: Partial<HarnessSpec>): HarnessSpec {
16
+ return {
17
+ name: "test-harness",
18
+ graph: {
19
+ entryNodeId: "node-a",
20
+ nodes: [
21
+ {
22
+ id: "node-a",
23
+ label: "Node A",
24
+ kind: "tool",
25
+ tool: "bash",
26
+ args: ["echo hello"],
27
+ },
28
+ ],
29
+ edges: [],
30
+ },
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function makeMinimalEnv(overrides?: Partial<EnvironmentModel>): EnvironmentModel {
36
+ return {
37
+ tools: [
38
+ { name: "bash", version: "5.0", available: true },
39
+ { name: "git", version: "2.39", available: true },
40
+ ],
41
+ resources: [
42
+ { name: "disk", type: "disk", available: true, limit: "500GB", usage: "45%" },
43
+ ],
44
+ constraints: [],
45
+ authState: [],
46
+ externalSystems: [],
47
+ discoveredAt: Date.now(),
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ function makeEmptyMemoryStore(): MemoryStore {
53
+ return {
54
+ async getMemory() { return null; },
55
+ async saveMemory() {},
56
+ async updateMemory() { throw new Error("not found"); },
57
+ async searchMemories() { return []; },
58
+ };
59
+ }
60
+
61
+ function makeMemoryStoreWithAdvice(advice: MemoryAdvice): MemoryStore {
62
+ return {
63
+ async getMemory() { return null; },
64
+ async saveMemory() {},
65
+ async updateMemory() { throw new Error("not found"); },
66
+ async searchMemories() {
67
+ return [
68
+ {
69
+ taskId: "prev-task-1",
70
+ successfulPatterns: ["node-a-before-node-b"],
71
+ failedPatterns: [],
72
+ mutationHistory: [],
73
+ effectivenessScore: 0.85,
74
+ lastUpdated: Date.now(),
75
+ },
76
+ ];
77
+ },
78
+ };
79
+ }
80
+
81
+ describe("DefaultMetaHarness", () => {
82
+ describe("constructor", () => {
83
+ it("creates instance with empty config", () => {
84
+ const harness = new DefaultMetaHarness({});
85
+ expect(harness).toBeDefined();
86
+ });
87
+
88
+ it("creates instance with full config", () => {
89
+ const config: MetaHarnessConfig = {
90
+ environmentModel: makeMinimalEnv(),
91
+ memoryStore: makeEmptyMemoryStore(),
92
+ capabilityRegistry: undefined,
93
+ maxVersions: 5,
94
+ mutationPolicy: { allowedMutations: ["add-node", "modify-node"], maxMutations: 10 },
95
+ };
96
+ const harness = new DefaultMetaHarness(config);
97
+ expect(harness).toBeDefined();
98
+ });
99
+ });
100
+
101
+ describe("discoverEnvironment", () => {
102
+ it("discovers environment without repo path", async () => {
103
+ const harness = new DefaultMetaHarness({});
104
+ const env = await harness.discoverEnvironment();
105
+
106
+ expect(env.tools.length).toBeGreaterThan(0);
107
+ expect(env.resources.length).toBeGreaterThan(0);
108
+ expect(env.discoveredAt).toBeGreaterThan(0);
109
+ });
110
+
111
+ it("discovers environment with repo path", async () => {
112
+ const harness = new DefaultMetaHarness({});
113
+ const env = await harness.discoverEnvironment(process.cwd());
114
+
115
+ expect(env.repoState).toBeDefined();
116
+ expect(env.repoState!.path).toBe(process.cwd());
117
+ });
118
+ });
119
+
120
+ describe("predictFailures", () => {
121
+ it("returns empty array when environment is healthy", async () => {
122
+ const harness = new DefaultMetaHarness({});
123
+ const spec = makeMinimalSpec();
124
+ const env = makeMinimalEnv();
125
+
126
+ const failures = await harness.predictFailures(spec, env);
127
+
128
+ expect(failures.length).toBe(0);
129
+ });
130
+
131
+ it("predicts tool failures for missing tools", async () => {
132
+ const harness = new DefaultMetaHarness({});
133
+ const spec: HarnessSpec = {
134
+ name: "test-harness",
135
+ graph: {
136
+ entryNodeId: "node-a",
137
+ nodes: [
138
+ {
139
+ id: "node-a",
140
+ kind: "tool",
141
+ tool: "kubectl",
142
+ args: ["get pods"],
143
+ },
144
+ ],
145
+ edges: [],
146
+ },
147
+ };
148
+ const env = makeMinimalEnv();
149
+
150
+ const failures = await harness.predictFailures(spec, env);
151
+
152
+ expect(failures.length).toBeGreaterThan(0);
153
+ expect(failures.some(f => f.class === "tool")).toBe(true);
154
+ });
155
+
156
+ it("predicts constraint-based failures", async () => {
157
+ const harness = new DefaultMetaHarness({});
158
+ const spec = makeMinimalSpec();
159
+ const env = makeMinimalEnv({
160
+ constraints: [
161
+ { type: "permission", description: "No write access", severity: "high" },
162
+ ],
163
+ });
164
+
165
+ const failures = await harness.predictFailures(spec, env);
166
+
167
+ expect(failures.length).toBeGreaterThan(0);
168
+ });
169
+ });
170
+
171
+ describe("synthesizePolicies", () => {
172
+ it("returns original spec when no failures", () => {
173
+ const harness = new DefaultMetaHarness({});
174
+ const spec = makeMinimalSpec();
175
+
176
+ const result = harness.synthesizePolicies(spec, []);
177
+
178
+ expect(result).toEqual(spec);
179
+ });
180
+
181
+ it("adds verification node for semantic failures", () => {
182
+ const harness = new DefaultMetaHarness({});
183
+ const spec = makeMinimalSpec();
184
+ const failures: FailureSignature[] = [
185
+ {
186
+ class: "semantic",
187
+ confidence: 0.9,
188
+ evidence: ["node: node-a", "test evidence"],
189
+ suggestedRecovery: [],
190
+ retryable: false,
191
+ requiresHumanIntervention: false,
192
+ },
193
+ ];
194
+
195
+ const result = harness.synthesizePolicies(spec, failures);
196
+
197
+ const nodeA = result.graph.nodes.find(n => n.id === "node-a");
198
+ expect(nodeA).toBeDefined();
199
+ expect(nodeA!.verificationPolicy).toBeDefined();
200
+ });
201
+
202
+ it("adds retry policy for network failures", () => {
203
+ const harness = new DefaultMetaHarness({});
204
+ const spec = makeMinimalSpec();
205
+ const failures: FailureSignature[] = [
206
+ {
207
+ class: "network",
208
+ confidence: 0.9,
209
+ evidence: ["node: node-a", "connection timeout"],
210
+ suggestedRecovery: [],
211
+ retryable: true,
212
+ requiresHumanIntervention: false,
213
+ },
214
+ ];
215
+
216
+ const result = harness.synthesizePolicies(spec, failures);
217
+
218
+ const nodeA = result.graph.nodes.find(n => n.id === "node-a");
219
+ expect(nodeA).toBeDefined();
220
+ expect(nodeA!.retryPolicy).toBeDefined();
221
+ });
222
+
223
+ it("adds auth check for auth failures", () => {
224
+ const harness = new DefaultMetaHarness({});
225
+ const spec: HarnessSpec = {
226
+ name: "test-harness",
227
+ graph: {
228
+ entryNodeId: "node-a",
229
+ nodes: [
230
+ {
231
+ id: "node-a",
232
+ kind: "tool",
233
+ tool: "git",
234
+ args: ["push"],
235
+ },
236
+ ],
237
+ edges: [],
238
+ },
239
+ };
240
+ const failures: FailureSignature[] = [
241
+ {
242
+ class: "auth",
243
+ confidence: 0.9,
244
+ evidence: ["node: node-a", "401 unauthorized"],
245
+ suggestedRecovery: [],
246
+ retryable: false,
247
+ requiresHumanIntervention: true,
248
+ },
249
+ ];
250
+
251
+ const result = harness.synthesizePolicies(spec, failures);
252
+
253
+ expect(result.graph.nodes.length).toBeGreaterThan(spec.graph.nodes.length);
254
+ });
255
+
256
+ it("applies multiple mutations for multiple failures", () => {
257
+ const harness = new DefaultMetaHarness({});
258
+ const spec: HarnessSpec = {
259
+ name: "test-harness",
260
+ graph: {
261
+ entryNodeId: "node-a",
262
+ nodes: [
263
+ {
264
+ id: "node-a",
265
+ kind: "tool",
266
+ tool: "git",
267
+ args: ["push"],
268
+ },
269
+ ],
270
+ edges: [],
271
+ },
272
+ };
273
+ const failures: FailureSignature[] = [
274
+ {
275
+ class: "auth",
276
+ confidence: 0.9,
277
+ evidence: ["node: node-a", "401"],
278
+ suggestedRecovery: [],
279
+ retryable: false,
280
+ requiresHumanIntervention: true,
281
+ },
282
+ {
283
+ class: "network",
284
+ confidence: 0.9,
285
+ evidence: ["node: node-a", "timeout"],
286
+ suggestedRecovery: [],
287
+ retryable: true,
288
+ requiresHumanIntervention: false,
289
+ },
290
+ ];
291
+
292
+ const result = harness.synthesizePolicies(spec, failures);
293
+
294
+ expect(result.graph.nodes.length).toBeGreaterThan(spec.graph.nodes.length);
295
+ });
296
+
297
+ it("does not duplicate mutations when node already has policy", () => {
298
+ const harness = new DefaultMetaHarness({});
299
+ const spec: HarnessSpec = {
300
+ name: "test-harness",
301
+ graph: {
302
+ entryNodeId: "node-a",
303
+ nodes: [
304
+ {
305
+ id: "node-a",
306
+ kind: "tool",
307
+ tool: "bash",
308
+ args: ["echo hello"],
309
+ retryPolicy: { maxAttempts: 3, backoff: "exponential" },
310
+ },
311
+ ],
312
+ edges: [],
313
+ },
314
+ };
315
+ const failures: FailureSignature[] = [
316
+ {
317
+ class: "network",
318
+ confidence: 0.9,
319
+ evidence: ["node-a", "timeout"],
320
+ suggestedRecovery: [],
321
+ retryable: true,
322
+ requiresHumanIntervention: false,
323
+ },
324
+ ];
325
+
326
+ const result = harness.synthesizePolicies(spec, failures);
327
+
328
+ const nodeA = result.graph.nodes.find(n => n.id === "node-a");
329
+ expect(nodeA).toBeDefined();
330
+ expect(nodeA!.retryPolicy).toBeDefined();
331
+ });
332
+ });
333
+
334
+ describe("generateHarness (full pipeline)", () => {
335
+ it("generates harness from intent string", async () => {
336
+ const harness = new DefaultMetaHarness({});
337
+
338
+ const result = await harness.generateHarness("Run tests and verify the build passes");
339
+
340
+ expect(result.spec).toBeDefined();
341
+ expect(result.spec.name).toBeDefined();
342
+ expect(result.predictedFailures).toBeDefined();
343
+ expect(result.optimizations).toBeDefined();
344
+ expect(result.readinessScore).toBeGreaterThan(0);
345
+ expect(result.readinessScore).toBeLessThanOrEqual(100);
346
+ });
347
+
348
+ it("includes environment analysis in result", async () => {
349
+ const harness = new DefaultMetaHarness({});
350
+
351
+ const result = await harness.generateHarness("Run tests");
352
+
353
+ expect(result.environmentAnalysis).toBeDefined();
354
+ });
355
+
356
+ it("queries memory store for advice when provided", async () => {
357
+ const memoryStore = makeMemoryStoreWithAdvice({
358
+ suggestions: ["Add verification after build step"],
359
+ warnings: [],
360
+ sourceTaskIds: ["prev-task-1"],
361
+ aggregateEffectiveness: 0.85,
362
+ });
363
+ const harness = new DefaultMetaHarness({ memoryStore });
364
+
365
+ const result = await harness.generateHarness("Run tests and verify");
366
+
367
+ expect(result.memoryAdvice).toBeDefined();
368
+ expect(result.memoryAdvice!.suggestions.length).toBeGreaterThan(0);
369
+ });
370
+
371
+ it("returns empty memory advice when no memory store", async () => {
372
+ const harness = new DefaultMetaHarness({});
373
+
374
+ const result = await harness.generateHarness("Run tests");
375
+
376
+ expect(result.memoryAdvice).toBeUndefined();
377
+ });
378
+
379
+ it("returns empty memory advice when store has no memories", async () => {
380
+ const harness = new DefaultMetaHarness({
381
+ memoryStore: makeEmptyMemoryStore(),
382
+ });
383
+
384
+ const result = await harness.generateHarness("Run tests");
385
+
386
+ expect(result.memoryAdvice).toBeUndefined();
387
+ });
388
+
389
+ it("predicts failures and synthesizes policies", async () => {
390
+ const harness = new DefaultMetaHarness({});
391
+
392
+ const result = await harness.generateHarness("Deploy to production using kubectl");
393
+
394
+ expect(result.predictedFailures).toBeDefined();
395
+ expect(result.spec).toBeDefined();
396
+ });
397
+
398
+ it("uses cached environment model when provided in config", async () => {
399
+ const cachedEnv = makeMinimalEnv();
400
+ const harness = new DefaultMetaHarness({
401
+ environmentModel: cachedEnv,
402
+ });
403
+
404
+ const result = await harness.generateHarness("Run tests");
405
+
406
+ expect(result.environmentAnalysis).toBeDefined();
407
+ });
408
+
409
+ it("includes optimizations in result", async () => {
410
+ const harness = new DefaultMetaHarness({});
411
+
412
+ const result = await harness.generateHarness("Run tests");
413
+
414
+ expect(result.optimizations).toBeDefined();
415
+ expect(Array.isArray(result.optimizations)).toBe(true);
416
+ });
417
+
418
+ it("calculates readiness score based on environment and failures", async () => {
419
+ const harness = new DefaultMetaHarness({});
420
+
421
+ const result = await harness.generateHarness("Run tests");
422
+
423
+ expect(result.readinessScore).toBeGreaterThanOrEqual(0);
424
+ expect(result.readinessScore).toBeLessThanOrEqual(100);
425
+ });
426
+
427
+ it("lower readiness score when predicted failures exist", async () => {
428
+ const harness = new DefaultMetaHarness({});
429
+
430
+ const result = await harness.generateHarness("Deploy using kubectl to production");
431
+
432
+ if (result.predictedFailures.length > 0) {
433
+ expect(result.readinessScore).toBeLessThan(100);
434
+ }
435
+ });
436
+ });
437
+
438
+ describe("capability-aware generation", () => {
439
+ it("generates harness without capability registry", async () => {
440
+ const harness = new DefaultMetaHarness({});
441
+
442
+ const result = await harness.generateHarness("Run tests");
443
+
444
+ expect(result.spec).toBeDefined();
445
+ });
446
+ });
447
+
448
+ describe("mutation policy enforcement", () => {
449
+ it("respects mutation policy during synthesis", () => {
450
+ const harness = new DefaultMetaHarness({
451
+ mutationPolicy: {
452
+ allowedMutations: ["add-node"],
453
+ maxMutations: 1,
454
+ },
455
+ });
456
+ const spec: HarnessSpec = {
457
+ name: "test-harness",
458
+ graph: {
459
+ entryNodeId: "node-a",
460
+ nodes: [
461
+ {
462
+ id: "node-a",
463
+ kind: "tool",
464
+ tool: "git",
465
+ args: ["push"],
466
+ },
467
+ ],
468
+ edges: [],
469
+ },
470
+ };
471
+ const failures: FailureSignature[] = [
472
+ {
473
+ class: "auth",
474
+ confidence: 0.9,
475
+ evidence: ["node: node-a", "401"],
476
+ suggestedRecovery: [],
477
+ retryable: false,
478
+ requiresHumanIntervention: true,
479
+ },
480
+ {
481
+ class: "network",
482
+ confidence: 0.9,
483
+ evidence: ["node: node-a", "timeout"],
484
+ suggestedRecovery: [],
485
+ retryable: true,
486
+ requiresHumanIntervention: false,
487
+ },
488
+ ];
489
+
490
+ const result = harness.synthesizePolicies(spec, failures);
491
+
492
+ expect(result.graph.nodes.length).toBeLessThanOrEqual(spec.graph.nodes.length + 1);
493
+ });
494
+ });
495
+
496
+ describe("composeHarnesses", () => {
497
+ it("chains multiple stages via meta-harness", () => {
498
+ const harness = new DefaultMetaHarness({});
499
+ const stages: HarnessStage[] = [
500
+ {
501
+ name: "research",
502
+ spec: makeMinimalSpec({ name: "research" }),
503
+ inputMapping: {},
504
+ },
505
+ {
506
+ name: "plan",
507
+ spec: makeMinimalSpec({ name: "plan" }),
508
+ inputMapping: {},
509
+ },
510
+ ];
511
+
512
+ const result = harness.composeHarnesses(stages);
513
+
514
+ expect(result.stageCount).toBe(2);
515
+ expect(result.totalNodes).toBe(2);
516
+ expect(result.combinedSpec.graph.nodes.length).toBe(2);
517
+ });
518
+
519
+ it("returns composition result with combined spec", () => {
520
+ const harness = new DefaultMetaHarness({});
521
+ const stages: HarnessStage[] = [
522
+ {
523
+ name: "single",
524
+ spec: makeMinimalSpec({ name: "single" }),
525
+ inputMapping: {},
526
+ },
527
+ ];
528
+
529
+ const result = harness.composeHarnesses(stages);
530
+
531
+ expect(result.combinedSpec.name).toBe("single");
532
+ expect(result.estimatedDurationMs).toBeGreaterThan(0);
533
+ });
534
+
535
+ it("prefixes node IDs to avoid collisions", () => {
536
+ const harness = new DefaultMetaHarness({});
537
+ const stages: HarnessStage[] = [
538
+ {
539
+ name: "alpha",
540
+ spec: {
541
+ name: "alpha",
542
+ graph: {
543
+ entryNodeId: "node-a",
544
+ nodes: [
545
+ { id: "node-a", kind: "tool", tool: "echo", args: ["a"] },
546
+ ],
547
+ edges: [],
548
+ },
549
+ },
550
+ inputMapping: {},
551
+ },
552
+ {
553
+ name: "beta",
554
+ spec: {
555
+ name: "beta",
556
+ graph: {
557
+ entryNodeId: "node-a",
558
+ nodes: [
559
+ { id: "node-a", kind: "tool", tool: "echo", args: ["b"] },
560
+ ],
561
+ edges: [],
562
+ },
563
+ },
564
+ inputMapping: {},
565
+ },
566
+ ];
567
+
568
+ const result = harness.composeHarnesses(stages);
569
+
570
+ const nodeIds = result.combinedSpec.graph.nodes.map((n) => n.id);
571
+ expect(nodeIds).toContain("alpha:node-a");
572
+ expect(nodeIds).toContain("beta:node-a");
573
+ });
574
+ });
575
+ });