@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.
- package/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +172 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +156 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +57 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +125 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +726 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +438 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +464 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +84 -0
- package/skills/pi-workflows/SKILL.md +247 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +74 -0
- package/workflows/create-handoff.workflow.yaml +58 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +55 -0
- package/workflows/plan-from-requirements.workflow.yaml +51 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- 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;
|