@davidorex/pi-workflows 0.2.0 → 0.3.1

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 (216) 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 +178 -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 +724 -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 +171 -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 +63 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +136 -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 +165 -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 +738 -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 +457 -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 +476 -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 +107 -0
  140. package/skills/pi-workflows/SKILL.md +185 -0
  141. package/skills/pi-workflows/references/bundled-resources.md +87 -0
  142. package/templates/architecture-designer/task.md +117 -0
  143. package/templates/architecture-inferrer/task.md +61 -0
  144. package/templates/gap-identifier/task.md +103 -0
  145. package/templates/handoff-writer/task.md +91 -0
  146. package/templates/plan-creator/task.md +143 -0
  147. package/templates/project-definer/task.md +67 -0
  148. package/templates/project-inferrer/task.md +56 -0
  149. package/templates/requirements-gatherer/task.md +90 -0
  150. package/workflows/analyze-existing-project.workflow.yaml +94 -0
  151. package/workflows/create-handoff.workflow.yaml +66 -0
  152. package/workflows/create-phase.workflow.yaml +1 -1
  153. package/workflows/do-gap.workflow.yaml +4 -4
  154. package/workflows/fix-audit.workflow.yaml +1 -1
  155. package/workflows/gap-to-phase.workflow.yaml +1 -1
  156. package/workflows/init-new-project.workflow.yaml +81 -0
  157. package/workflows/plan-from-requirements.workflow.yaml +59 -0
  158. package/workflows/self-implement.workflow.yaml +3 -3
  159. package/workflows/typed-analysis.workflow.yaml +4 -4
  160. package/src/agent-spec.test.ts +0 -289
  161. package/src/agent-spec.ts +0 -122
  162. package/src/block-validation.test.ts +0 -350
  163. package/src/checkpoint.test.ts +0 -237
  164. package/src/checkpoint.ts +0 -140
  165. package/src/completion.test.ts +0 -143
  166. package/src/completion.ts +0 -68
  167. package/src/dag.test.ts +0 -350
  168. package/src/dag.ts +0 -219
  169. package/src/dispatch.test.ts +0 -475
  170. package/src/dispatch.ts +0 -353
  171. package/src/expression.test.ts +0 -353
  172. package/src/expression.ts +0 -332
  173. package/src/format.test.ts +0 -80
  174. package/src/format.ts +0 -41
  175. package/src/graduated-failure.test.ts +0 -556
  176. package/src/index.test.ts +0 -114
  177. package/src/index.ts +0 -516
  178. package/src/loop.test.ts +0 -551
  179. package/src/output.test.ts +0 -213
  180. package/src/output.ts +0 -70
  181. package/src/parallel-integration.test.ts +0 -175
  182. package/src/resume.test.ts +0 -192
  183. package/src/state.test.ts +0 -192
  184. package/src/state.ts +0 -254
  185. package/src/step-agent.test.ts +0 -327
  186. package/src/step-agent.ts +0 -178
  187. package/src/step-command.test.ts +0 -330
  188. package/src/step-command.ts +0 -132
  189. package/src/step-foreach.test.ts +0 -647
  190. package/src/step-foreach.ts +0 -148
  191. package/src/step-gate.test.ts +0 -185
  192. package/src/step-gate.ts +0 -116
  193. package/src/step-loop.test.ts +0 -626
  194. package/src/step-loop.ts +0 -323
  195. package/src/step-parallel.test.ts +0 -475
  196. package/src/step-parallel.ts +0 -168
  197. package/src/step-pause.test.ts +0 -30
  198. package/src/step-shared.test.ts +0 -355
  199. package/src/step-shared.ts +0 -157
  200. package/src/step-transform.test.ts +0 -155
  201. package/src/step-transform.ts +0 -47
  202. package/src/template-integration.test.ts +0 -72
  203. package/src/template.test.ts +0 -162
  204. package/src/test-helpers.ts +0 -51
  205. package/src/tui.test.ts +0 -355
  206. package/src/tui.ts +0 -182
  207. package/src/types.ts +0 -195
  208. package/src/verifier-schema.test.ts +0 -226
  209. package/src/workflow-discovery.test.ts +0 -105
  210. package/src/workflow-discovery.ts +0 -114
  211. package/src/workflow-executor.test.ts +0 -1532
  212. package/src/workflow-executor.ts +0 -810
  213. package/src/workflow-sdk.test.ts +0 -238
  214. package/src/workflow-sdk.ts +0 -262
  215. package/src/workflow-spec.test.ts +0 -576
  216. package/src/workflow-spec.ts +0 -471
