@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,296 @@
1
+ import Ajv from "ajv";
2
+ import { harnessSpecSchema } from "./schema.js";
3
+ import type { HarnessSpec, TaskNode } from "./types.js";
4
+
5
+ const ajv = new Ajv({ allErrors: true });
6
+ const validateSchema = ajv.compile(harnessSpecSchema);
7
+
8
+ export type ValidationResult =
9
+ | { valid: true }
10
+ | { valid: false; errors: string[] };
11
+
12
+ export function validateHarnessSpec(spec: HarnessSpec): ValidationResult {
13
+ const errors: string[] = [];
14
+
15
+ // Step 1: Validate against JSON schema
16
+ const schemaValid = validateSchema(spec);
17
+ if (!schemaValid && validateSchema.errors) {
18
+ for (const err of validateSchema.errors) {
19
+ errors.push(`Schema error at ${err.instancePath || "root"}: ${err.message}`);
20
+ }
21
+ }
22
+
23
+ // Step 2: Structural validation
24
+ if (spec.graph) {
25
+ const nodeIds = new Set<string>();
26
+ const nodeKinds = new Map<string, string>();
27
+
28
+ // Check for duplicate node IDs and collect node metadata
29
+ for (const node of spec.graph.nodes) {
30
+ if (node.id) {
31
+ if (nodeIds.has(node.id)) {
32
+ errors.push(`Duplicate node ID: ${node.id}`);
33
+ }
34
+ nodeIds.add(node.id);
35
+ nodeKinds.set(node.id, node.kind);
36
+ }
37
+ }
38
+
39
+ // Validate entry node exists
40
+ if (spec.graph.entryNodeId && !nodeIds.has(spec.graph.entryNodeId)) {
41
+ errors.push(`Entry node not found: ${spec.graph.entryNodeId}`);
42
+ }
43
+
44
+ // Validate edge targets exist
45
+ for (const edge of spec.graph.edges) {
46
+ if (edge.from && !nodeIds.has(edge.from)) {
47
+ errors.push(`Edge references nonexistent source node: ${edge.from}`);
48
+ }
49
+ if (edge.to && !nodeIds.has(edge.to)) {
50
+ errors.push(`Edge references nonexistent target node: ${edge.to}`);
51
+ }
52
+ }
53
+
54
+ // Check for condition node references
55
+ for (const node of spec.graph.nodes) {
56
+ if (node.kind === "condition") {
57
+ const condNode = node as any;
58
+ if (condNode.thenNodeId) {
59
+ if (!nodeIds.has(condNode.thenNodeId)) {
60
+ errors.push(`Condition node ${node.id} references nonexistent thenNodeId: ${condNode.thenNodeId}`);
61
+ }
62
+ }
63
+ if (condNode.elseNodeId) {
64
+ if (!nodeIds.has(condNode.elseNodeId)) {
65
+ errors.push(`Condition node ${node.id} references nonexistent elseNodeId: ${condNode.elseNodeId}`);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // Check for merge node references
72
+ for (const node of spec.graph.nodes) {
73
+ if (node.kind === "merge") {
74
+ const mergeNode = node as any;
75
+ if (mergeNode.waitFor) {
76
+ // Issue 3: Validate waitFor is not empty
77
+ if (mergeNode.waitFor.length === 0) {
78
+ errors.push(`Merge node ${node.id} has empty waitFor array`);
79
+ }
80
+ for (const waitNodeId of mergeNode.waitFor) {
81
+ if (!nodeIds.has(waitNodeId)) {
82
+ errors.push(`Merge node ${node.id} references nonexistent waitFor node: ${waitNodeId}`);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ // Issue 2: Validate choice interactions have options
90
+ for (const node of spec.graph.nodes) {
91
+ if (node.kind === "human") {
92
+ const humanNode = node as any;
93
+ if (humanNode.interactionType === "choice" && (!humanNode.options || humanNode.options.length === 0)) {
94
+ errors.push(`Human node ${node.id} has interactionType "choice" but missing or empty options`);
95
+ }
96
+ }
97
+ }
98
+
99
+ // Issue 1: Proper reachability validation using BFS from entryNodeId
100
+ const reachableNodes = new Set<string>();
101
+ const verificationNodes = new Set<string>();
102
+
103
+ // Collect all nodes referenced by verification rules
104
+ for (const node of spec.graph.nodes) {
105
+ const nodeAny = node as any;
106
+ if (nodeAny.verificationPolicy?.rules) {
107
+ for (const rule of nodeAny.verificationPolicy.rules) {
108
+ if (rule.checkNodeId) {
109
+ verificationNodes.add(rule.checkNodeId);
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // For condition nodes used as verifiers, also mark their branches as verification nodes
116
+ // (they're not executed in the normal flow)
117
+ for (const node of spec.graph.nodes) {
118
+ if (node.kind === "condition" && verificationNodes.has(node.id)) {
119
+ const condNode = node as any;
120
+ if (condNode.thenNodeId) {
121
+ verificationNodes.add(condNode.thenNodeId);
122
+ }
123
+ if (condNode.elseNodeId) {
124
+ verificationNodes.add(condNode.elseNodeId);
125
+ }
126
+ }
127
+ }
128
+
129
+ if (spec.graph.entryNodeId) {
130
+ const queue: string[] = [spec.graph.entryNodeId];
131
+ reachableNodes.add(spec.graph.entryNodeId);
132
+
133
+ // Build adjacency map for edges
134
+ const edgeMap = new Map<string, string[]>();
135
+ for (const edge of spec.graph.edges) {
136
+ if (edge.from && edge.to) {
137
+ if (!edgeMap.has(edge.from)) {
138
+ edgeMap.set(edge.from, []);
139
+ }
140
+ edgeMap.get(edge.from)!.push(edge.to);
141
+ }
142
+ }
143
+
144
+ // Build condition node map
145
+ const conditionMap = new Map<string, { thenNodeId: string; elseNodeId: string }>();
146
+ for (const node of spec.graph.nodes) {
147
+ if (node.kind === "condition") {
148
+ const condNode = node as any;
149
+ conditionMap.set(node.id, {
150
+ thenNodeId: condNode.thenNodeId,
151
+ elseNodeId: condNode.elseNodeId
152
+ });
153
+ }
154
+ }
155
+
156
+ // BFS traversal
157
+ while (queue.length > 0) {
158
+ const current = queue.shift()!;
159
+
160
+ // Follow regular edges
161
+ const targets = edgeMap.get(current) || [];
162
+ for (const target of targets) {
163
+ if (!reachableNodes.has(target)) {
164
+ reachableNodes.add(target);
165
+ queue.push(target);
166
+ }
167
+ }
168
+
169
+ // Follow condition branches (only if not used as verification node)
170
+ if (!verificationNodes.has(current)) {
171
+ const condBranches = conditionMap.get(current);
172
+ if (condBranches) {
173
+ if (condBranches.thenNodeId && !reachableNodes.has(condBranches.thenNodeId)) {
174
+ reachableNodes.add(condBranches.thenNodeId);
175
+ queue.push(condBranches.thenNodeId);
176
+ }
177
+ if (condBranches.elseNodeId && !reachableNodes.has(condBranches.elseNodeId)) {
178
+ reachableNodes.add(condBranches.elseNodeId);
179
+ queue.push(condBranches.elseNodeId);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ // Check for unreachable nodes (excluding verification nodes)
186
+ for (const nodeId of nodeIds) {
187
+ if (!reachableNodes.has(nodeId) && !verificationNodes.has(nodeId)) {
188
+ errors.push(`Unreachable node: ${nodeId}`);
189
+ }
190
+ }
191
+ }
192
+
193
+ // Validate maxSteps is a positive integer
194
+ if (spec.executionPolicy?.maxSteps !== undefined) {
195
+ if (!Number.isInteger(spec.executionPolicy.maxSteps) || spec.executionPolicy.maxSteps < 1) {
196
+ errors.push("executionPolicy.maxSteps must be a positive integer");
197
+ }
198
+ }
199
+
200
+ // Validate costLimitUsd is a positive number
201
+ if (spec.executionPolicy?.costLimitUsd !== undefined) {
202
+ if (typeof spec.executionPolicy.costLimitUsd !== "number" || spec.executionPolicy.costLimitUsd <= 0) {
203
+ errors.push("executionPolicy.costLimitUsd must be a positive number");
204
+ }
205
+ }
206
+
207
+ // Validate retry policy is only on supported node kinds
208
+ const retryableKinds = new Set(["tool", "llm", "subworkflow"]);
209
+ for (const node of spec.graph.nodes) {
210
+ const nodeAny = node as any;
211
+ if (nodeAny.retryPolicy && !retryableKinds.has(node.kind)) {
212
+ errors.push(`retry policy not supported on node kind "${node.kind}" (node: ${node.id})`);
213
+ }
214
+ }
215
+
216
+ // Validate verification policy checkNodeId references and kind matching
217
+ for (const node of spec.graph.nodes) {
218
+ const nodeAny = node as any;
219
+ if (nodeAny.verificationPolicy?.rules) {
220
+ for (const rule of nodeAny.verificationPolicy.rules) {
221
+ if (rule.checkNodeId && !nodeIds.has(rule.checkNodeId)) {
222
+ errors.push(`Verification rule in node ${node.id} references nonexistent checkNodeId: ${rule.checkNodeId}`);
223
+ }
224
+ // Check for self-reference
225
+ if (rule.checkNodeId === node.id) {
226
+ errors.push(`Verification rule in node ${node.id} cannot reference itself`);
227
+ }
228
+
229
+ // Validate verification rule kind matches verifier node kind
230
+ if (rule.kind && rule.checkNodeId && nodeIds.has(rule.checkNodeId)) {
231
+ const verifierKind = nodeKinds.get(rule.checkNodeId);
232
+ if (rule.kind === "tool" && verifierKind !== "tool") {
233
+ errors.push(`Verification rule in node ${node.id} has kind "tool" but references verifier ${rule.checkNodeId} of kind "${verifierKind}"`);
234
+ } else if (rule.kind === "llm" && verifierKind !== "llm") {
235
+ errors.push(`Verification rule in node ${node.id} has kind "llm" but references verifier ${rule.checkNodeId} of kind "${verifierKind}"`);
236
+ } else if (rule.kind === "expression" && verifierKind !== "condition") {
237
+ errors.push(`Verification rule in node ${node.id} has kind "expression" but references verifier ${rule.checkNodeId} of kind "${verifierKind}" (expected "condition")`);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ // Check for circular verification dependencies using DFS cycle detection
245
+ const verificationGraph = new Map<string, Set<string>>();
246
+ for (const node of spec.graph.nodes) {
247
+ const nodeAny = node as any;
248
+ if (nodeAny.verificationPolicy?.rules) {
249
+ const deps = new Set<string>();
250
+ for (const rule of nodeAny.verificationPolicy.rules) {
251
+ if (rule.checkNodeId) {
252
+ deps.add(rule.checkNodeId);
253
+ }
254
+ }
255
+ verificationGraph.set(node.id, deps);
256
+ }
257
+ }
258
+
259
+ // Detect cycles of any length using DFS
260
+ const detectCycle = (start: string, visited: Set<string>, recStack: Set<string>, path: string[]): string[] | null => {
261
+ visited.add(start);
262
+ recStack.add(start);
263
+ path.push(start);
264
+
265
+ const deps = verificationGraph.get(start);
266
+ if (deps) {
267
+ for (const dep of deps) {
268
+ if (!visited.has(dep)) {
269
+ const cycle = detectCycle(dep, visited, recStack, [...path]);
270
+ if (cycle) return cycle;
271
+ } else if (recStack.has(dep)) {
272
+ // Found a cycle
273
+ const cycleStart = path.indexOf(dep);
274
+ return [...path.slice(cycleStart), dep];
275
+ }
276
+ }
277
+ }
278
+
279
+ recStack.delete(start);
280
+ return null;
281
+ };
282
+
283
+ const visited = new Set<string>();
284
+ for (const nodeId of verificationGraph.keys()) {
285
+ if (!visited.has(nodeId)) {
286
+ const cycle = detectCycle(nodeId, visited, new Set(), []);
287
+ if (cycle) {
288
+ errors.push(`Circular verification dependency detected: ${cycle.join(" -> ")}`);
289
+ break; // Report first cycle found
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ return errors.length === 0 ? { valid: true } : { valid: false, errors };
296
+ }
@@ -0,0 +1,43 @@
1
+ import type { FailureRecord } from "../failures/types.js";
2
+ import type { HarnessState } from "./types.js";
3
+
4
+ export function createHarnessState(input: unknown): HarnessState {
5
+ const inputs = input && typeof input === "object" && !Array.isArray(input)
6
+ ? structuredClone(input as Record<string, unknown>)
7
+ : {};
8
+
9
+ return {
10
+ inputs,
11
+ outputs: {},
12
+ nodeResults: {},
13
+ failures: [],
14
+ metrics: {
15
+ retries: 0,
16
+ durationMs: 0,
17
+ },
18
+ };
19
+ }
20
+
21
+ export function addFailure(state: HarnessState, failure: FailureRecord): void {
22
+ state.failures.push(failure);
23
+ }
24
+
25
+ export function recordNodeResult(state: HarnessState, nodeId: string, result: unknown): void {
26
+ state.nodeResults[nodeId] = result;
27
+ }
28
+
29
+ export function updateMetrics(
30
+ state: HarnessState,
31
+ metrics: { retries?: number; durationMs?: number },
32
+ ): void {
33
+ if (metrics.retries !== undefined) {
34
+ state.metrics.retries = metrics.retries;
35
+ }
36
+ if (metrics.durationMs !== undefined) {
37
+ state.metrics.durationMs = metrics.durationMs;
38
+ }
39
+ }
40
+
41
+ export function captureSnapshot(state: HarnessState): HarnessState {
42
+ return structuredClone(state);
43
+ }
@@ -0,0 +1,12 @@
1
+ import type { FailureRecord } from "../failures/types.js";
2
+
3
+ export interface HarnessState {
4
+ inputs: Record<string, unknown>;
5
+ outputs: Record<string, unknown>;
6
+ nodeResults: Record<string, unknown>;
7
+ failures: FailureRecord[];
8
+ metrics: {
9
+ retries: number;
10
+ durationMs: number;
11
+ };
12
+ }
@@ -0,0 +1,267 @@
1
+ import type { IntentIR, IntentStep, SupportedWorkflowFamily } from "./intent-ir.js";
2
+ import type { CapabilityRegistry } from "../capabilities/types.js";
3
+ import type { EnvironmentModel } from "../environment/types.js";
4
+ import { matchCapabilities } from "../capabilities/matcher.js";
5
+ import { analyzeEnvironment } from "../environment/analyzer.js";
6
+
7
+ export interface TaskGraph {
8
+ family: SupportedWorkflowFamily;
9
+ stages: WorkflowStage[];
10
+ inputs: Record<string, unknown>;
11
+ goal: string;
12
+ capabilityMatch?: {
13
+ matched: string[];
14
+ missing: string[];
15
+ };
16
+ environmentAnalysis?: {
17
+ missingTools: string[];
18
+ highRiskConstraints: string[];
19
+ readinessScore: number;
20
+ preparatorySteps: string[];
21
+ };
22
+ }
23
+
24
+ export interface WorkflowStage {
25
+ id: string;
26
+ type: "setup" | "reproduce" | "apply" | "verify" | "review" | "merge" | "approval";
27
+ dependencies: string[];
28
+ description: string;
29
+ requiredInputs: string[];
30
+ }
31
+
32
+ function stepKindToStageType(kind: IntentStep["kind"]): WorkflowStage["type"] {
33
+ switch (kind) {
34
+ case "tool":
35
+ return "setup";
36
+ case "llm":
37
+ return "review";
38
+ case "human":
39
+ return "approval";
40
+ case "condition":
41
+ return "verify";
42
+ default:
43
+ return "setup";
44
+ }
45
+ }
46
+
47
+ function buildCapabilityStages(intent: IntentIR, registry: CapabilityRegistry): { stages: WorkflowStage[]; matched: string[]; missing: string[] } {
48
+ const stages: WorkflowStage[] = [];
49
+ const requiredTools = intent.capabilities || intent.requiredTools;
50
+ const { matched, missing } = matchCapabilities(requiredTools, registry);
51
+
52
+ if (matched.length === 0) {
53
+ return { stages: [], matched: [], missing };
54
+ }
55
+
56
+ const setupStageId = "capability-setup";
57
+ stages.push({
58
+ id: setupStageId,
59
+ type: "setup",
60
+ dependencies: [],
61
+ description: `Verify required capabilities: ${matched.map(c => c.name).join(", ")}`,
62
+ requiredInputs: []
63
+ });
64
+
65
+ let prevStageId = setupStageId;
66
+
67
+ for (const cap of matched) {
68
+ if (cap.verification.length > 0) {
69
+ const verifyStageId = `capability-verify-${cap.id}`;
70
+ stages.push({
71
+ id: verifyStageId,
72
+ type: "verify",
73
+ dependencies: [prevStageId],
74
+ description: `Verify ${cap.name}: ${cap.verification.join("; ")}`,
75
+ requiredInputs: []
76
+ });
77
+ prevStageId = verifyStageId;
78
+ }
79
+
80
+ if (cap.risks.length > 0) {
81
+ const riskStageId = `capability-risk-${cap.id}`;
82
+ stages.push({
83
+ id: riskStageId,
84
+ type: "review",
85
+ dependencies: [prevStageId],
86
+ description: `Assess risks for ${cap.name}: ${cap.risks.join("; ")}`,
87
+ requiredInputs: []
88
+ });
89
+ prevStageId = riskStageId;
90
+ }
91
+
92
+ if (cap.kind === "human") {
93
+ stages.push({
94
+ id: `capability-approval-${cap.id}`,
95
+ type: "approval",
96
+ dependencies: [prevStageId],
97
+ description: `Human approval required: ${cap.name}`,
98
+ requiredInputs: []
99
+ });
100
+ }
101
+ }
102
+
103
+ return { stages, matched: matched.map(c => c.id), missing };
104
+ }
105
+
106
+ export function buildTaskGraph(
107
+ intent: IntentIR,
108
+ registry?: CapabilityRegistry,
109
+ environment?: EnvironmentModel
110
+ ): TaskGraph {
111
+ const stages: WorkflowStage[] = [];
112
+ let capabilityMatch: { matched: string[]; missing: string[] } | undefined;
113
+ let environmentAnalysis: TaskGraph["environmentAnalysis"];
114
+
115
+ if (environment) {
116
+ const analysis = analyzeEnvironment(environment);
117
+ const { matchedTools, missingTools, highRiskConstraints, readinessScore, preparatorySteps } = analysis;
118
+
119
+ environmentAnalysis = {
120
+ missingTools,
121
+ highRiskConstraints,
122
+ readinessScore,
123
+ preparatorySteps,
124
+ };
125
+
126
+ if (preparatorySteps.length > 0) {
127
+ stages.push({
128
+ id: "environment-prep",
129
+ type: "setup",
130
+ dependencies: [],
131
+ description: `Prepare environment: ${preparatorySteps.join("; ")}`,
132
+ requiredInputs: [],
133
+ });
134
+ }
135
+ }
136
+
137
+ const hasCapabilities = intent.capabilities && intent.capabilities.length > 0;
138
+ const useCapabilityPath = hasCapabilities && registry;
139
+
140
+ if (useCapabilityPath) {
141
+ const result = buildCapabilityStages(intent, registry);
142
+ stages.push(...result.stages);
143
+ capabilityMatch = { matched: result.matched, missing: result.missing };
144
+ }
145
+
146
+ if (intent.family === "patch-validation" && !useCapabilityPath) {
147
+ stages.push({
148
+ id: "setup-baseline",
149
+ type: "setup",
150
+ dependencies: [],
151
+ description: "Check out baseline ref",
152
+ requiredInputs: ["repoPath", "baselineRef"]
153
+ });
154
+
155
+ stages.push({
156
+ id: "reproduce-bug",
157
+ type: "reproduce",
158
+ dependencies: ["setup-baseline"],
159
+ description: "Reproduce the bug on baseline",
160
+ requiredInputs: ["reproduceCommands"]
161
+ });
162
+
163
+ stages.push({
164
+ id: "apply-candidate",
165
+ type: "apply",
166
+ dependencies: ["reproduce-bug"],
167
+ description: "Apply candidate fix",
168
+ requiredInputs: ["candidateBranch", "patchFilePath"] // One of these must be present
169
+ });
170
+
171
+ stages.push({
172
+ id: "verify-fix",
173
+ type: "verify",
174
+ dependencies: ["apply-candidate"],
175
+ description: "Verify fix resolves issue and passes regression tests",
176
+ requiredInputs: ["reproduceCommands", "verificationCommands"]
177
+ });
178
+
179
+ stages.push({
180
+ id: "review-results",
181
+ type: "review",
182
+ dependencies: ["verify-fix"],
183
+ description: "Review validation results",
184
+ requiredInputs: ["reviewInstructions"]
185
+ });
186
+
187
+ if (intent.humanCheckpoints.includes("approval-gate")) {
188
+ stages.push({
189
+ id: "approval-gate",
190
+ type: "approval",
191
+ dependencies: ["review-results"],
192
+ description: "Human approval required",
193
+ requiredInputs: []
194
+ });
195
+ }
196
+ } else if (intent.family === "pr-review-merge" && !useCapabilityPath) {
197
+ stages.push({
198
+ id: "setup-pr",
199
+ type: "setup",
200
+ dependencies: [],
201
+ description: "Fetch and checkout PR branches",
202
+ requiredInputs: ["repoPath", "sourceBranch", "targetBranch"]
203
+ });
204
+
205
+ stages.push({
206
+ id: "review-changes",
207
+ type: "review",
208
+ dependencies: ["setup-pr"],
209
+ description: "Review PR changes",
210
+ requiredInputs: ["reviewInstructions"]
211
+ });
212
+
213
+ stages.push({
214
+ id: "verify-tests",
215
+ type: "verify",
216
+ dependencies: ["review-changes"],
217
+ description: "Run verification tests",
218
+ requiredInputs: ["verificationCommands"]
219
+ });
220
+
221
+ stages.push({
222
+ id: "merge-pr",
223
+ type: "merge",
224
+ dependencies: ["verify-tests"],
225
+ description: "Merge PR",
226
+ requiredInputs: ["sourceBranch", "targetBranch"]
227
+ });
228
+ }
229
+
230
+ if (intent.steps && intent.steps.length > 0) {
231
+ for (let i = 0; i < intent.steps.length; i++) {
232
+ const step = intent.steps[i];
233
+ const prevStepId = i > 0 ? intent.steps[i - 1].id : undefined;
234
+
235
+ stages.push({
236
+ id: step.id,
237
+ type: stepKindToStageType(step.kind),
238
+ dependencies: prevStepId ? [prevStepId] : [],
239
+ description: step.label,
240
+ requiredInputs: []
241
+ });
242
+ }
243
+
244
+ if (intent.verificationTargets.length > 0) {
245
+ const lastStepId = intent.steps[intent.steps.length - 1].id;
246
+
247
+ for (let i = 0; i < intent.verificationTargets.length; i++) {
248
+ stages.push({
249
+ id: `verify-target-${i}`,
250
+ type: "verify",
251
+ dependencies: [lastStepId],
252
+ description: `Verify: ${intent.verificationTargets[i]}`,
253
+ requiredInputs: []
254
+ });
255
+ }
256
+ }
257
+ }
258
+
259
+ return {
260
+ family: intent.family,
261
+ stages,
262
+ inputs: intent.inputs,
263
+ goal: intent.goal,
264
+ capabilityMatch,
265
+ environmentAnalysis,
266
+ };
267
+ }