@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,308 @@
1
+ import type { HarnessSpec } from "../spec/types.js";
2
+ import type { HarnessExecutionTrace } from "../versioning/types.js";
3
+ import type { HarnessMutation } from "./types.js";
4
+ import type { ExecutionTraceEntry } from "../compiler/runtime-helpers.js";
5
+ import type { FailureSignature, FailureContext, FailureClass } from "../failures/ontology.js";
6
+
7
+ const FAILURE_THRESHOLD_FOR_VERIFICATION = 2;
8
+ const TRANSIENT_FAILURE_THRESHOLD = 2;
9
+
10
+ export function deriveMutationsFromTrace(
11
+ trace: HarnessExecutionTrace,
12
+ currentSpec: HarnessSpec,
13
+ ): HarnessMutation[] {
14
+ if (trace.entries.length === 0) {
15
+ return [];
16
+ }
17
+
18
+ const mutations: HarnessMutation[] = [];
19
+ const failuresByNode = groupFailuresByNode(trace.entries);
20
+
21
+ for (const [nodeId, failures] of failuresByNode) {
22
+ const node = currentSpec.graph.nodes.find((n) => n.id === nodeId);
23
+ if (!node) continue;
24
+
25
+ if (
26
+ failures.length > FAILURE_THRESHOLD_FOR_VERIFICATION &&
27
+ !node.verificationPolicy
28
+ ) {
29
+ mutations.push({
30
+ type: "add-verification",
31
+ params: {
32
+ nodeId,
33
+ verificationPolicy: {
34
+ rules: [
35
+ {
36
+ kind: "expression",
37
+ checkNodeId: `${nodeId}-verify`,
38
+ onFail: "retry",
39
+ maxAttempts: 2,
40
+ },
41
+ ],
42
+ },
43
+ },
44
+ });
45
+ }
46
+
47
+ const transientFailures = failures.filter(
48
+ (f) => f.details && (f.details as Record<string, unknown>).category === "transient",
49
+ );
50
+ if (
51
+ transientFailures.length >= TRANSIENT_FAILURE_THRESHOLD &&
52
+ !node.retryPolicy
53
+ ) {
54
+ mutations.push({
55
+ type: "modify-node",
56
+ params: {
57
+ nodeId,
58
+ changes: {
59
+ retryPolicy: {
60
+ maxAttempts: 3,
61
+ backoff: "exponential",
62
+ initialDelay: 1,
63
+ },
64
+ },
65
+ },
66
+ });
67
+ }
68
+ }
69
+
70
+ if (
71
+ trace.failureCount > 0 &&
72
+ currentSpec.humanPolicy &&
73
+ !(currentSpec.humanPolicy as Record<string, unknown>).approvalRequired
74
+ ) {
75
+ mutations.push({
76
+ type: "toggle-approval",
77
+ params: { approvalRequired: true },
78
+ });
79
+ }
80
+
81
+ return mutations;
82
+ }
83
+
84
+ export function deriveMutationsFromFailure(
85
+ signature: FailureSignature,
86
+ currentSpec: HarnessSpec,
87
+ context?: FailureContext,
88
+ ): HarnessMutation[] {
89
+ const nodeId = context?.nodeId;
90
+ if (!nodeId) {
91
+ return [];
92
+ }
93
+
94
+ const node = currentSpec.graph.nodes.find((n) => n.id === nodeId);
95
+ if (!node) {
96
+ return [];
97
+ }
98
+
99
+ const mutations: HarnessMutation[] = [];
100
+
101
+ switch (signature.class) {
102
+ case "auth":
103
+ mutations.push(createAuthCheckMutation(nodeId, currentSpec));
104
+ break;
105
+
106
+ case "tool":
107
+ mutations.push(createToolAvailabilityCheckMutation(nodeId, currentSpec));
108
+ break;
109
+
110
+ case "resource":
111
+ mutations.push(createResourceProvisioningCheckMutation(nodeId, currentSpec));
112
+ break;
113
+
114
+ case "network":
115
+ if (signature.retryable && !node.retryPolicy) {
116
+ mutations.push(createRetryBackoffMutation(nodeId));
117
+ }
118
+ break;
119
+
120
+ case "semantic":
121
+ if (!node.verificationPolicy) {
122
+ mutations.push(createVerificationMutation(nodeId));
123
+ }
124
+ break;
125
+
126
+ case "human":
127
+ mutations.push({
128
+ type: "toggle-approval",
129
+ params: { approvalRequired: true },
130
+ });
131
+ break;
132
+
133
+ case "environment-drift":
134
+ mutations.push(createEnvironmentCheckMutation(nodeId, currentSpec));
135
+ break;
136
+
137
+ case "unknown":
138
+ if (!node.verificationPolicy) {
139
+ mutations.push(createVerificationMutation(nodeId));
140
+ }
141
+ break;
142
+ }
143
+
144
+ return mutations;
145
+ }
146
+
147
+ function createAuthCheckMutation(
148
+ nodeId: string,
149
+ spec: HarnessSpec,
150
+ ): HarnessMutation {
151
+ const authCheckId = `${nodeId}-auth-check`;
152
+ const incomingEdge = spec.graph.edges.find((e) => e.to === nodeId);
153
+
154
+ return {
155
+ type: "add-node",
156
+ params: {
157
+ node: {
158
+ id: authCheckId,
159
+ label: `Auth check for ${nodeId}`,
160
+ kind: "tool" as const,
161
+ tool: "bash",
162
+ args: ["-c", "git remote -v >/dev/null 2>&1 || echo 'AUTH_FAILED'"],
163
+ },
164
+ edges: [
165
+ {
166
+ from: incomingEdge?.from ?? spec.graph.entryNodeId,
167
+ to: authCheckId,
168
+ },
169
+ { from: authCheckId, to: nodeId },
170
+ ],
171
+ },
172
+ };
173
+ }
174
+
175
+ function createToolAvailabilityCheckMutation(
176
+ nodeId: string,
177
+ spec: HarnessSpec,
178
+ ): HarnessMutation {
179
+ const checkId = `${nodeId}-tool-check`;
180
+ const incomingEdge = spec.graph.edges.find((e) => e.to === nodeId);
181
+
182
+ return {
183
+ type: "add-node",
184
+ params: {
185
+ node: {
186
+ id: checkId,
187
+ label: `Tool availability check for ${nodeId}`,
188
+ kind: "tool" as const,
189
+ tool: "bash",
190
+ args: ["-c", "command -v node >/dev/null 2>&1 || echo 'TOOL_MISSING'"],
191
+ },
192
+ edges: [
193
+ {
194
+ from: incomingEdge?.from ?? spec.graph.entryNodeId,
195
+ to: checkId,
196
+ },
197
+ { from: checkId, to: nodeId },
198
+ ],
199
+ },
200
+ };
201
+ }
202
+
203
+ function createResourceProvisioningCheckMutation(
204
+ nodeId: string,
205
+ spec: HarnessSpec,
206
+ ): HarnessMutation {
207
+ const checkId = `${nodeId}-resource-check`;
208
+ const incomingEdge = spec.graph.edges.find((e) => e.to === nodeId);
209
+
210
+ return {
211
+ type: "add-node",
212
+ params: {
213
+ node: {
214
+ id: checkId,
215
+ label: `Resource check for ${nodeId}`,
216
+ kind: "tool" as const,
217
+ tool: "bash",
218
+ args: ["-c", "df -h / | awk 'NR==2 {print $5}' | sed 's/%//' | awk '{if ($1 > 90) print \"DISK_FULL\"}'"],
219
+ },
220
+ edges: [
221
+ {
222
+ from: incomingEdge?.from ?? spec.graph.entryNodeId,
223
+ to: checkId,
224
+ },
225
+ { from: checkId, to: nodeId },
226
+ ],
227
+ },
228
+ };
229
+ }
230
+
231
+ function createRetryBackoffMutation(nodeId: string): HarnessMutation {
232
+ return {
233
+ type: "modify-node",
234
+ params: {
235
+ nodeId,
236
+ changes: {
237
+ retryPolicy: {
238
+ maxAttempts: 3,
239
+ backoff: "exponential",
240
+ initialDelay: 1,
241
+ },
242
+ },
243
+ },
244
+ };
245
+ }
246
+
247
+ function createVerificationMutation(nodeId: string): HarnessMutation {
248
+ return {
249
+ type: "add-verification",
250
+ params: {
251
+ nodeId,
252
+ verificationPolicy: {
253
+ rules: [
254
+ {
255
+ kind: "expression",
256
+ checkNodeId: `${nodeId}-verify`,
257
+ onFail: "retry",
258
+ maxAttempts: 2,
259
+ },
260
+ ],
261
+ },
262
+ },
263
+ };
264
+ }
265
+
266
+ function createEnvironmentCheckMutation(
267
+ nodeId: string,
268
+ spec: HarnessSpec,
269
+ ): HarnessMutation {
270
+ const checkId = `${nodeId}-env-check`;
271
+ const incomingEdge = spec.graph.edges.find((e) => e.to === nodeId);
272
+
273
+ return {
274
+ type: "add-node",
275
+ params: {
276
+ node: {
277
+ id: checkId,
278
+ label: `Environment check for ${nodeId}`,
279
+ kind: "tool" as const,
280
+ tool: "bash",
281
+ args: ["-c", "uname -a >/dev/null 2>&1 || echo 'ENV_CHECK_FAILED'"],
282
+ },
283
+ edges: [
284
+ {
285
+ from: incomingEdge?.from ?? spec.graph.entryNodeId,
286
+ to: checkId,
287
+ },
288
+ { from: checkId, to: nodeId },
289
+ ],
290
+ },
291
+ };
292
+ }
293
+
294
+ function groupFailuresByNode(
295
+ entries: ExecutionTraceEntry[],
296
+ ): Map<string, ExecutionTraceEntry[]> {
297
+ const grouped = new Map<string, ExecutionTraceEntry[]>();
298
+
299
+ for (const entry of entries) {
300
+ if (entry.phase === "failure") {
301
+ const existing = grouped.get(entry.nodeId) ?? [];
302
+ existing.push(entry);
303
+ grouped.set(entry.nodeId, existing);
304
+ }
305
+ }
306
+
307
+ return grouped;
308
+ }
@@ -0,0 +1,52 @@
1
+ import type { HarnessSpec } from "../spec/types.js";
2
+ import type { SpecDiff } from "./types.js";
3
+
4
+ export function diffSpecs(before: HarnessSpec, after: HarnessSpec): SpecDiff {
5
+ const beforeNodeIds = new Set(before.graph.nodes.map((n) => n.id));
6
+ const afterNodeIds = new Set(after.graph.nodes.map((n) => n.id));
7
+
8
+ const addedNodes: string[] = [];
9
+ const removedNodes: string[] = [];
10
+ const modifiedNodes: string[] = [];
11
+
12
+ for (const id of afterNodeIds) {
13
+ if (!beforeNodeIds.has(id)) {
14
+ addedNodes.push(id);
15
+ }
16
+ }
17
+
18
+ for (const id of beforeNodeIds) {
19
+ if (!afterNodeIds.has(id)) {
20
+ removedNodes.push(id);
21
+ }
22
+ }
23
+
24
+ for (const afterNode of after.graph.nodes) {
25
+ if (!beforeNodeIds.has(afterNode.id)) continue;
26
+ const beforeNode = before.graph.nodes.find((n) => n.id === afterNode.id);
27
+ if (beforeNode && JSON.stringify(beforeNode) !== JSON.stringify(afterNode)) {
28
+ modifiedNodes.push(afterNode.id);
29
+ }
30
+ }
31
+
32
+ const edgeKey = (e: { from: string; to: string }) => `${e.from}->${e.to}`;
33
+ const beforeEdges = new Map(before.graph.edges.map((e) => [edgeKey(e), e]));
34
+ const afterEdges = new Map(after.graph.edges.map((e) => [edgeKey(e), e]));
35
+
36
+ const addedEdges: Array<{ from: string; to: string }> = [];
37
+ const removedEdges: Array<{ from: string; to: string }> = [];
38
+
39
+ for (const [key, edge] of afterEdges) {
40
+ if (!beforeEdges.has(key)) {
41
+ addedEdges.push(edge);
42
+ }
43
+ }
44
+
45
+ for (const [key, edge] of beforeEdges) {
46
+ if (!afterEdges.has(key)) {
47
+ removedEdges.push(edge);
48
+ }
49
+ }
50
+
51
+ return { addedNodes, removedNodes, modifiedNodes, addedEdges, removedEdges };
52
+ }
@@ -0,0 +1,256 @@
1
+ import type { HarnessSpec, TaskNode, TaskEdge } from "../spec/types.js";
2
+ import { diffSpecs } from "./diff.js";
3
+ import type {
4
+ HarnessMutation,
5
+ MutationPolicy,
6
+ MutationResult,
7
+ AddNodeParams,
8
+ RemoveNodeParams,
9
+ ModifyNodeParams,
10
+ AddEdgeParams,
11
+ ToggleApprovalParams,
12
+ AddVerificationParams,
13
+ ReplaceNodeParams,
14
+ TightenGuardrailParams,
15
+ } from "./types.js";
16
+
17
+ export function mutateHarness(
18
+ spec: HarnessSpec,
19
+ mutations: HarnessMutation[],
20
+ policy?: MutationPolicy,
21
+ ): MutationResult {
22
+ if (policy) {
23
+ enforcePolicy(mutations, policy);
24
+ }
25
+
26
+ let current: HarnessSpec = structuredClone(spec);
27
+ const applied: HarnessMutation[] = [];
28
+
29
+ for (const mutation of mutations) {
30
+ switch (mutation.type) {
31
+ case "add-node":
32
+ current = applyAddNode(current, mutation.params as unknown as AddNodeParams);
33
+ break;
34
+ case "remove-node":
35
+ current = applyRemoveNode(current, mutation.params as unknown as RemoveNodeParams);
36
+ break;
37
+ case "modify-node":
38
+ current = applyModifyNode(current, mutation.params as unknown as ModifyNodeParams);
39
+ break;
40
+ case "add-edge":
41
+ current = applyAddEdge(current, mutation.params as unknown as AddEdgeParams);
42
+ break;
43
+ case "toggle-approval":
44
+ current = applyToggleApproval(current, mutation.params as unknown as ToggleApprovalParams);
45
+ break;
46
+ case "add-verification":
47
+ current = applyAddVerification(current, mutation.params as unknown as AddVerificationParams);
48
+ break;
49
+ case "replace-node":
50
+ current = applyReplaceNode(current, mutation.params as unknown as ReplaceNodeParams);
51
+ break;
52
+ case "tighten-guardrail":
53
+ current = applyTightenGuardrail(current, mutation.params as unknown as TightenGuardrailParams);
54
+ break;
55
+ }
56
+ applied.push(mutation);
57
+ }
58
+
59
+ return {
60
+ spec: current,
61
+ mutations: applied,
62
+ diff: diffSpecs(spec, current),
63
+ };
64
+ }
65
+
66
+ function enforcePolicy(mutations: HarnessMutation[], policy: MutationPolicy): void {
67
+ const allowed = new Set(policy.allowedMutations);
68
+ for (const mutation of mutations) {
69
+ if (!allowed.has(mutation.type)) {
70
+ throw new Error(`Mutation type "${mutation.type}" is not allowed by policy`);
71
+ }
72
+ }
73
+
74
+ if (mutations.length > policy.maxMutations) {
75
+ throw new Error(
76
+ `Mutation count ${mutations.length} exceeds maximum allowed ${policy.maxMutations}`,
77
+ );
78
+ }
79
+ }
80
+
81
+ function applyAddNode(spec: HarnessSpec, params: AddNodeParams): HarnessSpec {
82
+ const existing = spec.graph.nodes.find((n) => n.id === params.node.id);
83
+ if (existing) {
84
+ throw new Error(`Node "${params.node.id}" already exists`);
85
+ }
86
+
87
+ const nodes = [...spec.graph.nodes, params.node];
88
+ const edges = params.edges
89
+ ? [...spec.graph.edges, ...params.edges]
90
+ : [...spec.graph.edges];
91
+
92
+ return {
93
+ ...spec,
94
+ graph: { ...spec.graph, nodes, edges },
95
+ };
96
+ }
97
+
98
+ function applyRemoveNode(spec: HarnessSpec, params: RemoveNodeParams): HarnessSpec {
99
+ const node = spec.graph.nodes.find((n) => n.id === params.nodeId);
100
+ if (!node) {
101
+ throw new Error(`Node "${params.nodeId}" not found`);
102
+ }
103
+
104
+ const incoming = spec.graph.edges.filter((e) => e.to === params.nodeId);
105
+ const outgoing = spec.graph.edges.filter((e) => e.from === params.nodeId);
106
+
107
+ const remainingEdges = spec.graph.edges.filter(
108
+ (e) => e.from !== params.nodeId && e.to !== params.nodeId,
109
+ );
110
+
111
+ const bridgingEdges: TaskEdge[] = [];
112
+ for (const inEdge of incoming) {
113
+ for (const outEdge of outgoing) {
114
+ bridgingEdges.push({ from: inEdge.from, to: outEdge.to });
115
+ }
116
+ }
117
+
118
+ const nodes = spec.graph.nodes.filter((n) => n.id !== params.nodeId);
119
+ const edges = [...remainingEdges, ...bridgingEdges];
120
+
121
+ let entryNodeId = spec.graph.entryNodeId;
122
+ if (entryNodeId === params.nodeId && nodes.length > 0) {
123
+ entryNodeId = nodes[0].id;
124
+ }
125
+
126
+ return {
127
+ ...spec,
128
+ graph: { ...spec.graph, nodes, edges, entryNodeId },
129
+ };
130
+ }
131
+
132
+ function applyModifyNode(spec: HarnessSpec, params: ModifyNodeParams): HarnessSpec {
133
+ const idx = spec.graph.nodes.findIndex((n) => n.id === params.nodeId);
134
+ if (idx === -1) {
135
+ throw new Error(`Node "${params.nodeId}" not found`);
136
+ }
137
+
138
+ const { id: _id, kind: _kind, ...safeChanges } = params.changes as Record<string, unknown>;
139
+
140
+ const nodes = [...spec.graph.nodes];
141
+ nodes[idx] = { ...nodes[idx], ...safeChanges } as TaskNode;
142
+
143
+ return {
144
+ ...spec,
145
+ graph: { ...spec.graph, nodes },
146
+ };
147
+ }
148
+
149
+ function applyAddEdge(spec: HarnessSpec, params: AddEdgeParams): HarnessSpec {
150
+ const exists = spec.graph.edges.some(
151
+ (e) => e.from === params.edge.from && e.to === params.edge.to,
152
+ );
153
+ if (exists) {
154
+ throw new Error(
155
+ `Edge "${params.edge.from}->${params.edge.to}" already exists`,
156
+ );
157
+ }
158
+
159
+ const fromNode = spec.graph.nodes.find((n) => n.id === params.edge.from);
160
+ if (!fromNode) {
161
+ throw new Error(`Source node "${params.edge.from}" not found`);
162
+ }
163
+
164
+ const toNode = spec.graph.nodes.find((n) => n.id === params.edge.to);
165
+ if (!toNode) {
166
+ throw new Error(`Target node "${params.edge.to}" not found`);
167
+ }
168
+
169
+ return {
170
+ ...spec,
171
+ graph: { ...spec.graph, edges: [...spec.graph.edges, params.edge] },
172
+ };
173
+ }
174
+
175
+ function applyToggleApproval(
176
+ spec: HarnessSpec,
177
+ params: ToggleApprovalParams,
178
+ ): HarnessSpec {
179
+ const currentPolicy = spec.humanPolicy ?? {};
180
+ const currentApproval = (currentPolicy as Record<string, unknown>).approvalRequired as
181
+ | boolean
182
+ | undefined;
183
+
184
+ const newApproval =
185
+ params.approvalRequired !== undefined ? params.approvalRequired : !currentApproval;
186
+
187
+ return {
188
+ ...spec,
189
+ humanPolicy: {
190
+ ...currentPolicy,
191
+ approvalRequired: newApproval,
192
+ },
193
+ };
194
+ }
195
+
196
+ function applyAddVerification(
197
+ spec: HarnessSpec,
198
+ params: AddVerificationParams,
199
+ ): HarnessSpec {
200
+ const idx = spec.graph.nodes.findIndex((n) => n.id === params.nodeId);
201
+ if (idx === -1) {
202
+ throw new Error(`Node "${params.nodeId}" not found`);
203
+ }
204
+
205
+ const nodes = [...spec.graph.nodes];
206
+ nodes[idx] = {
207
+ ...nodes[idx],
208
+ verificationPolicy: params.verificationPolicy,
209
+ } as TaskNode;
210
+
211
+ return {
212
+ ...spec,
213
+ graph: { ...spec.graph, nodes },
214
+ };
215
+ }
216
+
217
+ function applyReplaceNode(
218
+ spec: HarnessSpec,
219
+ params: ReplaceNodeParams,
220
+ ): HarnessSpec {
221
+ const idx = spec.graph.nodes.findIndex((n) => n.id === params.nodeId);
222
+ if (idx === -1) {
223
+ throw new Error(`Node "${params.nodeId}" not found`);
224
+ }
225
+
226
+ const { id: _id, kind: _kind, ...safeChanges } = params.changes as Record<string, unknown>;
227
+
228
+ const nodes = [...spec.graph.nodes];
229
+ nodes[idx] = { ...nodes[idx], ...safeChanges } as TaskNode;
230
+
231
+ return {
232
+ ...spec,
233
+ graph: { ...spec.graph, nodes },
234
+ };
235
+ }
236
+
237
+ function applyTightenGuardrail(
238
+ spec: HarnessSpec,
239
+ params: TightenGuardrailParams,
240
+ ): HarnessSpec {
241
+ const idx = spec.graph.nodes.findIndex((n) => n.id === params.nodeId);
242
+ if (idx === -1) {
243
+ throw new Error(`Node "${params.nodeId}" not found`);
244
+ }
245
+
246
+ const nodes = [...spec.graph.nodes];
247
+ nodes[idx] = {
248
+ ...nodes[idx],
249
+ verificationPolicy: params.verificationPolicy,
250
+ } as TaskNode;
251
+
252
+ return {
253
+ ...spec,
254
+ graph: { ...spec.graph, nodes },
255
+ };
256
+ }
@@ -0,0 +1,84 @@
1
+ import type { HarnessSpec, TaskNode, TaskEdge } from "../spec/types.js";
2
+
3
+ export type MutationType =
4
+ | "add-node"
5
+ | "remove-node"
6
+ | "modify-node"
7
+ | "add-edge"
8
+ | "toggle-approval"
9
+ | "add-verification"
10
+ | "replace-node"
11
+ | "tighten-guardrail";
12
+
13
+ export type MutationTrigger =
14
+ | "node_failed"
15
+ | "confidence_low"
16
+ | "cost_high"
17
+ | "loop_detected"
18
+ | "retry_exhausted"
19
+ | "verification_failed"
20
+ | "tool_missing"
21
+ | "auth_expired";
22
+
23
+ export interface HarnessMutation {
24
+ type: MutationType;
25
+ params: Record<string, unknown>;
26
+ trigger?: MutationTrigger;
27
+ description?: string;
28
+ }
29
+
30
+ export interface MutationPolicy {
31
+ allowedMutations: MutationType[];
32
+ maxMutations: number;
33
+ }
34
+
35
+ export interface MutationResult {
36
+ spec: HarnessSpec;
37
+ mutations: HarnessMutation[];
38
+ diff: SpecDiff;
39
+ }
40
+
41
+ export interface SpecDiff {
42
+ addedNodes: string[];
43
+ removedNodes: string[];
44
+ modifiedNodes: string[];
45
+ addedEdges: Array<{ from: string; to: string }>;
46
+ removedEdges: Array<{ from: string; to: string }>;
47
+ }
48
+
49
+ export interface AddNodeParams {
50
+ node: TaskNode;
51
+ edges?: TaskEdge[];
52
+ }
53
+
54
+ export interface RemoveNodeParams {
55
+ nodeId: string;
56
+ }
57
+
58
+ export interface ModifyNodeParams {
59
+ nodeId: string;
60
+ changes: Partial<TaskNode>;
61
+ }
62
+
63
+ export interface AddEdgeParams {
64
+ edge: TaskEdge;
65
+ }
66
+
67
+ export interface ToggleApprovalParams {
68
+ approvalRequired?: boolean;
69
+ }
70
+
71
+ export interface AddVerificationParams {
72
+ nodeId: string;
73
+ verificationPolicy: NonNullable<TaskNode["verificationPolicy"]>;
74
+ }
75
+
76
+ export interface ReplaceNodeParams {
77
+ nodeId: string;
78
+ changes: Partial<TaskNode>;
79
+ }
80
+
81
+ export interface TightenGuardrailParams {
82
+ nodeId: string;
83
+ verificationPolicy: NonNullable<TaskNode["verificationPolicy"]>;
84
+ }