@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,209 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, win32 } from "node:path";
3
+ import type { HarnessSpec } from "../spec/types.js";
4
+ import { parseWorkflowRequest, type ReferenceWorkflowRequest } from "../reference/catalog.js";
5
+
6
+ export type ParsedCommandTarget =
7
+ | { kind: "reference"; request: ReferenceWorkflowRequest; runtimeInput: {} }
8
+ | { kind: "custom"; spec: HarnessSpec; runtimeInput: unknown };
9
+
10
+ type CommandName = "compile" | "run";
11
+
12
+ type GenericHarnessCommandRequest =
13
+ | {
14
+ spec: unknown;
15
+ input?: unknown;
16
+ }
17
+ | {
18
+ specPath: unknown;
19
+ input?: unknown;
20
+ };
21
+
22
+ export async function parseCommandTarget(args: string, commandName: CommandName): Promise<ParsedCommandTarget> {
23
+ const trimmed = args.trim();
24
+ if (!trimmed) {
25
+ throw new Error(buildUsage(commandName));
26
+ }
27
+
28
+ const prefixedPath = parsePrefixedPath(trimmed);
29
+ if (prefixedPath !== undefined) {
30
+ if (!isAbsoluteSpecPath(prefixedPath)) {
31
+ throw new Error("Spec path must be an absolute path");
32
+ }
33
+
34
+ return loadCustomSpecTarget(prefixedPath, {});
35
+ }
36
+
37
+ if (isAbsoluteSpecPath(trimmed)) {
38
+ return loadCustomSpecTarget(trimmed, {});
39
+ }
40
+
41
+ if (looksLikeRelativeSpecPath(trimmed)) {
42
+ throw new Error("Spec path must be an absolute path");
43
+ }
44
+
45
+ let parsed: unknown;
46
+ try {
47
+ parsed = JSON.parse(trimmed);
48
+ } catch {
49
+ throw new Error(buildInvalidInputMessage(commandName));
50
+ }
51
+
52
+ if (!parsed || typeof parsed !== "object") {
53
+ throw new Error(buildInvalidInputMessage(commandName));
54
+ }
55
+
56
+ const record = parsed as Record<string, unknown>;
57
+
58
+ if ("workflow" in record) {
59
+ return {
60
+ kind: "reference",
61
+ request: parseWorkflowRequest(trimmed),
62
+ runtimeInput: {},
63
+ };
64
+ }
65
+
66
+ if ("spec" in record || "specPath" in record) {
67
+ return parseGenericHarnessCommandRequest(record as GenericHarnessCommandRequest);
68
+ }
69
+
70
+ if (looksLikeHarnessSpecRecord(record)) {
71
+ return {
72
+ kind: "custom",
73
+ spec: record as HarnessSpec,
74
+ runtimeInput: {},
75
+ };
76
+ }
77
+
78
+ if (looksLikeLegacyReferenceRequest(record)) {
79
+ return {
80
+ kind: "reference",
81
+ request: parseWorkflowRequest(trimmed),
82
+ runtimeInput: {},
83
+ };
84
+ }
85
+
86
+ throw new Error(buildInvalidInputMessage(commandName));
87
+ }
88
+
89
+ function buildUsage(commandName: CommandName): string {
90
+ return `Usage: /lasso:${commandName} <workflow request JSON | HarnessSpec JSON | {spec|specPath,input?} | path:/abs/spec.json>`;
91
+ }
92
+
93
+ function buildInvalidInputMessage(commandName: CommandName): string {
94
+ return `Invalid ${commandName} input. Expected workflow request JSON, HarnessSpec JSON, {spec|specPath,input?}, or an absolute spec path.`;
95
+ }
96
+
97
+ function parsePrefixedPath(value: string): string | undefined {
98
+ if (!value.toLowerCase().startsWith("path:")) {
99
+ return undefined;
100
+ }
101
+
102
+ return value.slice("path:".length).trim();
103
+ }
104
+
105
+ function isAbsoluteSpecPath(value: string): boolean {
106
+ return isAbsolute(value) || win32.isAbsolute(value);
107
+ }
108
+
109
+ function looksLikeRelativeSpecPath(value: string): boolean {
110
+ const trimmed = value.trim();
111
+ if (!trimmed || trimmed.startsWith("{") || trimmed.startsWith("[")) {
112
+ return false;
113
+ }
114
+
115
+ if (trimmed.toLowerCase().startsWith("path:")) {
116
+ return true;
117
+ }
118
+
119
+ return trimmed.endsWith(".json") || trimmed.includes("/") || trimmed.includes("\\");
120
+ }
121
+
122
+ function looksLikeHarnessSpecRecord(record: Record<string, unknown>): boolean {
123
+ return "name" in record || "graph" in record;
124
+ }
125
+
126
+ function looksLikeLegacyReferenceRequest(record: Record<string, unknown>): boolean {
127
+ return (
128
+ "repoPath" in record
129
+ || "sourceBranch" in record
130
+ || "targetBranch" in record
131
+ || "reviewInstructions" in record
132
+ || "verificationCommands" in record
133
+ );
134
+ }
135
+
136
+ async function parseGenericHarnessCommandRequest(
137
+ record: GenericHarnessCommandRequest,
138
+ ): Promise<ParsedCommandTarget> {
139
+ const hasSpec = Object.prototype.hasOwnProperty.call(record, "spec");
140
+ const hasSpecPath = Object.prototype.hasOwnProperty.call(record, "specPath");
141
+
142
+ if (hasSpec === hasSpecPath) {
143
+ throw new Error("Generic harness request must include exactly one of `spec` or `specPath`");
144
+ }
145
+
146
+ const runtimeInput = Object.prototype.hasOwnProperty.call(record, "input") ? record.input : {};
147
+
148
+ if (hasSpecPath) {
149
+ if (typeof record.specPath !== "string") {
150
+ throw new Error("Generic harness request `specPath` must be a string");
151
+ }
152
+
153
+ if (!isAbsoluteSpecPath(record.specPath)) {
154
+ throw new Error("specPath must be an absolute path");
155
+ }
156
+
157
+ return loadCustomSpecTarget(record.specPath, runtimeInput);
158
+ }
159
+
160
+ if (typeof record.spec === "string") {
161
+ throw new Error("Generic harness request uses `specPath` for file paths, not `spec`");
162
+ }
163
+
164
+ if (!record.spec || typeof record.spec !== "object") {
165
+ throw new Error("Generic harness request `spec` must be a JSON object");
166
+ }
167
+
168
+ return {
169
+ kind: "custom",
170
+ spec: record.spec as HarnessSpec,
171
+ runtimeInput,
172
+ };
173
+ }
174
+
175
+ async function loadCustomSpecTarget(specPath: string, runtimeInput: unknown): Promise<ParsedCommandTarget> {
176
+ return {
177
+ kind: "custom",
178
+ spec: await loadHarnessSpecFromFile(specPath),
179
+ runtimeInput,
180
+ };
181
+ }
182
+
183
+ async function loadHarnessSpecFromFile(specPath: string): Promise<HarnessSpec> {
184
+ let raw: string;
185
+ try {
186
+ raw = await readFile(specPath, "utf8");
187
+ } catch (error) {
188
+ const errno = error as NodeJS.ErrnoException;
189
+ if (errno?.code === "ENOENT") {
190
+ throw new Error(`Spec file not found: ${specPath}`);
191
+ }
192
+
193
+ const message = errno?.message ?? String(error);
194
+ throw new Error(`Failed to read spec file: ${specPath} (${message})`);
195
+ }
196
+
197
+ let parsed: unknown;
198
+ try {
199
+ parsed = JSON.parse(raw);
200
+ } catch {
201
+ throw new Error(`Spec file must contain valid JSON: ${specPath}`);
202
+ }
203
+
204
+ if (!parsed || typeof parsed !== "object" || !looksLikeHarnessSpecRecord(parsed as Record<string, unknown>)) {
205
+ throw new Error(`Spec file must contain a HarnessSpec JSON object: ${specPath}`);
206
+ }
207
+
208
+ return parsed as HarnessSpec;
209
+ }
@@ -0,0 +1,351 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { RegisteredCommand, SourceInfo } from "@mariozechner/pi-coding-agent";
3
+ import { type WorkflowRegistry } from "pi-duroxide";
4
+ import { compileHarnessSpec, type CompiledHarnessWorkflow } from "../compiler/compile.js";
5
+ import { planWorkflowRequest } from "../planner/synthesize.js";
6
+ import type { PlannerResult } from "../planner/types.js";
7
+ import { parseReplanRequest, replanWorkflowRequest } from "../replanner/synthesize.js";
8
+ import type { ReplanResult } from "../replanner/types.js";
9
+ import type { HarnessSpec } from "../spec/types.js";
10
+ import { MAX_ADAPTIVE_VERSIONS } from "../replanner/runtime.js";
11
+ import { parseCommandTarget, type ParsedCommandTarget } from "./command-input.js";
12
+ import { buildReferenceHarnessSpec, type ReferenceWorkflowRequest } from "../reference/catalog.js";
13
+ import { prepareInitialAdaptiveInput } from "../replanner/runtime.js";
14
+
15
+ const compiledHarnesses = new Map<string, CompiledHarnessWorkflow>();
16
+ let lastCompiledHarnessName: string | undefined;
17
+
18
+ export function createLassoCommands(registry: WorkflowRegistry): RegisteredCommand[] {
19
+ const compileCommand: RegisteredCommand = {
20
+ name: "lasso:compile",
21
+ sourceInfo: extSourceInfo(),
22
+ description: "Compile either a bundled Lasso workflow request or a custom HarnessSpec payload.",
23
+ handler: async (args, ctx) => {
24
+ try {
25
+ const target = await parseCommandTarget(args, "compile");
26
+ const compiled = compileCommandTarget(target);
27
+ ctx.ui.notify(
28
+ [
29
+ `Compiled \`${compiled.name}\``,
30
+ `- spec nodes: ${compiled.spec.graph.nodes.length}`,
31
+ `- cir nodes: ${compiled.cir.nodes.length}`,
32
+ `- registered workflows: ${compiled.workflows.length}`,
33
+ ].join("\n"),
34
+ "info",
35
+ );
36
+ } catch (error) {
37
+ ctx.ui.notify(formatCommandError(error), "error");
38
+ }
39
+ },
40
+ };
41
+
42
+ const runCommand: RegisteredCommand = {
43
+ name: "lasso:run",
44
+ sourceInfo: extSourceInfo(),
45
+ description: "Compile, register, and start either a bundled Lasso workflow request or a custom HarnessSpec payload.",
46
+ handler: async (args, ctx) => {
47
+ try {
48
+ const target = await parseCommandTarget(args, "run");
49
+ const compiled = compileCommandTarget(target);
50
+ compiled.register();
51
+
52
+ const runtime = registry.getRuntime();
53
+ if (!runtime) {
54
+ ctx.ui.notify("Workflow runtime not available", "error");
55
+ return;
56
+ }
57
+
58
+ const client = runtime.getClient();
59
+ if (!client) {
60
+ ctx.ui.notify("Workflow runtime not started", "error");
61
+ return;
62
+ }
63
+
64
+ const runtimeInput = target.kind === "reference"
65
+ ? prepareInitialAdaptiveInput(target.request, compiled.spec, target.runtimeInput)
66
+ : target.runtimeInput;
67
+
68
+ const instanceId = randomUUID();
69
+ await client.startOrchestration(instanceId, compiled.name, runtimeInput);
70
+ ctx.ui.notify(`Started \`${compiled.name}\` (${instanceId})`, "info");
71
+ } catch (error) {
72
+ ctx.ui.notify(formatCommandError(error), "error");
73
+ }
74
+ },
75
+ };
76
+
77
+ const inspectCommand: RegisteredCommand = {
78
+ name: "lasso:inspect",
79
+ sourceInfo: extSourceInfo(),
80
+ description: "Show the compiled spec, CIR, and workflow runtime state for the latest or named Lasso workflow.",
81
+ handler: async (args, ctx) => {
82
+ try {
83
+ const name = args.trim() || lastCompiledHarnessName;
84
+ if (!name) {
85
+ ctx.ui.notify("No compiled Lasso workflow available. Run /lasso:compile or /lasso:run first.", "error");
86
+ return;
87
+ }
88
+
89
+ const compiled = compiledHarnesses.get(name);
90
+ if (!compiled) {
91
+ ctx.ui.notify(`No compiled Lasso workflow named \`${name}\` is available.`, "error");
92
+ return;
93
+ }
94
+
95
+ const runtime = registry.getRuntime();
96
+ const client = runtime?.getClient();
97
+ const instances = client ? await client.listAllInstances() : [];
98
+ const matchingInstances = instances.filter(instance => {
99
+ const record = instance as { name?: string };
100
+ return !record.name || record.name === compiled.name;
101
+ });
102
+
103
+ const lines = [
104
+ `### Lasso Workflow \`${compiled.name}\``,
105
+ "",
106
+ "#### Spec",
107
+ "```json",
108
+ JSON.stringify(compiled.spec, null, 2),
109
+ "```",
110
+ "",
111
+ "#### CIR",
112
+ "```json",
113
+ JSON.stringify(compiled.cir, null, 2),
114
+ "```",
115
+ "",
116
+ "#### Runtime State",
117
+ "```json",
118
+ JSON.stringify(matchingInstances, null, 2),
119
+ "```",
120
+ ];
121
+
122
+ if (compiled.adaptive) {
123
+ const { currentVersion, lineage } = compiled.adaptive;
124
+ lines.push(
125
+ "",
126
+ "#### Adaptive Lineage",
127
+ "",
128
+ `Version: ${currentVersion.version}`,
129
+ `Parent: ${currentVersion.parentVersion ?? "none"}`,
130
+ `Reason: ${currentVersion.reason}`,
131
+ );
132
+
133
+ if (lineage.length > 0) {
134
+ lines.push(
135
+ "",
136
+ "| # | Outcome | Duration | Failures | Needs Input |",
137
+ "|---|---------|----------|----------|-------------|",
138
+ );
139
+
140
+ for (const entry of lineage) {
141
+ const duration = entry.metrics.durationMs >= 1000
142
+ ? `${(entry.metrics.durationMs / 1000).toFixed(1)}s`
143
+ : `${entry.metrics.durationMs}ms`;
144
+ const needsInput = entry.failures.some(f => f.rootCause === "human_block") ? "yes" : "no";
145
+ lines.push(
146
+ `| ${entry.version} | ${entry.terminalNodeId} | ${duration} | ${entry.failures.length} | ${needsInput} |`,
147
+ );
148
+ }
149
+ }
150
+
151
+ const status = currentVersion.version >= MAX_ADAPTIVE_VERSIONS ? "stopped" : `evolving (version ${currentVersion.version} of ${MAX_ADAPTIVE_VERSIONS})`;
152
+ lines.push("", `Status: ${status}`);
153
+ }
154
+
155
+ ctx.ui.notify(lines.join("\n"), "info");
156
+ } catch (error) {
157
+ ctx.ui.notify(formatCommandError(error), "error");
158
+ }
159
+ },
160
+ };
161
+
162
+ const planCommand: RegisteredCommand = {
163
+ name: "lasso:plan",
164
+ sourceInfo: extSourceInfo(),
165
+ description: "Draft a reference workflow request envelope from a freeform brief without compiling or running it.",
166
+ handler: async (args, ctx) => {
167
+ try {
168
+ if (!args.trim()) {
169
+ ctx.ui.notify("Usage: /lasso:plan <freeform brief>", "error");
170
+ return;
171
+ }
172
+
173
+ const result = planWorkflowRequest(args);
174
+ ctx.ui.notify(renderPlannerResult(result), "info");
175
+ } catch (error) {
176
+ ctx.ui.notify(formatCommandError(error), "error");
177
+ }
178
+ },
179
+ };
180
+
181
+ const replanCommand: RegisteredCommand = {
182
+ name: "lasso:replan",
183
+ sourceInfo: extSourceInfo(),
184
+ description: "Draft a revised workflow request from a prior request plus explicit outcome signals without compiling or running it.",
185
+ handler: async (args, ctx) => {
186
+ try {
187
+ if (!args.trim()) {
188
+ ctx.ui.notify("Usage: /lasso:replan <replan request JSON>", "error");
189
+ return;
190
+ }
191
+
192
+ const request = parseReplanRequest(args);
193
+ const result = replanWorkflowRequest(request);
194
+ ctx.ui.notify(renderReplannerResult(result), "info");
195
+ } catch (error) {
196
+ ctx.ui.notify(formatCommandError(error), "error");
197
+ }
198
+ },
199
+ };
200
+
201
+ return [compileCommand, runCommand, inspectCommand, planCommand, replanCommand];
202
+ }
203
+
204
+ export function clearCompiledHarnesses(): void {
205
+ compiledHarnesses.clear();
206
+ lastCompiledHarnessName = undefined;
207
+ }
208
+
209
+ export function compileCommandTarget(target: ParsedCommandTarget): CompiledHarnessWorkflow {
210
+ if (target.kind === "reference") {
211
+ return compileReferenceHarness(target.request);
212
+ }
213
+
214
+ return compileCustomHarness(target.spec);
215
+ }
216
+
217
+ export function compileReferenceHarness(request: ReferenceWorkflowRequest): CompiledHarnessWorkflow {
218
+ const spec = buildReferenceHarnessSpec(request);
219
+ return compileCustomHarness(spec);
220
+ }
221
+
222
+ function compileCustomHarness(spec: HarnessSpec): CompiledHarnessWorkflow {
223
+ const compiled = compileHarnessSpec(spec);
224
+ compiledHarnesses.set(compiled.name, compiled);
225
+ lastCompiledHarnessName = compiled.name;
226
+ return compiled;
227
+ }
228
+
229
+ function extSourceInfo(): SourceInfo {
230
+ return { path: "", source: "extension", scope: "temporary", origin: "top-level", baseDir: undefined };
231
+ }
232
+
233
+ function formatCommandError(error: unknown): string {
234
+ if (error instanceof Error) {
235
+ return error.message;
236
+ }
237
+
238
+ return String(error);
239
+ }
240
+
241
+ function renderPlannerResult(result: PlannerResult): string {
242
+ if (result.status === "draft_request") {
243
+ const lines = [
244
+ `### Planner Draft \`${result.workflow}\``,
245
+ "",
246
+ "#### Rationale",
247
+ ...result.rationale.map(item => `- ${item}`),
248
+ ];
249
+
250
+ if (result.warnings.length > 0) {
251
+ lines.push("", "#### Warnings", ...result.warnings.map(item => `- ${item}`));
252
+ }
253
+
254
+ lines.push(
255
+ "",
256
+ "#### Request JSON",
257
+ "```json",
258
+ JSON.stringify(result.request, null, 2),
259
+ "```",
260
+ "",
261
+ "Next: pass this JSON into `/lasso:compile` or `/lasso:run` when you are ready.",
262
+ );
263
+
264
+ return lines.join("\n");
265
+ }
266
+
267
+ const lines = ["### Planner Needs Clarification"];
268
+
269
+ if (result.candidateWorkflow) {
270
+ lines.push("", `Likely workflow: \`${result.candidateWorkflow}\``);
271
+ }
272
+
273
+ lines.push("", "#### Reasons", ...result.reasons.map(item => `- ${item}`));
274
+
275
+ if (result.missingFields.length > 0) {
276
+ lines.push("", "#### Missing Fields", ...result.missingFields.map(item => `- ${item}`));
277
+ }
278
+
279
+ lines.push("", "#### Guidance", ...result.guidance.map(item => `- ${item}`));
280
+
281
+ return lines.join("\n");
282
+ }
283
+
284
+ function renderReplannerResult(result: ReplanResult): string {
285
+ if (result.status === "draft_request") {
286
+ const lines = [
287
+ `### Replan Draft \`${result.workflow}\``,
288
+ "",
289
+ `- Trigger: \`${result.trigger}\``,
290
+ `- Risk level: \`${result.riskLevel}\``,
291
+ "",
292
+ "#### Rationale",
293
+ ...result.rationale.map(item => `- ${item}`),
294
+ ];
295
+
296
+ if (result.warnings.length > 0) {
297
+ lines.push("", "#### Warnings", ...result.warnings.map(item => `- ${item}`));
298
+ }
299
+
300
+ if (result.changes.length > 0) {
301
+ lines.push("", "#### Changes", ...result.changes.map(item => `- ${item}`));
302
+ }
303
+
304
+ lines.push(
305
+ "",
306
+ "#### Request JSON",
307
+ "```json",
308
+ JSON.stringify(result.request, null, 2),
309
+ "```",
310
+ "",
311
+ "Next: pass this JSON into `/lasso:compile` or `/lasso:run` when you are ready.",
312
+ );
313
+
314
+ return lines.join("\n");
315
+ }
316
+
317
+ if (result.status === "needs_operator_input") {
318
+ const lines = ["### Replan Needs Operator Input"];
319
+
320
+ if (result.candidateWorkflow) {
321
+ lines.push("", `Likely workflow: \`${result.candidateWorkflow}\``);
322
+ }
323
+
324
+ lines.push(
325
+ `Risk level: \`${result.riskLevel}\``,
326
+ "",
327
+ "#### Reasons",
328
+ ...result.reasons.map(item => `- ${item}`),
329
+ );
330
+
331
+ if (result.missingFields.length > 0) {
332
+ lines.push("", "#### Missing Fields", ...result.missingFields.map(item => `- ${item}`));
333
+ }
334
+
335
+ lines.push("", "#### Guidance", ...result.guidance.map(item => `- ${item}`));
336
+ return lines.join("\n");
337
+ }
338
+
339
+ return [
340
+ "### Replan Stop",
341
+ "",
342
+ `Workflow: \`${result.workflow}\``,
343
+ `Risk level: \`${result.riskLevel}\``,
344
+ "",
345
+ "#### Reasons",
346
+ ...result.reasons.map(item => `- ${item}`),
347
+ "",
348
+ "#### Guidance",
349
+ ...result.guidance.map(item => `- ${item}`),
350
+ ].join("\n");
351
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import workflowExtension, { getWorkflowRegistry, type WorkflowRegistry } from "pi-duroxide";
3
+ import { createLassoCommands } from "./commands.js";
4
+
5
+ export default async function lassoExtension(pi: ExtensionAPI) {
6
+ await workflowExtension(pi);
7
+
8
+ const registry = getWorkflowRegistry();
9
+ if (!registry) {
10
+ return;
11
+ }
12
+
13
+ for (const command of createLassoCommands(registry)) {
14
+ pi.registerCommand(command.name, command);
15
+ }
16
+ }
@@ -0,0 +1,83 @@
1
+ import type { ReferenceWorkflowRequest } from "../reference/catalog.js";
2
+ import { parsePromptOrSkill } from "../synthesis/skill-parser.js";
3
+ import { buildTaskGraph } from "../synthesis/graph-builder.js";
4
+ import { analyzeRisks } from "../synthesis/risk-analyzer.js";
5
+ import { synthesizePolicy } from "../synthesis/policy-builder.js";
6
+ import type { PlannerResult } from "./types.js";
7
+ import type { CapabilityRegistry } from "../capabilities/types.js";
8
+ import type { EnvironmentModel } from "../environment/types.js";
9
+
10
+ export function planWorkflowRequest(
11
+ brief: string,
12
+ registry?: CapabilityRegistry,
13
+ environment?: EnvironmentModel
14
+ ): PlannerResult {
15
+ // Reject empty briefs
16
+ if (!brief || brief.trim().length === 0) {
17
+ return {
18
+ status: "needs_clarification",
19
+ reasons: ["Brief is empty"],
20
+ missingFields: ["brief"],
21
+ guidance: [
22
+ "Please provide a workflow description including repo path, workflow type (PR review/merge or patch validation), and required commands."
23
+ ]
24
+ };
25
+ }
26
+
27
+ // Parse the brief or skill markdown into IntentIR
28
+ const parseResult = parsePromptOrSkill(brief);
29
+
30
+ // Handle rejection
31
+ if ("rejected" in parseResult) {
32
+ // Populate missingFields when workflow type is ambiguous
33
+ const missingFields = parseResult.reasons.some(r => r.includes("workflow type") || r.includes("workflow family"))
34
+ ? ["workflow type"]
35
+ : [];
36
+
37
+ return {
38
+ status: "needs_clarification",
39
+ candidateWorkflow: parseResult.candidateFamily,
40
+ reasons: parseResult.reasons,
41
+ missingFields,
42
+ guidance: parseResult.guidance
43
+ };
44
+ }
45
+
46
+ const intent = parseResult.intent;
47
+
48
+ // Build task graph from intent
49
+ const graph = buildTaskGraph(intent, registry, environment);
50
+
51
+ // Analyze risks
52
+ const risks = analyzeRisks(graph, registry);
53
+
54
+ // Synthesize policy
55
+ const policyResult = synthesizePolicy(graph, risks);
56
+
57
+ // Handle policy synthesis failure
58
+ if (!policyResult.success) {
59
+ return {
60
+ status: "needs_clarification",
61
+ candidateWorkflow: intent.family,
62
+ reasons: policyResult.reasons,
63
+ missingFields: policyResult.missingFields,
64
+ guidance: policyResult.guidance
65
+ };
66
+ }
67
+
68
+ const policy = policyResult.policy;
69
+
70
+ // Build the request
71
+ const request: ReferenceWorkflowRequest = {
72
+ workflow: policy.workflow,
73
+ input: policy.bundle
74
+ };
75
+
76
+ return {
77
+ status: "draft_request",
78
+ workflow: policy.workflow,
79
+ request,
80
+ rationale: policy.rationale,
81
+ warnings: policy.warnings
82
+ };
83
+ }