package/dist/index.js ADDED
@@ -0,0 +1,724 @@
1
+ /**
2
+ * Extension entry point — registers workflow tools and the `/workflow` command
3
+ * for discovering, executing, and managing multi-step workflow runs.
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { Key } from "@mariozechner/pi-tui";
8
+ import { Type } from "@sinclair/typebox";
9
+ import { createAgentLoader } from "./agent-spec.js";
10
+ import { findIncompleteRun, formatIncompleteRun, validateResumeCompatibility } from "./checkpoint.js";
11
+ import { discoverWorkflows, findWorkflow } from "./workflow-discovery.js";
12
+ import { executeWorkflow, requestPause } from "./workflow-executor.js";
13
+ import { availableAgents, availableSchemas, availableTemplates, filterNames, stepTypes, validateWorkflow, } from "./workflow-sdk.js";
14
+ import { WORKFLOWS_DIR } from "./workflows-dir.js";
15
+ // ── Helper functions ────────────────────────────────────────────────────────
16
+ function listWorkflowNames(cwd) {
17
+ const workflows = discoverWorkflows(cwd);
18
+ if (workflows.length === 0)
19
+ return "(none)";
20
+ return workflows.map((w) => w.name).join(", ");
21
+ }
22
+ /**
23
+ * Summarize a JSON Schema's expected shape for error messages.
24
+ * Produces something like: { path: string (required), question?: string }
25
+ */
26
+ function summarizeInputSchema(schema) {
27
+ if (!schema)
28
+ return "(any)";
29
+ const props = schema.properties;
30
+ if (!props)
31
+ return JSON.stringify(schema);
32
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
33
+ const fields = Object.entries(props).map(([key, val]) => {
34
+ const type = val?.type || "unknown";
35
+ const req = required.has(key);
36
+ return req ? `${key}: ${type} (required)` : `${key}?: ${type}`;
37
+ });
38
+ return `{ ${fields.join(", ")} }`;
39
+ }
40
+ /**
41
+ * Prompt for workflow input fields. Supports `source` field for select-from-file.
42
+ * Returns the input object, or null if user cancelled.
43
+ */
44
+ async function promptForInput(spec, ctx) {
45
+ const schema = spec.input;
46
+ if (!schema)
47
+ return {};
48
+ const props = schema.properties;
49
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
50
+ if (!props)
51
+ return {};
52
+ const inputObj = {};
53
+ for (const [key, val] of Object.entries(props)) {
54
+ if (!required.has(key) || val?.default !== undefined)
55
+ continue;
56
+ if (!ctx.hasUI) {
57
+ ctx.ui.notify("Workflow input prompts require interactive mode.", "warning");
58
+ return null;
59
+ }
60
+ const source = val?.source;
61
+ if (source?.file && source?.array) {
62
+ // Source-based select: load options from a JSON file
63
+ const filePath = String(source.file);
64
+ const arrayField = String(source.array);
65
+ const labelField = String(source.label || "id");
66
+ const valueField = String(source.value || "id");
67
+ const filter = source.filter;
68
+ try {
69
+ const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
70
+ let items = data[arrayField];
71
+ if (filter) {
72
+ items = items.filter((item) => Object.entries(filter).every(([k, v]) => item[k] === v));
73
+ }
74
+ if (items.length === 0) {
75
+ ctx.ui.notify(`No items found in ${filePath} matching filter.`, "warning");
76
+ return null;
77
+ }
78
+ const options = items.map((item) => String(item[labelField] ?? ""));
79
+ const desc = val?.description || key;
80
+ const selected = await ctx.ui.select(desc, options);
81
+ if (selected == null)
82
+ return null;
83
+ // Map label back to value
84
+ const selectedItem = items.find((item) => String(item[labelField]) === selected);
85
+ inputObj[key] = selectedItem ? String(selectedItem[valueField]) : selected;
86
+ }
87
+ catch {
88
+ ctx.ui.notify(`Failed to load options from ${filePath}`, "warning");
89
+ return null;
90
+ }
91
+ }
92
+ else {
93
+ // Standard text input
94
+ const type = val?.type || "string";
95
+ const desc = val?.description || "";
96
+ const prompt = desc ? `${key} (${type}): ${desc}` : `${key} (${type})`;
97
+ const value = await ctx.ui.input(prompt);
98
+ if (value == null)
99
+ return null;
100
+ if (type === "number") {
101
+ inputObj[key] = Number(value);
102
+ }
103
+ else if (type === "array" || type === "object") {
104
+ try {
105
+ inputObj[key] = JSON.parse(value);
106
+ }
107
+ catch {
108
+ inputObj[key] = value;
109
+ }
110
+ }
111
+ else {
112
+ inputObj[key] = value;
113
+ }
114
+ }
115
+ }
116
+ return inputObj;
117
+ }
118
+ function formatToolResult(result) {
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
+ // ── Shared logic (used by both tools and commands) ──────────────────────────
126
+ /**
127
+ * Validate one or all workflow specs. Returns structured results suitable
128
+ * for both the /workflow validate command and the workflow-validate tool.
129
+ */
130
+ function runValidation(cwd, name) {
131
+ const workflows = name
132
+ ? [findWorkflow(name, cwd)].filter(Boolean)
133
+ : discoverWorkflows(cwd);
134
+ if (workflows.length === 0)
135
+ return { found: false, results: [] };
136
+ const results = workflows.map((spec) => {
137
+ const result = validateWorkflow(spec, cwd);
138
+ return { name: spec.name, valid: result.valid, issues: result.issues };
139
+ });
140
+ return { found: true, results };
141
+ }
142
+ /**
143
+ * Aggregate workflow vocabulary — step types, filters, available agents,
144
+ * workflows, schemas, templates. Shared between the /workflow status
145
+ * command and the workflow-status tool.
146
+ */
147
+ function gatherWorkflowStatus(cwd) {
148
+ const workflows = discoverWorkflows(cwd);
149
+ const agents = availableAgents(cwd);
150
+ const schemas = availableSchemas(cwd);
151
+ const templates = availableTemplates(cwd);
152
+ const types = stepTypes();
153
+ const filters = filterNames();
154
+ return {
155
+ stepTypes: types.map((t) => t.name),
156
+ filters,
157
+ workflows: workflows.map((w) => ({ name: w.name, description: w.description, source: w.source })),
158
+ agents: agents.map((a) => ({
159
+ name: a.name,
160
+ role: a.role,
161
+ description: a.description,
162
+ model: a.model,
163
+ tools: a.tools,
164
+ outputFormat: a.outputFormat,
165
+ })),
166
+ schemas: schemas.length,
167
+ templates: templates.length,
168
+ };
169
+ }
170
+ /**
171
+ * Initialize .workflows/ directory structure. Returns list of created
172
+ * directory paths (relative to cwd). Shared between the /workflow init
173
+ * command and the workflow-init tool.
174
+ */
175
+ function initWorkflowDirs(cwd) {
176
+ const workflowsDir = path.join(cwd, WORKFLOWS_DIR);
177
+ const runsDir = path.join(workflowsDir, "runs");
178
+ const created = [];
179
+ for (const dir of [workflowsDir, runsDir]) {
180
+ if (!fs.existsSync(dir)) {
181
+ fs.mkdirSync(dir, { recursive: true });
182
+ created.push(path.relative(cwd, dir) + "/");
183
+ }
184
+ }
185
+ return { created };
186
+ }
187
+ // ── Command handlers ────────────────────────────────────────────────────────
188
+ async function handleList(ctx, pi) {
189
+ const workflows = discoverWorkflows(ctx.cwd);
190
+ if (workflows.length === 0) {
191
+ ctx.ui.notify("No workflows found in .workflows/ or ~/.pi/agent/workflows/", "info");
192
+ return;
193
+ }
194
+ const options = workflows.map((w) => {
195
+ const source = w.source === "project" ? "[project]" : "[user]";
196
+ const desc = w.description ? ` — ${w.description}` : "";
197
+ return `${w.name} ${source}${desc}`;
198
+ });
199
+ if (!ctx.hasUI) {
200
+ ctx.ui.notify("Workflow list requires interactive mode.", "warning");
201
+ return;
202
+ }
203
+ const selected = await ctx.ui.select("Run workflow", options);
204
+ if (!selected)
205
+ return; // user cancelled
206
+ const name = selected.split(" ")[0];
207
+ const spec = findWorkflow(name, ctx.cwd);
208
+ if (!spec) {
209
+ ctx.ui.notify(`Workflow '${name}' not found.`, "warning");
210
+ return;
211
+ }
212
+ // Prompt for required input fields
213
+ const input = await promptForInput(spec, ctx);
214
+ if (input === null)
215
+ return;
216
+ const inputJson = Object.keys(input).length > 0 ? JSON.stringify(input) : undefined;
217
+ const rawArgs = inputJson ? `${name} --input '${inputJson}'` : name;
218
+ await handleRun(rawArgs, ctx, pi);
219
+ }
220
+ async function handleRun(rawArgs, ctx, pi) {
221
+ // Extract workflow name (first token) and --input value (everything after --input flag)
222
+ const inputFlagIdx = rawArgs.indexOf("--input");
223
+ let namePart;
224
+ let inputJson;
225
+ if (inputFlagIdx !== -1) {
226
+ namePart = rawArgs.slice(0, inputFlagIdx).trim();
227
+ inputJson = rawArgs.slice(inputFlagIdx + "--input".length).trim();
228
+ // Strip surrounding single or double quotes
229
+ if ((inputJson.startsWith("'") && inputJson.endsWith("'")) ||
230
+ (inputJson.startsWith('"') && inputJson.endsWith('"'))) {
231
+ inputJson = inputJson.slice(1, -1);
232
+ }
233
+ }
234
+ else {
235
+ namePart = rawArgs.trim();
236
+ }
237
+ const name = namePart.split(/\s+/)[0];
238
+ if (!name) {
239
+ ctx.ui.notify("Usage: /workflow run <name> [--input '<json>']", "warning");
240
+ return;
241
+ }
242
+ const spec = findWorkflow(name, ctx.cwd);
243
+ if (!spec) {
244
+ ctx.ui.notify(`Workflow '${name}' not found.`, "warning");
245
+ return;
246
+ }
247
+ // Check for resumable run before starting fresh
248
+ const incomplete = findIncompleteRun(ctx.cwd, spec.name);
249
+ if (incomplete) {
250
+ const compat = validateResumeCompatibility(incomplete.state, spec);
251
+ if (!compat) {
252
+ const summary = formatIncompleteRun(incomplete, spec);
253
+ if (!ctx.hasUI) {
254
+ // Non-interactive mode: auto-resume incomplete run
255
+ try {
256
+ await executeWorkflow(spec, incomplete.state.input, {
257
+ ctx,
258
+ pi,
259
+ loadAgent: createAgentLoader(ctx.cwd),
260
+ resume: {
261
+ runId: incomplete.runId,
262
+ runDir: incomplete.runDir,
263
+ state: incomplete.state,
264
+ },
265
+ });
266
+ }
267
+ catch (err) {
268
+ ctx.ui.notify(`Resume failed: ${err instanceof Error ? err.message : String(err)}`, "error");
269
+ }
270
+ return;
271
+ }
272
+ const choice = await ctx.ui.select(`${summary}\n\nResume this run?`, [
273
+ "Yes — resume from checkpoint",
274
+ "No — start fresh",
275
+ ]);
276
+ if (choice === "Yes — resume from checkpoint") {
277
+ try {
278
+ await executeWorkflow(spec, incomplete.state.input, {
279
+ ctx,
280
+ pi,
281
+ loadAgent: createAgentLoader(ctx.cwd),
282
+ resume: {
283
+ runId: incomplete.runId,
284
+ runDir: incomplete.runDir,
285
+ state: incomplete.state,
286
+ },
287
+ });
288
+ }
289
+ catch (err) {
290
+ ctx.ui.notify(`Resume failed: ${err instanceof Error ? err.message : String(err)}`, "error");
291
+ }
292
+ return;
293
+ }
294
+ // User chose fresh — fall through to normal execution
295
+ }
296
+ }
297
+ // Parse input: --input JSON, or infer single required field from positional arg
298
+ let input = {};
299
+ if (inputJson) {
300
+ try {
301
+ input = JSON.parse(inputJson);
302
+ }
303
+ catch {
304
+ ctx.ui.notify(`Invalid JSON for --input: ${inputJson}`, "warning");
305
+ return;
306
+ }
307
+ }
308
+ else if (spec.input) {
309
+ const prompted = await promptForInput(spec, ctx);
310
+ if (prompted === null)
311
+ return;
312
+ input = prompted;
313
+ }
314
+ try {
315
+ await executeWorkflow(spec, input, {
316
+ ctx,
317
+ pi,
318
+ loadAgent: createAgentLoader(ctx.cwd),
319
+ });
320
+ // Result is injected into conversation by executeWorkflow via sendMessage
321
+ }
322
+ catch (err) {
323
+ ctx.ui.notify(`Workflow '${name}' failed: ${err instanceof Error ? err.message : String(err)}`, "error");
324
+ }
325
+ }
326
+ async function handleResume(rawArgs, ctx, pi) {
327
+ const name = rawArgs.trim().split(/\s+/)[0];
328
+ if (!name) {
329
+ ctx.ui.notify("Usage: /workflow resume <name>", "warning");
330
+ return;
331
+ }
332
+ const spec = findWorkflow(name, ctx.cwd);
333
+ if (!spec) {
334
+ ctx.ui.notify(`Workflow '${name}' not found.`, "warning");
335
+ return;
336
+ }
337
+ const incomplete = findIncompleteRun(ctx.cwd, spec.name);
338
+ if (!incomplete) {
339
+ ctx.ui.notify(`No incomplete runs found for '${name}'.`, "info");
340
+ return;
341
+ }
342
+ // Validate compatibility
343
+ const compat = validateResumeCompatibility(incomplete.state, spec);
344
+ if (compat) {
345
+ ctx.ui.notify(`Cannot resume: ${compat}`, "warning");
346
+ return;
347
+ }
348
+ // Show summary and confirm
349
+ const summary = formatIncompleteRun(incomplete, spec);
350
+ if (!ctx.hasUI) {
351
+ // Non-interactive mode: auto-resume without confirmation
352
+ }
353
+ else {
354
+ const choice = await ctx.ui.select(`${summary}\n\nResume this run?`, ["Yes — resume", "No — cancel"]);
355
+ if (choice !== "Yes — resume")
356
+ return;
357
+ }
358
+ try {
359
+ await executeWorkflow(spec, incomplete.state.input, {
360
+ ctx,
361
+ pi,
362
+ loadAgent: createAgentLoader(ctx.cwd),
363
+ resume: {
364
+ runId: incomplete.runId,
365
+ runDir: incomplete.runDir,
366
+ state: incomplete.state,
367
+ },
368
+ });
369
+ }
370
+ catch (err) {
371
+ ctx.ui.notify(`Resume failed: ${err instanceof Error ? err.message : String(err)}`, "error");
372
+ }
373
+ }
374
+ function handleValidate(args, ctx) {
375
+ const name = args.trim() || undefined;
376
+ const { found, results } = runValidation(ctx.cwd, name);
377
+ if (name && !found) {
378
+ ctx.ui.notify(`Workflow '${name}' not found.`, "warning");
379
+ return;
380
+ }
381
+ if (!found) {
382
+ ctx.ui.notify("No workflows found.", "info");
383
+ return;
384
+ }
385
+ const lines = [];
386
+ let totalErrors = 0;
387
+ let totalWarnings = 0;
388
+ for (const r of results) {
389
+ const errors = r.issues.filter((i) => i.severity === "error").length;
390
+ const warnings = r.issues.filter((i) => i.severity === "warning").length;
391
+ totalErrors += errors;
392
+ totalWarnings += warnings;
393
+ const icon = r.valid ? "\u2713" : "\u2717";
394
+ lines.push(`${icon} ${r.name} (${errors} errors, ${warnings} warnings)`);
395
+ for (const issue of r.issues) {
396
+ lines.push(` ${issue.severity === "error" ? "\u2717" : "\u26a0"} ${issue.field}: ${issue.message}`);
397
+ }
398
+ }
399
+ lines.push("");
400
+ lines.push(`${results.length} workflow(s), ${totalErrors} error(s), ${totalWarnings} warning(s)`);
401
+ ctx.ui.notify(lines.join("\n"), totalErrors > 0 ? "error" : "info");
402
+ }
403
+ function handleWorkflowInit(ctx) {
404
+ const { created } = initWorkflowDirs(ctx.cwd);
405
+ if (created.length > 0) {
406
+ ctx.ui.notify(`Workflows initialized: created ${created.join(", ")}`, "info");
407
+ }
408
+ else {
409
+ ctx.ui.notify("Workflows already initialized — nothing to do.", "info");
410
+ }
411
+ }
412
+ // ── Extension factory ───────────────────────────────────────────────────────
413
+ const extension = (pi) => {
414
+ // ── Tool: workflow ──────────────────────────────────────────────────────
415
+ pi.registerTool({
416
+ name: "workflow",
417
+ label: "Workflow",
418
+ description: "Run a named workflow with typed input. Discovers workflows from .workflows/ and ~/.pi/agent/workflows/.",
419
+ promptSnippet: "Run a multi-step workflow with typed data flow between agents",
420
+ parameters: Type.Object({
421
+ workflow: Type.String({ description: "Name of the workflow to run" }),
422
+ input: Type.Optional(Type.Unknown({ description: "Input data for the workflow (validated against workflow's input schema)" })),
423
+ fresh: Type.Optional(Type.String({ description: "Set to 'true' to start a fresh run, ignoring any incomplete prior runs" })),
424
+ }),
425
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
426
+ const spec = findWorkflow(params.workflow, ctx.cwd);
427
+ if (!spec) {
428
+ throw new Error(`Workflow '${params.workflow}' not found. Available workflows: ${listWorkflowNames(ctx.cwd)}`);
429
+ }
430
+ // Defensive: if input arrives as a JSON string (e.g. from Type.Unknown()),
431
+ // parse it into an object.
432
+ let input = params.input ?? {};
433
+ if (typeof input === "string") {
434
+ try {
435
+ input = JSON.parse(input);
436
+ }
437
+ catch {
438
+ // leave as string — validation will catch it if schema expects object
439
+ }
440
+ }
441
+ // Check for resumable run (unless explicitly requesting fresh)
442
+ let resumeOpts;
443
+ if (params.fresh !== "true") {
444
+ const incomplete = findIncompleteRun(ctx.cwd, spec.name);
445
+ if (incomplete) {
446
+ const compat = validateResumeCompatibility(incomplete.state, spec);
447
+ if (!compat) {
448
+ resumeOpts = {
449
+ runId: incomplete.runId,
450
+ runDir: incomplete.runDir,
451
+ state: incomplete.state,
452
+ };
453
+ }
454
+ // If incompatible, silently start fresh
455
+ }
456
+ }
457
+ const result = await executeWorkflow(spec, input, {
458
+ ctx,
459
+ pi,
460
+ signal,
461
+ loadAgent: createAgentLoader(ctx.cwd),
462
+ resume: resumeOpts,
463
+ });
464
+ return {
465
+ content: [{ type: "text", text: formatToolResult(result) }],
466
+ details: result,
467
+ };
468
+ },
469
+ });
470
+ // ── Tool: workflow-list ─────────────────────────────────────────────────
471
+ pi.registerTool({
472
+ name: "workflow-list",
473
+ label: "Workflow List",
474
+ description: "List available workflows with names, descriptions, and sources.",
475
+ promptSnippet: "List available workflows with names, descriptions, and sources",
476
+ parameters: Type.Object({}),
477
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
478
+ const workflows = discoverWorkflows(ctx.cwd);
479
+ const items = workflows.map((w) => ({
480
+ name: w.name,
481
+ description: w.description,
482
+ source: w.source,
483
+ }));
484
+ return {
485
+ details: undefined,
486
+ content: [{ type: "text", text: JSON.stringify(items, null, 2) }],
487
+ };
488
+ },
489
+ });
490
+ // ── Tool: workflow-agents ──────────────────────────────────────────────
491
+ pi.registerTool({
492
+ name: "workflow-agents",
493
+ label: "Workflow Agents",
494
+ description: "List available agents with full specs, or inspect a single agent by name. Returns role, description, model, tools, output format/schema, prompt template paths.",
495
+ promptSnippet: "List available agents with specs, or inspect a single agent by name",
496
+ parameters: Type.Object({
497
+ name: Type.Optional(Type.String({ description: "Agent name to inspect (omit to list all)" })),
498
+ }),
499
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
500
+ const agents = availableAgents(ctx.cwd);
501
+ if (params.name) {
502
+ const agent = agents.find((a) => a.name === params.name);
503
+ if (!agent) {
504
+ throw new Error(`Agent '${params.name}' not found. Available: ${agents.map((a) => a.name).join(", ")}`);
505
+ }
506
+ return {
507
+ details: undefined,
508
+ content: [{ type: "text", text: JSON.stringify(agent, null, 2) }],
509
+ };
510
+ }
511
+ const items = agents.map((a) => ({
512
+ name: a.name,
513
+ role: a.role,
514
+ description: a.description,
515
+ model: a.model,
516
+ tools: a.tools,
517
+ extensions: a.extensions,
518
+ skills: a.skills,
519
+ outputFormat: a.outputFormat,
520
+ outputSchema: a.outputSchema,
521
+ promptTemplate: a.promptTemplate,
522
+ taskTemplate: a.taskTemplate,
523
+ }));
524
+ return {
525
+ details: undefined,
526
+ content: [{ type: "text", text: JSON.stringify(items, null, 2) }],
527
+ };
528
+ },
529
+ });
530
+ // ── Tool: workflow-validate ─────────────────────────────────────────────
531
+ pi.registerTool({
532
+ name: "workflow-validate",
533
+ label: "Workflow Validate",
534
+ description: "Validate workflow specs — check agents, schemas, step references, and filters.",
535
+ promptSnippet: "Validate workflow specs — check agents, schemas, step references, filters",
536
+ parameters: Type.Object({
537
+ name: Type.Optional(Type.String({ description: "Workflow name to validate (omit to validate all)" })),
538
+ }),
539
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
540
+ const { found, results } = runValidation(ctx.cwd, params.name);
541
+ if (params.name && !found) {
542
+ throw new Error(`Workflow '${params.name}' not found. Available workflows: ${listWorkflowNames(ctx.cwd)}`);
543
+ }
544
+ return {
545
+ details: undefined,
546
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
547
+ };
548
+ },
549
+ });
550
+ // ── Tool: workflow-status ───────────────────────────────────────────────
551
+ pi.registerTool({
552
+ name: "workflow-status",
553
+ label: "Workflow Status",
554
+ description: "Get workflow vocabulary — step types, filters, available agents, workflows, schemas, templates.",
555
+ promptSnippet: "Get workflow vocabulary — step types, filters, available agents, workflows, schemas",
556
+ parameters: Type.Object({}),
557
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
558
+ const status = gatherWorkflowStatus(ctx.cwd);
559
+ return {
560
+ details: undefined,
561
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
562
+ };
563
+ },
564
+ });
565
+ // ── Tool: workflow-init ─────────────────────────────────────────────────
566
+ pi.registerTool({
567
+ name: "workflow-init",
568
+ label: "Workflow Init",
569
+ description: "Initialize .workflows/ directory for workflow run state.",
570
+ promptSnippet: "Initialize .workflows/ directory for workflow run state",
571
+ parameters: Type.Object({}),
572
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
573
+ const result = initWorkflowDirs(ctx.cwd);
574
+ return {
575
+ details: undefined,
576
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
577
+ };
578
+ },
579
+ });
580
+ const workflowNameCompletions = (argPrefix) => {
581
+ const workflows = discoverWorkflows(process.cwd());
582
+ return workflows
583
+ .filter((w) => w.name.startsWith(argPrefix))
584
+ .map((w) => ({ value: w.name, label: w.name, description: w.description || "" }));
585
+ };
586
+ const WORKFLOW_SUBCOMMANDS = {
587
+ init: {
588
+ description: "Initialize .workflows/ directory",
589
+ handler: (_args, ctx) => handleWorkflowInit(ctx),
590
+ },
591
+ list: {
592
+ description: "List and select a workflow to run",
593
+ handler: (_args, ctx) => handleList(ctx, pi),
594
+ },
595
+ run: {
596
+ description: "Run a workflow by name",
597
+ handler: (args, ctx) => handleRun(args, ctx, pi),
598
+ getCompletions: workflowNameCompletions,
599
+ },
600
+ resume: {
601
+ description: "Resume an incomplete workflow run",
602
+ handler: (args, ctx) => handleResume(args, ctx, pi),
603
+ getCompletions: workflowNameCompletions,
604
+ },
605
+ validate: {
606
+ description: "Validate workflow specs",
607
+ handler: (args, ctx) => handleValidate(args, ctx),
608
+ getCompletions: workflowNameCompletions,
609
+ },
610
+ status: {
611
+ description: "Show workflow vocabulary and discovery",
612
+ handler: (_args, ctx) => {
613
+ const status = gatherWorkflowStatus(ctx.cwd);
614
+ const lines = [];
615
+ lines.push(`Step types: ${status.stepTypes.join(", ")}`);
616
+ lines.push(`Filters: ${status.filters.join(", ")}`);
617
+ const wfs = status.workflows;
618
+ lines.push(`Workflows: ${wfs.length} (${wfs.map((w) => w.name).join(", ")})`);
619
+ const ags = status.agents;
620
+ lines.push(`Agents: ${ags.length} (${ags.map((a) => a.name).join(", ")})`);
621
+ lines.push(`Schemas: ${status.schemas}`);
622
+ lines.push(`Templates: ${status.templates}`);
623
+ ctx.ui.notify(lines.join("\n"), "info");
624
+ },
625
+ },
626
+ help: {
627
+ description: "Show available subcommands",
628
+ handler: (_args, ctx) => {
629
+ const lines = ["Usage: /workflow <subcommand> [args]", ""];
630
+ for (const [name, entry] of Object.entries(WORKFLOW_SUBCOMMANDS)) {
631
+ lines.push(` ${name.padEnd(12)} ${entry.description}`);
632
+ }
633
+ ctx.ui.notify(lines.join("\n"), "info");
634
+ },
635
+ },
636
+ };
637
+ pi.registerCommand("workflow", {
638
+ description: "List and run workflows",
639
+ getArgumentCompletions: (prefix) => {
640
+ const tokens = prefix.split(/\s+/);
641
+ const partial = tokens[tokens.length - 1];
642
+ // Level 0: completing the subcommand name
643
+ if (tokens.length <= 1) {
644
+ return Object.entries(WORKFLOW_SUBCOMMANDS)
645
+ .filter(([name]) => name.startsWith(partial))
646
+ .map(([name, entry]) => ({ value: name, label: name, description: entry.description }));
647
+ }
648
+ // Level 1+: delegate to subcommand's getCompletions
649
+ const subName = tokens[0];
650
+ const sub = WORKFLOW_SUBCOMMANDS[subName];
651
+ if (sub?.getCompletions) {
652
+ const argPrefix = tokens.slice(1).join(" ");
653
+ const items = sub.getCompletions(argPrefix);
654
+ if (items) {
655
+ return items.map((item) => ({ ...item, value: `${subName} ${item.value}` }));
656
+ }
657
+ }
658
+ return null;
659
+ },
660
+ async handler(args, ctx) {
661
+ const trimmed = args.trim();
662
+ const spaceIdx = trimmed.indexOf(" ");
663
+ const subcommand = spaceIdx === -1 ? trimmed || "list" : trimmed.slice(0, spaceIdx);
664
+ const rest = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
665
+ const entry = WORKFLOW_SUBCOMMANDS[subcommand];
666
+ if (!entry) {
667
+ const names = Object.keys(WORKFLOW_SUBCOMMANDS).join(", ");
668
+ ctx.ui.notify(`Unknown subcommand: ${subcommand}. Available: ${names}`, "warning");
669
+ return;
670
+ }
671
+ await entry.handler(rest, ctx);
672
+ },
673
+ });
674
+ // ── Keybindings ──
675
+ if (Key) {
676
+ pi.registerShortcut(Key.ctrl("h"), {
677
+ description: "Pause running workflow",
678
+ handler: async (ctx) => {
679
+ requestPause();
680
+ ctx.ui.notify("Pause requested — workflow will pause after current step completes.", "info");
681
+ },
682
+ });
683
+ pi.registerShortcut(Key.ctrl("j"), {
684
+ description: "Resume paused workflow",
685
+ handler: async (ctx) => {
686
+ const workflows = discoverWorkflows(ctx.cwd);
687
+ let found = null;
688
+ for (const wfSpec of workflows) {
689
+ const incomplete = findIncompleteRun(ctx.cwd, wfSpec.name);
690
+ if (incomplete) {
691
+ const compat = validateResumeCompatibility(incomplete.state, wfSpec);
692
+ if (!compat) {
693
+ found = { spec: wfSpec, incomplete };
694
+ break;
695
+ }
696
+ }
697
+ }
698
+ if (!found) {
699
+ ctx.ui.notify("No paused or incomplete workflows to resume.", "info");
700
+ return;
701
+ }
702
+ const summary = formatIncompleteRun(found.incomplete, found.spec);
703
+ ctx.ui.notify(`Resuming: ${summary}`, "info");
704
+ try {
705
+ await executeWorkflow(found.spec, found.incomplete.state.input, {
706
+ ctx,
707
+ pi,
708
+ loadAgent: createAgentLoader(ctx.cwd),
709
+ resume: {
710
+ runId: found.incomplete.runId,
711
+ runDir: found.incomplete.runDir,
712
+ state: found.incomplete.state,
713
+ },
714
+ });
715
+ }
716
+ catch (err) {
717
+ ctx.ui.notify(`Resume failed: ${err instanceof Error ? err.message : String(err)}`, "error");
718
+ }
719
+ },
720
+ });
721
+ }
722
+ };
723
+ export default extension;
724
+ //# sourceMappingURL=index.js.map