@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,619 @@
1
+ import { parseWorkflowRequest, type ReferenceWorkflowRequest } from "../reference/catalog.js";
2
+ import type { LocalPatchValidationBundle, LocalPrBundle } from "../reference/types.js";
3
+ import {
4
+ classifyPatchValidationRisk,
5
+ classifyPrReviewMergeRisk,
6
+ describeAbortReason,
7
+ normalizeNotes,
8
+ notesContainAny,
9
+ } from "./risk-rules.js";
10
+ import type {
11
+ PatchValidationObservedOutcome,
12
+ PatchValidationTerminalNodeId,
13
+ PrReviewMergeObservedOutcome,
14
+ PrReviewMergeTerminalNodeId,
15
+ ReplanAbortReason,
16
+ ReplanRequest,
17
+ ReplanResult,
18
+ ReplanWorkflow,
19
+ RiskLevel,
20
+ } from "./types.js";
21
+
22
+ const PATCH_VALIDATION_TERMINALS: PatchValidationTerminalNodeId[] = [
23
+ "validated-fix",
24
+ "not-reproduced",
25
+ "apply-failed",
26
+ "candidate-failed",
27
+ "rejected",
28
+ ];
29
+
30
+ const PR_REVIEW_TERMINALS: PrReviewMergeTerminalNodeId[] = [
31
+ "complete-success",
32
+ "reject-verification",
33
+ "reject-human",
34
+ "merge-conflict",
35
+ ];
36
+
37
+ export function parseReplanRequest(args: string): ReplanRequest {
38
+ const trimmed = args.trim();
39
+ if (!trimmed) {
40
+ throw new Error("Usage: /lasso:replan <replan request JSON>");
41
+ }
42
+
43
+ let parsed: unknown;
44
+ try {
45
+ parsed = JSON.parse(trimmed);
46
+ } catch {
47
+ throw new Error("Invalid replan request JSON");
48
+ }
49
+
50
+ if (!isRecord(parsed)) {
51
+ throw new Error("Invalid replan request shape");
52
+ }
53
+
54
+ const workflow = parseWorkflow(parsed.workflow);
55
+ const originalRequest = parseOriginalRequest(workflow, parsed.originalRequest);
56
+ const observedOutcome = parseObservedOutcome(workflow, parsed.observedOutcome);
57
+
58
+ return {
59
+ workflow,
60
+ originalRequest,
61
+ observedOutcome,
62
+ } as ReplanRequest;
63
+ }
64
+
65
+ export function replanWorkflowRequest(request: ReplanRequest): ReplanResult {
66
+ validateParsedRequest(request);
67
+
68
+ if (request.workflow === "patch-validation") {
69
+ return replanPatchValidation(request.originalRequest, request.observedOutcome);
70
+ }
71
+
72
+ return replanPrReviewMerge(request.originalRequest, request.observedOutcome);
73
+ }
74
+
75
+ function replanPatchValidation(
76
+ originalRequest: { workflow: "patch-validation"; input: LocalPatchValidationBundle },
77
+ observedOutcome: PatchValidationObservedOutcome,
78
+ ): ReplanResult {
79
+ const risk = classifyPatchValidationRisk(originalRequest.input, observedOutcome);
80
+ const notes = normalizeNotes(observedOutcome.notes);
81
+
82
+ if (observedOutcome.terminalNodeId === "rejected") {
83
+ return {
84
+ status: "stop",
85
+ workflow: "patch-validation",
86
+ riskLevel: "high",
87
+ reasons: [
88
+ ...risk.reasons,
89
+ "Do not auto-replan a patch-validation request after explicit human rejection.",
90
+ ],
91
+ guidance: [
92
+ "Review the candidate fix manually before deciding whether to produce a new request.",
93
+ "If you retry, update the candidate itself or the review instructions intentionally rather than relying on automatic replanning.",
94
+ ],
95
+ };
96
+ }
97
+
98
+ if (observedOutcome.aborted) {
99
+ return replanAbortedPatchValidation(originalRequest.input, observedOutcome, risk.riskLevel);
100
+ }
101
+
102
+ if (observedOutcome.terminalNodeId === "validated-fix") {
103
+ if (!originalRequest.input.approvalRequired && risk.riskLevel === "high") {
104
+ const request: ReferenceWorkflowRequest = {
105
+ workflow: "patch-validation",
106
+ input: {
107
+ ...originalRequest.input,
108
+ approvalRequired: true,
109
+ },
110
+ };
111
+
112
+ return {
113
+ status: "draft_request",
114
+ workflow: "patch-validation",
115
+ request,
116
+ trigger: "risk-escalation",
117
+ riskLevel: risk.riskLevel,
118
+ rationale: [
119
+ ...risk.reasons,
120
+ "The previous attempt already validated the candidate, so the safest deterministic v1 replan is to rerun with a human approval gate.",
121
+ ],
122
+ warnings: risk.warnings,
123
+ changes: ["approvalRequired: false -> true"],
124
+ };
125
+ }
126
+
127
+ return {
128
+ status: "stop",
129
+ workflow: "patch-validation",
130
+ riskLevel: ensureStopRiskLevel(risk.riskLevel),
131
+ reasons: [
132
+ ...risk.reasons,
133
+ "The previous patch-validation attempt already succeeded and there is no further safe automatic mutation to make.",
134
+ ],
135
+ guidance: [
136
+ "Reuse the existing request if you intentionally want to rerun it.",
137
+ "If you need different behavior, edit the request explicitly before compiling or running again.",
138
+ ],
139
+ };
140
+ }
141
+
142
+ if (observedOutcome.terminalNodeId === "not-reproduced") {
143
+ return {
144
+ status: "needs_operator_input",
145
+ candidateWorkflow: "patch-validation",
146
+ riskLevel: ensureInteractiveRiskLevel(risk.riskLevel, "medium"),
147
+ reasons: [
148
+ ...risk.reasons,
149
+ "The baseline reproduce commands did not fail, so Lasso cannot tell whether the requested bug is still present on the chosen baseline.",
150
+ ],
151
+ missingFields: ["baselineRef", "reproduceCommands"],
152
+ guidance: [
153
+ "Provide a baselineRef that still contains the bug, or tighten reproduceCommands so they fail on that baseline.",
154
+ "Do not auto-retry until the baseline bug reproduction is explicit and trustworthy.",
155
+ ],
156
+ };
157
+ }
158
+
159
+ if (observedOutcome.terminalNodeId === "apply-failed") {
160
+ return {
161
+ status: "needs_operator_input",
162
+ candidateWorkflow: "patch-validation",
163
+ riskLevel: ensureInteractiveRiskLevel(risk.riskLevel, "medium"),
164
+ reasons: [
165
+ ...risk.reasons,
166
+ "The candidate could not be applied cleanly, so automatic replanning cannot infer a corrected candidate source.",
167
+ ],
168
+ missingFields: ["candidateSource"],
169
+ guidance: [
170
+ "Provide a corrected branch name or patch file path for candidateSource.",
171
+ "If the candidate failed because of repo setup, include operator notes describing that setup issue before retrying.",
172
+ ],
173
+ };
174
+ }
175
+
176
+ const candidateFailedFromVerification = notesContainAny(notes, ["verification", "verify", "regression"]);
177
+ const candidateFailedFromReproduction = notesContainAny(notes, ["reproduce", "reproduction", "still failing"]);
178
+
179
+ if (observedOutcome.terminalNodeId === "candidate-failed") {
180
+ const missingFields = candidateFailedFromVerification
181
+ ? ["candidateSource", "verificationCommands"]
182
+ : candidateFailedFromReproduction
183
+ ? ["candidateSource"]
184
+ : ["candidateSource", "observedOutcome.notes"];
185
+
186
+ const guidance = candidateFailedFromVerification
187
+ ? [
188
+ "Provide a revised candidateSource and review whether verificationCommands are too broad or now catching a real regression.",
189
+ "Keep the previous verification details in observedOutcome.notes so the next attempt is explainable.",
190
+ ]
191
+ : candidateFailedFromReproduction
192
+ ? [
193
+ "Provide a different candidateSource because the previous fix still reproduced the bug.",
194
+ "You can keep verificationCommands unchanged unless the next candidate changes the broader validation surface.",
195
+ ]
196
+ : [
197
+ "Provide a revised candidateSource and add observedOutcome.notes explaining whether reproduction still failed or broader verification failed.",
198
+ "Lasso will not guess whether this was a bug-reproduction failure or a regression failure.",
199
+ ];
200
+
201
+ return {
202
+ status: "needs_operator_input",
203
+ candidateWorkflow: "patch-validation",
204
+ riskLevel: ensureInteractiveRiskLevel(risk.riskLevel, "medium"),
205
+ reasons: [
206
+ ...risk.reasons,
207
+ "The previous candidate did not validate, and Lasso cannot infer a safe replacement candidate automatically.",
208
+ ],
209
+ missingFields,
210
+ guidance,
211
+ };
212
+ }
213
+
214
+ return {
215
+ status: "needs_operator_input",
216
+ candidateWorkflow: "patch-validation",
217
+ riskLevel: ensureInteractiveRiskLevel(risk.riskLevel, "medium"),
218
+ reasons: [
219
+ ...risk.reasons,
220
+ "The observed patch-validation outcome did not match a supported replanning rule.",
221
+ ],
222
+ missingFields: ["observedOutcome.notes"],
223
+ guidance: [
224
+ "Add operator notes explaining what happened so the next request can be revised intentionally.",
225
+ ],
226
+ };
227
+ }
228
+
229
+ function replanAbortedPatchValidation(
230
+ input: LocalPatchValidationBundle,
231
+ observedOutcome: PatchValidationObservedOutcome,
232
+ riskLevel: RiskLevel,
233
+ ): ReplanResult {
234
+ switch (observedOutcome.abortReason) {
235
+ case "manual-stop":
236
+ return {
237
+ status: "stop",
238
+ workflow: "patch-validation",
239
+ riskLevel: "high",
240
+ reasons: [
241
+ `The previous patch-validation attempt was stopped manually.`,
242
+ "Automatic replanning should not override an explicit operator stop.",
243
+ ],
244
+ guidance: [
245
+ "Review the prior run manually before deciding whether to construct a new request.",
246
+ ],
247
+ };
248
+ case "setup-failure":
249
+ return {
250
+ status: "needs_operator_input",
251
+ candidateWorkflow: "patch-validation",
252
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
253
+ reasons: [
254
+ `The previous patch-validation attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
255
+ "Setup failures usually mean the repository path, baseline ref, or candidate source needs correction.",
256
+ ],
257
+ missingFields: ["repoPath", "baselineRef", "candidateSource"],
258
+ guidance: [
259
+ `Verify that repoPath points at a disposable repository, baselineRef resolves cleanly, and candidateSource still exists in ${input.repoPath}.`,
260
+ "Add observedOutcome.notes if the setup failure came from a more specific cause such as a missing patch file.",
261
+ ],
262
+ };
263
+ case "retry-exhaustion":
264
+ return {
265
+ status: "needs_operator_input",
266
+ candidateWorkflow: "patch-validation",
267
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
268
+ reasons: [
269
+ `The previous patch-validation attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
270
+ "Retry exhaustion usually means the verification environment or command set needs human diagnosis before another attempt.",
271
+ ],
272
+ missingFields: ["verificationCommands", "observedOutcome.notes"],
273
+ guidance: [
274
+ "Review verificationCommands for flaky or environment-sensitive checks before retrying.",
275
+ "Use observedOutcome.notes to record what kept failing so the next request is auditable.",
276
+ ],
277
+ };
278
+ case "timeout":
279
+ return {
280
+ status: "needs_operator_input",
281
+ candidateWorkflow: "patch-validation",
282
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
283
+ reasons: [
284
+ `The previous patch-validation attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
285
+ "The current request does not expose timeout tuning, so a human must decide whether the commands or environment need to change.",
286
+ ],
287
+ missingFields: ["observedOutcome.notes"],
288
+ guidance: [
289
+ "Record which step timed out in observedOutcome.notes and decide whether reproduceCommands or verificationCommands should be shortened or split.",
290
+ ],
291
+ };
292
+ case "unknown":
293
+ default:
294
+ return {
295
+ status: "needs_operator_input",
296
+ candidateWorkflow: "patch-validation",
297
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
298
+ reasons: [
299
+ `The previous patch-validation attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
300
+ "Lasso cannot safely revise the request until the operator explains what failed.",
301
+ ],
302
+ missingFields: ["observedOutcome.notes"],
303
+ guidance: [
304
+ "Provide observedOutcome.notes describing the failure before attempting another replan.",
305
+ ],
306
+ };
307
+ }
308
+ }
309
+
310
+ function replanPrReviewMerge(
311
+ originalRequest: { workflow: "pr-review-merge"; input: LocalPrBundle },
312
+ observedOutcome: PrReviewMergeObservedOutcome,
313
+ ): ReplanResult {
314
+ const risk = classifyPrReviewMergeRisk(originalRequest.input, observedOutcome);
315
+
316
+ if (observedOutcome.terminalNodeId === "reject-human") {
317
+ return {
318
+ status: "stop",
319
+ workflow: "pr-review-merge",
320
+ riskLevel: "high",
321
+ reasons: [
322
+ ...risk.reasons,
323
+ "Do not auto-replan after an explicit human rejection of the merge.",
324
+ ],
325
+ guidance: [
326
+ "Review the source branch manually before deciding whether to produce a new merge request.",
327
+ ],
328
+ };
329
+ }
330
+
331
+ if (observedOutcome.aborted) {
332
+ return replanAbortedPrReviewMerge(originalRequest.input, observedOutcome, risk.riskLevel);
333
+ }
334
+
335
+ if (observedOutcome.terminalNodeId === "complete-success") {
336
+ return {
337
+ status: "stop",
338
+ workflow: "pr-review-merge",
339
+ riskLevel: "medium",
340
+ reasons: [
341
+ ...risk.reasons,
342
+ "The previous PR review + merge attempt already succeeded and there is no safe automatic request mutation for the success path.",
343
+ ],
344
+ guidance: [
345
+ "Reuse the existing request only if you intentionally want another local simulation.",
346
+ ],
347
+ };
348
+ }
349
+
350
+ if (observedOutcome.terminalNodeId === "reject-verification") {
351
+ return {
352
+ status: "needs_operator_input",
353
+ candidateWorkflow: "pr-review-merge",
354
+ riskLevel: ensureInteractiveRiskLevel(risk.riskLevel, "medium"),
355
+ reasons: [
356
+ ...risk.reasons,
357
+ "Verification failed before merge, and Lasso cannot infer whether the fix is in the branch content or the verification command set.",
358
+ ],
359
+ missingFields: ["sourceBranch", "verificationCommands"],
360
+ guidance: [
361
+ "Update the source branch with the intended fixes and review verificationCommands before retrying.",
362
+ "If the failure came from a broader environment issue, describe it in observedOutcome.notes.",
363
+ ],
364
+ };
365
+ }
366
+
367
+ if (observedOutcome.terminalNodeId === "merge-conflict") {
368
+ return {
369
+ status: "needs_operator_input",
370
+ candidateWorkflow: "pr-review-merge",
371
+ riskLevel: ensureInteractiveRiskLevel(risk.riskLevel, "medium"),
372
+ reasons: [
373
+ ...risk.reasons,
374
+ "The merge hit a conflict, and Lasso cannot resolve or rename the source branch automatically.",
375
+ ],
376
+ missingFields: ["sourceBranch"],
377
+ guidance: [
378
+ "Update the source branch against the current target branch, then provide the refreshed sourceBranch for the next attempt.",
379
+ ],
380
+ };
381
+ }
382
+
383
+ return {
384
+ status: "needs_operator_input",
385
+ candidateWorkflow: "pr-review-merge",
386
+ riskLevel: ensureInteractiveRiskLevel(risk.riskLevel, "medium"),
387
+ reasons: [
388
+ ...risk.reasons,
389
+ "The observed PR review + merge outcome did not match a supported replanning rule.",
390
+ ],
391
+ missingFields: ["observedOutcome.notes"],
392
+ guidance: [
393
+ "Add observedOutcome.notes describing what happened so the next attempt can be revised intentionally.",
394
+ ],
395
+ };
396
+ }
397
+
398
+ function replanAbortedPrReviewMerge(
399
+ input: LocalPrBundle,
400
+ observedOutcome: PrReviewMergeObservedOutcome,
401
+ riskLevel: RiskLevel,
402
+ ): ReplanResult {
403
+ switch (observedOutcome.abortReason) {
404
+ case "manual-stop":
405
+ return {
406
+ status: "stop",
407
+ workflow: "pr-review-merge",
408
+ riskLevel: "high",
409
+ reasons: [
410
+ "The previous PR review + merge attempt was stopped manually.",
411
+ "Automatic replanning should not override an explicit operator stop.",
412
+ ],
413
+ guidance: [
414
+ "Review the attempted merge manually before constructing a new request.",
415
+ ],
416
+ };
417
+ case "retry-exhaustion":
418
+ return {
419
+ status: "needs_operator_input",
420
+ candidateWorkflow: "pr-review-merge",
421
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
422
+ reasons: [
423
+ `The previous PR review + merge attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
424
+ "Post-merge verification kept failing or retrying, and Lasso cannot repair that automatically.",
425
+ ],
426
+ missingFields: ["verificationCommands", "observedOutcome.notes"],
427
+ guidance: [
428
+ "Inspect the post-merge verification behavior before retrying and capture the failure details in observedOutcome.notes.",
429
+ "Adjust verificationCommands or fix the source branch intentionally rather than relying on automatic replanning.",
430
+ ],
431
+ };
432
+ case "setup-failure":
433
+ return {
434
+ status: "needs_operator_input",
435
+ candidateWorkflow: "pr-review-merge",
436
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
437
+ reasons: [
438
+ `The previous PR review + merge attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
439
+ "Setup failures usually mean the repository path or branch mapping needs correction.",
440
+ ],
441
+ missingFields: ["repoPath", "sourceBranch", "targetBranch"],
442
+ guidance: [
443
+ `Verify that repoPath points at a disposable repository and that ${input.sourceBranch} and ${input.targetBranch} still resolve correctly.`,
444
+ ],
445
+ };
446
+ case "timeout":
447
+ return {
448
+ status: "needs_operator_input",
449
+ candidateWorkflow: "pr-review-merge",
450
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
451
+ reasons: [
452
+ `The previous PR review + merge attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
453
+ "Timeout handling is not a request-level knob in v1, so a human must decide what to change before retrying.",
454
+ ],
455
+ missingFields: ["observedOutcome.notes"],
456
+ guidance: [
457
+ "Record which step timed out in observedOutcome.notes and decide whether the repo state or verification commands need to change before rerunning.",
458
+ ],
459
+ };
460
+ case "unknown":
461
+ default:
462
+ return {
463
+ status: "needs_operator_input",
464
+ candidateWorkflow: "pr-review-merge",
465
+ riskLevel: ensureInteractiveRiskLevel(riskLevel, "medium"),
466
+ reasons: [
467
+ `The previous PR review + merge attempt aborted due to ${describeAbortReason(observedOutcome.abortReason)}.`,
468
+ "Lasso needs more operator context before it can suggest a safe next attempt.",
469
+ ],
470
+ missingFields: ["observedOutcome.notes"],
471
+ guidance: [
472
+ "Provide observedOutcome.notes explaining what failed before retrying.",
473
+ ],
474
+ };
475
+ }
476
+ }
477
+
478
+ function parseWorkflow(value: unknown): ReplanWorkflow {
479
+ if (value === "patch-validation" || value === "pr-review-merge") {
480
+ return value;
481
+ }
482
+
483
+ throw new Error("Invalid replan request workflow");
484
+ }
485
+
486
+ function parseOriginalRequest(workflow: ReplanWorkflow, value: unknown): ReplanRequest["originalRequest"] {
487
+ const parsed = parseWorkflowRequest(JSON.stringify(value));
488
+ if (parsed.workflow !== workflow) {
489
+ throw new Error("Replan workflow does not match original request workflow");
490
+ }
491
+
492
+ return parsed;
493
+ }
494
+
495
+ function parseObservedOutcome(
496
+ workflow: ReplanWorkflow,
497
+ value: unknown,
498
+ ): PatchValidationObservedOutcome | PrReviewMergeObservedOutcome {
499
+ if (!isRecord(value)) {
500
+ throw new Error("Invalid replan observedOutcome");
501
+ }
502
+
503
+ const notes = parseNotes(value.notes);
504
+ const aborted = parseOptionalBoolean(value.aborted, "observedOutcome.aborted");
505
+ const terminalNodeId = value.terminalNodeId;
506
+ const abortReason = parseAbortReason(value.abortReason, aborted);
507
+
508
+ if (terminalNodeId !== undefined && typeof terminalNodeId !== "string") {
509
+ throw new Error("observedOutcome.terminalNodeId must be a string");
510
+ }
511
+
512
+ if (terminalNodeId !== undefined && aborted) {
513
+ throw new Error("observedOutcome cannot include both terminalNodeId and aborted: true");
514
+ }
515
+
516
+ if (terminalNodeId === undefined && !aborted) {
517
+ throw new Error("observedOutcome must include terminalNodeId or aborted: true");
518
+ }
519
+
520
+ if (workflow === "patch-validation") {
521
+ if (terminalNodeId !== undefined && !PATCH_VALIDATION_TERMINALS.includes(terminalNodeId as PatchValidationTerminalNodeId)) {
522
+ throw new Error(`Unsupported patch-validation terminalNodeId: ${terminalNodeId}`);
523
+ }
524
+
525
+ return {
526
+ terminalNodeId: terminalNodeId as PatchValidationTerminalNodeId | undefined,
527
+ aborted: aborted || undefined,
528
+ abortReason,
529
+ notes,
530
+ };
531
+ }
532
+
533
+ if (terminalNodeId !== undefined && !PR_REVIEW_TERMINALS.includes(terminalNodeId as PrReviewMergeTerminalNodeId)) {
534
+ throw new Error(`Unsupported pr-review-merge terminalNodeId: ${terminalNodeId}`);
535
+ }
536
+
537
+ return {
538
+ terminalNodeId: terminalNodeId as PrReviewMergeTerminalNodeId | undefined,
539
+ aborted: aborted || undefined,
540
+ abortReason,
541
+ notes,
542
+ };
543
+ }
544
+
545
+ function parseNotes(value: unknown): string[] | undefined {
546
+ if (value === undefined) {
547
+ return undefined;
548
+ }
549
+
550
+ if (!Array.isArray(value) || value.some(note => typeof note !== "string")) {
551
+ throw new Error("observedOutcome.notes must be an array of strings");
552
+ }
553
+
554
+ const normalized = normalizeNotes(value);
555
+ return normalized.length > 0 ? normalized : undefined;
556
+ }
557
+
558
+ function parseOptionalBoolean(value: unknown, fieldName: string): boolean {
559
+ if (value === undefined) {
560
+ return false;
561
+ }
562
+
563
+ if (typeof value !== "boolean") {
564
+ throw new Error(`${fieldName} must be a boolean`);
565
+ }
566
+
567
+ return value;
568
+ }
569
+
570
+ function parseAbortReason(value: unknown, aborted: boolean): ReplanAbortReason | undefined {
571
+ if (!aborted) {
572
+ if (value !== undefined) {
573
+ throw new Error("observedOutcome.abortReason requires aborted: true");
574
+ }
575
+ return undefined;
576
+ }
577
+
578
+ if (
579
+ value === "setup-failure"
580
+ || value === "retry-exhaustion"
581
+ || value === "timeout"
582
+ || value === "manual-stop"
583
+ || value === "unknown"
584
+ ) {
585
+ return value;
586
+ }
587
+
588
+ throw new Error("observedOutcome.abortReason must be one of: setup-failure, retry-exhaustion, timeout, manual-stop, unknown");
589
+ }
590
+
591
+ function validateParsedRequest(request: ReplanRequest): void {
592
+ if (request.workflow !== request.originalRequest.workflow) {
593
+ throw new Error("Replan workflow does not match original request workflow");
594
+ }
595
+
596
+ const observedOutcome = request.observedOutcome;
597
+ if (observedOutcome.terminalNodeId !== undefined && observedOutcome.aborted) {
598
+ throw new Error("observedOutcome cannot include both terminalNodeId and aborted: true");
599
+ }
600
+
601
+ if (observedOutcome.aborted && observedOutcome.abortReason === undefined) {
602
+ throw new Error("observedOutcome.abortReason is required when aborted: true");
603
+ }
604
+ }
605
+
606
+ function ensureInteractiveRiskLevel(current: RiskLevel, minimum: Exclude<RiskLevel, "low">): RiskLevel {
607
+ if (minimum === "high" || current === "high") {
608
+ return "high";
609
+ }
610
+ return current === "low" ? "medium" : current;
611
+ }
612
+
613
+ function ensureStopRiskLevel(current: RiskLevel): "medium" | "high" {
614
+ return current === "high" ? "high" : "medium";
615
+ }
616
+
617
+ function isRecord(value: unknown): value is Record<string, unknown> {
618
+ return value !== null && typeof value === "object";
619
+ }
@@ -0,0 +1,73 @@
1
+ import type { ReferenceWorkflowRequest } from "../reference/catalog.js";
2
+ import type { LocalPatchValidationBundle, LocalPrBundle } from "../reference/types.js";
3
+
4
+ export type ReplanWorkflow = "patch-validation" | "pr-review-merge";
5
+ export type ReplanTrigger = "risk-escalation" | "failure-recovery";
6
+ export type RiskLevel = "low" | "medium" | "high";
7
+ export type ReplanAbortReason = "setup-failure" | "retry-exhaustion" | "timeout" | "manual-stop" | "unknown";
8
+
9
+ export type PatchValidationTerminalNodeId =
10
+ | "validated-fix"
11
+ | "not-reproduced"
12
+ | "apply-failed"
13
+ | "candidate-failed"
14
+ | "rejected";
15
+
16
+ export type PrReviewMergeTerminalNodeId =
17
+ | "complete-success"
18
+ | "reject-verification"
19
+ | "reject-human"
20
+ | "merge-conflict";
21
+
22
+ interface BaseObservedOutcome {
23
+ aborted?: boolean;
24
+ abortReason?: ReplanAbortReason;
25
+ notes?: string[];
26
+ }
27
+
28
+ export interface PatchValidationObservedOutcome extends BaseObservedOutcome {
29
+ terminalNodeId?: PatchValidationTerminalNodeId;
30
+ }
31
+
32
+ export interface PrReviewMergeObservedOutcome extends BaseObservedOutcome {
33
+ terminalNodeId?: PrReviewMergeTerminalNodeId;
34
+ }
35
+
36
+ export type ReplanRequest =
37
+ | {
38
+ workflow: "patch-validation";
39
+ originalRequest: { workflow: "patch-validation"; input: LocalPatchValidationBundle };
40
+ observedOutcome: PatchValidationObservedOutcome;
41
+ }
42
+ | {
43
+ workflow: "pr-review-merge";
44
+ originalRequest: { workflow: "pr-review-merge"; input: LocalPrBundle };
45
+ observedOutcome: PrReviewMergeObservedOutcome;
46
+ };
47
+
48
+ export type ReplanResult =
49
+ | {
50
+ status: "draft_request";
51
+ workflow: ReplanWorkflow;
52
+ request: ReferenceWorkflowRequest;
53
+ trigger: ReplanTrigger;
54
+ riskLevel: RiskLevel;
55
+ rationale: string[];
56
+ warnings: string[];
57
+ changes: string[];
58
+ }
59
+ | {
60
+ status: "needs_operator_input";
61
+ candidateWorkflow?: ReplanWorkflow;
62
+ riskLevel: RiskLevel;
63
+ reasons: string[];
64
+ missingFields: string[];
65
+ guidance: string[];
66
+ }
67
+ | {
68
+ status: "stop";
69
+ workflow: ReplanWorkflow;
70
+ riskLevel: "medium" | "high";
71
+ reasons: string[];
72
+ guidance: string[];
73
+ };