pi-workflow-engine 0.12.0 → 1.0.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 (92) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +106 -391
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
  5. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
  6. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
  7. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
  9. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
  12. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
  16. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
  17. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
  18. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
  19. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
  20. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
  21. package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
  22. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
  23. package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
  24. package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
  25. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
  26. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
  27. package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
  28. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
  29. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
  30. package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
  31. package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
  32. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
  33. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
  34. package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
  35. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
  36. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
  37. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
  38. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
  39. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
  40. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
  41. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
  42. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
  43. package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
  44. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
  52. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
  53. package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
  54. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
  55. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
  56. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
  57. package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
  58. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
  59. package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
  60. package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
  61. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
  62. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
  63. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
  64. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
  65. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
  68. package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
  69. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
  70. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
  71. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
  72. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
  73. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
  79. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
  80. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
  81. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
  82. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
  83. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
  84. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
  85. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
  86. package/USAGE.md +90 -17
  87. package/package.json +1 -2
  88. package/skills/workflow-code-review-actions/SKILL.md +12 -23
  89. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
  90. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
  91. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
  92. package/.pi/settings.json +0 -3
@@ -1,26 +1,21 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { Type } from "typebox";
3
- import { VERSION, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { Text, type AutocompleteItem } from "@earendil-works/pi-tui";
5
- import { isAdvisoryReport } from "./src/advisory-schema.ts";
3
+ import { getSelectListTheme, keyText, VERSION, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { AutocompleteItem, SelectItem } from "@earendil-works/pi-tui";
6
5
  import type { WorkflowProgressSnapshot } from "./src/progress-types.ts";
7
- import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
8
- import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
9
- import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./src/ui/workflow-viewer-layout.ts";
6
+ import type { WorkflowRunState } from "./src/workflow-run-record.ts";
7
+ import { WORKFLOW_TOOL_NAME, type LoadedWorkflow, type WorkflowModule, type WorkflowProgressSource, type WorkflowRef } from "./src/types.ts";
8
+ import { showWorkflowInspector } from "./src/ui/workflow-inspector.ts";
9
+ import { WorkflowPicker } from "./src/ui/workflow-picker.ts";
10
10
  import type { PerfSink } from "./src/perf.ts";
11
- import type { WorkflowUsageSnapshot } from "./src/usage.ts";
12
11
  import { ADAPTIVE_WORKFLOW_GUIDANCE, registerDynamax } from "./src/dynamax.ts";
13
12
  import { sessionKey } from "./src/session-identity.ts";
14
13
  import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
15
14
  import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
15
+ import { isWorkflowResult, renderWorkflowResult, type WorkflowResultHints } from "./src/ui/workflow-result-renderer.ts";
16
+ import { renderWorkflowToolCall, renderWorkflowToolResult } from "./src/ui/workflow-tool-renderer.ts";
16
17
  import {
17
- formatWorkflowDetailLines,
18
- isWorkflowResult,
19
- renderWorkflowResult,
20
- } from "./src/ui/workflow-result-renderer.ts";
21
- import {
22
- parseWorkflowBudgetString,
23
- parseWorkflowIntegerString,
18
+ parseWorkflowInvocation,
24
19
  resolveWorkflowRunOptions,
25
20
  type ResolvedWorkflowRunOptions,
26
21
  WORKFLOW_AGENT_TIMEOUT_MAX_MS,
@@ -36,7 +31,8 @@ import {
36
31
  WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
37
32
  WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
38
33
  } from "./src/options.ts";
39
- import { executeWorkflowInvocation, type WorkflowExecution, type WorkflowPerfDetails } from "./src/workflow-execution.ts";
34
+ import { executeWorkflowInvocation, type WorkflowResultEnvelope } from "./src/workflow-execution.ts";
35
+ import { formatWorkflowResultForContext } from "./src/workflow-result-context.ts";
40
36
  import { registerWorkflowModelProfileCommand } from "./src/model-profile-command.ts";
41
37
  import { BackgroundWorkflowCoordinator } from "./src/background-workflows.ts";
42
38
  import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./src/background-workflow-tool.ts";
@@ -48,54 +44,6 @@ import { formatWorkflowInspection, workflowInspectionSnapshot } from "./src/ui/w
48
44
  /** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
49
45
  const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
50
46
 
51
- function summarize(result: unknown): string {
52
- if (typeof result === "object" && result !== null && "summary" in result && typeof result.summary === "string") return result.summary;
53
- return typeof result === "string" ? result : "Workflow finished.";
54
- }
55
-
56
- function formatMessageContent(
57
- name: string,
58
- result: unknown,
59
- usage?: WorkflowUsageSnapshot,
60
- perf?: WorkflowPerfDetails,
61
- metadata?: WorkflowRunMetadata,
62
- ): string {
63
- const details = formatWorkflowDetailLines({ usage, perf, metadata });
64
- return `## Workflow: ${name}\n\n${formatResultForContext(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
65
- }
66
-
67
- function formatResultForContext(result: unknown): string {
68
- if (!isAdvisoryReport(result)) return summarize(result);
69
-
70
- const lines = [result.summary];
71
- if (result.findings.length > 0) {
72
- lines.push("", "Findings:");
73
- result.findings.forEach((finding, index) => {
74
- const id = `R${String(index + 1).padStart(3, "0")}`;
75
- lines.push(
76
- `\n### ${id}: ${finding.summary}`,
77
- `- Severity: ${finding.severity}`,
78
- `- Confidence: ${finding.confidence}`,
79
- `- Category: ${finding.category}`,
80
- `- Location: ${finding.locations.map(formatFindingLocation).join(", ")}`,
81
- `- Impact: ${finding.impact}`,
82
- `- Evidence: ${finding.evidence.length > 0 ? finding.evidence.join("; ") : "(none cited)"}`,
83
- `- Recommendation: ${finding.recommendation}`,
84
- );
85
- });
86
- }
87
- if (result.nextSteps.length > 0) {
88
- lines.push("", "Next steps:", ...result.nextSteps.map((step) => `- ${step}`));
89
- }
90
- return lines.join("\n");
91
- }
92
-
93
- function formatFindingLocation(location: { readonly file: string; readonly line?: number; readonly symbol?: string }): string {
94
- const line = location.line === undefined ? "" : `:${location.line}`;
95
- const symbol = location.symbol === undefined ? "" : ` (${location.symbol})`;
96
- return `${location.file}${line}${symbol}`;
97
- }
98
-
99
47
  type DiscoveryModule = typeof import("./src/discovery.ts");
100
48
  type EngineModule = typeof import("./src/engine.ts");
101
49
  type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
@@ -133,7 +81,7 @@ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Pro
133
81
  }
134
82
 
135
83
  const AUTHOR_TEMP_WORKFLOW_VALUE = "__author-temporary-workflow__";
136
- const AUTHOR_TEMP_WORKFLOW_LABEL = "Author temporary one-shot workflow";
84
+ const AUTHOR_TEMP_WORKFLOW_LABEL = "+ author a one-off workflow";
137
85
  const AUTHOR_TEMP_WORKFLOW_DESCRIPTION = "Ask the host agent to author and run an inline workflow.";
138
86
 
139
87
  const WORKFLOW_OPTION_COMPLETIONS = [
@@ -147,7 +95,7 @@ const WORKFLOW_OPTION_COMPLETIONS = [
147
95
  { value: "--parallel-limit=", description: "Set the parallel submission limit" },
148
96
  { value: "--max-agents=", description: "Set the maximum admitted live agents" },
149
97
  { value: "--agent-timeout-ms=", description: "Set the timeout for each agent attempt" },
150
- { value: "--agent-retries=", description: "Set retries for each agent call" },
98
+ { value: "--agent-retries=", description: "Restart an agent after pi's in-session turn retries are exhausted" },
151
99
  { value: "--budget=", description: "Set the workflow output-token budget" },
152
100
  { value: "--resume=", description: "Resume from a retained workflow run ID" },
153
101
  ] as const;
@@ -168,21 +116,22 @@ async function workflowArgumentCompletions(argumentPrefix: string): Promise<Auto
168
116
  return completeCurrentArgument(argumentPrefix, WORKFLOW_OPTION_COMPLETIONS);
169
117
  }
170
118
 
119
+ /** Registered workflows in discovery order (built-ins first), then the author option. */
171
120
  async function selectWorkflowValue(workflows: ReadonlyMap<string, WorkflowModule>, ctx: ExtensionCommandContext): Promise<string | undefined> {
121
+ const workflowItems: SelectItem[] = [...workflows.values()].map((workflow) => ({
122
+ value: workflow.meta.name,
123
+ label: workflow.meta.name,
124
+ description: workflow.meta.description,
125
+ }));
126
+ if (ctx.mode === "tui") {
127
+ const items = [...workflowItems, { value: AUTHOR_TEMP_WORKFLOW_VALUE, label: AUTHOR_TEMP_WORKFLOW_LABEL }];
128
+ return await ctx.ui.custom<string | undefined>((...[, theme, , done]) => new WorkflowPicker(items, theme, getSelectListTheme(), done));
129
+ }
172
130
  const choices = [
173
- {
174
- value: AUTHOR_TEMP_WORKFLOW_VALUE,
175
- display: `${AUTHOR_TEMP_WORKFLOW_LABEL} — ${AUTHOR_TEMP_WORKFLOW_DESCRIPTION}`,
176
- },
177
- ...[...workflows.values()].map((workflow) => ({
178
- value: workflow.meta.name,
179
- display: `${workflow.meta.name} — ${workflow.meta.description}`,
180
- })),
131
+ ...workflowItems.map((item) => ({ value: item.value, display: `${item.label} — ${item.description}` })),
132
+ { value: AUTHOR_TEMP_WORKFLOW_VALUE, display: `${AUTHOR_TEMP_WORKFLOW_LABEL} — ${AUTHOR_TEMP_WORKFLOW_DESCRIPTION}` },
181
133
  ];
182
- const selected = await ctx.ui.select(
183
- "Run workflow",
184
- choices.map((choice) => choice.display),
185
- );
134
+ const selected = await ctx.ui.select("Run a workflow", choices.map((choice) => choice.display));
186
135
  return choices.find((choice) => choice.display === selected)?.value;
187
136
  }
188
137
 
@@ -191,6 +140,7 @@ export interface LastWorkflowInspection {
191
140
  readonly args: string;
192
141
  readonly completedAt: number;
193
142
  readonly snapshot: WorkflowProgressSnapshot;
143
+ readonly state?: () => WorkflowRunState;
194
144
  }
195
145
 
196
146
  export interface ActiveWorkflowInspection {
@@ -198,6 +148,7 @@ export interface ActiveWorkflowInspection {
198
148
  readonly args: string;
199
149
  readonly startedAt: number;
200
150
  readonly snapshot: () => WorkflowProgressSnapshot;
151
+ readonly state?: () => WorkflowRunState;
201
152
  }
202
153
 
203
154
  interface SessionWorkflowInspections {
@@ -206,22 +157,13 @@ interface SessionWorkflowInspections {
206
157
  }
207
158
 
208
159
  const workflowInspections = new WeakMap<ExtensionAPI, Map<string, SessionWorkflowInspections>>();
209
- let latestWorkflowInspection: LastWorkflowInspection | undefined;
210
- let latestActiveWorkflowInspection: ActiveWorkflowInspection | undefined;
211
-
212
- export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
213
- return latestWorkflowInspection;
214
- }
215
160
 
216
161
  export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
217
162
  if (!ctx.hasUI || ctx.mode !== "tui") {
218
163
  ctx.ui.notify(formatWorkflowInspection(inspection), "info");
219
164
  return;
220
165
  }
221
- await ctx.ui.custom<void>(
222
- (tui, theme, _keybindings, done) => new WorkflowInspector(() => workflowInspectionSnapshot(inspection), tui, theme, () => done(undefined)),
223
- WORKFLOW_VIEWER_OVERLAY_OPTIONS,
224
- );
166
+ await showWorkflowInspector(ctx.ui, () => workflowInspectionSnapshot(inspection), () => ({ state: inspection.state?.() }));
225
167
  }
226
168
 
227
169
  function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
@@ -244,190 +186,7 @@ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionCo
244
186
  }
245
187
 
246
188
  function bindActiveWorkflowInspection(name: string, args: string, source: WorkflowProgressSource): ActiveWorkflowInspection {
247
- return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot() };
248
- }
249
-
250
- export interface WorkflowInvocation {
251
- name: string;
252
- args: string;
253
- options: WorkflowRunOptions;
254
- refreshDiscovery?: boolean;
255
- optionErrors?: string[];
256
- authorBrief?: string;
257
- }
258
-
259
- export function parseWorkflowInvocation(input: string): WorkflowInvocation {
260
- const trimmed = input.trim();
261
- const space = trimmed.indexOf(" ");
262
- const name = space === -1 ? trimmed : trimmed.slice(0, space);
263
- const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
264
- const { args, options, refreshDiscovery, optionErrors } = parseWorkflowOptions(rest);
265
- const invocation: WorkflowInvocation = { name, args, options };
266
- if (refreshDiscovery) invocation.refreshDiscovery = refreshDiscovery;
267
- if (optionErrors) invocation.optionErrors = optionErrors;
268
- return invocation;
269
- }
270
-
271
- const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
272
- const INVALID_RESUME_OPTION = "--resume requires a workflow run id";
273
- const INVALID_MAX_AGENTS_OPTION = "--max-agents requires an integer";
274
- const INVALID_AGENT_TIMEOUT_OPTION = "--agent-timeout-ms requires an integer";
275
- const INVALID_AGENT_RETRIES_OPTION = "--agent-retries requires an integer";
276
- const INVALID_EDITED_RESUME_OPTION = "--resume-edited requires --resume <run-id>";
277
-
278
- function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
279
- const tokens = input.split(/\s+/).filter(Boolean);
280
- const kept: string[] = [];
281
- const options: WorkflowRunOptions = {};
282
- const optionErrors: string[] = [];
283
- let refreshDiscovery = false;
284
- for (let i = 0; i < tokens.length; i++) {
285
- const token = tokens[i];
286
- if (token === "--inspect") {
287
- options.inspect = true;
288
- continue;
289
- }
290
- if (token === "--refresh") {
291
- refreshDiscovery = true;
292
- continue;
293
- }
294
- if (token === "--perf") {
295
- options.perf = true;
296
- continue;
297
- }
298
- if (token === "--resume-edited") {
299
- options.resumeEditedWorkflow = true;
300
- continue;
301
- }
302
- if (token === "--result-viewer" || token === "--review-viewer") {
303
- options.resultViewer = "open";
304
- continue;
305
- }
306
- if (token === "--no-result-viewer" || token === "--no-review-viewer") {
307
- options.resultViewer = "skip";
308
- continue;
309
- }
310
- if (token.startsWith("--concurrency=")) {
311
- options.concurrency = parseNumericOption(token.slice("--concurrency=".length));
312
- continue;
313
- }
314
- if (token === "--concurrency") {
315
- const next = tokens[i + 1];
316
- options.concurrency = parseNumericOption(next);
317
- if (next !== undefined) i++;
318
- continue;
319
- }
320
- if (token.startsWith("--parallel-limit=")) {
321
- options.parallelSubmissionLimit = parseNumericOption(token.slice("--parallel-limit=".length));
322
- continue;
323
- }
324
- if (token === "--parallel-limit") {
325
- const next = tokens[i + 1];
326
- options.parallelSubmissionLimit = parseNumericOption(next);
327
- if (next !== undefined) i++;
328
- continue;
329
- }
330
- if (token.startsWith("--max-agents=")) {
331
- const parsed = parseWorkflowIntegerString(token.slice("--max-agents=".length));
332
- if (parsed === undefined) optionErrors.push(INVALID_MAX_AGENTS_OPTION);
333
- else options.maxAgents = parsed;
334
- continue;
335
- }
336
- if (token === "--max-agents") {
337
- const next = tokens[i + 1];
338
- const parsed = parseWorkflowIntegerString(next);
339
- if (parsed === undefined) optionErrors.push(INVALID_MAX_AGENTS_OPTION);
340
- else {
341
- options.maxAgents = parsed;
342
- i++;
343
- }
344
- continue;
345
- }
346
- if (token.startsWith("--agent-timeout-ms=")) {
347
- const parsed = parseWorkflowIntegerString(token.slice("--agent-timeout-ms=".length));
348
- if (parsed === undefined) optionErrors.push(INVALID_AGENT_TIMEOUT_OPTION);
349
- else options.agentTimeoutMs = parsed;
350
- continue;
351
- }
352
- if (token === "--agent-timeout-ms") {
353
- const next = tokens[i + 1];
354
- const parsed = parseWorkflowIntegerString(next);
355
- if (parsed === undefined) optionErrors.push(INVALID_AGENT_TIMEOUT_OPTION);
356
- else {
357
- options.agentTimeoutMs = parsed;
358
- i++;
359
- }
360
- continue;
361
- }
362
- if (token.startsWith("--agent-retries=")) {
363
- const parsed = parseWorkflowIntegerString(token.slice("--agent-retries=".length));
364
- if (parsed === undefined) optionErrors.push(INVALID_AGENT_RETRIES_OPTION);
365
- else options.agentRetries = parsed;
366
- continue;
367
- }
368
- if (token === "--agent-retries") {
369
- const next = tokens[i + 1];
370
- const parsed = parseWorkflowIntegerString(next);
371
- if (parsed === undefined) optionErrors.push(INVALID_AGENT_RETRIES_OPTION);
372
- else {
373
- options.agentRetries = parsed;
374
- i++;
375
- }
376
- continue;
377
- }
378
- if (token.startsWith("--budget=")) {
379
- const parsed = parseBudgetOption(token.slice("--budget=".length));
380
- if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
381
- else options.budget = parsed;
382
- continue;
383
- }
384
- if (token === "--budget") {
385
- const next = tokens[i + 1];
386
- const parsed = next === undefined ? undefined : parseBudgetOption(next);
387
- if (parsed === undefined) {
388
- optionErrors.push(INVALID_BUDGET_OPTION);
389
- } else {
390
- options.budget = parsed;
391
- i++;
392
- }
393
- continue;
394
- }
395
- if (token.startsWith("--resume=")) {
396
- const value = token.slice("--resume=".length).trim();
397
- if (value === "") optionErrors.push(INVALID_RESUME_OPTION);
398
- else options.resumeFromRunId = value;
399
- continue;
400
- }
401
- if (token === "--resume") {
402
- const next = tokens[i + 1];
403
- if (next === undefined || next.startsWith("--")) {
404
- optionErrors.push(INVALID_RESUME_OPTION);
405
- } else {
406
- options.resumeFromRunId = next;
407
- i++;
408
- }
409
- continue;
410
- }
411
- kept.push(token);
412
- }
413
- if (options.resumeEditedWorkflow && !options.resumeFromRunId) optionErrors.push(INVALID_EDITED_RESUME_OPTION);
414
- return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
415
- }
416
-
417
- function parseBudgetOption(value: string): number | undefined {
418
- return parseWorkflowBudgetString(value);
419
- }
420
-
421
- function parseNumericOption(value: string | undefined): number | undefined {
422
- if (value === undefined) return undefined;
423
- const parsed = Number(value);
424
- return Number.isFinite(parsed) ? parsed : undefined;
425
- }
426
-
427
- function compactInlinePreview(script: string | undefined): string {
428
- if (!script) return "";
429
- const compact = script.replace(/\s+/g, " ").trim();
430
- return compact.length > 60 ? `${compact.slice(0, 57)}…` : compact;
189
+ return { name, args, startedAt: Date.now(), snapshot: () => source.snapshot(), state: source.state };
431
190
  }
