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