@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,113 @@
1
+ import type { HarnessSpec } from "../spec/types.js";
2
+ import type { TaskNode } from "../spec/types.js";
3
+ import { buildPatchValidationHarnessSpec } from "../reference/patch-validation.js";
4
+ import { buildPrReviewMergeHarnessSpec } from "../reference/pr-review-merge.js";
5
+ import type { LocalPatchValidationBundle, LocalPrBundle } from "../reference/types.js";
6
+ import type { TaskGraph, WorkflowStage } from "./graph-builder.js";
7
+ import type { RiskModel } from "./risk-analyzer.js";
8
+ import { synthesizePolicy } from "./policy-builder.js";
9
+
10
+ interface HarnessSynthesisResult {
11
+ spec: HarnessSpec;
12
+ rationale: string[];
13
+ warnings: string[];
14
+ }
15
+
16
+ function stageTypeToNodeKind(stageType: WorkflowStage["type"]): TaskNode["kind"] {
17
+ switch (stageType) {
18
+ case "setup":
19
+ case "reproduce":
20
+ case "apply":
21
+ case "verify":
22
+ case "merge":
23
+ return "tool";
24
+ case "review":
25
+ return "llm";
26
+ case "approval":
27
+ return "human";
28
+ default:
29
+ return "tool";
30
+ }
31
+ }
32
+
33
+ function buildGenericHarnessSpec(graph: TaskGraph): HarnessSpec {
34
+ const nodes: TaskNode[] = [];
35
+ const edges: Array<{ from: string; to: string }> = [];
36
+
37
+ if (graph.stages.length === 0) {
38
+ // Empty graph — create a single no-op tool node
39
+ const entryId = `${graph.family}-noop`;
40
+ nodes.push({
41
+ id: entryId,
42
+ label: `Execute ${graph.family}`,
43
+ kind: "tool",
44
+ tool: "echo",
45
+ args: [`Running ${graph.family} workflow`]
46
+ });
47
+ return {
48
+ name: graph.family,
49
+ graph: { entryNodeId: entryId, nodes, edges }
50
+ };
51
+ }
52
+
53
+ // Build nodes from stages
54
+ for (const stage of graph.stages) {
55
+ const kind = stageTypeToNodeKind(stage.type);
56
+ const node: TaskNode = kind === "tool"
57
+ ? { id: stage.id, label: stage.description, kind: "tool", tool: "echo", args: [stage.description] }
58
+ : kind === "llm"
59
+ ? { id: stage.id, label: stage.description, kind: "llm", provider: "generic", model: "default", prompt: stage.description }
60
+ : { id: stage.id, label: stage.description, kind: "human", prompt: stage.description, interactionType: "approval" };
61
+ nodes.push(node);
62
+ }
63
+
64
+ // Build edges from dependencies
65
+ for (const stage of graph.stages) {
66
+ for (const dep of stage.dependencies) {
67
+ edges.push({ from: dep, to: stage.id });
68
+ }
69
+ }
70
+
71
+ // Entry node is the first stage with no dependencies
72
+ const entryNode = graph.stages.find(s => s.dependencies.length === 0);
73
+ const entryNodeId = entryNode ? entryNode.id : graph.stages[0].id;
74
+
75
+ return {
76
+ name: graph.family,
77
+ graph: { entryNodeId, nodes, edges }
78
+ };
79
+ }
80
+
81
+ function synthesizeHarnessResult(
82
+ graph: TaskGraph,
83
+ risks: RiskModel
84
+ ): HarnessSynthesisResult {
85
+ // Derive policy from graph and risks
86
+ const policyResult = synthesizePolicy(graph, risks);
87
+
88
+ if (!policyResult.success) {
89
+ throw new Error(`Policy synthesis failed: ${policyResult.reasons.join(", ")}`);
90
+ }
91
+
92
+ const policy = policyResult.policy;
93
+ let spec: HarnessSpec;
94
+
95
+ if (policy.workflow === "patch-validation") {
96
+ spec = buildPatchValidationHarnessSpec(policy.bundle as LocalPatchValidationBundle);
97
+ } else if (policy.workflow === "pr-review-merge") {
98
+ spec = buildPrReviewMergeHarnessSpec(policy.bundle as LocalPrBundle);
99
+ } else {
100
+ // Generic fallback for custom workflow families
101
+ spec = buildGenericHarnessSpec(graph);
102
+ }
103
+
104
+ return {
105
+ spec,
106
+ rationale: policy.rationale,
107
+ warnings: policy.warnings
108
+ };
109
+ }
110
+
111
+ export function synthesizeHarness(graph: TaskGraph, risks: RiskModel): HarnessSpec {
112
+ return synthesizeHarnessResult(graph, risks).spec;
113
+ }
@@ -0,0 +1,63 @@
1
+ export type SupportedWorkflowFamily = string;
2
+
3
+ export type IntentStepKind = "tool" | "llm" | "human" | "condition";
4
+
5
+ export interface IntentStep {
6
+ id: string;
7
+ label: string;
8
+ kind: IntentStepKind;
9
+ command?: string;
10
+ prompt?: string;
11
+ description?: string;
12
+ }
13
+
14
+ export interface IntentIR {
15
+ family: SupportedWorkflowFamily;
16
+ goal: string;
17
+ inputs: Record<string, unknown>;
18
+ requiredTools: string[];
19
+ humanCheckpoints: string[];
20
+ verificationTargets: string[];
21
+ steps?: IntentStep[];
22
+ capabilities?: string[];
23
+ }
24
+
25
+ export interface UnsupportedIntentRejection {
26
+ rejected: true;
27
+ reasons: string[];
28
+ candidateFamily?: string;
29
+ guidance: string[];
30
+ }
31
+
32
+ export type IntentParseResult =
33
+ | { success: true; intent: IntentIR }
34
+ | UnsupportedIntentRejection;
35
+
36
+ export function validateIntent(intent: IntentIR): UnsupportedIntentRejection | null {
37
+ if (!intent.family || typeof intent.family !== "string" || intent.family.trim().length === 0) {
38
+ return {
39
+ rejected: true,
40
+ reasons: ["Workflow family is missing or empty"],
41
+ guidance: [
42
+ "Please specify a workflow family (e.g., 'patch-validation', 'pr-review-merge', or any custom name)"
43
+ ]
44
+ };
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ export function rejectUnsupportedIntent(
51
+ reasons: string[],
52
+ candidateFamily?: string,
53
+ guidance?: string[]
54
+ ): UnsupportedIntentRejection {
55
+ return {
56
+ rejected: true,
57
+ reasons,
58
+ candidateFamily,
59
+ guidance: guidance || [
60
+ "Please specify a workflow family with the required inputs for that workflow type"
61
+ ]
62
+ };
63
+ }
@@ -0,0 +1,320 @@
1
+ import type { LocalPatchValidationBundle, LocalPrBundle, LocalCandidateSource } from "../reference/types.js";
2
+ import type { TaskGraph } from "./graph-builder.js";
3
+ import type { RiskModel } from "./risk-analyzer.js";
4
+
5
+ export interface PolicyBundle {
6
+ workflow: string;
7
+ bundle: LocalPatchValidationBundle | LocalPrBundle | Record<string, unknown>;
8
+ rationale: string[];
9
+ warnings: string[];
10
+ missingFields: string[];
11
+ }
12
+
13
+ export type PolicyResult =
14
+ | { success: true; policy: PolicyBundle }
15
+ | { success: false; reasons: string[]; missingFields: string[]; guidance: string[] };
16
+
17
+ export function synthesizePolicy(graph: TaskGraph, risks: RiskModel): PolicyResult {
18
+ const missingFields: string[] = [];
19
+ const warnings: string[] = [];
20
+ const rationale: string[] = [];
21
+
22
+ // Add risk-based warnings
23
+ if (risks.mitigations.length > 0) {
24
+ warnings.push(...risks.mitigations);
25
+ }
26
+
27
+ const hasSteps = graph.stages.some(s => s.id.startsWith("step-"));
28
+ const hasCapabilities = graph.capabilityMatch !== undefined;
29
+
30
+ if (hasCapabilities) {
31
+ rationale.push(
32
+ `Capability-driven workflow: ${graph.family}`,
33
+ `Matched capabilities: ${graph.capabilityMatch!.matched.join(", ") || "none"}`,
34
+ `Risk level: ${risks.overallRisk}`
35
+ );
36
+
37
+ if (graph.capabilityMatch!.missing.length > 0) {
38
+ warnings.push(`Missing capabilities: ${graph.capabilityMatch!.missing.join(", ")}`);
39
+ }
40
+
41
+ if (risks.capabilityRisk) {
42
+ for (const factor of risks.capabilityRisk.riskFactors) {
43
+ warnings.push(`Risk: ${factor}`);
44
+ }
45
+ }
46
+
47
+ const bundle: Record<string, unknown> = { ...graph.inputs };
48
+
49
+ return {
50
+ success: true,
51
+ policy: {
52
+ workflow: graph.family,
53
+ bundle,
54
+ rationale,
55
+ warnings,
56
+ missingFields: []
57
+ }
58
+ };
59
+ }
60
+
61
+ if (hasSteps) {
62
+ rationale.push(
63
+ `Workflow includes ${graph.stages.filter(s => s.id.startsWith("step-")).length} custom step(s)`,
64
+ `Risk level: ${risks.overallRisk}`
65
+ );
66
+
67
+ if (graph.family === "patch-validation") {
68
+ if (!graph.inputs.repoPath) missingFields.push("repoPath");
69
+ if (!graph.inputs.baselineRef) missingFields.push("baselineRef");
70
+
71
+ const hasCandidateBranch = graph.inputs.candidateBranch && (graph.inputs.candidateBranch as string).length > 0;
72
+ const hasPatchFile = graph.inputs.patchFilePath && (graph.inputs.patchFilePath as string).length > 0;
73
+
74
+ if (!hasCandidateBranch && !hasPatchFile) {
75
+ missingFields.push("candidateSource (branch or patchFile)");
76
+ }
77
+
78
+ if (missingFields.length > 0) {
79
+ return {
80
+ success: false,
81
+ reasons: [`Patch validation with steps requires: ${missingFields.join(", ")}`],
82
+ missingFields,
83
+ guidance: [
84
+ "For patch-validation with custom steps, provide:",
85
+ "- repoPath",
86
+ "- baselineRef",
87
+ "- candidateSource (branch or .patch file path)"
88
+ ]
89
+ };
90
+ }
91
+
92
+ const candidateSource: LocalCandidateSource = hasPatchFile
93
+ ? { kind: "patchFile", value: graph.inputs.patchFilePath as string }
94
+ : { kind: "branch", value: graph.inputs.candidateBranch as string };
95
+
96
+ const bundle: LocalPatchValidationBundle = {
97
+ repoPath: graph.inputs.repoPath as string,
98
+ baselineRef: graph.inputs.baselineRef as string,
99
+ candidateSource,
100
+ reproduceCommands: (graph.inputs.reproduceCommands as string[]) || [],
101
+ verificationCommands: (graph.inputs.verificationCommands as string[]) || [],
102
+ reviewInstructions: (graph.inputs.reviewInstructions as string) || "",
103
+ approvalRequired: risks.approvalRequired
104
+ };
105
+
106
+ rationale.push(`Classified as patch-validation with custom steps`);
107
+
108
+ return {
109
+ success: true,
110
+ policy: {
111
+ workflow: "patch-validation",
112
+ bundle,
113
+ rationale,
114
+ warnings,
115
+ missingFields: []
116
+ }
117
+ };
118
+ } else if (graph.family === "pr-review-merge") {
119
+ if (!graph.inputs.repoPath) missingFields.push("repoPath");
120
+ if (!graph.inputs.sourceBranch) missingFields.push("sourceBranch");
121
+ if (!graph.inputs.targetBranch) missingFields.push("targetBranch");
122
+
123
+ if (missingFields.length > 0) {
124
+ return {
125
+ success: false,
126
+ reasons: [`PR review/merge with steps requires: ${missingFields.join(", ")}`],
127
+ missingFields,
128
+ guidance: [
129
+ "For pr-review-merge with custom steps, provide:",
130
+ "- repoPath (absolute path)",
131
+ "- sourceBranch",
132
+ "- targetBranch"
133
+ ]
134
+ };
135
+ }
136
+
137
+ const bundle: LocalPrBundle = {
138
+ repoPath: graph.inputs.repoPath as string,
139
+ sourceBranch: graph.inputs.sourceBranch as string,
140
+ targetBranch: graph.inputs.targetBranch as string,
141
+ reviewInstructions: (graph.inputs.reviewInstructions as string) || "",
142
+ verificationCommands: (graph.inputs.verificationCommands as string[]) || []
143
+ };
144
+
145
+ rationale.push(`Classified as pr-review-merge with custom steps`);
146
+
147
+ return {
148
+ success: true,
149
+ policy: {
150
+ workflow: "pr-review-merge",
151
+ bundle,
152
+ rationale,
153
+ warnings,
154
+ missingFields: []
155
+ }
156
+ };
157
+ }
158
+ }
159
+
160
+ if (graph.family === "patch-validation") {
161
+ // Validate required fields
162
+ if (!graph.inputs.repoPath) missingFields.push("repoPath");
163
+ if (!graph.inputs.baselineRef) missingFields.push("baselineRef");
164
+ if (!graph.inputs.reviewInstructions) missingFields.push("reviewInstructions");
165
+
166
+ const hasCandidateBranch = graph.inputs.candidateBranch && (graph.inputs.candidateBranch as string).length > 0;
167
+ const hasPatchFile = graph.inputs.patchFilePath && (graph.inputs.patchFilePath as string).length > 0;
168
+
169
+ if (!hasCandidateBranch && !hasPatchFile) {
170
+ missingFields.push("candidateSource (branch or patchFile)");
171
+ }
172
+
173
+ if (!graph.inputs.reproduceCommands || (graph.inputs.reproduceCommands as string[]).length === 0) {
174
+ missingFields.push("reproduceCommands");
175
+ }
176
+
177
+ if (!graph.inputs.verificationCommands || (graph.inputs.verificationCommands as string[]).length === 0) {
178
+ missingFields.push("verificationCommands");
179
+ }
180
+
181
+ if (missingFields.length > 0) {
182
+ return {
183
+ success: false,
184
+ reasons: [`Patch validation requires: ${missingFields.join(", ")}`],
185
+ missingFields,
186
+ guidance: [
187
+ "For patch-validation, provide:",
188
+ "- repoPath",
189
+ "- baselineRef",
190
+ "- candidateSource (branch or .patch file path)",
191
+ "- reproduceCommands (should fail on baseline)",
192
+ "- verificationCommands (should still pass after fix)",
193
+ "- reviewInstructions"
194
+ ]
195
+ };
196
+ }
197
+
198
+ // Build candidate source
199
+ const candidateSource: LocalCandidateSource = hasPatchFile
200
+ ? { kind: "patchFile", value: graph.inputs.patchFilePath as string }
201
+ : { kind: "branch", value: graph.inputs.candidateBranch as string };
202
+
203
+ // Determine approval requirement from risks or explicit input
204
+ const approvalRequired = risks.approvalRequired;
205
+
206
+ const bundle: LocalPatchValidationBundle = {
207
+ repoPath: graph.inputs.repoPath as string,
208
+ baselineRef: graph.inputs.baselineRef as string,
209
+ candidateSource,
210
+ reproduceCommands: graph.inputs.reproduceCommands as string[],
211
+ verificationCommands: graph.inputs.verificationCommands as string[],
212
+ reviewInstructions: graph.inputs.reviewInstructions as string,
213
+ approvalRequired
214
+ };
215
+
216
+ if (!risks.approvalRequired) {
217
+ warnings.push("approvalRequired not specified; defaulting to false");
218
+ }
219
+
220
+ const candidateDesc = candidateSource.kind === "patchFile"
221
+ ? `patch file: ${candidateSource.value}`
222
+ : `candidate branch: ${candidateSource.value}`;
223
+
224
+ rationale.push(
225
+ `Classified as patch-validation workflow`,
226
+ `Baseline: ${graph.inputs.baselineRef}`,
227
+ `Candidate: ${candidateDesc}`,
228
+ `Reproduce: ${(graph.inputs.reproduceCommands as string[]).length} command(s)`,
229
+ `Verify: ${(graph.inputs.verificationCommands as string[]).length} command(s)`,
230
+ `Approval required: ${approvalRequired}`,
231
+ `Risk level: ${risks.overallRisk}`
232
+ );
233
+
234
+ return {
235
+ success: true,
236
+ policy: {
237
+ workflow: "patch-validation",
238
+ bundle,
239
+ rationale,
240
+ warnings,
241
+ missingFields: []
242
+ }
243
+ };
244
+ } else if (graph.family === "pr-review-merge") {
245
+ // Validate required fields
246
+ if (!graph.inputs.repoPath) missingFields.push("repoPath");
247
+ if (!graph.inputs.sourceBranch) missingFields.push("sourceBranch");
248
+ if (!graph.inputs.targetBranch) missingFields.push("targetBranch");
249
+ if (!graph.inputs.reviewInstructions) missingFields.push("reviewInstructions");
250
+ if (!graph.inputs.verificationCommands || (graph.inputs.verificationCommands as string[]).length === 0) {
251
+ missingFields.push("verificationCommands");
252
+ }
253
+
254
+ if (missingFields.length > 0) {
255
+ return {
256
+ success: false,
257
+ reasons: [`PR review/merge requires: ${missingFields.join(", ")}`],
258
+ missingFields,
259
+ guidance: [
260
+ "For pr-review-merge, provide:",
261
+ "- repoPath (absolute path)",
262
+ "- sourceBranch",
263
+ "- targetBranch",
264
+ "- reviewInstructions",
265
+ "- verificationCommands (array of test commands)"
266
+ ]
267
+ };
268
+ }
269
+
270
+ const bundle: LocalPrBundle = {
271
+ repoPath: graph.inputs.repoPath as string,
272
+ sourceBranch: graph.inputs.sourceBranch as string,
273
+ targetBranch: graph.inputs.targetBranch as string,
274
+ reviewInstructions: graph.inputs.reviewInstructions as string,
275
+ verificationCommands: graph.inputs.verificationCommands as string[]
276
+ };
277
+
278
+ rationale.push(
279
+ `Classified as pr-review-merge workflow`,
280
+ `Source branch: ${graph.inputs.sourceBranch}`,
281
+ `Target branch: ${graph.inputs.targetBranch}`,
282
+ `Verification: ${(graph.inputs.verificationCommands as string[]).length} command(s)`,
283
+ `Risk level: ${risks.overallRisk}`
284
+ );
285
+
286
+ return {
287
+ success: true,
288
+ policy: {
289
+ workflow: "pr-review-merge",
290
+ bundle,
291
+ rationale,
292
+ warnings,
293
+ missingFields: []
294
+ }
295
+ };
296
+ }
297
+
298
+ // Generic fallback for custom workflow families
299
+ rationale.push(
300
+ `Classified as custom workflow: ${graph.family}`,
301
+ `Risk level: ${risks.overallRisk}`
302
+ );
303
+
304
+ if (graph.stages.length > 0) {
305
+ rationale.push(`Workflow has ${graph.stages.length} stage(s)`);
306
+ }
307
+
308
+ const bundle: Record<string, unknown> = { ...graph.inputs };
309
+
310
+ return {
311
+ success: true,
312
+ policy: {
313
+ workflow: graph.family,
314
+ bundle,
315
+ rationale,
316
+ warnings,
317
+ missingFields: []
318
+ }
319
+ };
320
+ }
@@ -0,0 +1,182 @@
1
+ import type { TaskGraph, WorkflowStage } from "./graph-builder.js";
2
+ import type { CapabilityRegistry } from "../capabilities/types.js";
3
+ import { matchCapabilities } from "../capabilities/matcher.js";
4
+
5
+ export type RiskLevel = "low" | "medium" | "high";
6
+
7
+ export interface CapabilityRisk {
8
+ allAvailable: boolean;
9
+ missing: string[];
10
+ riskFactors: string[];
11
+ }
12
+
13
+ export interface RiskModel {
14
+ overallRisk: RiskLevel;
15
+ approvalRequired: boolean;
16
+ candidateSourceKind: "branch" | "patchFile" | "pr" | null;
17
+ verificationBreadth: "narrow" | "moderate" | "comprehensive";
18
+ stageRisks: Map<string, StageRisk>;
19
+ mitigations: string[];
20
+ capabilityRisk?: CapabilityRisk;
21
+ }
22
+
23
+ export interface StageRisk {
24
+ stageId: string;
25
+ risk: RiskLevel;
26
+ factors: string[];
27
+ }
28
+
29
+ export function analyzeRisks(graph: TaskGraph, registry?: CapabilityRegistry): RiskModel {
30
+ const stageRisks = new Map<string, StageRisk>();
31
+ let approvalRequired = false;
32
+ let candidateSourceKind: "branch" | "patchFile" | "pr" | null = null;
33
+ let capabilityRisk: CapabilityRisk | undefined;
34
+
35
+ if (graph.capabilityMatch && registry) {
36
+ const requiredTools = graph.capabilityMatch.matched.concat(graph.capabilityMatch.missing);
37
+ const { matched, missing } = matchCapabilities(requiredTools, registry);
38
+
39
+ const riskFactors: string[] = [];
40
+
41
+ for (const cap of matched) {
42
+ riskFactors.push(...cap.risks);
43
+ }
44
+
45
+ if (missing.length > 0) {
46
+ riskFactors.push(`Missing capabilities: ${missing.join(", ")}`);
47
+ }
48
+
49
+ capabilityRisk = {
50
+ allAvailable: missing.length === 0,
51
+ missing,
52
+ riskFactors
53
+ };
54
+
55
+ if (missing.length > 0) {
56
+ for (const stage of graph.stages) {
57
+ stageRisks.set(stage.id, {
58
+ stageId: stage.id,
59
+ risk: "high",
60
+ factors: [`Missing required capabilities: ${missing.join(", ")}`]
61
+ });
62
+ }
63
+ }
64
+ }
65
+
66
+ // Determine candidate source kind
67
+ if (graph.family === "patch-validation") {
68
+ if (graph.inputs.patchFilePath) {
69
+ candidateSourceKind = "patchFile";
70
+ } else if (graph.inputs.candidateBranch) {
71
+ candidateSourceKind = "branch";
72
+ }
73
+
74
+ if (graph.inputs.approvalRequired === true) {
75
+ approvalRequired = true;
76
+ }
77
+ } else if (graph.family === "pr-review-merge") {
78
+ candidateSourceKind = "pr";
79
+ }
80
+
81
+ // Analyze verification breadth
82
+ const verificationCommands = graph.inputs.verificationCommands as string[] | undefined;
83
+ const reproduceCommands = graph.inputs.reproduceCommands as string[] | undefined;
84
+
85
+ const totalCommands = (verificationCommands?.length || 0) + (reproduceCommands?.length || 0);
86
+ let verificationBreadth: "narrow" | "moderate" | "comprehensive";
87
+
88
+ if (totalCommands === 0) {
89
+ verificationBreadth = "narrow";
90
+ } else if (totalCommands <= 2) {
91
+ verificationBreadth = "moderate";
92
+ } else {
93
+ verificationBreadth = "comprehensive";
94
+ }
95
+
96
+ // Analyze each stage
97
+ for (const stage of graph.stages) {
98
+ const factors: string[] = [];
99
+ let risk: RiskLevel = "low";
100
+
101
+ const isCapabilityStage = stage.id.startsWith("capability-");
102
+
103
+ if (isCapabilityStage && capabilityRisk) {
104
+ if (stage.type === "verify") {
105
+ risk = capabilityRisk.allAvailable ? "low" : "medium";
106
+ factors.push(capabilityRisk.allAvailable ? "All capabilities verified" : "Some capabilities missing");
107
+ } else if (stage.type === "review") {
108
+ risk = capabilityRisk.allAvailable ? "low" : "medium";
109
+ factors.push(...capabilityRisk.riskFactors.slice(0, 2));
110
+ } else if (stage.type === "approval") {
111
+ risk = "low";
112
+ factors.push("Human gate prevents automatic progression");
113
+ } else if (stage.type === "setup") {
114
+ risk = "low";
115
+ factors.push("Capability verification setup");
116
+ }
117
+ } else if (stage.type === "reproduce" || stage.type === "verify") {
118
+ if (verificationBreadth === "narrow") {
119
+ risk = "medium";
120
+ factors.push("Limited test coverage");
121
+ } else if (verificationBreadth === "moderate") {
122
+ risk = "low";
123
+ factors.push("Moderate test coverage");
124
+ } else {
125
+ risk = "low";
126
+ factors.push("Comprehensive test coverage");
127
+ }
128
+ } else if (stage.type === "apply") {
129
+ if (candidateSourceKind === "patchFile") {
130
+ risk = "medium";
131
+ factors.push("Patch file may not apply cleanly");
132
+ } else {
133
+ risk = "low";
134
+ factors.push("Branch-based candidate");
135
+ }
136
+ } else if (stage.type === "merge") {
137
+ risk = "medium";
138
+ factors.push("Merge operation modifies target branch");
139
+ } else if (stage.type === "approval") {
140
+ risk = "low";
141
+ factors.push("Human gate prevents automatic progression");
142
+ }
143
+
144
+ stageRisks.set(stage.id, { stageId: stage.id, risk, factors });
145
+ }
146
+
147
+ // Determine overall risk
148
+ let overallRisk: RiskLevel = "low";
149
+ for (const stageRisk of stageRisks.values()) {
150
+ if (stageRisk.risk === "high") {
151
+ overallRisk = "high";
152
+ break;
153
+ } else if (stageRisk.risk === "medium") {
154
+ overallRisk = "medium";
155
+ }
156
+ }
157
+
158
+ // Generate mitigations
159
+ const mitigations: string[] = [];
160
+
161
+ if (verificationBreadth === "narrow") {
162
+ mitigations.push("Consider adding more verification commands for better coverage");
163
+ }
164
+
165
+ if (candidateSourceKind === "patchFile") {
166
+ mitigations.push("Patch application failures will be handled gracefully");
167
+ }
168
+
169
+ if (!approvalRequired && graph.family === "patch-validation") {
170
+ mitigations.push("Consider setting approvalRequired: true for production changes");
171
+ }
172
+
173
+ return {
174
+ overallRisk,
175
+ approvalRequired,
176
+ candidateSourceKind,
177
+ verificationBreadth,
178
+ stageRisks,
179
+ mitigations,
180
+ capabilityRisk
181
+ };
182
+ }