432
191
 
433
192
  export function buildTemporaryWorkflowAuthorPrompt(brief: string): string {
@@ -484,27 +243,30 @@ export function inlineCompileErrorResult(message: string): WorkflowToolErrorResu
484
243
  return { content: [{ type: "text", text: `Inline workflow did not compile: ${message}` }], details: { error: "inline_compile_error", message } };
485
244
  }
486
245
 
246
+ export type WorkflowPickerSelection =
247
+ | { readonly kind: "run"; readonly name: string; readonly args: string }
248
+ | { readonly kind: "author"; readonly brief: string };
249
+
487
250
  export async function pickWorkflow(
488
251
  workflows: ReadonlyMap<string, WorkflowModule>,
489
252
  ctx: ExtensionCommandContext,
490
- ): Promise<WorkflowInvocation | undefined> {
491
- const choice = await selectWorkflowValue(workflows, ctx);
492
- if (!choice) return undefined;
253
+ ): Promise<WorkflowPickerSelection | undefined> {
254
+ const name = await selectWorkflowValue(workflows, ctx);
255
+ if (!name) return undefined;
493
256
 
494
- if (choice === AUTHOR_TEMP_WORKFLOW_VALUE || choice === AUTHOR_TEMP_WORKFLOW_LABEL) {
257
+ if (name === AUTHOR_TEMP_WORKFLOW_VALUE) {
495
258
  const brief = await ctx.ui.editor(
496
259
  "Describe temporary workflow",
497
260
  "Goal:\n\nAgents to run:\n- \n\nFinal output should include:\n- summary\n- findings\n- next steps\n",
498
261
  );
499
262
  const trimmed = brief?.trim();
500
- if (!trimmed) return undefined;
501
- return { name: "", args: "", options: {}, authorBrief: trimmed };
263
+ return trimmed ? { kind: "author", brief: trimmed } : undefined;
502
264
  }
503
265
 
504
- const separator = choice.indexOf(" — ");
505
- const name = separator === -1 ? choice : choice.slice(0, separator);
506
- const args = name === "code-review" ? (await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff"))?.trim() ?? "" : "";
507
- return { name, args, options: {} };
266
+ if (name !== "code-review") return { kind: "run", name, args: "" };
267
+ // Escape resolves undefined and cancels; only a submitted blank target means auto-detect.
268
+ const target = await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff");
269
+ return target === undefined ? undefined : { kind: "run", name, args: target.trim() };
508
270
  }
509
271
 
510
272
  export async function sendWorkflowResult(
@@ -513,36 +275,14 @@ export async function sendWorkflowResult(
513
275
  name: string,
514
276
  mod: LoadedWorkflow,
515
277
  args: string,
516
- options: WorkflowRunOptions,
278
+ options: ResolvedWorkflowRunOptions,
517
279
  perfRecorder?: PerfSink,
518
280
  reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
519
281
  ): Promise<void> {
520
- await sendResolvedWorkflowResult(
521
- pi,
522
- ctx,
523
- name,
524
- mod,
525
- args,
526
- resolveWorkflowRunOptions(options),
527
- perfRecorder,
528
- reviewSessions,
529
- );
530
- }
531
-
532
- async function sendResolvedWorkflowResult(
533
- pi: ExtensionAPI,
534
- ctx: ExtensionContext,
535
- name: string,
536
- mod: LoadedWorkflow,
537
- args: string,
538
- options: ResolvedWorkflowRunOptions,
539
- perfRecorder: PerfSink | undefined,
540
- reviewSessions: ReviewSessionCoordinator,
541
- ): Promise<void> {
542
- const execution = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
543
- reviewSessions.remember(ctx, execution, options);
544
- sendWorkflowExecution(pi, execution);
545
- await reviewSessions.present(ctx, execution, options);
282
+ const envelope = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
283
+ const retained = reviewSessions.remember(ctx, envelope, options);
284
+ publishWorkflowResult(pi, envelope);
285
+ await reviewSessions.present(ctx, retained, options);
546
286
  }
547
287
 
548
288
  async function executeResolvedWorkflow(
@@ -553,7 +293,7 @@ async function executeResolvedWorkflow(
553
293
  args: string,
554
294
  options: ResolvedWorkflowRunOptions,
555
295
  perfRecorder?: PerfSink,
556
- ): Promise<WorkflowExecution> {
296
+ ): Promise<WorkflowResultEnvelope> {
557
297
  const { runResolvedWorkflow } = await loadEngine();
558
298
  let liveInspection: ActiveWorkflowInspection | undefined;
559
299
  const inspections = workflowInspectionState(pi, ctx);
@@ -570,35 +310,25 @@ async function executeResolvedWorkflow(
570
310
  if (source) {
571
311
  liveInspection = bindActiveWorkflowInspection(name, args, source);
572
312
  inspections.active = liveInspection;
573
- latestActiveWorkflowInspection = liveInspection;
574
313
  } else if (inspections.active === liveInspection) {
575
314
  inspections.active = undefined;
576
- if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
577
315
  liveInspection = undefined;
578
316
  }
579
317
  },
580
318
  onProgressSnapshot(snapshot) {
581
- const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
582
- inspections.last = completed;
583
- latestWorkflowInspection = completed;
319
+ // The durable state settles after this final snapshot; keep reading it from the live source.
320
+ inspections.last = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot, state: liveInspection?.state };
584
321
  },
585
322
  });
586
323
  }
587
324
 
588
- function sendWorkflowExecution(pi: ExtensionAPI, execution: WorkflowExecution): void {
589
- const { name } = execution.envelope;
325
+ function publishWorkflowResult(pi: ExtensionAPI, envelope: WorkflowResultEnvelope): void {
590
326
  pi.sendMessage(
591
327
  {
592
328
  customType: "workflow-result",
593
- content: formatMessageContent(
594
- name,
595
- execution.envelope.result,
596
- execution.envelope.usage,
597
- execution.envelope.perf,
598
- execution.metadata,
599
- ),
329
+ content: formatWorkflowResultForContext(envelope),
600
330
  display: true,
601
- details: execution.envelope,
331
+ details: envelope,
602
332
  },
603
333
  { triggerTurn: false },
604
334
  );
@@ -610,7 +340,7 @@ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordina
610
340
  const perfRecorder = await createInvocationPerf(options);
611
341
  return await executeResolvedWorkflow(pi, ctx, workflow.meta.name, workflow, "", options, perfRecorder);
612
342
  },
613
- publish: (execution) => sendWorkflowExecution(pi, execution),
343
+ publish: (envelope) => publishWorkflowResult(pi, envelope),
614
344
  });
