@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.
- package/README.md +707 -0
- package/docs/agent-wrangling.png +0 -0
- package/package.json +26 -0
- package/src/capabilities/matcher.ts +25 -0
- package/src/capabilities/registry.ts +103 -0
- package/src/capabilities/types.ts +15 -0
- package/src/cir/lower.ts +253 -0
- package/src/cir/optimize.ts +251 -0
- package/src/cir/types.ts +131 -0
- package/src/cir/validate.ts +265 -0
- package/src/compiler/compile.ts +601 -0
- package/src/compiler/feedback.ts +471 -0
- package/src/compiler/runtime-helpers.ts +455 -0
- package/src/composition/chain.ts +58 -0
- package/src/composition/conditional.ts +76 -0
- package/src/composition/parallel.ts +75 -0
- package/src/composition/types.ts +105 -0
- package/src/environment/analyzer.ts +56 -0
- package/src/environment/discovery.ts +179 -0
- package/src/environment/types.ts +68 -0
- package/src/failures/classifiers.ts +134 -0
- package/src/failures/generator.ts +421 -0
- package/src/failures/map-reference-failures.ts +23 -0
- package/src/failures/ontology.ts +210 -0
- package/src/failures/recovery.ts +214 -0
- package/src/failures/types.ts +14 -0
- package/src/index.ts +67 -0
- package/src/memory/advisor.ts +132 -0
- package/src/memory/extractor.ts +166 -0
- package/src/memory/store.ts +107 -0
- package/src/memory/types.ts +53 -0
- package/src/metaharness/engine.ts +256 -0
- package/src/metaharness/predictor.ts +168 -0
- package/src/metaharness/types.ts +40 -0
- package/src/mutation/derive.ts +308 -0
- package/src/mutation/diff.ts +52 -0
- package/src/mutation/engine.ts +256 -0
- package/src/mutation/types.ts +84 -0
- package/src/pi/command-input.ts +209 -0
- package/src/pi/commands.ts +351 -0
- package/src/pi/extension.ts +16 -0
- package/src/planner/synthesize.ts +83 -0
- package/src/planner/template-rules.ts +183 -0
- package/src/planner/types.ts +42 -0
- package/src/reference/catalog.ts +128 -0
- package/src/reference/patch-validation-strategies.ts +170 -0
- package/src/reference/patch-validation.ts +174 -0
- package/src/reference/pr-review-merge.ts +155 -0
- package/src/reference/strategies.ts +126 -0
- package/src/reference/types.ts +33 -0
- package/src/replanner/risk-rules.ts +161 -0
- package/src/replanner/runtime.ts +308 -0
- package/src/replanner/synthesize.ts +619 -0
- package/src/replanner/types.ts +73 -0
- package/src/spec/schema.ts +254 -0
- package/src/spec/types.ts +319 -0
- package/src/spec/validate.ts +296 -0
- package/src/state/snapshots.ts +43 -0
- package/src/state/types.ts +12 -0
- package/src/synthesis/graph-builder.ts +267 -0
- package/src/synthesis/harness-builder.ts +113 -0
- package/src/synthesis/intent-ir.ts +63 -0
- package/src/synthesis/policy-builder.ts +320 -0
- package/src/synthesis/risk-analyzer.ts +182 -0
- package/src/synthesis/skill-parser.ts +441 -0
- package/src/verification/engine.ts +230 -0
- package/src/versioning/file-store.ts +103 -0
- package/src/versioning/history.ts +43 -0
- package/src/versioning/store.ts +16 -0
- package/src/versioning/types.ts +31 -0
- package/test/capabilities/matcher.test.ts +67 -0
- package/test/capabilities/registry.test.ts +136 -0
- package/test/capabilities/synthesis.test.ts +264 -0
- package/test/cir/lower.test.ts +417 -0
- package/test/cir/optimize.test.ts +266 -0
- package/test/cir/validate.test.ts +368 -0
- package/test/compiler/adaptive-runtime.test.ts +157 -0
- package/test/compiler/compile.test.ts +1198 -0
- package/test/compiler/feedback.test.ts +784 -0
- package/test/compiler/guardrails.test.ts +191 -0
- package/test/compiler/trace.test.ts +404 -0
- package/test/composition/chain.test.ts +328 -0
- package/test/composition/conditional.test.ts +241 -0
- package/test/composition/parallel.test.ts +215 -0
- package/test/environment/analyzer.test.ts +204 -0
- package/test/environment/discovery.test.ts +149 -0
- package/test/failures/classifiers.test.ts +287 -0
- package/test/failures/generator.test.ts +203 -0
- package/test/failures/ontology.test.ts +439 -0
- package/test/failures/recovery.test.ts +300 -0
- package/test/helpers/createFixtureRepo.ts +84 -0
- package/test/helpers/createPatchValidationFixture.ts +144 -0
- package/test/helpers/runCompiledWorkflow.ts +208 -0
- package/test/memory/advisor.test.ts +332 -0
- package/test/memory/extractor.test.ts +295 -0
- package/test/memory/store.test.ts +244 -0
- package/test/metaharness/engine.test.ts +575 -0
- package/test/metaharness/predictor.test.ts +436 -0
- package/test/mutation/derive-failure.test.ts +209 -0
- package/test/mutation/engine.test.ts +622 -0
- package/test/package-smoke.test.ts +29 -0
- package/test/pi/command-input.test.ts +153 -0
- package/test/pi/commands.test.ts +623 -0
- package/test/planner/classify-template.test.ts +32 -0
- package/test/planner/synthesize.test.ts +901 -0
- package/test/reference/PatchValidation.failures.test.ts +137 -0
- package/test/reference/PatchValidation.test.ts +326 -0
- package/test/reference/PrReviewMerge.failures.test.ts +121 -0
- package/test/reference/PrReviewMerge.test.ts +55 -0
- package/test/reference/catalog-open.test.ts +70 -0
- package/test/replanner/runtime.test.ts +207 -0
- package/test/replanner/synthesize.test.ts +303 -0
- package/test/spec/validate.test.ts +1056 -0
- package/test/state/snapshots.test.ts +264 -0
- package/test/synthesis/custom-workflow.test.ts +264 -0
- package/test/synthesis/graph-builder.test.ts +370 -0
- package/test/synthesis/harness-builder.test.ts +128 -0
- package/test/synthesis/policy-builder.test.ts +149 -0
- package/test/synthesis/risk-analyzer.test.ts +230 -0
- package/test/synthesis/skill-parser.test.ts +796 -0
- package/test/verification/engine.test.ts +509 -0
- package/test/versioning/history.test.ts +144 -0
- package/test/versioning/store.test.ts +254 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ExtractionResult, WorkflowTemplate } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function classifyTemplate(brief: string): WorkflowTemplate {
|
|
4
|
+
const lower = brief.toLowerCase();
|
|
5
|
+
|
|
6
|
+
// Strong PR-specific indicators (without overlap)
|
|
7
|
+
const prIndicators = [
|
|
8
|
+
"pull request",
|
|
9
|
+
"pr review",
|
|
10
|
+
"pr merge",
|
|
11
|
+
"review the pr",
|
|
12
|
+
"merge the pr",
|
|
13
|
+
" pr ",
|
|
14
|
+
"source branch",
|
|
15
|
+
"target branch"
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Strong patch-specific indicators
|
|
19
|
+
const patchIndicators = [
|
|
20
|
+
"patch validation",
|
|
21
|
+
".patch",
|
|
22
|
+
".diff",
|
|
23
|
+
"baseline",
|
|
24
|
+
"reproduce"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const hasPrSignals = prIndicators.some(indicator => lower.includes(indicator));
|
|
28
|
+
const hasPatchSignals = patchIndicators.some(indicator => lower.includes(indicator));
|
|
29
|
+
|
|
30
|
+
// If both or neither, it's ambiguous
|
|
31
|
+
if (hasPrSignals && hasPatchSignals) {
|
|
32
|
+
return "ambiguous";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (hasPrSignals) {
|
|
36
|
+
return "pr-review-merge";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (hasPatchSignals) {
|
|
40
|
+
return "patch-validation";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If no strong signals, it's a custom workflow
|
|
44
|
+
return "custom";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function extractFields(brief: string): ExtractionResult {
|
|
48
|
+
const template = classifyTemplate(brief);
|
|
49
|
+
const result: ExtractionResult = { template };
|
|
50
|
+
|
|
51
|
+
// Extract repo path
|
|
52
|
+
result.repoPath = extractRepoPath(brief);
|
|
53
|
+
|
|
54
|
+
if (template === "pr-review-merge") {
|
|
55
|
+
result.sourceBranch = extractField(brief, ["source branch", "sourceBranch", "source:", "from branch"]);
|
|
56
|
+
result.targetBranch = extractField(brief, ["target branch", "targetBranch", "target:", "to branch", "into branch"]);
|
|
57
|
+
result.verificationCommands = extractCommands(brief, ["verification", "verify", "test"]);
|
|
58
|
+
result.reviewInstructions = extractReviewInstructions(brief);
|
|
59
|
+
} else if (template === "patch-validation") {
|
|
60
|
+
result.baselineRef = extractField(brief, ["baseline", "baselineRef", "base:", "baseline:"]);
|
|
61
|
+
result.candidateBranch = extractField(brief, ["candidate branch", "candidateBranch", "candidate:", "fix branch"]);
|
|
62
|
+
result.patchFilePath = extractPatchFile(brief);
|
|
63
|
+
result.reproduceCommands = extractCommands(brief, ["reproduce", "repro"]);
|
|
64
|
+
result.verificationCommands = extractCommands(brief, ["verification", "verify", "test"]);
|
|
65
|
+
result.reviewInstructions = extractReviewInstructions(brief);
|
|
66
|
+
result.approvalRequired = extractApprovalFlag(brief);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractRepoPath(brief: string): string | undefined {
|
|
73
|
+
// Look for explicit repo path patterns
|
|
74
|
+
const patterns = [
|
|
75
|
+
/repoPath:\s*["']?([^"'\s\n]+)["']?/i,
|
|
76
|
+
/repo:\s*["']?([^"'\s\n]+)["']?/i,
|
|
77
|
+
/repository:\s*["']?([^"'\s\n]+)["']?/i,
|
|
78
|
+
/path:\s*["']?([A-Za-z]:[\\/][^"'\s\n]+|\/[^"'\s\n]+)["']?/i
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const pattern of patterns) {
|
|
82
|
+
const match = brief.match(pattern);
|
|
83
|
+
if (match && match[1]) {
|
|
84
|
+
return match[1].trim();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractField(brief: string, keywords: string[]): string | undefined {
|
|
92
|
+
for (const keyword of keywords) {
|
|
93
|
+
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const separatorPattern = keyword.endsWith(":") ? "\\s*" : "\\s*[:\\s]+";
|
|
95
|
+
const pattern = new RegExp(`${escapedKeyword}${separatorPattern}["']?([^"'\\s\\n,;]+)["']?`, "i");
|
|
96
|
+
const match = brief.match(pattern);
|
|
97
|
+
if (match && match[1]) {
|
|
98
|
+
return match[1].trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractPatchFile(brief: string): string | undefined {
|
|
105
|
+
// Look for .patch or .diff file mentions
|
|
106
|
+
const patchPattern = /["']?([^\s"']+\.(?:patch|diff))["']?/i;
|
|
107
|
+
const match = brief.match(patchPattern);
|
|
108
|
+
if (match && match[1]) {
|
|
109
|
+
return match[1].trim();
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractCommands(brief: string, keywords: string[]): string[] | undefined {
|
|
115
|
+
const commandsSet = new Set<string>();
|
|
116
|
+
|
|
117
|
+
for (const keyword of keywords) {
|
|
118
|
+
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
119
|
+
// Look for commands after keyword with various formats
|
|
120
|
+
const patterns = [
|
|
121
|
+
new RegExp(`${escapedKeyword}\\s+commands?:\\s*\\[([^\\]]+)\\]`, "i"),
|
|
122
|
+
new RegExp(`${escapedKeyword}\\s+commands?:\\s*["']([^"']+)["']`, "i"),
|
|
123
|
+
new RegExp(`${escapedKeyword}:\\s*\\[([^\\]]+)\\]`, "i")
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
for (const pattern of patterns) {
|
|
127
|
+
const match = brief.match(pattern);
|
|
128
|
+
if (match && match[1]) {
|
|
129
|
+
const cmdList = match[1]
|
|
130
|
+
.split(/,\s*/)
|
|
131
|
+
.map(cmd => cmd.trim().replace(/^["']|["']$/g, ''))
|
|
132
|
+
.filter(cmd => cmd.length > 0);
|
|
133
|
+
cmdList.forEach(cmd => commandsSet.add(cmd));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const commands = Array.from(commandsSet);
|
|
139
|
+
return commands.length > 0 ? commands : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractReviewInstructions(brief: string): string | undefined {
|
|
143
|
+
const patterns = [
|
|
144
|
+
/reviewInstructions:\s*["']([^"']+)["']/i,
|
|
145
|
+
/review instructions:\s*["']([^"']+)["']/i,
|
|
146
|
+
/instructions:\s*["']([^"']+)["']/i
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
for (const pattern of patterns) {
|
|
150
|
+
const match = brief.match(pattern);
|
|
151
|
+
if (match && match[1]) {
|
|
152
|
+
return match[1].trim();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If not explicitly provided, use a generic fallback only if we have other fields
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractApprovalFlag(brief: string): boolean | undefined {
|
|
161
|
+
const lower = brief.toLowerCase();
|
|
162
|
+
|
|
163
|
+
// Explicit approval signals (case insensitive)
|
|
164
|
+
if (
|
|
165
|
+
lower.includes("approval required") ||
|
|
166
|
+
lower.includes("approvalrequired: true") ||
|
|
167
|
+
lower.includes("needs approval")
|
|
168
|
+
) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Explicit no-approval signals
|
|
173
|
+
if (
|
|
174
|
+
lower.includes("no approval") ||
|
|
175
|
+
lower.includes("approvalrequired: false") ||
|
|
176
|
+
lower.includes("auto approve")
|
|
177
|
+
) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Default: undefined (let synthesize determine based on template)
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReferenceWorkflowRequest } from "../reference/catalog.js";
|
|
2
|
+
import type { parsePromptOrSkill } from "../synthesis/skill-parser.js";
|
|
3
|
+
import type { buildTaskGraph } from "../synthesis/graph-builder.js";
|
|
4
|
+
import type { analyzeRisks } from "../synthesis/risk-analyzer.js";
|
|
5
|
+
import type { synthesizePolicy } from "../synthesis/policy-builder.js";
|
|
6
|
+
|
|
7
|
+
export type PlannerResult =
|
|
8
|
+
| {
|
|
9
|
+
status: "draft_request";
|
|
10
|
+
workflow: string;
|
|
11
|
+
request: ReferenceWorkflowRequest;
|
|
12
|
+
rationale: string[];
|
|
13
|
+
warnings: string[];
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
status: "needs_clarification";
|
|
17
|
+
candidateWorkflow?: string;
|
|
18
|
+
reasons: string[];
|
|
19
|
+
missingFields: string[];
|
|
20
|
+
guidance: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type WorkflowTemplate = "patch-validation" | "pr-review-merge" | "custom" | "ambiguous";
|
|
24
|
+
|
|
25
|
+
export interface ExtractionResult {
|
|
26
|
+
template: WorkflowTemplate;
|
|
27
|
+
repoPath?: string;
|
|
28
|
+
// pr-review-merge fields
|
|
29
|
+
sourceBranch?: string;
|
|
30
|
+
targetBranch?: string;
|
|
31
|
+
// patch-validation fields
|
|
32
|
+
baselineRef?: string;
|
|
33
|
+
candidateBranch?: string;
|
|
34
|
+
patchFilePath?: string;
|
|
35
|
+
reproduceCommands?: string[];
|
|
36
|
+
verificationCommands?: string[];
|
|
37
|
+
reviewInstructions?: string;
|
|
38
|
+
approvalRequired?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Re-export synthesis types that planner users may need
|
|
42
|
+
export type { parsePromptOrSkill, buildTaskGraph, analyzeRisks, synthesizePolicy };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { HarnessSpec } from "../spec/types.js";
|
|
2
|
+
import { buildPatchValidationHarnessSpec } from "./patch-validation.js";
|
|
3
|
+
import { buildPrReviewMergeHarnessSpec } from "./pr-review-merge.js";
|
|
4
|
+
import type { LocalCandidateSource, LocalPatchValidationBundle, LocalPrBundle } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export type ReferenceWorkflowRequest =
|
|
7
|
+
| { workflow: "pr-review-merge"; input: LocalPrBundle }
|
|
8
|
+
| { workflow: "patch-validation"; input: LocalPatchValidationBundle }
|
|
9
|
+
| { workflow: string; input: Record<string, unknown> };
|
|
10
|
+
|
|
11
|
+
export function parseWorkflowRequest(args: string): ReferenceWorkflowRequest {
|
|
12
|
+
const trimmed = args.trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
throw new Error("Usage: /lasso:<compile|run> <workflow request JSON>");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let parsed: unknown;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(trimmed);
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error("Invalid workflow request JSON");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!parsed || typeof parsed !== "object") {
|
|
25
|
+
throw new Error("Invalid workflow request shape");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const record = parsed as Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
if ("workflow" in record) {
|
|
31
|
+
const workflow = record.workflow;
|
|
32
|
+
|
|
33
|
+
if (workflow === "pr-review-merge") {
|
|
34
|
+
if (!isPrBundleInput(record.input)) {
|
|
35
|
+
throw new Error("Invalid pr-review-merge input");
|
|
36
|
+
}
|
|
37
|
+
return { workflow: "pr-review-merge", input: record.input };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (workflow === "patch-validation") {
|
|
41
|
+
if (!isPatchValidationInput(record.input)) {
|
|
42
|
+
throw new Error("Invalid patch-validation input");
|
|
43
|
+
}
|
|
44
|
+
return { workflow: "patch-validation", input: record.input };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Generic fallback for arbitrary workflow families
|
|
48
|
+
if (typeof workflow === "string" && workflow.trim().length > 0) {
|
|
49
|
+
const input = record.input;
|
|
50
|
+
if (!input || typeof input !== "object") {
|
|
51
|
+
throw new Error(`Invalid input for workflow: ${workflow}`);
|
|
52
|
+
}
|
|
53
|
+
return { workflow, input: input as Record<string, unknown> };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error(`Unknown workflow: ${String(workflow)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Legacy raw LocalPrBundle shorthand for pr-review-merge
|
|
60
|
+
if (isLocalPrBundle(record)) {
|
|
61
|
+
return { workflow: "pr-review-merge", input: record };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error("Invalid workflow request shape");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildReferenceHarnessSpec(request: ReferenceWorkflowRequest): HarnessSpec {
|
|
68
|
+
if (request.workflow === "pr-review-merge") {
|
|
69
|
+
return buildPrReviewMergeHarnessSpec(request.input as LocalPrBundle);
|
|
70
|
+
}
|
|
71
|
+
if (request.workflow === "patch-validation") {
|
|
72
|
+
return buildPatchValidationHarnessSpec(request.input as LocalPatchValidationBundle);
|
|
73
|
+
}
|
|
74
|
+
// Generic fallback for custom workflows
|
|
75
|
+
const entryId = `${request.workflow}-entry`;
|
|
76
|
+
return {
|
|
77
|
+
name: request.workflow,
|
|
78
|
+
graph: {
|
|
79
|
+
entryNodeId: entryId,
|
|
80
|
+
nodes: [{
|
|
81
|
+
id: entryId,
|
|
82
|
+
label: `Execute ${request.workflow}`,
|
|
83
|
+
kind: "tool",
|
|
84
|
+
tool: "echo",
|
|
85
|
+
args: [`Running ${request.workflow} workflow`],
|
|
86
|
+
}],
|
|
87
|
+
edges: [],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isLocalPrBundle(value: Record<string, unknown>): value is LocalPrBundle {
|
|
93
|
+
return (
|
|
94
|
+
typeof value.repoPath === "string"
|
|
95
|
+
&& typeof value.sourceBranch === "string"
|
|
96
|
+
&& typeof value.targetBranch === "string"
|
|
97
|
+
&& typeof value.reviewInstructions === "string"
|
|
98
|
+
&& Array.isArray(value.verificationCommands)
|
|
99
|
+
&& value.verificationCommands.every(c => typeof c === "string")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isPrBundleInput(value: unknown): value is LocalPrBundle {
|
|
104
|
+
if (!value || typeof value !== "object") return false;
|
|
105
|
+
return isLocalPrBundle(value as Record<string, unknown>);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isPatchValidationInput(value: unknown): value is LocalPatchValidationBundle {
|
|
109
|
+
if (!value || typeof value !== "object") return false;
|
|
110
|
+
const r = value as Record<string, unknown>;
|
|
111
|
+
return (
|
|
112
|
+
typeof r.repoPath === "string"
|
|
113
|
+
&& typeof r.baselineRef === "string"
|
|
114
|
+
&& isCandidateSource(r.candidateSource)
|
|
115
|
+
&& Array.isArray(r.reproduceCommands)
|
|
116
|
+
&& r.reproduceCommands.every(c => typeof c === "string")
|
|
117
|
+
&& Array.isArray(r.verificationCommands)
|
|
118
|
+
&& r.verificationCommands.every(c => typeof c === "string")
|
|
119
|
+
&& typeof r.reviewInstructions === "string"
|
|
120
|
+
&& typeof r.approvalRequired === "boolean"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isCandidateSource(value: unknown): value is LocalCandidateSource {
|
|
125
|
+
if (!value || typeof value !== "object") return false;
|
|
126
|
+
const r = value as Record<string, unknown>;
|
|
127
|
+
return typeof r.value === "string" && (r.kind === "branch" || r.kind === "patchFile");
|
|
128
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { LocalCandidateSource, LocalPatchValidationBundle } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Node ID constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export const patchValidationNodeIds = {
|
|
8
|
+
runBaseline: "run-baseline",
|
|
9
|
+
gateNotReproduced: "gate-not-reproduced",
|
|
10
|
+
applyCandidate: "apply-candidate",
|
|
11
|
+
gateApplyFailed: "gate-apply-failed",
|
|
12
|
+
runCandidateReproduce: "run-candidate-reproduce",
|
|
13
|
+
gateCandidateStillFails: "gate-candidate-still-fails",
|
|
14
|
+
runVerification: "run-verification",
|
|
15
|
+
gateCandidateVerification: "gate-candidate-verification",
|
|
16
|
+
summarise: "summarise",
|
|
17
|
+
humanApprove: "human-approve",
|
|
18
|
+
checkHumanApproval: "check-human-approval",
|
|
19
|
+
validatedFix: "validated-fix",
|
|
20
|
+
notReproduced: "not-reproduced",
|
|
21
|
+
applyFailed: "apply-failed",
|
|
22
|
+
candidateFailed: "candidate-failed",
|
|
23
|
+
rejected: "rejected",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Bash tool builders
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Runs the reproduce commands on the baseline ref.
|
|
32
|
+
* Emits { reproduced: true } when they fail (expected baseline signal)
|
|
33
|
+
* and { reproduced: false } when they unexpectedly pass on the baseline.
|
|
34
|
+
*
|
|
35
|
+
* Precondition: `bundle.baselineRef` must resolve cleanly in the repo.
|
|
36
|
+
* A checkout failure is a hard setup/precondition error — the tool exits
|
|
37
|
+
* non-zero and aborts the workflow rather than routing to a terminal node.
|
|
38
|
+
*/
|
|
39
|
+
export function buildBaselineReproduceTool(bundle: LocalPatchValidationBundle) {
|
|
40
|
+
const lines = [
|
|
41
|
+
`git checkout ${shellQuote(bundle.baselineRef)} >/dev/null 2>&1 || exit 1`,
|
|
42
|
+
"reproduced=true",
|
|
43
|
+
...bundle.reproduceCommands.flatMap(cmd => [
|
|
44
|
+
`if (${cmd}) >/dev/null 2>&1; then`,
|
|
45
|
+
" reproduced=false",
|
|
46
|
+
"fi",
|
|
47
|
+
]),
|
|
48
|
+
`printf '%s\\n' "{\\"reproduced\\":$reproduced}"`,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
return buildBashTool(lines.join("\n"), bundle.repoPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Applies the candidate source (branch checkout or patch application) to the repo.
|
|
55
|
+
* Emits { applied: true } on success, { applied: false, reason: "..." } on failure.
|
|
56
|
+
*/
|
|
57
|
+
export function buildApplyCandidateTool(bundle: LocalPatchValidationBundle) {
|
|
58
|
+
const lines = [
|
|
59
|
+
`if ! git checkout ${shellQuote(bundle.baselineRef)} >/dev/null 2>&1; then`,
|
|
60
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ applied: false, reason: "baseline checkout failed" }))}`,
|
|
61
|
+
" exit 0",
|
|
62
|
+
"fi",
|
|
63
|
+
...buildCandidateApplicationLines(bundle.candidateSource),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return buildBashTool(lines.join("\n"), bundle.repoPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildCandidateApplicationLines(source: LocalCandidateSource): string[] {
|
|
70
|
+
if (source.kind === "branch") {
|
|
71
|
+
return [
|
|
72
|
+
`if git checkout ${shellQuote(source.value)} >/dev/null 2>&1; then`,
|
|
73
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ applied: true }))}`,
|
|
74
|
+
"else",
|
|
75
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ applied: false, reason: "branch checkout failed" }))}`,
|
|
76
|
+
"fi",
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return [
|
|
81
|
+
`if git apply ${shellQuote(source.value)} >/dev/null 2>&1; then`,
|
|
82
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ applied: true }))}`,
|
|
83
|
+
"else",
|
|
84
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ applied: false, reason: "patch apply failed" }))}`,
|
|
85
|
+
"fi",
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Reruns the reproduce commands after the candidate has been applied.
|
|
90
|
+
* Emits { reproduced: false } when the bug is fixed, { reproduced: true } when it still fails.
|
|
91
|
+
*/
|
|
92
|
+
export function buildCandidateReproduceTool(bundle: LocalPatchValidationBundle) {
|
|
93
|
+
const lines = [
|
|
94
|
+
"reproduced=false",
|
|
95
|
+
...bundle.reproduceCommands.flatMap(cmd => [
|
|
96
|
+
`if ! (${cmd}) >/dev/null 2>&1; then`,
|
|
97
|
+
" reproduced=true",
|
|
98
|
+
"fi",
|
|
99
|
+
]),
|
|
100
|
+
`printf '%s\\n' "{\\"reproduced\\":$reproduced}"`,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
return buildBashTool(lines.join("\n"), bundle.repoPath);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Runs verification commands (regression guard) after the candidate is applied.
|
|
107
|
+
* Emits { passed: true } when all pass, { passed: false, command: "..." } on first failure.
|
|
108
|
+
*/
|
|
109
|
+
export function buildVerificationTool(bundle: LocalPatchValidationBundle) {
|
|
110
|
+
const lines = [
|
|
111
|
+
...bundle.verificationCommands.flatMap(cmd => [
|
|
112
|
+
`if ! (${cmd}) >/dev/null 2>&1; then`,
|
|
113
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ passed: false, command: cmd }))}`,
|
|
114
|
+
" exit 0",
|
|
115
|
+
"fi",
|
|
116
|
+
]),
|
|
117
|
+
`printf '%s\\n' ${shellQuote(JSON.stringify({ passed: true }))}`,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return buildBashTool(lines.join("\n"), bundle.repoPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Condition helpers
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
/** True when baseline reproduction produced { reproduced: true }. */
|
|
128
|
+
export function buildBaselineReproducedCondition(): string {
|
|
129
|
+
return `${patchValidationNodeIds.runBaseline}.reproduced`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True when candidate application produced { applied: true }. */
|
|
133
|
+
export function buildCandidateAppliedCondition(): string {
|
|
134
|
+
return `${patchValidationNodeIds.applyCandidate}.applied`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True when candidate reproduction produced { reproduced: false } (bug no longer reproduces). */
|
|
138
|
+
export function buildCandidateFixedCondition(): string {
|
|
139
|
+
return `!${patchValidationNodeIds.runCandidateReproduce}.reproduced`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** True when verification produced { passed: true }. */
|
|
143
|
+
export function buildVerificationPassedCondition(): string {
|
|
144
|
+
return `${patchValidationNodeIds.runVerification}.passed`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** True when human approval produced { approved: true }. */
|
|
148
|
+
export function buildHumanApprovedCondition(): string {
|
|
149
|
+
return `${patchValidationNodeIds.humanApprove}.approved`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Internal helpers
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
function buildBashTool(script: string, cwd: string) {
|
|
157
|
+
return {
|
|
158
|
+
tool: "bash",
|
|
159
|
+
args: ["-lc", script],
|
|
160
|
+
cwd,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function shellQuote(value: string): string {
|
|
165
|
+
if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
170
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { HarnessSpec } from "../spec/types.js";
|
|
2
|
+
import {
|
|
3
|
+
buildApplyCandidateTool,
|
|
4
|
+
buildBaselineReproducedCondition,
|
|
5
|
+
buildBaselineReproduceTool,
|
|
6
|
+
buildCandidateAppliedCondition,
|
|
7
|
+
buildCandidateFixedCondition,
|
|
8
|
+
buildCandidateReproduceTool,
|
|
9
|
+
buildHumanApprovedCondition,
|
|
10
|
+
buildVerificationPassedCondition,
|
|
11
|
+
buildVerificationTool,
|
|
12
|
+
patchValidationNodeIds,
|
|
13
|
+
} from "./patch-validation-strategies.js";
|
|
14
|
+
import type { LocalPatchValidationBundle } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export function buildPatchValidationHarnessSpec(bundle: LocalPatchValidationBundle): HarnessSpec {
|
|
17
|
+
if (bundle.reproduceCommands.length === 0) {
|
|
18
|
+
throw new Error("Patch validation requires at least one reproduce command");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (bundle.verificationCommands.length === 0) {
|
|
22
|
+
throw new Error("Patch validation requires at least one verification command");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ids = patchValidationNodeIds;
|
|
26
|
+
|
|
27
|
+
const afterVerification = bundle.approvalRequired ? ids.summarise : ids.validatedFix;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: "patch-validation",
|
|
31
|
+
executionPolicy: {
|
|
32
|
+
timeout: 300,
|
|
33
|
+
},
|
|
34
|
+
humanPolicy: {
|
|
35
|
+
defaultTimeout: 300,
|
|
36
|
+
},
|
|
37
|
+
observabilityPolicy: {
|
|
38
|
+
tracing: true,
|
|
39
|
+
logLevel: "info",
|
|
40
|
+
},
|
|
41
|
+
graph: {
|
|
42
|
+
entryNodeId: ids.runBaseline,
|
|
43
|
+
nodes: [
|
|
44
|
+
{
|
|
45
|
+
id: ids.runBaseline,
|
|
46
|
+
label: "Run baseline reproduction",
|
|
47
|
+
kind: "tool",
|
|
48
|
+
...buildBaselineReproduceTool(bundle),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: ids.gateNotReproduced,
|
|
52
|
+
kind: "condition",
|
|
53
|
+
condition: buildBaselineReproducedCondition(),
|
|
54
|
+
thenNodeId: ids.applyCandidate,
|
|
55
|
+
elseNodeId: ids.notReproduced,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: ids.applyCandidate,
|
|
59
|
+
label: "Apply candidate",
|
|
60
|
+
kind: "tool",
|
|
61
|
+
...buildApplyCandidateTool(bundle),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: ids.gateApplyFailed,
|
|
65
|
+
kind: "condition",
|
|
66
|
+
condition: buildCandidateAppliedCondition(),
|
|
67
|
+
thenNodeId: ids.runCandidateReproduce,
|
|
68
|
+
elseNodeId: ids.applyFailed,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: ids.runCandidateReproduce,
|
|
72
|
+
label: "Rerun reproduction on candidate",
|
|
73
|
+
kind: "tool",
|
|
74
|
+
...buildCandidateReproduceTool(bundle),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: ids.gateCandidateStillFails,
|
|
78
|
+
kind: "condition",
|
|
79
|
+
condition: buildCandidateFixedCondition(),
|
|
80
|
+
thenNodeId: ids.runVerification,
|
|
81
|
+
elseNodeId: ids.candidateFailed,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: ids.runVerification,
|
|
85
|
+
label: "Run verification commands",
|
|
86
|
+
kind: "tool",
|
|
87
|
+
...buildVerificationTool(bundle),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: ids.gateCandidateVerification,
|
|
91
|
+
kind: "condition",
|
|
92
|
+
condition: buildVerificationPassedCondition(),
|
|
93
|
+
thenNodeId: afterVerification,
|
|
94
|
+
elseNodeId: ids.candidateFailed,
|
|
95
|
+
},
|
|
96
|
+
...(bundle.approvalRequired
|
|
97
|
+
? [
|
|
98
|
+
{
|
|
99
|
+
id: ids.summarise,
|
|
100
|
+
kind: "llm" as const,
|
|
101
|
+
provider: "anthropic",
|
|
102
|
+
model: "claude-sonnet",
|
|
103
|
+
prompt: buildSummaryPrompt(bundle),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: ids.humanApprove,
|
|
107
|
+
kind: "human" as const,
|
|
108
|
+
prompt: `Review the patch-validation summary produced by the '${ids.summarise}' step (available in workflow state as '${ids.summarise}.summary') and approve or reject the candidate fix.\n\nInstructions: ${bundle.reviewInstructions}`,
|
|
109
|
+
interactionType: "approval" as const,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: ids.checkHumanApproval,
|
|
113
|
+
kind: "condition" as const,
|
|
114
|
+
condition: buildHumanApprovedCondition(),
|
|
115
|
+
thenNodeId: ids.validatedFix,
|
|
116
|
+
elseNodeId: ids.rejected,
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
: []),
|
|
120
|
+
{
|
|
121
|
+
id: ids.validatedFix,
|
|
122
|
+
kind: "subworkflow",
|
|
123
|
+
specRef: ids.validatedFix,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: ids.notReproduced,
|
|
127
|
+
kind: "subworkflow",
|
|
128
|
+
specRef: ids.notReproduced,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: ids.applyFailed,
|
|
132
|
+
kind: "subworkflow",
|
|
133
|
+
specRef: ids.applyFailed,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: ids.candidateFailed,
|
|
137
|
+
kind: "subworkflow",
|
|
138
|
+
specRef: ids.candidateFailed,
|
|
139
|
+
},
|
|
140
|
+
...(bundle.approvalRequired
|
|
141
|
+
? [
|
|
142
|
+
{
|
|
143
|
+
id: ids.rejected,
|
|
144
|
+
kind: "subworkflow" as const,
|
|
145
|
+
specRef: ids.rejected,
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
: []),
|
|
149
|
+
],
|
|
150
|
+
edges: [
|
|
151
|
+
{ from: ids.runBaseline, to: ids.gateNotReproduced },
|
|
152
|
+
{ from: ids.applyCandidate, to: ids.gateApplyFailed },
|
|
153
|
+
{ from: ids.runCandidateReproduce, to: ids.gateCandidateStillFails },
|
|
154
|
+
{ from: ids.runVerification, to: ids.gateCandidateVerification },
|
|
155
|
+
...(bundle.approvalRequired
|
|
156
|
+
? [
|
|
157
|
+
{ from: ids.summarise, to: ids.humanApprove },
|
|
158
|
+
{ from: ids.humanApprove, to: ids.checkHumanApproval },
|
|
159
|
+
]
|
|
160
|
+
: []),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildSummaryPrompt(bundle: LocalPatchValidationBundle): string {
|
|
167
|
+
return [
|
|
168
|
+
"Summarise the patch-validation run and return JSON with a single string field `summary`.",
|
|
169
|
+
`Repository: ${bundle.repoPath}`,
|
|
170
|
+
`Baseline ref: ${bundle.baselineRef}`,
|
|
171
|
+
`Candidate source: ${JSON.stringify(bundle.candidateSource)}`,
|
|
172
|
+
`Instructions: ${bundle.reviewInstructions}`,
|
|
173
|
+
].join("\n");
|
|
174
|
+
}
|