@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,796 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parsePromptOrSkill, parseSkillMarkdown } from "../../src/synthesis/skill-parser.js";
3
+ import { buildTaskGraph } from "../../src/synthesis/graph-builder.js";
4
+ import { analyzeRisks } from "../../src/synthesis/risk-analyzer.js";
5
+ import { synthesizePolicy } from "../../src/synthesis/policy-builder.js";
6
+ import { synthesizeHarness } from "../../src/synthesis/harness-builder.js";
7
+
8
+ describe("skill-parser", () => {
9
+ describe("parsePromptOrSkill", () => {
10
+ describe("freeform PR review brief", () => {
11
+ it("should parse a complete PR review brief", () => {
12
+ const brief = `
13
+ Review the PR from feature-branch to main
14
+ repoPath: /path/to/repo
15
+ sourceBranch: feature-branch
16
+ targetBranch: main
17
+ reviewInstructions: "Check for security issues"
18
+ verification commands: [npm test, npm run lint]
19
+ `;
20
+
21
+ const result = parsePromptOrSkill(brief);
22
+
23
+ expect(result).toHaveProperty("success", true);
24
+ if ("success" in result && result.success) {
25
+ expect(result.intent.family).toBe("pr-review-merge");
26
+ expect(result.intent.inputs.repoPath).toBe("/path/to/repo");
27
+ expect(result.intent.inputs.sourceBranch).toBe("feature-branch");
28
+ expect(result.intent.inputs.targetBranch).toBe("main");
29
+ expect(result.intent.verificationTargets).toHaveLength(2);
30
+ }
31
+ });
32
+ });
33
+
34
+ describe("freeform patch-validation brief", () => {
35
+ it("should parse a patch validation brief with branch", () => {
36
+ const brief = `
37
+ Validate the patch for baseline v1.0.0
38
+ repoPath: /path/to/repo
39
+ baseline: v1.0.0
40
+ candidate branch: fix-branch
41
+ reproduce commands: [npm run fail-test]
42
+ verification commands: [npm test]
43
+ reviewInstructions: "Ensure the fix works"
44
+ `;
45
+
46
+ const result = parsePromptOrSkill(brief);
47
+
48
+ expect(result).toHaveProperty("success", true);
49
+ if ("success" in result && result.success) {
50
+ expect(result.intent.family).toBe("patch-validation");
51
+ expect(result.intent.inputs.baselineRef).toBe("v1.0.0");
52
+ expect(result.intent.inputs.candidateBranch).toBe("fix-branch");
53
+ }
54
+ });
55
+
56
+ it("should parse a patch validation brief with patch file", () => {
57
+ const brief = `
58
+ Validate the patch file fix.patch against baseline main
59
+ repoPath: /path/to/repo
60
+ baseline: main
61
+ patchFile: fix.patch
62
+ reproduce: [node test-fail.js]
63
+ verify: [npm test]
64
+ reviewInstructions: "Check if bug is fixed"
65
+ `;
66
+
67
+ const result = parsePromptOrSkill(brief);
68
+
69
+ expect(result).toHaveProperty("success", true);
70
+ if ("success" in result && result.success) {
71
+ expect(result.intent.family).toBe("patch-validation");
72
+ expect(result.intent.inputs.patchFilePath).toBe("fix.patch");
73
+ }
74
+ });
75
+
76
+ it("should include approval checkpoint when approvalRequired is true", () => {
77
+ const brief = `
78
+ patch validation with approval required
79
+ repoPath: /path/to/repo
80
+ baseline: main
81
+ candidate branch: fix-branch
82
+ reproduce: [npm run fail]
83
+ verify: [npm test]
84
+ reviewInstructions: "Validate fix"
85
+ approvalRequired: true
86
+ `;
87
+
88
+ const result = parsePromptOrSkill(brief);
89
+
90
+ expect(result).toHaveProperty("success", true);
91
+ if ("success" in result && result.success) {
92
+ expect(result.intent.humanCheckpoints).toContain("approval-gate");
93
+ }
94
+ });
95
+ });
96
+
97
+ describe("skill-markdown input", () => {
98
+ it("should parse skill markdown for PR review", () => {
99
+ const skillMd = `
100
+ # PR Review Workflow
101
+
102
+ workflow: pr-review-merge
103
+
104
+ ## Inputs
105
+ - repoPath: /path/to/repo
106
+ - sourceBranch: feature
107
+ - targetBranch: main
108
+ - reviewInstructions: Check code quality
109
+
110
+ ## Verification
111
+ - npm test
112
+ - npm run lint
113
+ `;
114
+
115
+ const result = parsePromptOrSkill(skillMd);
116
+
117
+ expect(result).toHaveProperty("success", true);
118
+ if ("success" in result && result.success) {
119
+ expect(result.intent.family).toBe("pr-review-merge");
120
+ expect(result.intent.inputs.repoPath).toBe("/path/to/repo");
121
+ expect(result.intent.inputs.sourceBranch).toBe("feature");
122
+ expect(result.intent.verificationTargets).toHaveLength(2);
123
+ }
124
+ });
125
+
126
+ it("should parse skill markdown for patch validation", () => {
127
+ const skillMd = `
128
+ # Patch Validation Workflow
129
+
130
+ workflow: patch-validation
131
+
132
+ ## Inputs
133
+ - repoPath: /path/to/repo
134
+ - baselineRef: v1.0.0
135
+ - candidateBranch: fix-branch
136
+ - reproduceCommands: [npm run fail-test]
137
+ - reviewInstructions: Validate the fix
138
+
139
+ ## Verification
140
+ - npm test
141
+ - npm run integration
142
+ `;
143
+
144
+ const result = parsePromptOrSkill(skillMd);
145
+
146
+ expect(result).toHaveProperty("success", true);
147
+ if ("success" in result && result.success) {
148
+ expect(result.intent.family).toBe("patch-validation");
149
+ expect(result.intent.inputs.baselineRef).toBe("v1.0.0");
150
+ }
151
+ });
152
+
153
+ it("should normalize array-like inputs in skill markdown for PR review", () => {
154
+ const skillMd = `
155
+ # PR Review Workflow
156
+
157
+ workflow: pr-review-merge
158
+
159
+ ## Inputs
160
+ - repoPath: /path/to/repo
161
+ - sourceBranch: feature
162
+ - targetBranch: main
163
+ - reviewInstructions: Check code quality
164
+ - verificationCommands: [npm test, npm run lint]
165
+
166
+ ## Verification
167
+ - npm run e2e
168
+ `;
169
+
170
+ const result = parsePromptOrSkill(skillMd);
171
+
172
+ expect(result).toHaveProperty("success", true);
173
+ if ("success" in result && result.success) {
174
+ expect(result.intent.family).toBe("pr-review-merge");
175
+ // verificationCommands should be normalized from string "[npm test, npm run lint]" to array
176
+ expect(result.intent.inputs.verificationCommands).toEqual(["npm test", "npm run lint", "npm run e2e"]);
177
+ // Verification section should merge/append to verificationCommands
178
+ expect(result.intent.verificationTargets).toContain("npm test");
179
+ expect(result.intent.verificationTargets).toContain("npm run lint");
180
+ expect(result.intent.verificationTargets).toContain("npm run e2e");
181
+ }
182
+ });
183
+
184
+ it("should normalize array-like and boolean inputs in skill markdown for patch validation", () => {
185
+ const skillMd = `
186
+ # Patch Validation Workflow
187
+
188
+ workflow: patch-validation
189
+
190
+ ## Inputs
191
+ - repoPath: /path/to/repo
192
+ - baselineRef: v1.0.0
193
+ - candidateBranch: fix-branch
194
+ - reproduceCommands: [npm run fail-test, node reproduce.js]
195
+ - verificationCommands: [npm test]
196
+ - reviewInstructions: Validate the fix
197
+ - approvalRequired: true
198
+
199
+ ## Verification
200
+ - npm run integration
201
+ `;
202
+
203
+ const result = parsePromptOrSkill(skillMd);
204
+
205
+ expect(result).toHaveProperty("success", true);
206
+ if ("success" in result && result.success) {
207
+ expect(result.intent.family).toBe("patch-validation");
208
+ // reproduceCommands should be normalized from string to array
209
+ expect(result.intent.inputs.reproduceCommands).toEqual(["npm run fail-test", "node reproduce.js"]);
210
+ // verificationCommands should include both inputs section and Verification section
211
+ expect(result.intent.inputs.verificationCommands).toEqual(["npm test", "npm run integration"]);
212
+ // approvalRequired should be normalized from string "true" to boolean true
213
+ expect(result.intent.inputs.approvalRequired).toBe(true);
214
+ expect(result.intent.humanCheckpoints).toContain("approval-gate");
215
+ }
216
+ });
217
+
218
+ it("should populate verificationCommands from Verification section when not in inputs", () => {
219
+ const skillMd = `
220
+ # PR Review Workflow
221
+
222
+ workflow: pr-review-merge
223
+
224
+ ## Inputs
225
+ - repoPath: /path/to/repo
226
+ - sourceBranch: feature
227
+ - targetBranch: main
228
+ - reviewInstructions: Check code quality
229
+
230
+ ## Verification
231
+ - npm test
232
+ - npm run lint
233
+ `;
234
+
235
+ const result = parsePromptOrSkill(skillMd);
236
+
237
+ expect(result).toHaveProperty("success", true);
238
+ if ("success" in result && result.success) {
239
+ expect(result.intent.family).toBe("pr-review-merge");
240
+ // Verification section should populate inputs.verificationCommands
241
+ expect(result.intent.inputs.verificationCommands).toEqual(["npm test", "npm run lint"]);
242
+ expect(result.intent.verificationTargets).toEqual(["npm test", "npm run lint"]);
243
+ }
244
+ });
245
+ });
246
+
247
+ describe("unsupported/underspecified cases", () => {
248
+ it("should reject empty input", () => {
249
+ const result = parsePromptOrSkill("");
250
+
251
+ expect(result).toHaveProperty("rejected", true);
252
+ if ("rejected" in result) {
253
+ expect(result.reasons).toContain("Input is empty");
254
+ }
255
+ });
256
+
257
+ it("should accept ambiguous input as custom workflow", () => {
258
+ const brief = "Do something with a repository at /path/to/repo";
259
+
260
+ const result = parsePromptOrSkill(brief);
261
+
262
+ expect(result).toHaveProperty("success", true);
263
+ if ("success" in result && result.success) {
264
+ expect(result.intent.family).toBe("custom");
265
+ }
266
+ });
267
+
268
+ it("should accept unsupported workflow as custom workflow", () => {
269
+ const brief = `
270
+ workflow: deploy-to-production
271
+ repoPath: /path/to/repo
272
+ `;
273
+
274
+ const result = parsePromptOrSkill(brief);
275
+
276
+ // Now accepted as a custom workflow family
277
+ expect(result).toHaveProperty("success", true);
278
+ if ("success" in result && result.success) {
279
+ expect(result.intent.family).toBe("deploy-to-production");
280
+ }
281
+ });
282
+ });
283
+ });
284
+
285
+ describe("parseSkillMarkdown", () => {
286
+ it("should extract title from markdown", () => {
287
+ const md = `# My Workflow Title`;
288
+ const skill = parseSkillMarkdown(md);
289
+ expect(skill.title).toBe("My Workflow Title");
290
+ });
291
+
292
+ it("should extract workflow type", () => {
293
+ const md = `workflow: patch-validation`;
294
+ const skill = parseSkillMarkdown(md);
295
+ expect(skill.workflow).toBe("patch-validation");
296
+ });
297
+
298
+ it("should extract inputs section", () => {
299
+ const md = `
300
+ ## Inputs
301
+ - repoPath: /path/to/repo
302
+ - branch: main
303
+ `;
304
+ const skill = parseSkillMarkdown(md);
305
+ expect(skill.inputs).toEqual({
306
+ repoPath: "/path/to/repo",
307
+ branch: "main"
308
+ });
309
+ });
310
+ });
311
+
312
+ describe("normalizeInputValue edge cases", () => {
313
+ it("should handle commands with commas inside quoted strings", () => {
314
+ const skillMd = `
315
+ # PR Review Workflow
316
+
317
+ workflow: pr-review-merge
318
+
319
+ ## Inputs
320
+ - repoPath: /path/to/repo
321
+ - sourceBranch: feature
322
+ - targetBranch: main
323
+ - reviewInstructions: Check code quality
324
+ - verificationCommands: ["echo 'hello, world'", "npm test"]
325
+ `;
326
+
327
+ const result = parsePromptOrSkill(skillMd);
328
+
329
+ expect(result).toHaveProperty("success", true);
330
+ if ("success" in result && result.success) {
331
+ expect(result.intent.inputs.verificationCommands).toEqual([
332
+ "echo 'hello, world'",
333
+ "npm test"
334
+ ]);
335
+ }
336
+ });
337
+
338
+ it("should handle commands with commas in double quotes", () => {
339
+ const skillMd = `
340
+ # Patch Validation
341
+
342
+ workflow: patch-validation
343
+
344
+ ## Inputs
345
+ - repoPath: /path/to/repo
346
+ - baselineRef: main
347
+ - candidateBranch: fix
348
+ - reproduceCommands: ["node test.js --data 'a, b, c'", "npm run fail"]
349
+ - verificationCommands: ["npm test"]
350
+ - reviewInstructions: Validate fix
351
+ `;
352
+
353
+ const result = parsePromptOrSkill(skillMd);
354
+
355
+ expect(result).toHaveProperty("success", true);
356
+ if ("success" in result && result.success) {
357
+ expect(result.intent.inputs.reproduceCommands).toEqual([
358
+ "node test.js --data 'a, b, c'",
359
+ "npm run fail"
360
+ ]);
361
+ }
362
+ });
363
+
364
+ it("should normalize empty array string [] to empty array", () => {
365
+ const skillMd = `
366
+ # PR Review Workflow
367
+
368
+ workflow: pr-review-merge
369
+
370
+ ## Inputs
371
+ - repoPath: /path/to/repo
372
+ - sourceBranch: feature
373
+ - targetBranch: main
374
+ - reviewInstructions: Check code quality
375
+ - verificationCommands: []
376
+ `;
377
+
378
+ const result = parsePromptOrSkill(skillMd);
379
+
380
+ expect(result).toHaveProperty("success", true);
381
+ if ("success" in result && result.success) {
382
+ // Should be normalized to an empty array, not remain as string "[]"
383
+ expect(Array.isArray(result.intent.inputs.verificationCommands)).toBe(true);
384
+ expect((result.intent.inputs.verificationCommands as string[]).length).toBe(0);
385
+ }
386
+ });
387
+
388
+ it("should handle arrays with quoted strings containing brackets", () => {
389
+ const skillMd = `
390
+ # Test Workflow
391
+
392
+ workflow: pr-review-merge
393
+
394
+ ## Inputs
395
+ - repoPath: /path/to/repo
396
+ - sourceBranch: feature
397
+ - targetBranch: main
398
+ - reviewInstructions: Check
399
+ - verificationCommands: ["echo '[test]'", "npm test"]
400
+ `;
401
+
402
+ const result = parsePromptOrSkill(skillMd);
403
+
404
+ expect(result).toHaveProperty("success", true);
405
+ if ("success" in result && result.success) {
406
+ expect(result.intent.inputs.verificationCommands).toEqual([
407
+ "echo '[test]'",
408
+ "npm test"
409
+ ]);
410
+ }
411
+ });
412
+
413
+ describe("steps field mapping to IntentIR", () => {
414
+ it("should populate intent.steps from skill markdown steps section", () => {
415
+ const skillMd = `
416
+ # Custom Workflow
417
+
418
+ workflow: patch-validation
419
+
420
+ ## Inputs
421
+ - repoPath: /path/to/repo
422
+ - baselineRef: main
423
+ - candidateBranch: fix
424
+ - reproduceCommands: [npm run fail]
425
+ - verificationCommands: [npm test]
426
+ - reviewInstructions: Validate fix
427
+
428
+ ## Steps
429
+ - [tool] Run npm install
430
+ - [tool] Apply the patch
431
+ - [llm] Summarize changes
432
+ - [human] Review the output
433
+ `;
434
+
435
+ const result = parsePromptOrSkill(skillMd);
436
+
437
+ expect(result).toHaveProperty("success", true);
438
+ if ("success" in result && result.success) {
439
+ expect(result.intent.steps).toBeDefined();
440
+ expect(result.intent.steps).toHaveLength(4);
441
+ }
442
+ });
443
+
444
+ it("should infer step kind from [tool] prefix", () => {
445
+ const skillMd = `
446
+ # Test
447
+
448
+ workflow: patch-validation
449
+
450
+ ## Inputs
451
+ - repoPath: /repo
452
+ - baselineRef: main
453
+ - candidateBranch: fix
454
+ - reproduceCommands: [npm run fail]
455
+ - verificationCommands: [npm test]
456
+ - reviewInstructions: Check
457
+
458
+ ## Steps
459
+ - [tool] npm install
460
+ `;
461
+
462
+ const result = parsePromptOrSkill(skillMd);
463
+
464
+ expect(result).toHaveProperty("success", true);
465
+ if ("success" in result && result.success) {
466
+ expect(result.intent.steps![0].kind).toBe("tool");
467
+ expect(result.intent.steps![0].label).toBe("npm install");
468
+ }
469
+ });
470
+
471
+ it("should infer step kind from [llm] prefix", () => {
472
+ const skillMd = `
473
+ # Test
474
+
475
+ workflow: patch-validation
476
+
477
+ ## Inputs
478
+ - repoPath: /repo
479
+ - baselineRef: main
480
+ - candidateBranch: fix
481
+ - reproduceCommands: [npm run fail]
482
+ - verificationCommands: [npm test]
483
+ - reviewInstructions: Check
484
+
485
+ ## Steps
486
+ - [llm] Analyze code quality
487
+ `;
488
+
489
+ const result = parsePromptOrSkill(skillMd);
490
+
491
+ expect(result).toHaveProperty("success", true);
492
+ if ("success" in result && result.success) {
493
+ expect(result.intent.steps![0].kind).toBe("llm");
494
+ expect(result.intent.steps![0].label).toBe("Analyze code quality");
495
+ }
496
+ });
497
+
498
+ it("should infer step kind from [human] prefix", () => {
499
+ const skillMd = `
500
+ # Test
501
+
502
+ workflow: patch-validation
503
+
504
+ ## Inputs
505
+ - repoPath: /repo
506
+ - baselineRef: main
507
+ - candidateBranch: fix
508
+ - reproduceCommands: [npm run fail]
509
+ - verificationCommands: [npm test]
510
+ - reviewInstructions: Check
511
+
512
+ ## Steps
513
+ - [human] Approve changes
514
+ `;
515
+
516
+ const result = parsePromptOrSkill(skillMd);
517
+
518
+ expect(result).toHaveProperty("success", true);
519
+ if ("success" in result && result.success) {
520
+ expect(result.intent.steps![0].kind).toBe("human");
521
+ expect(result.intent.steps![0].label).toBe("Approve changes");
522
+ }
523
+ });
524
+
525
+ it("should infer step kind from [condition] prefix", () => {
526
+ const skillMd = `
527
+ # Test
528
+
529
+ workflow: patch-validation
530
+
531
+ ## Inputs
532
+ - repoPath: /repo
533
+ - baselineRef: main
534
+ - candidateBranch: fix
535
+ - reproduceCommands: [npm run fail]
536
+ - verificationCommands: [npm test]
537
+ - reviewInstructions: Check
538
+
539
+ ## Steps
540
+ - [condition] Tests passed?
541
+ `;
542
+
543
+ const result = parsePromptOrSkill(skillMd);
544
+
545
+ expect(result).toHaveProperty("success", true);
546
+ if ("success" in result && result.success) {
547
+ expect(result.intent.steps![0].kind).toBe("condition");
548
+ }
549
+ });
550
+
551
+ it("should default to tool kind when no prefix is present", () => {
552
+ const skillMd = `
553
+ # Test
554
+
555
+ workflow: patch-validation
556
+
557
+ ## Inputs
558
+ - repoPath: /repo
559
+ - baselineRef: main
560
+ - candidateBranch: fix
561
+ - reproduceCommands: [npm run fail]
562
+ - verificationCommands: [npm test]
563
+ - reviewInstructions: Check
564
+
565
+ ## Steps
566
+ - npm install
567
+ `;
568
+
569
+ const result = parsePromptOrSkill(skillMd);
570
+
571
+ expect(result).toHaveProperty("success", true);
572
+ if ("success" in result && result.success) {
573
+ expect(result.intent.steps![0].kind).toBe("tool");
574
+ expect(result.intent.steps![0].label).toBe("npm install");
575
+ }
576
+ });
577
+
578
+ it("should preserve step order", () => {
579
+ const skillMd = `
580
+ # Test
581
+
582
+ workflow: patch-validation
583
+
584
+ ## Inputs
585
+ - repoPath: /repo
586
+ - baselineRef: main
587
+ - candidateBranch: fix
588
+ - reproduceCommands: [npm run fail]
589
+ - verificationCommands: [npm test]
590
+ - reviewInstructions: Check
591
+
592
+ ## Steps
593
+ - [tool] First step
594
+ - [llm] Second step
595
+ - [human] Third step
596
+ `;
597
+
598
+ const result = parsePromptOrSkill(skillMd);
599
+
600
+ expect(result).toHaveProperty("success", true);
601
+ if ("success" in result && result.success) {
602
+ expect(result.intent.steps![0].label).toBe("First step");
603
+ expect(result.intent.steps![1].label).toBe("Second step");
604
+ expect(result.intent.steps![2].label).toBe("Third step");
605
+ }
606
+ });
607
+
608
+ it("should generate unique step ids from label text", () => {
609
+ const skillMd = `
610
+ # Test
611
+
612
+ workflow: patch-validation
613
+
614
+ ## Inputs
615
+ - repoPath: /repo
616
+ - baselineRef: main
617
+ - candidateBranch: fix
618
+ - reproduceCommands: [npm run fail]
619
+ - verificationCommands: [npm test]
620
+ - reviewInstructions: Check
621
+
622
+ ## Steps
623
+ - [tool] Run tests
624
+ - [tool] Run linter
625
+ `;
626
+
627
+ const result = parsePromptOrSkill(skillMd);
628
+
629
+ expect(result).toHaveProperty("success", true);
630
+ if ("success" in result && result.success) {
631
+ expect(result.intent.steps![0].id).not.toBe(result.intent.steps![1].id);
632
+ }
633
+ });
634
+
635
+ it("should not populate steps when no steps section exists", () => {
636
+ const skillMd = `
637
+ # Test
638
+
639
+ workflow: patch-validation
640
+
641
+ ## Inputs
642
+ - repoPath: /repo
643
+ - baselineRef: main
644
+ - candidateBranch: fix
645
+ - reproduceCommands: [npm run fail]
646
+ - verificationCommands: [npm test]
647
+ - reviewInstructions: Check
648
+ `;
649
+
650
+ const result = parsePromptOrSkill(skillMd);
651
+
652
+ expect(result).toHaveProperty("success", true);
653
+ if ("success" in result && result.success) {
654
+ expect(result.intent.steps).toBeUndefined();
655
+ }
656
+ });
657
+ });
658
+
659
+ it("should normalize single command strings into command arrays", () => {
660
+ const skillMd = `
661
+ # Patch Validation
662
+
663
+ workflow: patch-validation
664
+
665
+ ## Inputs
666
+ - repoPath: /path/to/repo
667
+ - baselineRef: main
668
+ - candidateBranch: fix
669
+ - reproduceCommands: "npm run fail"
670
+ - verificationCommands: 'npm test'
671
+ - reviewInstructions: "Validate fix"
672
+ `;
673
+
674
+ const result = parsePromptOrSkill(skillMd);
675
+
676
+ expect(result).toHaveProperty("success", true);
677
+ if ("success" in result && result.success) {
678
+ expect(result.intent.inputs.reproduceCommands).toEqual(["npm run fail"]);
679
+ expect(result.intent.inputs.verificationCommands).toEqual(["npm test"]);
680
+ expect(result.intent.inputs.reviewInstructions).toBe("Validate fix");
681
+ }
682
+ });
683
+ });
684
+
685
+ describe("full pipeline: skill markdown with steps to harness", () => {
686
+ it("should compile skill markdown with custom steps all the way to harness spec", () => {
687
+ const skillMd = `
688
+ # Custom Build Pipeline
689
+
690
+ workflow: patch-validation
691
+
692
+ ## Inputs
693
+ - repoPath: /path/to/repo
694
+ - baselineRef: v1.0.0
695
+ - candidateBranch: fix-branch
696
+ - reproduceCommands: [npm run fail-test]
697
+ - verificationCommands: [npm test]
698
+ - reviewInstructions: Validate the fix
699
+
700
+ ## Steps
701
+ - [tool] npm install
702
+ - [tool] npm run build
703
+ - [llm] Summarize build output
704
+ - [human] Review summary
705
+
706
+ ## Verification
707
+ - npm test
708
+ `;
709
+
710
+ const intentResult = parsePromptOrSkill(skillMd);
711
+ expect(intentResult).toHaveProperty("success", true);
712
+
713
+ if (!("success" in intentResult) || !intentResult.success) return;
714
+
715
+ const intent = intentResult.intent;
716
+ expect(intent.steps).toHaveLength(4);
717
+
718
+ const graph = buildTaskGraph(intent);
719
+ expect(graph.stages.find(s => s.id === "step-1")).toBeDefined();
720
+ expect(graph.stages.find(s => s.id === "step-4")).toBeDefined();
721
+
722
+ const verifyNodes = graph.stages.filter(s => s.id.startsWith("verify-target-"));
723
+ expect(verifyNodes.length).toBeGreaterThan(0);
724
+
725
+ const risks = analyzeRisks(graph);
726
+ const policyResult = synthesizePolicy(graph, risks);
727
+ expect(policyResult.success).toBe(true);
728
+
729
+ const spec = synthesizeHarness(graph, risks);
730
+ expect(spec).toBeDefined();
731
+ expect(spec.graph.nodes.length).toBeGreaterThan(0);
732
+ });
733
+
734
+ it("should still compile bundled patch-validation workflow without steps", () => {
735
+ const skillMd = `
736
+ # Standard Patch Validation
737
+
738
+ workflow: patch-validation
739
+
740
+ ## Inputs
741
+ - repoPath: /path/to/repo
742
+ - baselineRef: v1.0.0
743
+ - candidateBranch: fix-branch
744
+ - reproduceCommands: [npm run fail-test]
745
+ - verificationCommands: [npm test]
746
+ - reviewInstructions: Validate the fix
747
+ `;
748
+
749
+ const intentResult = parsePromptOrSkill(skillMd);
750
+ expect(intentResult).toHaveProperty("success", true);
751
+
752
+ if (!("success" in intentResult) || !intentResult.success) return;
753
+
754
+ const graph = buildTaskGraph(intentResult.intent);
755
+ expect(graph.stages.map(s => s.type)).toEqual([
756
+ "setup", "reproduce", "apply", "verify", "review"
757
+ ]);
758
+
759
+ const risks = analyzeRisks(graph);
760
+ const spec = synthesizeHarness(graph, risks);
761
+ expect(spec.graph.entryNodeId).toBe("run-baseline");
762
+ });
763
+
764
+ it("should still compile bundled pr-review-merge workflow without steps", () => {
765
+ const skillMd = `
766
+ # Standard PR Review
767
+
768
+ workflow: pr-review-merge
769
+
770
+ ## Inputs
771
+ - repoPath: /path/to/repo
772
+ - sourceBranch: feature
773
+ - targetBranch: main
774
+ - reviewInstructions: Check code quality
775
+
776
+ ## Verification
777
+ - npm test
778
+ - npm run lint
779
+ `;
780
+
781
+ const intentResult = parsePromptOrSkill(skillMd);
782
+ expect(intentResult).toHaveProperty("success", true);
783
+
784
+ if (!("success" in intentResult) || !intentResult.success) return;
785
+
786
+ const graph = buildTaskGraph(intentResult.intent);
787
+ expect(graph.stages.map(s => s.type)).toEqual([
788
+ "setup", "review", "verify", "merge"
789
+ ]);
790
+
791
+ const risks = analyzeRisks(graph);
792
+ const spec = synthesizeHarness(graph, risks);
793
+ expect(spec.graph.entryNodeId).toBe("load-pr");
794
+ });
795
+ });
796
+ });