615
345
  }
616
346
 
@@ -625,12 +355,12 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
625
355
  },
626
356
  async execute(ctx, name, workflow, options) {
627
357
  const perfRecorder = await createInvocationPerf(options);
628
- const execution = await executeResolvedWorkflow(pi, ctx, name, workflow, "", options, perfRecorder);
629
- reviewSessions.remember(ctx, execution, options);
358
+ const envelope = await executeResolvedWorkflow(pi, ctx, name, workflow, "", options, perfRecorder);
359
+ reviewSessions.remember(ctx, envelope, options);
630
360
  },
631
361
  });
632
362
  backgroundWorkflows.onRunSettled((ctx, runId) => workflowRuns.runSettled(ctx, runId));
633
- registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
363
+ const dynamax = registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
634
364
  registerWorkflowModelProfileCommand(pi);
635
365
  registerWorkflowRunCommand(pi, workflowRuns);
636
366
  pi.on("session_start", async (_event, ctx) => {
@@ -658,13 +388,8 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
658
388
 
659
389
  pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
660
390
  const details = message.details;
661
- if (isWorkflowResult(details)) {
662
- return renderWorkflowResult(details.name, details.result, expanded, theme, details.usage, {
663
- runId: details.runId,
664
- resumedFromRunId: details.resumedFromRunId,
665
- }, details.perf);
666
- }
667
- return renderWorkflowResult("workflow", details ?? message.content, expanded, theme);
391
+ const view = isWorkflowResult(details) ? details : { name: "workflow", result: details ?? message.content };
392
+ return renderWorkflowResult(view, expanded, theme, workflowResultHints(shortcuts));
668
393
  });
