@davidorex/pi-workflows 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 (115) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +169 -0
  3. package/agents/audit-fixer.agent.yaml +18 -0
  4. package/agents/code-explorer.agent.yaml +11 -0
  5. package/agents/decomposer.agent.yaml +51 -0
  6. package/agents/investigator.agent.yaml +49 -0
  7. package/agents/pattern-analyzer.agent.yaml +21 -0
  8. package/agents/phase-author.agent.yaml +16 -0
  9. package/agents/plan-decomposer.agent.yaml +12 -0
  10. package/agents/quality-analyzer.agent.yaml +21 -0
  11. package/agents/researcher.agent.yaml +38 -0
  12. package/agents/spec-implementer.agent.yaml +12 -0
  13. package/agents/structure-analyzer.agent.yaml +21 -0
  14. package/agents/synthesizer.agent.yaml +23 -0
  15. package/agents/verifier.agent.yaml +12 -0
  16. package/package.json +40 -0
  17. package/schemas/decomposition-specs.schema.json +50 -0
  18. package/schemas/execution-results.schema.json +50 -0
  19. package/schemas/investigation-findings.schema.json +45 -0
  20. package/schemas/pattern-analysis.schema.json +43 -0
  21. package/schemas/plan-breakdown.schema.json +22 -0
  22. package/schemas/quality-analysis.schema.json +35 -0
  23. package/schemas/research-findings.schema.json +44 -0
  24. package/schemas/structure-analysis.schema.json +42 -0
  25. package/schemas/synthesis.schema.json +33 -0
  26. package/schemas/verifier-output.schema.json +96 -0
  27. package/src/agent-spec.test.ts +289 -0
  28. package/src/agent-spec.ts +122 -0
  29. package/src/block-validation.test.ts +350 -0
  30. package/src/checkpoint.test.ts +237 -0
  31. package/src/checkpoint.ts +139 -0
  32. package/src/completion.test.ts +143 -0
  33. package/src/completion.ts +68 -0
  34. package/src/dag.test.ts +350 -0
  35. package/src/dag.ts +219 -0
  36. package/src/dispatch.test.ts +473 -0
  37. package/src/dispatch.ts +353 -0
  38. package/src/expression.test.ts +353 -0
  39. package/src/expression.ts +332 -0
  40. package/src/format.test.ts +80 -0
  41. package/src/format.ts +41 -0
  42. package/src/graduated-failure.test.ts +556 -0
  43. package/src/index.test.ts +114 -0
  44. package/src/index.ts +488 -0
  45. package/src/loop.test.ts +549 -0
  46. package/src/output.test.ts +213 -0
  47. package/src/output.ts +70 -0
  48. package/src/parallel-integration.test.ts +175 -0
  49. package/src/resume.test.ts +192 -0
  50. package/src/state.test.ts +192 -0
  51. package/src/state.ts +253 -0
  52. package/src/step-agent.test.ts +327 -0
  53. package/src/step-agent.ts +178 -0
  54. package/src/step-command.test.ts +330 -0
  55. package/src/step-command.ts +132 -0
  56. package/src/step-foreach.test.ts +647 -0
  57. package/src/step-foreach.ts +148 -0
  58. package/src/step-gate.test.ts +185 -0
  59. package/src/step-gate.ts +116 -0
  60. package/src/step-loop.test.ts +626 -0
  61. package/src/step-loop.ts +323 -0
  62. package/src/step-parallel.test.ts +475 -0
  63. package/src/step-parallel.ts +168 -0
  64. package/src/step-pause.test.ts +30 -0
  65. package/src/step-pause.ts +27 -0
  66. package/src/step-shared.test.ts +355 -0
  67. package/src/step-shared.ts +157 -0
  68. package/src/step-transform.test.ts +155 -0
  69. package/src/step-transform.ts +47 -0
  70. package/src/template-integration.test.ts +72 -0
  71. package/src/template.test.ts +162 -0
  72. package/src/template.ts +92 -0
  73. package/src/test-helpers.ts +51 -0
  74. package/src/tui.test.ts +355 -0
  75. package/src/tui.ts +182 -0
  76. package/src/types.ts +195 -0
  77. package/src/verifier-schema.test.ts +226 -0
  78. package/src/workflow-discovery.test.ts +105 -0
  79. package/src/workflow-discovery.ts +113 -0
  80. package/src/workflow-executor.test.ts +1530 -0
  81. package/src/workflow-executor.ts +810 -0
  82. package/src/workflow-sdk.test.ts +238 -0
  83. package/src/workflow-sdk.ts +262 -0
  84. package/src/workflow-spec.test.ts +576 -0
  85. package/src/workflow-spec.ts +471 -0
  86. package/templates/analyzers/base-analyzer.md +9 -0
  87. package/templates/analyzers/macros.md +1 -0
  88. package/templates/analyzers/patterns-task.md +11 -0
  89. package/templates/analyzers/patterns.md +15 -0
  90. package/templates/analyzers/quality-task.md +11 -0
  91. package/templates/analyzers/quality.md +15 -0
  92. package/templates/analyzers/structure-task.md +17 -0
  93. package/templates/analyzers/structure.md +15 -0
  94. package/templates/audit-fixer/task.md +67 -0
  95. package/templates/decomposer/task.md +56 -0
  96. package/templates/explorer/system.md +3 -0
  97. package/templates/explorer/task.md +9 -0
  98. package/templates/investigator/task.md +30 -0
  99. package/templates/phase-author/task.md +74 -0
  100. package/templates/plan-decomposer/task.md +46 -0
  101. package/templates/researcher/task.md +26 -0
  102. package/templates/spec-implementer/task.md +53 -0
  103. package/templates/synthesizer/system.md +3 -0
  104. package/templates/synthesizer/task.md +38 -0
  105. package/templates/verifier/task.md +57 -0
  106. package/workflows/create-phase.workflow.yaml +67 -0
  107. package/workflows/do-gap.workflow.yaml +153 -0
  108. package/workflows/fix-audit.workflow.yaml +217 -0
  109. package/workflows/gap-to-phase.workflow.yaml +98 -0
  110. package/workflows/parallel-analysis.workflow.yaml +62 -0
  111. package/workflows/parallel-explicit.workflow.yaml +42 -0
  112. package/workflows/pausable-analysis.workflow.yaml +44 -0
  113. package/workflows/resumable-analysis.workflow.yaml +42 -0
  114. package/workflows/self-implement.workflow.yaml +63 -0
  115. package/workflows/typed-analysis.workflow.yaml +63 -0
