@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,901 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { planWorkflowRequest } from "../../src/planner/synthesize.js";
3
+
4
+ describe("planWorkflowRequest", () => {
5
+ describe("empty brief", () => {
6
+ it("rejects empty string", () => {
7
+ const result = planWorkflowRequest("");
8
+ expect(result.status).toBe("needs_clarification");
9
+ if (result.status === "needs_clarification") {
10
+ expect(result.reasons).toContain("Brief is empty");
11
+ expect(result.missingFields).toContain("brief");
12
+ expect(Array.isArray(result.guidance)).toBe(true);
13
+ expect(result.guidance.length).toBeGreaterThan(0);
14
+ }
15
+ });
16
+
17
+ it("rejects whitespace-only string", () => {
18
+ const result = planWorkflowRequest(" \n\t ");
19
+ expect(result.status).toBe("needs_clarification");
20
+ if (result.status === "needs_clarification") {
21
+ expect(result.reasons).toContain("Brief is empty");
22
+ expect(Array.isArray(result.guidance)).toBe(true);
23
+ }
24
+ });
25
+ });
26
+
27
+ describe("pr-review-merge happy path", () => {
28
+ it("extracts all fields from complete brief", () => {
29
+ const brief = `
30
+ PR review workflow:
31
+ repoPath: /Users/test/repo
32
+ source branch: feature-xyz
33
+ target branch: main
34
+ verification commands: ["npm test", "npm run lint"]
35
+ reviewInstructions: "Check for breaking changes"
36
+ `;
37
+
38
+ const result = planWorkflowRequest(brief);
39
+ expect(result.status).toBe("draft_request");
40
+
41
+ if (result.status === "draft_request") {
42
+ expect(result.workflow).toBe("pr-review-merge");
43
+ expect(result.request.workflow).toBe("pr-review-merge");
44
+ expect(Array.isArray(result.rationale)).toBe(true);
45
+ expect(result.rationale.length).toBeGreaterThan(0);
46
+ expect(result.rationale.some(r => r.includes("pr-review-merge"))).toBe(true);
47
+ expect(Array.isArray(result.warnings)).toBe(true);
48
+
49
+ if (result.request.workflow === "pr-review-merge") {
50
+ const input = result.request.input;
51
+ expect(input.repoPath).toBe("/Users/test/repo");
52
+ expect(input.sourceBranch).toBe("feature-xyz");
53
+ expect(input.targetBranch).toBe("main");
54
+ expect(input.verificationCommands).toEqual(["npm test", "npm run lint"]);
55
+ expect(input.reviewInstructions).toBe("Check for breaking changes");
56
+ }
57
+ }
58
+ });
59
+
60
+ it("extracts a Windows repo path from a generic path label", () => {
61
+ const brief = `
62
+ PR review workflow:
63
+ path: "C:/Users/test/repo"
64
+ source branch: feature-xyz
65
+ target branch: main
66
+ verification commands: ["npm test"]
67
+ reviewInstructions: "Check for breaking changes"
68
+ `;
69
+
70
+ const result = planWorkflowRequest(brief);
71
+ expect(result.status).toBe("draft_request");
72
+
73
+ if (result.status === "draft_request" && result.request.workflow === "pr-review-merge") {
74
+ expect(result.request.input.repoPath).toBe("C:/Users/test/repo");
75
+ }
76
+ });
77
+
78
+ it("extracts colon-suffixed branch fields without requiring a space", () => {
79
+ const brief = `
80
+ PR review workflow:
81
+ repoPath: /Users/test/repo
82
+ source:feature-xyz
83
+ target:main
84
+ verification commands: ["npm test"]
85
+ reviewInstructions: "Check for breaking changes"
86
+ `;
87
+
88
+ const result = planWorkflowRequest(brief);
89
+ expect(result.status).toBe("draft_request");
90
+
91
+ if (result.status === "draft_request" && result.request.workflow === "pr-review-merge") {
92
+ expect(result.request.input.sourceBranch).toBe("feature-xyz");
93
+ expect(result.request.input.targetBranch).toBe("main");
94
+ }
95
+ });
96
+
97
+ it("requires explicit reviewInstructions", () => {
98
+ const brief = `
99
+ Pull request review:
100
+ repoPath: /opt/project
101
+ source branch: dev
102
+ target branch: staging
103
+ verification: ["make test"]
104
+ `;
105
+
106
+ const result = planWorkflowRequest(brief);
107
+ expect(result.status).toBe("needs_clarification");
108
+
109
+ if (result.status === "needs_clarification") {
110
+ expect(result.candidateWorkflow).toBe("pr-review-merge");
111
+ expect(result.missingFields).toContain("reviewInstructions");
112
+ expect(result.guidance.some(g => g.includes("reviewInstructions"))).toBe(true);
113
+ }
114
+ });
115
+ });
116
+
117
+ describe("pr-review-merge missing fields", () => {
118
+ it("treats bare PR phrasing as a pr-review-merge request", () => {
119
+ const brief = `
120
+ Review the PR
121
+ repoPath: /repo
122
+ reviewInstructions: "Review carefully"
123
+ verification: ["test"]
124
+ `;
125
+
126
+ const result = planWorkflowRequest(brief);
127
+ expect(result.status).toBe("needs_clarification");
128
+
129
+ if (result.status === "needs_clarification") {
130
+ expect(result.candidateWorkflow).toBe("pr-review-merge");
131
+ expect(result.missingFields).toContain("sourceBranch");
132
+ expect(result.missingFields).toContain("targetBranch");
133
+ }
134
+ });
135
+
136
+ it("flags missing repoPath", () => {
137
+ const brief = `
138
+ PR merge:
139
+ source branch: feature
140
+ target branch: main
141
+ reviewInstructions: "Review carefully"
142
+ verification: ["test"]
143
+ `;
144
+
145
+ const result = planWorkflowRequest(brief);
146
+ expect(result.status).toBe("needs_clarification");
147
+
148
+ if (result.status === "needs_clarification") {
149
+ expect(result.candidateWorkflow).toBe("pr-review-merge");
150
+ expect(result.missingFields).toContain("repoPath");
151
+ expect(Array.isArray(result.guidance)).toBe(true);
152
+ expect(result.guidance.some(g => g.includes("repoPath"))).toBe(true);
153
+ }
154
+ });
155
+
156
+ it("flags missing sourceBranch", () => {
157
+ const brief = `
158
+ PR review:
159
+ repoPath: /repo
160
+ target branch: main
161
+ reviewInstructions: "Review carefully"
162
+ verification: ["test"]
163
+ `;
164
+
165
+ const result = planWorkflowRequest(brief);
166
+ expect(result.status).toBe("needs_clarification");
167
+
168
+ if (result.status === "needs_clarification") {
169
+ expect(result.candidateWorkflow).toBe("pr-review-merge");
170
+ expect(result.missingFields).toContain("sourceBranch");
171
+ }
172
+ });
173
+
174
+ it("flags missing targetBranch", () => {
175
+ const brief = `
176
+ PR merge:
177
+ repoPath: /repo
178
+ source branch: feature
179
+ reviewInstructions: "Review carefully"
180
+ verification: ["test"]
181
+ `;
182
+
183
+ const result = planWorkflowRequest(brief);
184
+ expect(result.status).toBe("needs_clarification");
185
+
186
+ if (result.status === "needs_clarification") {
187
+ expect(result.candidateWorkflow).toBe("pr-review-merge");
188
+ expect(result.missingFields).toContain("targetBranch");
189
+ }
190
+ });
191
+
192
+ it("flags missing verificationCommands", () => {
193
+ const brief = `
194
+ PR review:
195
+ repoPath: /repo
196
+ source branch: feature
197
+ target branch: main
198
+ reviewInstructions: "Review carefully"
199
+ `;
200
+
201
+ const result = planWorkflowRequest(brief);
202
+ expect(result.status).toBe("needs_clarification");
203
+
204
+ if (result.status === "needs_clarification") {
205
+ expect(result.candidateWorkflow).toBe("pr-review-merge");
206
+ expect(result.missingFields).toContain("verificationCommands");
207
+ }
208
+ });
209
+ });
210
+
211
+ describe("patch-validation happy path with branch", () => {
212
+ it("extracts all fields from complete brief", () => {
213
+ const brief = `
214
+ Patch validation workflow:
215
+ repoPath: /home/user/project
216
+ baseline: v1.2.3
217
+ candidate branch: bugfix-123
218
+ reproduce commands: ["npm run reproduce-bug"]
219
+ verification commands: ["npm test", "npm run integration"]
220
+ reviewInstructions: "Verify the bug is fixed"
221
+ approval required
222
+ `;
223
+
224
+ const result = planWorkflowRequest(brief);
225
+ expect(result.status).toBe("draft_request");
226
+
227
+ if (result.status === "draft_request") {
228
+ expect(result.workflow).toBe("patch-validation");
229
+ expect(result.request.workflow).toBe("patch-validation");
230
+ expect(Array.isArray(result.rationale)).toBe(true);
231
+ expect(result.rationale.length).toBeGreaterThan(0);
232
+ expect(result.rationale.some(r => r.includes("patch-validation"))).toBe(true);
233
+ expect(Array.isArray(result.warnings)).toBe(true);
234
+
235
+ if (result.request.workflow === "patch-validation") {
236
+ const input = result.request.input;
237
+ expect(input.repoPath).toBe("/home/user/project");
238
+ expect(input.baselineRef).toBe("v1.2.3");
239
+ expect(input.candidateSource.kind).toBe("branch");
240
+ expect(input.candidateSource.value).toBe("bugfix-123");
241
+ expect(input.reproduceCommands).toEqual(["npm run reproduce-bug"]);
242
+ expect(input.verificationCommands).toEqual(["npm test", "npm run integration"]);
243
+ expect(input.reviewInstructions).toBe("Verify the bug is fixed");
244
+ expect(input.approvalRequired).toBe(true);
245
+ }
246
+ }
247
+ });
248
+
249
+ it("extracts colon-suffixed patch fields without requiring a space", () => {
250
+ const brief = `
251
+ Patch validation workflow:
252
+ repoPath: /home/user/project
253
+ baseline:main
254
+ candidate:fix-branch
255
+ reproduce commands: ["npm run reproduce-bug"]
256
+ verification commands: ["npm test"]
257
+ reviewInstructions: "Verify the bug is fixed"
258
+ `;
259
+
260
+ const result = planWorkflowRequest(brief);
261
+ expect(result.status).toBe("draft_request");
262
+
263
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
264
+ expect(result.request.input.baselineRef).toBe("main");
265
+ expect(result.request.input.candidateSource.kind).toBe("branch");
266
+ expect(result.request.input.candidateSource.value).toBe("fix-branch");
267
+ }
268
+ });
269
+
270
+ it("defaults approvalRequired to false when not specified", () => {
271
+ const brief = `
272
+ Patch validation:
273
+ repoPath: /project
274
+ baseline: main
275
+ candidate branch: fix-branch
276
+ reproduce: ["./run-bug.sh"]
277
+ verification: ["make test"]
278
+ reviewInstructions: "Validate the fix"
279
+ `;
280
+
281
+ const result = planWorkflowRequest(brief);
282
+ expect(result.status).toBe("draft_request");
283
+
284
+ if (result.status === "draft_request") {
285
+ expect(result.workflow).toBe("patch-validation");
286
+ expect(Array.isArray(result.rationale)).toBe(true);
287
+ expect(result.rationale.length).toBeGreaterThan(0);
288
+ expect(result.warnings.some(w => w.includes("approvalRequired"))).toBe(true);
289
+
290
+ if (result.request.workflow === "patch-validation") {
291
+ expect(result.request.input.approvalRequired).toBe(false);
292
+ }
293
+ }
294
+ });
295
+ });
296
+
297
+ describe("patch-validation with patch file", () => {
298
+ it("extracts patchFile as candidate source", () => {
299
+ const brief = `
300
+ Validate this patch:
301
+ repoPath: /code/project
302
+ baseline: v2.0.0
303
+ candidate: /patches/fix.patch
304
+ reproduce commands: ["npm run fail"]
305
+ verification commands: ["npm test"]
306
+ reviewInstructions: "Validate the patch candidate"
307
+ `;
308
+
309
+ const result = planWorkflowRequest(brief);
310
+ expect(result.status).toBe("draft_request");
311
+
312
+ if (result.status === "draft_request") {
313
+ expect(result.workflow).toBe("patch-validation");
314
+ expect(Array.isArray(result.rationale)).toBe(true);
315
+ expect(result.rationale.length).toBeGreaterThan(0);
316
+
317
+ if (result.request.workflow === "patch-validation") {
318
+ const input = result.request.input;
319
+ expect(input.candidateSource.kind).toBe("patchFile");
320
+ expect(input.candidateSource.value).toBe("/patches/fix.patch");
321
+ }
322
+ }
323
+ });
324
+
325
+ it("detects .diff file extension", () => {
326
+ const brief = `
327
+ Patch validation:
328
+ repo: /app
329
+ baseline: stable
330
+ Apply change.diff
331
+ reproduce: ["test-bug"]
332
+ verify: ["test-suite"]
333
+ reviewInstructions: "Review the diff-based fix"
334
+ `;
335
+
336
+ const result = planWorkflowRequest(brief);
337
+ expect(result.status).toBe("draft_request");
338
+
339
+ if (result.status === "draft_request") {
340
+ expect(result.workflow).toBe("patch-validation");
341
+ expect(Array.isArray(result.rationale)).toBe(true);
342
+ expect(result.rationale.length).toBeGreaterThan(0);
343
+
344
+ if (result.request.workflow === "patch-validation") {
345
+ const input = result.request.input;
346
+ expect(input.candidateSource.kind).toBe("patchFile");
347
+ expect(input.candidateSource.value).toBe("change.diff");
348
+ }
349
+ }
350
+ });
351
+ });
352
+
353
+ describe("patch-validation missing fields", () => {
354
+ it("flags missing repoPath", () => {
355
+ const brief = `
356
+ Validate patch:
357
+ baseline: main
358
+ candidate branch: fix
359
+ reproduce: ["bug"]
360
+ verify: ["test"]
361
+ reviewInstructions: "Validate the fix"
362
+ `;
363
+
364
+ const result = planWorkflowRequest(brief);
365
+ expect(result.status).toBe("needs_clarification");
366
+
367
+ if (result.status === "needs_clarification") {
368
+ expect(result.candidateWorkflow).toBe("patch-validation");
369
+ expect(result.missingFields).toContain("repoPath");
370
+ }
371
+ });
372
+
373
+ it("flags missing baselineRef", () => {
374
+ const brief = `
375
+ Patch validation:
376
+ repoPath: /code
377
+ candidate branch: fix
378
+ reproduce: ["bug"]
379
+ verify: ["test"]
380
+ reviewInstructions: "Validate the fix"
381
+ `;
382
+
383
+ const result = planWorkflowRequest(brief);
384
+ expect(result.status).toBe("needs_clarification");
385
+
386
+ if (result.status === "needs_clarification") {
387
+ expect(result.candidateWorkflow).toBe("patch-validation");
388
+ expect(result.missingFields).toContain("baselineRef");
389
+ }
390
+ });
391
+
392
+ it("flags missing candidateSource", () => {
393
+ const brief = `
394
+ Validate this fix:
395
+ repoPath: /app
396
+ baseline: v1.0
397
+ reproduce: ["fail"]
398
+ verify: ["pass"]
399
+ reviewInstructions: "Validate the fix"
400
+ `;
401
+
402
+ const result = planWorkflowRequest(brief);
403
+ expect(result.status).toBe("needs_clarification");
404
+
405
+ if (result.status === "needs_clarification") {
406
+ expect(result.candidateWorkflow).toBe("patch-validation");
407
+ expect(result.missingFields).toContain("candidateSource (branch or patchFile)");
408
+ }
409
+ });
410
+
411
+ it("flags missing reproduceCommands", () => {
412
+ const brief = `
413
+ Patch validation:
414
+ repoPath: /code
415
+ baseline: main
416
+ candidate branch: fix
417
+ verify: ["test"]
418
+ reviewInstructions: "Validate the fix"
419
+ `;
420
+
421
+ const result = planWorkflowRequest(brief);
422
+ expect(result.status).toBe("needs_clarification");
423
+
424
+ if (result.status === "needs_clarification") {
425
+ expect(result.candidateWorkflow).toBe("patch-validation");
426
+ expect(result.missingFields).toContain("reproduceCommands");
427
+ }
428
+ });
429
+
430
+ it("flags missing verificationCommands", () => {
431
+ const brief = `
432
+ Validate patch:
433
+ repoPath: /code
434
+ baseline: main
435
+ candidate branch: fix
436
+ reproduce: ["bug"]
437
+ reviewInstructions: "Validate the fix"
438
+ `;
439
+
440
+ const result = planWorkflowRequest(brief);
441
+ expect(result.status).toBe("needs_clarification");
442
+
443
+ if (result.status === "needs_clarification") {
444
+ expect(result.candidateWorkflow).toBe("patch-validation");
445
+ expect(result.missingFields).toContain("verificationCommands");
446
+ }
447
+ });
448
+ });
449
+
450
+ describe("ambiguous briefs", () => {
451
+ it("rejects brief with both PR and patch signals", () => {
452
+ const brief = `
453
+ Review this PR and validate the patch:
454
+ source branch: feature
455
+ baseline: main
456
+ candidate: fix.patch
457
+ `;
458
+
459
+ const result = planWorkflowRequest(brief);
460
+ expect(result.status).toBe("needs_clarification");
461
+
462
+ if (result.status === "needs_clarification") {
463
+ expect(result.reasons[0]).toContain("Could not determine workflow type");
464
+ expect(result.missingFields).toContain("workflow type");
465
+ expect(result.candidateWorkflow).toBeUndefined();
466
+ expect(Array.isArray(result.guidance)).toBe(true);
467
+ }
468
+ });
469
+
470
+ it("accepts brief with no workflow signals as custom", () => {
471
+ const brief = `
472
+ repoPath: /code/app
473
+ Do some work on the codebase
474
+ `;
475
+
476
+ const result = planWorkflowRequest(brief);
477
+ expect(result.status).toBe("draft_request");
478
+
479
+ if (result.status === "draft_request") {
480
+ expect(result.workflow).toBe("custom");
481
+ }
482
+ });
483
+
484
+ it("accepts vague request as custom", () => {
485
+ const brief = "Check the code";
486
+
487
+ const result = planWorkflowRequest(brief);
488
+ expect(result.status).toBe("draft_request");
489
+
490
+ if (result.status === "draft_request") {
491
+ expect(result.workflow).toBe("custom");
492
+ }
493
+ });
494
+ });
495
+
496
+ describe("approval flag extraction", () => {
497
+ it("detects 'approval required' phrase", () => {
498
+ const brief = `
499
+ Patch validation with approval required:
500
+ repo: /app
501
+ baseline: v1
502
+ candidate branch: fix
503
+ reproduce: ["bug"]
504
+ verify: ["test"]
505
+ reviewInstructions: "Validate the fix"
506
+ `;
507
+
508
+ const result = planWorkflowRequest(brief);
509
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
510
+ expect(result.request.input.approvalRequired).toBe(true);
511
+ }
512
+ });
513
+
514
+ it("detects 'approvalRequired: true' format", () => {
515
+ const brief = `
516
+ Validate patch:
517
+ repoPath: /code
518
+ baseline: stable
519
+ candidate branch: hotfix
520
+ reproduce: ["fail-test"]
521
+ verify: ["pass-test"]
522
+ reviewInstructions: "Validate the hotfix"
523
+ approvalRequired: true
524
+ `;
525
+
526
+ const result = planWorkflowRequest(brief);
527
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
528
+ expect(result.request.input.approvalRequired).toBe(true);
529
+ }
530
+ });
531
+
532
+ it("detects 'no approval' phrase", () => {
533
+ const brief = `
534
+ Patch validation, no approval needed:
535
+ repo: /app
536
+ baseline: v2
537
+ candidate branch: auto-fix
538
+ reproduce: ["bug"]
539
+ verify: ["test"]
540
+ reviewInstructions: "Validate the automatic fix"
541
+ `;
542
+
543
+ const result = planWorkflowRequest(brief);
544
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
545
+ expect(result.request.input.approvalRequired).toBe(false);
546
+ }
547
+ });
548
+
549
+ it("detects 'approvalRequired: false' format", () => {
550
+ const brief = `
551
+ Validate:
552
+ repoPath: /src
553
+ baseline: main
554
+ candidate branch: fix
555
+ reproduce: ["fail"]
556
+ verify: ["pass"]
557
+ reviewInstructions: "Validate the fix"
558
+ approvalRequired: false
559
+ `;
560
+
561
+ const result = planWorkflowRequest(brief);
562
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
563
+ expect(result.request.input.approvalRequired).toBe(false);
564
+ }
565
+ });
566
+ });
567
+
568
+ describe("request envelope compatibility", () => {
569
+ it("returns ReferenceWorkflowRequest shape for pr-review-merge", () => {
570
+ const brief = `
571
+ PR review:
572
+ repoPath: /test
573
+ source branch: feat
574
+ target branch: main
575
+ reviewInstructions: "Review the feature branch"
576
+ verify: ["test"]
577
+ `;
578
+
579
+ const result = planWorkflowRequest(brief);
580
+ expect(result.status).toBe("draft_request");
581
+
582
+ if (result.status === "draft_request") {
583
+ expect(result.workflow).toBe("pr-review-merge");
584
+ expect(result.request).toHaveProperty("workflow");
585
+ expect(result.request).toHaveProperty("input");
586
+ expect(result.request.workflow).toBe("pr-review-merge");
587
+ expect(Array.isArray(result.rationale)).toBe(true);
588
+ expect(result.rationale.length).toBeGreaterThan(0);
589
+ expect(Array.isArray(result.warnings)).toBe(true);
590
+
591
+ if (result.request.workflow === "pr-review-merge") {
592
+ const input = result.request.input;
593
+ expect(input).toHaveProperty("repoPath");
594
+ expect(input).toHaveProperty("sourceBranch");
595
+ expect(input).toHaveProperty("targetBranch");
596
+ expect(input).toHaveProperty("reviewInstructions");
597
+ expect(input).toHaveProperty("verificationCommands");
598
+ expect(Array.isArray(input.verificationCommands)).toBe(true);
599
+ }
600
+ }
601
+ });
602
+
603
+ it("returns ReferenceWorkflowRequest shape for patch-validation", () => {
604
+ const brief = `
605
+ Validate patch:
606
+ repo: /app
607
+ baseline: v1
608
+ candidate branch: fix
609
+ reproduce: ["bug"]
610
+ verify: ["test"]
611
+ reviewInstructions: "Validate the candidate"
612
+ `;
613
+
614
+ const result = planWorkflowRequest(brief);
615
+ expect(result.status).toBe("draft_request");
616
+
617
+ if (result.status === "draft_request") {
618
+ expect(result.workflow).toBe("patch-validation");
619
+ expect(result.request).toHaveProperty("workflow");
620
+ expect(result.request).toHaveProperty("input");
621
+ expect(result.request.workflow).toBe("patch-validation");
622
+ expect(Array.isArray(result.rationale)).toBe(true);
623
+ expect(result.rationale.length).toBeGreaterThan(0);
624
+ expect(Array.isArray(result.warnings)).toBe(true);
625
+
626
+ if (result.request.workflow === "patch-validation") {
627
+ const input = result.request.input;
628
+ expect(input).toHaveProperty("repoPath");
629
+ expect(input).toHaveProperty("baselineRef");
630
+ expect(input).toHaveProperty("candidateSource");
631
+ expect(input.candidateSource).toHaveProperty("kind");
632
+ expect(input.candidateSource).toHaveProperty("value");
633
+ expect(input).toHaveProperty("reproduceCommands");
634
+ expect(input).toHaveProperty("verificationCommands");
635
+ expect(input).toHaveProperty("reviewInstructions");
636
+ expect(input).toHaveProperty("approvalRequired");
637
+ expect(Array.isArray(input.reproduceCommands)).toBe(true);
638
+ expect(Array.isArray(input.verificationCommands)).toBe(true);
639
+ expect(typeof input.approvalRequired).toBe("boolean");
640
+ }
641
+ }
642
+ });
643
+ });
644
+
645
+ describe("skill-markdown at planner level", () => {
646
+ it("should handle PR review skill markdown with normalization", () => {
647
+ const skillMarkdown = `
648
+ # PR Review Workflow
649
+
650
+ workflow: pr-review-merge
651
+
652
+ ## Inputs
653
+ - repoPath: /Users/test/repo
654
+ - sourceBranch: feature
655
+ - targetBranch: main
656
+ - reviewInstructions: Check code quality
657
+ - verificationCommands: [npm test, npm run lint]
658
+
659
+ ## Verification
660
+ - npm run e2e
661
+ `;
662
+
663
+ const result = planWorkflowRequest(skillMarkdown);
664
+ expect(result.status).toBe("draft_request");
665
+
666
+ if (result.status === "draft_request" && result.request.workflow === "pr-review-merge") {
667
+ const input = result.request.input;
668
+ expect(input.repoPath).toBe("/Users/test/repo");
669
+ expect(input.sourceBranch).toBe("feature");
670
+ expect(input.targetBranch).toBe("main");
671
+ expect(input.reviewInstructions).toBe("Check code quality");
672
+ // Should be normalized to array including both inputs and verification section
673
+ expect(Array.isArray(input.verificationCommands)).toBe(true);
674
+ expect(input.verificationCommands).toEqual(["npm test", "npm run lint", "npm run e2e"]);
675
+ }
676
+ });
677
+
678
+ it("should handle patch validation skill markdown with array and boolean normalization", () => {
679
+ const skillMarkdown = `
680
+ # Patch Validation Workflow
681
+
682
+ workflow: patch-validation
683
+
684
+ ## Inputs
685
+ - repoPath: /home/user/project
686
+ - baselineRef: v1.0.0
687
+ - candidateBranch: fix-branch
688
+ - reproduceCommands: [npm run fail-test, node reproduce.js]
689
+ - verificationCommands: [npm test]
690
+ - reviewInstructions: Validate the fix
691
+ - approvalRequired: true
692
+
693
+ ## Verification
694
+ - npm run integration
695
+ `;
696
+
697
+ const result = planWorkflowRequest(skillMarkdown);
698
+ expect(result.status).toBe("draft_request");
699
+
700
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
701
+ const input = result.request.input;
702
+ expect(input.repoPath).toBe("/home/user/project");
703
+ expect(input.baselineRef).toBe("v1.0.0");
704
+ expect(input.candidateSource.kind).toBe("branch");
705
+ expect(input.candidateSource.value).toBe("fix-branch");
706
+ // Arrays should be normalized from string representation
707
+ expect(Array.isArray(input.reproduceCommands)).toBe(true);
708
+ expect(input.reproduceCommands).toEqual(["npm run fail-test", "node reproduce.js"]);
709
+ expect(Array.isArray(input.verificationCommands)).toBe(true);
710
+ expect(input.verificationCommands).toEqual(["npm test", "npm run integration"]);
711
+ // Boolean should be normalized from string "true"
712
+ expect(typeof input.approvalRequired).toBe("boolean");
713
+ expect(input.approvalRequired).toBe(true);
714
+ }
715
+ });
716
+
717
+ it("should populate verificationCommands from Verification section when not in inputs", () => {
718
+ const skillMarkdown = `
719
+ # Patch Validation Workflow
720
+
721
+ workflow: patch-validation
722
+
723
+ ## Inputs
724
+ - repoPath: /home/user/project
725
+ - baselineRef: main
726
+ - candidateBranch: fix
727
+ - reproduceCommands: [npm run fail]
728
+ - reviewInstructions: Check fix
729
+
730
+ ## Verification
731
+ - npm test
732
+ - npm run integration
733
+ `;
734
+
735
+ const result = planWorkflowRequest(skillMarkdown);
736
+ expect(result.status).toBe("draft_request");
737
+
738
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
739
+ const input = result.request.input;
740
+ // Verification section should populate verificationCommands
741
+ expect(Array.isArray(input.verificationCommands)).toBe(true);
742
+ expect(input.verificationCommands).toEqual(["npm test", "npm run integration"]);
743
+ }
744
+ });
745
+ });
746
+
747
+ describe("empty command array validation", () => {
748
+ it("should reject PR review when verificationCommands is empty array", () => {
749
+ const skillMarkdown = `
750
+ # PR Review Workflow
751
+
752
+ workflow: pr-review-merge
753
+
754
+ ## Inputs
755
+ - repoPath: /Users/test/repo
756
+ - sourceBranch: feature
757
+ - targetBranch: main
758
+ - reviewInstructions: Check code quality
759
+ - verificationCommands: []
760
+ `;
761
+
762
+ const result = planWorkflowRequest(skillMarkdown);
763
+ expect(result.status).toBe("needs_clarification");
764
+
765
+ if (result.status === "needs_clarification") {
766
+ expect(result.missingFields).toContain("verificationCommands");
767
+ expect(result.candidateWorkflow).toBe("pr-review-merge");
768
+ }
769
+ });
770
+
771
+ it("should reject patch validation when reproduceCommands is empty array", () => {
772
+ const skillMarkdown = `
773
+ # Patch Validation
774
+
775
+ workflow: patch-validation
776
+
777
+ ## Inputs
778
+ - repoPath: /home/user/project
779
+ - baselineRef: main
780
+ - candidateBranch: fix-branch
781
+ - reproduceCommands: []
782
+ - verificationCommands: [npm test]
783
+ - reviewInstructions: Validate fix
784
+ `;
785
+
786
+ const result = planWorkflowRequest(skillMarkdown);
787
+ expect(result.status).toBe("needs_clarification");
788
+
789
+ if (result.status === "needs_clarification") {
790
+ expect(result.missingFields).toContain("reproduceCommands");
791
+ expect(result.candidateWorkflow).toBe("patch-validation");
792
+ }
793
+ });
794
+
795
+ it("should reject patch validation when verificationCommands is empty array", () => {
796
+ const skillMarkdown = `
797
+ # Patch Validation
798
+
799
+ workflow: patch-validation
800
+
801
+ ## Inputs
802
+ - repoPath: /home/user/project
803
+ - baselineRef: main
804
+ - candidateBranch: fix-branch
805
+ - reproduceCommands: [npm run fail]
806
+ - verificationCommands: []
807
+ - reviewInstructions: Validate fix
808
+ `;
809
+
810
+ const result = planWorkflowRequest(skillMarkdown);
811
+ expect(result.status).toBe("needs_clarification");
812
+
813
+ if (result.status === "needs_clarification") {
814
+ expect(result.missingFields).toContain("verificationCommands");
815
+ expect(result.candidateWorkflow).toBe("patch-validation");
816
+ }
817
+ });
818
+ });
819
+
820
+ describe("commands with commas in quoted strings", () => {
821
+ it("should preserve commas inside quoted command strings", () => {
822
+ const skillMarkdown = `
823
+ # PR Review Workflow
824
+
825
+ workflow: pr-review-merge
826
+
827
+ ## Inputs
828
+ - repoPath: /Users/test/repo
829
+ - sourceBranch: feature
830
+ - targetBranch: main
831
+ - reviewInstructions: Check code quality
832
+ - verificationCommands: ["echo 'hello, world'", "npm test --reporter 'json, summary'"]
833
+ `;
834
+
835
+ const result = planWorkflowRequest(skillMarkdown);
836
+ expect(result.status).toBe("draft_request");
837
+
838
+ if (result.status === "draft_request" && result.request.workflow === "pr-review-merge") {
839
+ const input = result.request.input;
840
+ expect(input.verificationCommands).toEqual([
841
+ "echo 'hello, world'",
842
+ "npm test --reporter 'json, summary'"
843
+ ]);
844
+ }
845
+ });
846
+
847
+ it("should handle commands with double-quoted strings containing commas", () => {
848
+ const skillMarkdown = `
849
+ # Patch Validation
850
+
851
+ workflow: patch-validation
852
+
853
+ ## Inputs
854
+ - repoPath: /home/user/project
855
+ - baselineRef: main
856
+ - candidateBranch: fix-branch
857
+ - reproduceCommands: ["node test.js --data \\"a, b, c\\"", "npm run fail"]
858
+ - verificationCommands: ["npm test"]
859
+ - reviewInstructions: Validate fix
860
+ `;
861
+
862
+ const result = planWorkflowRequest(skillMarkdown);
863
+ expect(result.status).toBe("draft_request");
864
+
865
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
866
+ const input = result.request.input;
867
+ expect(input.reproduceCommands).toEqual([
868
+ 'node test.js --data "a, b, c"',
869
+ "npm run fail"
870
+ ]);
871
+ }
872
+ });
873
+ });
874
+
875
+ describe("single command string normalization", () => {
876
+ it("should normalize quoted single command strings into arrays", () => {
877
+ const skillMarkdown = `
878
+ # Patch Validation
879
+
880
+ workflow: patch-validation
881
+
882
+ ## Inputs
883
+ - repoPath: /home/user/project
884
+ - baselineRef: main
885
+ - candidateBranch: fix-branch
886
+ - reproduceCommands: "npm run fail"
887
+ - verificationCommands: 'npm test'
888
+ - reviewInstructions: "Validate fix"
889
+ `;
890
+
891
+ const result = planWorkflowRequest(skillMarkdown);
892
+ expect(result.status).toBe("draft_request");
893
+
894
+ if (result.status === "draft_request" && result.request.workflow === "patch-validation") {
895
+ expect(result.request.input.reproduceCommands).toEqual(["npm run fail"]);
896
+ expect(result.request.input.verificationCommands).toEqual(["npm test"]);
897
+ expect(result.request.input.reviewInstructions).toBe("Validate fix");
898
+ }
899
+ });
900
+ });
901
+ });