669
394
 
670
395
  pi.registerCommand("workflow:inspector", {
@@ -697,40 +422,54 @@ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShort
697
422
  description: "Run a multi-agent workflow: /workflow <name> [args]",
698
423
  getArgumentCompletions: workflowArgumentCompletions,
699
424
  handler: async (args: string, ctx: ExtensionCommandContext) => {
700
- const direct = parseWorkflowInvocation(args);
701
- if (direct.optionErrors?.length) {
702
- ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
425
+ const invocation = parseWorkflowInvocation(args);
426
+ if (invocation.optionErrors.length > 0) {
427
+ ctx.ui.notify(`Invalid workflow option: ${invocation.optionErrors.join("; ")}`, "warning");
703
428
  return;
704
429
  }
705
- const directOptions = resolveWorkflowRunOptions(direct.options);
706
- const perfRecorder = await createInvocationPerf(directOptions);
430
+ const options = resolveWorkflowRunOptions(invocation.options);
431
+ const perfRecorder = await createInvocationPerf(options);
707
432
  const { discoverWorkflows } = await loadDiscovery();
708
- const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
433
+ const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: invocation.refreshDiscovery, perf: perfRecorder });
709
434
  const available = [...workflows.keys()].join(", ") || "(none)";
710
- const invocation = direct.name ? direct : ctx.hasUI ? await pickWorkflow(workflows, ctx) : undefined;
711
-
712
- if (!invocation) {
435
+ if (!invocation.name && !ctx.hasUI) {
713
436
  ctx.ui.notify(`Usage: /workflow <name> [args]. Available: ${available}`, "warning");
714
437
  return;
715
438
  }
716
-
717
- if (invocation.authorBrief) {
718
- pi.sendUserMessage(buildTemporaryWorkflowAuthorPrompt(invocation.authorBrief));
439
+ const selection: WorkflowPickerSelection | undefined = invocation.name
440
+ ? { kind: "run", name: invocation.name, args: invocation.args }
441
+ : await pickWorkflow(workflows, ctx);
442
+ // Dismissing the picker or one of its follow-up prompts is a deliberate cancel, not a usage error.
443
+ if (!selection) return;
444
+
445
+ if (selection.kind === "author") {
446
+ // A send while the agent streams is rejected, which would leave the one-shot armed for an unrelated prompt.
447
+ await ctx.waitForIdle();
448
+ dynamax.markOneShot(ctx);
449
+ pi.sendUserMessage(buildTemporaryWorkflowAuthorPrompt(selection.brief));
719
450
  return;
720
451
  }
721
452
 
722
- const mod = workflows.get(invocation.name);
453
+ const mod = workflows.get(selection.name);
723
454
  if (!mod) {
724
- ctx.ui.notify(`Unknown workflow "${invocation.name}". Available: ${available}`, "error");
455
+ ctx.ui.notify(`Unknown workflow "${selection.name}". Available: ${available}`, "error");
725
456
  return;
726
457
  }
727
458
 
728
- const effectiveOptions = invocation === direct ? directOptions : resolveWorkflowRunOptions(invocation.options);
729
- await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
459
+ await sendWorkflowResult(pi, ctx, selection.name, mod, selection.args, options, perfRecorder, reviewSessions);
730
460
  },
731
461
  });
732
462
 
733
- registerWorkflowTool(pi, reviewSessions, backgroundWorkflows);
463
+ registerWorkflowTool(pi, reviewSessions, backgroundWorkflows, shortcuts);
464
+ }
465
+
466
+ /** Result footers advertise the configured triage and inspector shortcuts and pi's current expand key, read at render time. */
467
+ function workflowResultHints(shortcuts: DynamaxShortcuts): WorkflowResultHints {
468
+ return {
469
+ triage: shortcuts.results ?? "/workflow:results",
470
+ inspect: shortcuts.inspector ?? "/workflow:inspector",
471
+ expand: keyText("app.tools.expand") || undefined,
472
+ };
734
473
  }