package/src/index.ts ADDED
@@ -0,0 +1,488 @@
1
+ /**
2
+ * Extension entry point — registers the `workflow` tool and `/workflow` command
3
+ * for discovering, executing, and managing multi-step workflow runs.
4
+ */
5
+ import { discoverWorkflows, findWorkflow } from "./workflow-discovery.ts";
6
+ import { executeWorkflow, requestPause } from "./workflow-executor.ts";
7
+ import { findIncompleteRun, validateResumeCompatibility, formatIncompleteRun } from "./checkpoint.ts";
8
+ import { createAgentLoader } from "./agent-spec.ts";
9
+ import type { WorkflowResult } from "./types.ts";
10
+ import fs from "node:fs";
11
+
12
+ import { Key } from "@mariozechner/pi-tui";
13
+ import { Type } from "@sinclair/typebox";
14
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
15
+ import type { AgentToolUpdateCallback, AgentToolResult } from "@mariozechner/pi-coding-agent";
16
+
17
+ // ── Helper functions ────────────────────────────────────────────────────────
18
+
19
+ function listWorkflowNames(cwd: string): string {
20
+ const workflows = discoverWorkflows(cwd);
21
+ if (workflows.length === 0) return "(none)";
22
+ return workflows.map((w) => w.name).join(", ");
23
+ }
24
+
25
+ /**
26
+ * Summarize a JSON Schema's expected shape for error messages.
27
+ * Produces something like: { path: string (required), question?: string }
28
+ */
29
+ function summarizeInputSchema(schema: Record<string, unknown> | undefined): string {
30
+ if (!schema) return "(any)";
31
+ const props = schema.properties as Record<string, any> | undefined;
32
+ if (!props) return JSON.stringify(schema);
33
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
34
+ const fields = Object.entries(props).map(([key, val]) => {
35
+ const type = val?.type || "unknown";
36
+ const req = required.has(key);
37
+ return req ? `${key}: ${type} (required)` : `${key}?: ${type}`;
38
+ });
39
+ return `{ ${fields.join(", ")} }`;
40
+ }
41
+
42
+ /**
43
+ * Prompt for workflow input fields. Supports `source` field for select-from-file.
44
+ * Returns the input object, or null if user cancelled.
45
+ */
46
+ async function promptForInput(
47
+ spec: { input?: unknown },
48
+ ctx: ExtensionCommandContext,
49
+ ): Promise<Record<string, unknown> | null> {
50
+ const schema = spec.input as Record<string, unknown> | undefined;
51
+ if (!schema) return {};
52
+
53
+ const props = schema.properties as Record<string, Record<string, unknown>> | undefined;
54
+ const required = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []);
55
+ if (!props) return {};
56
+
57
+ const inputObj: Record<string, unknown> = {};
58
+ for (const [key, val] of Object.entries(props)) {
59
+ if (!required.has(key) || val?.default !== undefined) continue;
60
+ if (!ctx.hasUI) {
61
+ ctx.ui.notify("Workflow input prompts require interactive mode.", "warning");
62
+ return null;
63
+ }
64
+
65
+ const source = val?.source as Record<string, unknown> | undefined;
66
+ if (source?.file && source?.array) {
67
+ // Source-based select: load options from a JSON file
68
+ const filePath = String(source.file);
69
+ const arrayField = String(source.array);
70
+ const labelField = String(source.label || "id");
71
+ const valueField = String(source.value || "id");
72
+ const filter = source.filter as Record<string, unknown> | undefined;
73
+
74
+ try {
75
+ const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
76
+ let items = data[arrayField] as Record<string, unknown>[];
77
+ if (filter) {
78
+ items = items.filter((item) =>
79
+ Object.entries(filter).every(([k, v]) => item[k] === v),
80
+ );
81
+ }
82
+ if (items.length === 0) {
83
+ ctx.ui.notify(`No items found in ${filePath} matching filter.`, "warning");
84
+ return null;
85
+ }
86
+ const options = items.map((item) => String(item[labelField] ?? ""));
87
+ const desc = (val?.description as string) || key;
88
+ const selected = await ctx.ui.select(desc, options);
89
+ if (selected == null) return null;
90
+ // Map label back to value
91
+ const selectedItem = items.find((item) => String(item[labelField]) === selected);
92
+ inputObj[key] = selectedItem ? String(selectedItem[valueField]) : selected;
93
+ } catch {
94
+ ctx.ui.notify(`Failed to load options from ${filePath}`, "warning");
95
+ return null;
96
+ }
97
+ } else {
98
+ // Standard text input
99
+ const type = (val?.type as string) || "string";
100
+ const desc = (val?.description as string) || "";
101
+ const prompt = desc ? `${key} (${type}): ${desc}` : `${key} (${type})`;
102
+ const value = await ctx.ui.input(prompt);
103
+ if (value == null) return null;
104
+ if (type === "number") {
105
+ inputObj[key] = Number(value);
106
+ } else if (type === "array" || type === "object") {
107
+ try { inputObj[key] = JSON.parse(value); } catch { inputObj[key] = value; }
108
+ } else {
109
+ inputObj[key] = value;
110
+ }
111
+ }
112
+ }
113
+ return inputObj;
114
+ }
115
+
116
+ function formatToolResult(result: WorkflowResult): string {
117
+ const status = result.status === "completed" ? "completed" : "failed";
118
+ const stepSummary = Object.entries(result.steps)
119
+ .map(([name, s]) => `${s.status === "completed" ? "\u2713" : "\u2717"} ${name}`)
120
+ .join(", ");
121
+ return `Workflow '${result.workflow}' ${status}: ${stepSummary}. Run dir: ${result.runDir}`;
122
+ }
123
+
124
+ // ── Command handlers ────────────────────────────────────────────────────────
125
+
126
+ async function handleList(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
127
+ const workflows = discoverWorkflows(ctx.cwd);
128
+ if (workflows.length === 0) {
129
+ ctx.ui.notify("No workflows found in .pi/workflows/ or ~/.pi/agent/workflows/", "info");
130
+ return;
131
+ }
132
+
133
+ const options = workflows.map((w) => {
134
+ const source = w.source === "project" ? "[project]" : "[user]";
135
+ const desc = w.description ? ` — ${w.description}` : "";
136
+ return `${w.name} ${source}${desc}`;
137
+ });
138
+
139
+ if (!ctx.hasUI) {
140
+ ctx.ui.notify("Workflow list requires interactive mode.", "warning");
141
+ return;
142
+ }
143
+ const selected = await ctx.ui.select("Run workflow", options);
144
+ if (!selected) return; // user cancelled
145
+
146
+ const name = selected.split(" ")[0];
147
+ const spec = findWorkflow(name, ctx.cwd);
148
+ if (!spec) {
149
+ ctx.ui.notify(`Workflow '${name}' not found.`, "warning");
150
+ return;
151
+ }
152
+
153
+ // Prompt for required input fields
154
+ const input = await promptForInput(spec, ctx);
155
+ if (input === null) return;
156
+
157
+ const inputJson = Object.keys(input).length > 0
158
+ ? JSON.stringify(input)
159
+ : undefined;
160
+ const rawArgs = inputJson ? `${name} --input '${inputJson}'` : name;
161
+ await handleRun(rawArgs, ctx, pi);
162
+ }
163
+
164
+ async function handleRun(rawArgs: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
165
+ // Extract workflow name (first token) and --input value (everything after --input flag)
166
+ const inputFlagIdx = rawArgs.indexOf("--input");
167
+ let namePart: string;
168
+ let inputJson: string | undefined;
169
+
170
+ if (inputFlagIdx !== -1) {
171
+ namePart = rawArgs.slice(0, inputFlagIdx).trim();
172
+ inputJson = rawArgs.slice(inputFlagIdx + "--input".length).trim();
173
+ // Strip surrounding single or double quotes
174
+ if ((inputJson.startsWith("'") && inputJson.endsWith("'")) ||
175
+ (inputJson.startsWith('"') && inputJson.endsWith('"'))) {
176
+ inputJson = inputJson.slice(1, -1);
177
+ }
178
+ } else {
179
+ namePart = rawArgs.trim();
180
+ }
181
+
182
+ const name = namePart.split(/\s+/)[0];
183
+ if (!name) {
184
+ ctx.ui.notify("Usage: /workflow run <name> [--input '<json>']", "warning");
185
+ return;
186
+ }
187
+
188
+ const spec = findWorkflow(name, ctx.cwd);
189
+ if (!spec) {
190
+ ctx.ui.notify(`Workflow '${name}' not found.`, "warning");
191
+ return;
192
+ }
193
+
194
+ // Check for resumable run before starting fresh
195
+ const incomplete = findIncompleteRun(ctx.cwd, spec.name);
196
+ if (incomplete) {
197
+ const compat = validateResumeCompatibility(incomplete.state, spec);
198
+ if (!compat) {
199
+ const summary = formatIncompleteRun(incomplete, spec);
200
+ if (!ctx.hasUI) {
201
+ // Non-interactive mode: auto-resume incomplete run
202
+ try {
203
+ await executeWorkflow(spec, incomplete.state.input, {
204
+ ctx,
205
+ pi,
206
+ loadAgent: createAgentLoader(ctx.cwd),
207
+ resume: {
208
+ runId: incomplete.runId,
209
+ runDir: incomplete.runDir,
210
+ state: incomplete.state,
211
+ },
212
+ });
213
+ } catch (err) {
214
+ ctx.ui.notify(
215
+ `Resume failed: ${err instanceof Error ? err.message : String(err)}`,
216
+ "error",
217
+ );
218
+ }
219
+ return;
220
+ }
221
+ const choice = await ctx.ui.select(
222
+ `${summary}\n\nResume this run?`,
223
+ ["Yes — resume from checkpoint", "No — start fresh"],
224
+ );
225
+ if (choice === "Yes — resume from checkpoint") {
226
+ try {
227
+ await executeWorkflow(spec, incomplete.state.input, {
228
+ ctx,
229
+ pi,
230
+ loadAgent: createAgentLoader(ctx.cwd),
231
+ resume: {
232
+ runId: incomplete.runId,
233
+ runDir: incomplete.runDir,
234
+ state: incomplete.state,
235
+ },
236
+ });
237
+ } catch (err) {
238
+ ctx.ui.notify(
239
+ `Resume failed: ${err instanceof Error ? err.message : String(err)}`,
240
+ "error",
241
+ );
242
+ }
243
+ return;
244
+ }
245
+ // User chose fresh — fall through to normal execution
246
+ }
247
+ }
248
+
249
+ // Parse input: --input JSON, or infer single required field from positional arg
250
+ let input: unknown = {};
251
+ if (inputJson) {
252
+ try {
253
+ input = JSON.parse(inputJson);
254
+ } catch {
255
+ ctx.ui.notify(`Invalid JSON for --input: ${inputJson}`, "warning");
256
+ return;
257
+ }
258
+ } else if (spec.input) {
259
+ const prompted = await promptForInput(spec, ctx);
260
+ if (prompted === null) return;
261
+ input = prompted;
262
+ }
263
+
264
+ try {
265
+ await executeWorkflow(spec, input, {
266
+ ctx,
267
+ pi,
268
+ loadAgent: createAgentLoader(ctx.cwd),
269
+ });
270
+ // Result is injected into conversation by executeWorkflow via sendMessage
271
+ } catch (err) {
272
+ ctx.ui.notify(
273
+ `Workflow '${name}' failed: ${err instanceof Error ? err.message : String(err)}`,
274
+ "error",
275
+ );
276
+ }
277
+ }
278
+
279
+ async function handleResume(rawArgs: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
280
+ const name = rawArgs.trim().split(/\s+/)[0];
281
+ if (!name) {
282
+ ctx.ui.notify("Usage: /workflow resume <name>", "warning");
283
+ return;
284
+ }
285
+
286
+ const spec = findWorkflow(name, ctx.cwd);
287
+ if (!spec) {
288
+ ctx.ui.notify(`Workflow '${name}' not found.`, "warning");
289
+ return;
290
+ }
291
+
292
+ const incomplete = findIncompleteRun(ctx.cwd, spec.name);
293
+ if (!incomplete) {
294
+ ctx.ui.notify(`No incomplete runs found for '${name}'.`, "info");
295
+ return;
296
+ }
297
+
298
+ // Validate compatibility
299
+ const compat = validateResumeCompatibility(incomplete.state, spec);
300
+ if (compat) {
301
+ ctx.ui.notify(`Cannot resume: ${compat}`, "warning");
302
+ return;
303
+ }
304
+
305
+ // Show summary and confirm
306
+ const summary = formatIncompleteRun(incomplete, spec);
307
+ if (!ctx.hasUI) {
308
+ // Non-interactive mode: auto-resume without confirmation
309
+ } else {
310
+ const choice = await ctx.ui.select(
311
+ `${summary}\n\nResume this run?`,
312
+ ["Yes — resume", "No — cancel"],
313
+ );
314
+ if (choice !== "Yes — resume") return;
315
+ }
316
+
317
+ try {
318
+ await executeWorkflow(spec, incomplete.state.input, {
319
+ ctx,
320
+ pi,
321
+ loadAgent: createAgentLoader(ctx.cwd),
322
+ resume: {
323
+ runId: incomplete.runId,
324
+ runDir: incomplete.runDir,
325
+ state: incomplete.state,
326
+ },
327
+ });
328
+ } catch (err) {
329
+ ctx.ui.notify(
330
+ `Resume failed: ${err instanceof Error ? err.message : String(err)}`,
331
+ "error",
332
+ );
333
+ }
334
+ }
335
+
336
+ // ── Extension factory ───────────────────────────────────────────────────────
337
+
338
+ const extension = (pi: ExtensionAPI) => {
339
+ // ── Tool: workflow ──────────────────────────────────────────────────────
340
+
341
+ pi.registerTool({
342
+ name: "workflow",
343
+ label: "Workflow",
344
+ description: "Run a named workflow with typed input. Discovers workflows from .pi/workflows/ and ~/.pi/agent/workflows/.",
345
+ promptSnippet: "Run a multi-step workflow with typed data flow between agents",
346
+ parameters: Type.Object({
347
+ workflow: Type.String({ description: "Name of the workflow to run" }),
348
+ input: Type.Optional(Type.Unknown({ description: "Input data for the workflow (validated against workflow's input schema)" })),
349
+ fresh: Type.Optional(Type.String({ description: "Set to 'true' to start a fresh run, ignoring any incomplete prior runs" })),
350
+ }),
351
+
352
+ async execute(toolCallId: string, params: { workflow: string; input?: unknown; fresh?: string }, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback | undefined, ctx: ExtensionContext) {
353
+ const spec = findWorkflow(params.workflow, ctx.cwd);
354
+ if (!spec) {
355
+ throw new Error(`Workflow '${params.workflow}' not found. Available workflows: ${listWorkflowNames(ctx.cwd)}`);
356
+ }
357
+
358
+ // Defensive: if input arrives as a JSON string (e.g. from Type.Unknown()),
359
+ // parse it into an object.
360
+ let input = params.input ?? {};
361
+ if (typeof input === "string") {
362
+ try {
363
+ input = JSON.parse(input);
364
+ } catch {
365
+ // leave as string — validation will catch it if schema expects object
366
+ }
367
+ }
368
+
369
+ // Check for resumable run (unless explicitly requesting fresh)
370
+ let resumeOpts: { runId: string; runDir: string; state: import("./types.ts").ExecutionState } | undefined;
371
+ if (params.fresh !== "true") {
372
+ const incomplete = findIncompleteRun(ctx.cwd, spec.name);
373
+ if (incomplete) {
374
+ const compat = validateResumeCompatibility(incomplete.state, spec);
375
+ if (!compat) {
376
+ resumeOpts = {
377
+ runId: incomplete.runId,
378
+ runDir: incomplete.runDir,
379
+ state: incomplete.state,
380
+ };
381
+ }
382
+ // If incompatible, silently start fresh
383
+ }
384
+ }
385
+
386
+ const result = await executeWorkflow(spec, input, {
387
+ ctx,
388
+ pi,
389
+ signal,
390
+ loadAgent: createAgentLoader(ctx.cwd),
391
+ resume: resumeOpts,
392
+ });
393
+
394
+ return {
395
+ content: [{ type: "text", text: formatToolResult(result) }],
396
+ details: result,
397
+ };
398
+ },
399
+ });
400
+
401
+ // ── Command: /workflow ──────────────────────────────────────────────────
402
+
403
+ pi.registerCommand("workflow", {
404
+ description: "List and run workflows",
405
+ getArgumentCompletions: (prefix: string) => {
406
+ const subcommands = ["run", "list", "resume"];
407
+ return subcommands
408
+ .filter((s) => s.startsWith(prefix))
409
+ .map((s) => ({ value: s, label: s }));
410
+ },
411
+
412
+ async handler(args: string, ctx: ExtensionCommandContext) {
413
+ const trimmed = args.trim();
414
+ const spaceIdx = trimmed.indexOf(" ");
415
+ const subcommand = spaceIdx === -1 ? trimmed || "list" : trimmed.slice(0, spaceIdx);
416
+ const rest = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
417
+
418
+ if (subcommand === "list") {
419
+ await handleList(ctx, pi);
420
+ } else if (subcommand === "run") {
421
+ await handleRun(rest, ctx, pi);
422
+ } else if (subcommand === "resume") {
423
+ await handleResume(rest, ctx, pi);
424
+ } else {
425
+ ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: list, run, resume`, "warning");
426
+ }
427
+ },
428
+ });
429
+
430
+ // ── Keybindings ──
431
+
432
+ if (Key) {
433
+ pi.registerShortcut(Key.ctrl("h"), {
434
+ description: "Pause running workflow",
435
+ handler: async (ctx: ExtensionContext) => {
436
+ requestPause();
437
+ ctx.ui.notify("Pause requested — workflow will pause after current step completes.", "info");
438
+ },
439
+ });
440
+
441
+ pi.registerShortcut(Key.ctrl("j"), {
442
+ description: "Resume paused workflow",
443
+ handler: async (ctx: ExtensionContext) => {
444
+ const workflows = discoverWorkflows(ctx.cwd);
445
+ let found: { spec: any; incomplete: any } | null = null;
446
+
447
+ for (const wfSpec of workflows) {
448
+ const incomplete = findIncompleteRun(ctx.cwd, wfSpec.name);
449
+ if (incomplete) {
450
+ const compat = validateResumeCompatibility(incomplete.state, wfSpec);
451
+ if (!compat) {
452
+ found = { spec: wfSpec, incomplete };
453
+ break;
454
+ }
455
+ }
456
+ }
457
+
458
+ if (!found) {
459
+ ctx.ui.notify("No paused or incomplete workflows to resume.", "info");
460
+ return;
461
+ }
462
+
463
+ const summary = formatIncompleteRun(found.incomplete, found.spec);
464
+ ctx.ui.notify(`Resuming: ${summary}`, "info");
465
+
466
+ try {
467
+ await executeWorkflow(found.spec, found.incomplete.state.input, {
468
+ ctx,
469
+ pi,
470
+ loadAgent: createAgentLoader(ctx.cwd),
471
+ resume: {
472
+ runId: found.incomplete.runId,
473
+ runDir: found.incomplete.runDir,
474
+ state: found.incomplete.state,
475
+ },
476
+ });
477
+ } catch (err) {
478
+ ctx.ui.notify(
479
+ `Resume failed: ${err instanceof Error ? err.message : String(err)}`,
480
+ "error",
481
+ );
482
+ }
483
+ },
484
+ });
485
+ }
486
+ };
487
+
488
+ export default extension;