735
474
 
736
475
  /** Register the host-facing workflow tool independently from command and lifecycle surfaces. */
@@ -738,9 +477,10 @@ function registerWorkflowTool(
738
477
  pi: ExtensionAPI,
739
478
  reviewSessions: ReviewSessionCoordinator,
740
479
  backgroundWorkflows: BackgroundWorkflowCoordinator,
480
+ shortcuts: DynamaxShortcuts,
741
481
  ): void {
742
482
  pi.registerTool({
743
- name: "workflow",
483
+ name: WORKFLOW_TOOL_NAME,
744
484
  label: "Workflow",
745
485
  description:
746
486
  "ONLY call workflow when the user opted into multi-agent orchestration via the literal token `dynamax`, sticky `/workflow:dynamax on`, an explicit request to run or author a workflow, or a command/skill instruction. Runs either a registered named workflow or an inline one-off workflow script (fan-out → verify → synthesize), synchronously by default or explicitly in the background.",
@@ -780,7 +520,7 @@ function registerWorkflowTool(
780
520
  ),
781
521
  agentRetries: Type.Optional(
782
522
  Type.Integer({
783
- description: `Retries per agent for classified transient provider failures; clamped to ${WORKFLOW_AGENT_RETRIES_MIN}-${WORKFLOW_AGENT_RETRIES_MAX}`,
523
+ description: `Whole-agent restarts after a transient provider failure that pi's in-session turn retry (the user's pi retry settings) could not recover; clamped to ${WORKFLOW_AGENT_RETRIES_MIN}-${WORKFLOW_AGENT_RETRIES_MAX}`,
784
524
  }),
785
525
  ),
786
526
  autoResumeOnUsageLimit: Type.Optional(
@@ -815,29 +555,12 @@ function registerWorkflowTool(
815
555
  background: Type.Optional(Type.Boolean({ description: "Return a durable run ID immediately and deliver completion to this conversation later" })),
816
556
  }),
817
557
  renderCall(args, theme) {
818
- const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
819
- const background = args.background ? ` ${theme.fg("dim", "(background)")}` : "";
820
- if (args.name?.trim()) {
821
- return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name.trim())}${background}${suffix}`, 0, 0);
822
- }
823
- const preview = compactInlinePreview(args.script);
824
- const previewSuffix = preview ? ` ${theme.fg("dim", preview)}` : "";
825
- return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", "inline")}${background}${suffix}${previewSuffix}`, 0, 0);
558
+ return renderWorkflowToolCall(args, theme);
826
559
  },
827
- renderResult(result, { expanded, isPartial }, theme) {
828
- if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
829
- const details = result.details;
830
- if (isWorkflowResult(details)) {
831
- return renderWorkflowResult(details.name, details.result, expanded, theme, details.usage, {
832
- runId: details.runId,
833
- resumedFromRunId: details.resumedFromRunId,
834
- }, details.perf);
835
- }
836
- const first = result.content[0];
837
- const text = first?.type === "text" ? first.text : "Workflow finished.";
838
- return new Text(theme.fg("muted", text), 0, 0);
560
+ renderResult(result, options, theme, context) {
561
+ return renderWorkflowToolResult(result, options, theme, context.args, workflowResultHints(shortcuts));
839
562
  },
840
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
563
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
841
564
  const request = normalizeWorkflowToolRequest(params);
842
565
  if (request.kind === "error") return invalidWorkflowInvocationResult();
843
566
  const resumeFromRunId = params.resumeFromRunId?.trim();
@@ -859,7 +582,6 @@ function registerWorkflowTool(
859
582
  }
860
583
 
861
584
  const runOptions = resolveWorkflowRunOptions({
862
- inspect: ctx.hasUI && ctx.mode === "tui",
863
585
  concurrency: params.concurrency,
864
586
  parallelSubmissionLimit: params.parallelSubmissionLimit,
865
587
  maxAgents: params.maxAgents,
@@ -910,7 +632,7 @@ function registerWorkflowTool(
910
632
  name: resultName,
911
633
  options: runOptions,
912
634
  async execute(backgroundCtx, backgroundOptions) {
913
- const execution = await executeResolvedWorkflow(
635
+ const envelope = await executeResolvedWorkflow(
914
636
  pi,
915
637
  backgroundCtx,
916
638
  resultName,
@@ -919,24 +641,17 @@ function registerWorkflowTool(
919
641
  backgroundOptions,
920
642
  perfRecorder,
921
643
  );
922
- reviewSessions.remember(ctx, execution, backgroundOptions);
644
+ reviewSessions.remember(ctx, envelope, backgroundOptions);
923
645
  },
924
646
  });
925
647
  }
926
- const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
927
- reviewSessions.remember(ctx, execution, runOptions);
648
+ // Marks the tool row as running (rendered `● running <name>`) until the envelope replaces it.
649
+ onUpdate?.({ content: [{ type: "text", text: `Running workflow ${resultName}.` }], details: { state: "running", name: resultName } });
650
+ const envelope = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
651
+ reviewSessions.remember(ctx, envelope, runOptions);
928
652
  return {
929
- content: [{
930
- type: "text",
931
- text: formatMessageContent(
932
- resultName,
933
- execution.envelope.result,
934
- execution.envelope.usage,
935
- execution.envelope.perf,
936
- execution.metadata,
937
- ),
938
- }],
939
- details: execution.envelope,
653
+ content: [{ type: "text", text: formatWorkflowResultForContext(envelope) }],
654
+ details: envelope,
940
655
  };
941
656
  },
942
657
  });