@agimon-ai/doompi-workflow 0.0.1-alpha.97 → 0.0.1-alpha.99

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 (41) hide show
  1. package/README.md +7 -0
  2. package/dist/apiContracts.cjs +3 -0
  3. package/dist/apiContracts.cjs.map +1 -1
  4. package/dist/apiContracts.d.mts.map +1 -1
  5. package/dist/apiContracts.mjs +3 -0
  6. package/dist/apiContracts.mjs.map +1 -1
  7. package/dist/extensions/pi.cjs +102 -644
  8. package/dist/extensions/pi.cjs.map +1 -1
  9. package/dist/extensions/pi.d.mts.map +1 -1
  10. package/dist/extensions/pi.mjs +49 -591
  11. package/dist/extensions/pi.mjs.map +1 -1
  12. package/dist/extensions/server.cjs +1718 -608
  13. package/dist/extensions/server.cjs.map +1 -1
  14. package/dist/extensions/server.d.mts.map +1 -1
  15. package/dist/extensions/server.mjs +1720 -610
  16. package/dist/extensions/server.mjs.map +1 -1
  17. package/dist/extensions/web.mjs +644 -218
  18. package/dist/extensions/web.mjs.map +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.d.mts +10 -3
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +1 -1
  23. package/dist/{workflowLaunchCommand-Bhr-PvdD.mjs → workflowLaunchCommand-BLrxmzdd.mjs} +14 -7
  24. package/dist/workflowLaunchCommand-BLrxmzdd.mjs.map +1 -0
  25. package/dist/{workflowLaunchCommand-7y8xg5YF.cjs → workflowLaunchCommand-DJyDB96d.cjs} +14 -7
  26. package/dist/workflowLaunchCommand-DJyDB96d.cjs.map +1 -0
  27. package/dist/workflowTerminal-Bb4bLtLu.cjs +962 -0
  28. package/dist/workflowTerminal-Bb4bLtLu.cjs.map +1 -0
  29. package/dist/workflowTerminal-QzufjHCa.mjs +730 -0
  30. package/dist/workflowTerminal-QzufjHCa.mjs.map +1 -0
  31. package/package.json +9 -9
  32. package/src/prompts/doompi-author-workflow/SKILL.md +26 -0
  33. package/src/prompts/doompi-use-workflow/SKILL.md +1 -1
  34. package/src/types/webWorkflowTerminal.ts +16 -0
  35. package/src/types/webWorkflows.ts +20 -0
  36. package/dist/workflowLaunchCommand-7y8xg5YF.cjs.map +0 -1
  37. package/dist/workflowLaunchCommand-Bhr-PvdD.mjs.map +0 -1
  38. package/dist/workflowTerminal-Bj4KKpoA.mjs +0 -148
  39. package/dist/workflowTerminal-Bj4KKpoA.mjs.map +0 -1
  40. package/dist/workflowTerminal-WM_cpxIO.cjs +0 -200
  41. package/dist/workflowTerminal-WM_cpxIO.cjs.map +0 -1
@@ -1,204 +1,24 @@
1
- import { i as validateWorkflowLaunch, n as parseWorkflowLaunchCommand, r as resolveWorkflowEntry, t as isLaunchParseFailure } from "../workflowLaunchCommand-Bhr-PvdD.mjs";
2
- import { i as workflowSkillDirectory, n as registerRunProvider, t as createWorkflowTerminalService } from "../workflowTerminal-Bj4KKpoA.mjs";
1
+ import { i as validateWorkflowLaunch, n as parseWorkflowLaunchCommand, r as resolveWorkflowEntry, t as isLaunchParseFailure } from "../workflowLaunchCommand-BLrxmzdd.mjs";
2
+ import { A as LEADER_RECOVER_ACTION, C as LAUNCH_WORKFLOW_TOOL_NAME, D as LEADER_ENABLE_ACTION, E as LEADER_DISABLE_ACTION, M as PACKAGE_SOURCE, N as WORKFLOW_LAUNCH_ID_ENV, O as LEADER_LABEL, P as WORKFLOW_RUN_TOOL_NAME, S as resolveDispatcherParentSession, T as LEADER_DETAIL, _ as workflowSkillDirectory, a as createWorkflowLaunchExecutor, b as dispatcherTools, c as workflowToolRestriction, d as launchNotice, f as runsForSession, h as withOptions, i as appendGuidance, j as LIST_WORKFLOWS_TOOL_NAME, k as LEADER_MANAGE_ACTION, l as finishedRunSummary, m as toolResultText, n as registerRunProvider, o as launchFailureOptions, p as toAgentToolResult, r as actionGuidance, s as reportProgress, t as createWorkflowTerminalService, u as isSessionRun, v as createWorkflowFence, w as LEADER_CATALOG_ACTION, x as isWorkflowDispatcherProcess, y as dispatcherToolRestriction } from "../workflowTerminal-QzufjHCa.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { defineMessageRenderer, definePiExtension, definePiTool, piToolContributions } from "@agimon-ai/doompi-core/piExtension";
5
5
  import { composeRootHooks, defineRoot, defineRoutedContribution } from "@agimon-ai/doompi-core/extensionFile";
6
6
  import { DOOM_SKILL_SOURCES_SERVICE, requireDoomSkillSourcesService } from "@agimon-ai/doompi-core/skills";
7
7
  import { DOOM_UI_HUB_SERVICE, requireDoomUiHub } from "@agimon-ai/doompi-core/uiHub";
8
- import { AsyncLocalStorage } from "node:async_hooks";
9
- import { SUBAGENT_PARENT_SESSION_ENV, SUBAGENT_ROOT_SESSION_ENV, readChildProcessContext, resolveRootSessionId } from "@agimon-ai/doompi-core/childProcess";
8
+ import { SUBAGENT_ROOT_SESSION_ENV, resolveRootSessionId } from "@agimon-ai/doompi-core/childProcess";
10
9
  import { existsSync } from "node:fs";
11
10
  import { readFile } from "node:fs/promises";
12
11
  import { basename, extname, resolve } from "node:path";
13
- import { WorkflowTerminalService, createEmbeddedWorkflowFeature, currentWorkflowPosition, readWorkflowProgress, summarizeWorkflowProgress } from "@agimon-ai/workflow-mcp";
12
+ import { WorkflowTerminalService, compatibleCommands, createEmbeddedWorkflowFeature, currentWorkflowPosition, readWorkflowProgress, summarizeWorkflowProgress } from "@agimon-ai/workflow-mcp";
14
13
  import { z } from "zod";
14
+ import { randomUUID } from "node:crypto";
15
15
  import { DoomToolCall, DoomToolResult, renderToolHeading } from "@agimon-ai/doompi-ui/toolChrome";
16
16
  import { spawn } from "node:child_process";
17
- import { randomUUID } from "node:crypto";
18
17
  import { DOOM_NARRATION_SERVICE, createNarrationRequest, requireDoomNarrationService } from "@agimon-ai/doompi-core/narration";
19
18
  import { createDoomReadinessCoordinator, readDoomReadinessCoordinator } from "@agimon-ai/doompi-core/readiness";
20
19
  import { defineMinorMode, piMinorModes } from "@agimon-ai/doompi-minor-mode";
21
20
  import { createDoomTelemetry } from "@agimon-ai/doompi-telemetry";
22
21
  import { Box, Key, Text, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
23
- //#region src/constants/workflow.ts
24
- const PACKAGE_SOURCE = "@agimon-ai/doompi-workflow";
25
- const LEADER_LABEL = "workflows";
26
- const LEADER_ENABLE_ACTION = "workflow.enable";
27
- const LEADER_DISABLE_ACTION = "workflow.disable";
28
- const LEADER_MANAGE_ACTION = "workflow.manage";
29
- const LEADER_RECOVER_ACTION = "workflow.recover";
30
- const LEADER_CATALOG_ACTION = "workflow.catalog";
31
- const LEADER_DETAIL = "multi-step agent runs";
32
- const LIST_WORKFLOWS_TOOL_NAME = "list_workflows";
33
- const LAUNCH_WORKFLOW_TOOL_NAME = "launch_workflow";
34
- const WORKFLOW_RUN_TOOL_NAME$1 = "workflow_run";
35
- const WORKFLOW_PI_TOOL_NAMES = [
36
- LIST_WORKFLOWS_TOOL_NAME,
37
- LAUNCH_WORKFLOW_TOOL_NAME,
38
- WORKFLOW_RUN_TOOL_NAME$1
39
- ];
40
- //#endregion
41
- //#region src/services/workflowFence/index.ts
42
- const DISPATCHER_AGENT_NAME = "agiflow-dispatcher";
43
- const SESSION_START_EVENT = "session_start";
44
- const INACTIVE_RUNTIME_MESSAGE = "The workflow runtime is no longer active.";
45
- const CLEANUP_SCOPE = Symbol("doom-workflow-cleanup");
46
- const DISPATCHER_TOOL_NAMES = /* @__PURE__ */ new Set(["list_workflows", "launch_workflow"]);
47
- const ROOT_SCOPED_TOOL_NAMES = /* @__PURE__ */ new Set(["launch_workflow"]);
48
- const WORKFLOW_RUN_TOOL_NAME = "workflow_run";
49
- function boundValue(target, property) {
50
- const value = Reflect.get(target, property, target);
51
- return typeof value === "function" ? value.bind(target) : value;
52
- }
53
- function withParentSession(ctx, parentSessionId) {
54
- const sessionManager = new Proxy(ctx.sessionManager, { get(target, property) {
55
- if (property === "getSessionId") return () => parentSessionId;
56
- return boundValue(target, property);
57
- } });
58
- return new Proxy(ctx, { get(target, property) {
59
- if (property === "sessionManager") return sessionManager;
60
- return boundValue(target, property);
61
- } });
62
- }
63
- /**
64
- * A dispatcher child launches workflows for its root session and never runs one
65
- * itself, so `workflow_run` stays off its surface even if something registers it.
66
- */
67
- function dispatcherToolRestriction() {
68
- return (incoming) => incoming.filter((name) => name !== WORKFLOW_RUN_TOOL_NAME);
69
- }
70
- /** Restrict a dispatcher child to discovery and root-owned launch. */
71
- function dispatcherTools(tools, parentSessionId, environment) {
72
- const rootSessionId = parentSessionId ? resolveRootSessionId(parentSessionId, environment) : void 0;
73
- return tools.filter((tool) => DISPATCHER_TOOL_NAMES.has(tool.name)).map((tool) => {
74
- if (!rootSessionId || !ROOT_SCOPED_TOOL_NAMES.has(tool.name)) return tool;
75
- return {
76
- ...tool,
77
- execute: (toolCallId, params, signal, onUpdate, ctx) => tool.execute(toolCallId, params, signal, onUpdate, withParentSession(ctx, rootSessionId))
78
- };
79
- });
80
- }
81
- function resolveDispatcherParentSession(environment) {
82
- return readChildProcessContext(environment)?.parentSessionId ?? (environment[SUBAGENT_PARENT_SESSION_ENV]?.trim() || void 0);
83
- }
84
- function isWorkflowDispatcherProcess(environment) {
85
- if (readChildProcessContext(environment)?.mode === DISPATCHER_AGENT_NAME) return true;
86
- return environment.PI_SUBAGENT_CHILD_AGENT === DISPATCHER_AGENT_NAME && Boolean(environment[SUBAGENT_PARENT_SESSION_ENV]?.trim());
87
- }
88
- /** Fence every callback retained by Pi after this package root is replaced. */
89
- function createWorkflowFence(host, signal) {
90
- const scope = new AsyncLocalStorage();
91
- let active = true;
92
- let disposing = false;
93
- let generation = 0;
94
- let lastSessionStartEvent;
95
- let lastSessionStartContext;
96
- const isCurrentInvocation = () => {
97
- const currentScope = scope.getStore();
98
- if (!active || signal?.aborted && currentScope !== CLEANUP_SCOPE) return false;
99
- return currentScope === void 0 || currentScope === CLEANUP_SCOPE || currentScope === generation;
100
- };
101
- const ownsContext = (contextGeneration) => {
102
- const currentScope = scope.getStore();
103
- if (!active || signal?.aborted && currentScope !== CLEANUP_SCOPE) return false;
104
- return currentScope === CLEANUP_SCOPE || contextGeneration === generation && (currentScope === void 0 || currentScope === contextGeneration);
105
- };
106
- const guardContext = (context, contextGeneration) => {
107
- const guardedUi = new Proxy(context.ui, { get(target, property) {
108
- const value = Reflect.get(target, property, target);
109
- if (typeof value !== "function") return value;
110
- return (...args) => {
111
- if (ownsContext(contextGeneration)) return Reflect.apply(value, target, args);
112
- if (property === "confirm") return Promise.resolve(false);
113
- if (property === "custom" || property === "editor" || property === "input" || property === "select") return Promise.resolve(void 0);
114
- };
115
- } });
116
- return new Proxy(context, { get(target, property) {
117
- if (property === "ui") return guardedUi;
118
- return boundValue(target, property);
119
- } });
120
- };
121
- const tool = (definition) => {
122
- return {
123
- ...definition,
124
- execute: async (toolCallId, params, signal, onUpdate, context) => {
125
- const executionGeneration = generation;
126
- if (!active || disposing || signal?.aborted) throw new Error(INACTIVE_RUNTIME_MESSAGE);
127
- const guardedContext = guardContext(context, executionGeneration);
128
- const guardedUpdate = onUpdate ? (update) => {
129
- if (active && executionGeneration === generation && !signal?.aborted) onUpdate(update);
130
- } : void 0;
131
- const result = await scope.run(executionGeneration, () => definition.execute(toolCallId, params, signal, guardedUpdate, guardedContext));
132
- if (!active || disposing || executionGeneration !== generation) throw new Error(INACTIVE_RUNTIME_MESSAGE);
133
- return result;
134
- }
135
- };
136
- };
137
- const command = (name, options) => {
138
- return [name, {
139
- ...options,
140
- handler: async (args, context) => {
141
- const executionGeneration = generation;
142
- if (!active || disposing) return;
143
- await scope.run(executionGeneration, () => options.handler(args, guardContext(context, executionGeneration)));
144
- }
145
- }];
146
- };
147
- const shortcut = (key, options) => {
148
- return [key, {
149
- ...options,
150
- handler: (context) => {
151
- const executionGeneration = generation;
152
- if (!active || disposing) return;
153
- return scope.run(executionGeneration, () => options.handler(guardContext(context, executionGeneration)));
154
- }
155
- }];
156
- };
157
- const events = (handlers) => Object.fromEntries(Object.entries(handlers).map(([eventName, handler]) => [eventName, (event, context) => {
158
- if (!active || disposing) return void 0;
159
- if (eventName === SESSION_START_EVENT && (event !== lastSessionStartEvent || context !== lastSessionStartContext)) {
160
- generation += 1;
161
- lastSessionStartEvent = event;
162
- lastSessionStartContext = context;
163
- }
164
- const eventGeneration = generation;
165
- return scope.run(eventGeneration, () => handler(event, guardContext(context, eventGeneration)));
166
- }]));
167
- return {
168
- pi: new Proxy(host, { get(target, property) {
169
- if (property === "sendMessage") return (...args) => {
170
- if (isCurrentInvocation()) target.sendMessage(...args);
171
- };
172
- if (property === "sendUserMessage") return (...args) => {
173
- if (isCurrentInvocation()) target.sendUserMessage(...args);
174
- };
175
- if (property === "exec") return (...args) => isCurrentInvocation() ? target.exec(...args) : Promise.resolve({
176
- code: 1,
177
- killed: false,
178
- stderr: INACTIVE_RUNTIME_MESSAGE,
179
- stdout: ""
180
- });
181
- return boundValue(target, property);
182
- } }),
183
- tool,
184
- command,
185
- shortcut,
186
- events,
187
- beginDisposal() {
188
- if (disposing || !active) return;
189
- disposing = true;
190
- generation += 1;
191
- },
192
- finishDisposal() {
193
- active = false;
194
- disposing = false;
195
- generation += 1;
196
- },
197
- isCurrentInvocation,
198
- runCleanup: (operation) => scope.run(CLEANUP_SCOPE, operation)
199
- };
200
- }
201
- //#endregion
202
22
  //#region src/schemas/workflowPi.ts
203
23
  const RUN_SELECTOR_FIELDS = {
204
24
  runKey: z.string().min(1).describe("Run key of the workflow run."),
@@ -269,392 +89,6 @@ const workflowCatalogPageSchema = z.object({
269
89
  }).passthrough()).describe("Workflow definitions on this page.")
270
90
  }).passthrough();
271
91
  //#endregion
272
- //#region src/services/piToolBridge/index.ts
273
- /**
274
- * Environment variable stamped onto every run launched from a Pi session.
275
- *
276
- * `RunWorkflowService` forwards `env` to the inner CLI invocation across the
277
- * `launch-command` delegation boundary and persists it on the run record, so
278
- * this survives backgrounding and identifies the launching session afterwards.
279
- */
280
- const PI_SESSION_ENV = "PI_SESSION_ID";
281
- /** Operator override for the per-session launch ceiling. */
282
- const MAX_CONCURRENT_ENV = "WORKFLOW_MCP_MAX_CONCURRENT";
283
- const DEFAULT_MAX_CONCURRENT = 5;
284
- /** Resolve the per-session launch ceiling from the environment. */
285
- function resolveMaxConcurrent(env = process.env) {
286
- const parsed = Number(env[MAX_CONCURRENT_ENV]);
287
- return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONCURRENT;
288
- }
289
- /**
290
- * True when this session launched the run.
291
- *
292
- * Fails closed on a missing session id: an unstamped record (a CLI launch) and
293
- * a call arriving before the session id is known both match nothing, so no run
294
- * ever leaks across sessions through an undefined-equals-undefined comparison.
295
- */
296
- function isSessionRun(record, sessionId) {
297
- return sessionId !== void 0 && record.env?.["PI_SESSION_ID"] === sessionId;
298
- }
299
- /** Runs launched by one Pi session that are still running. */
300
- function runsForSession(records, sessionId) {
301
- return records.filter((record) => isSessionRun(record, sessionId) && record.stage === "running");
302
- }
303
- /**
304
- * What the caller is told when the registry, not the launcher, acknowledged a
305
- * launch.
306
- *
307
- * Leads with the run key because it is the handle every other workflow action
308
- * takes, and it is no longer available any other way: the launcher's own output
309
- * is not waited for, so nothing else in this result carries it.
310
- */
311
- function launchedRunSummary(record) {
312
- return [
313
- `Started ${record.displayName} in workspace ${record.workspace}.`,
314
- `Run key: ${record.runKey}`,
315
- "The run is registered and going. This call returned at that point rather than waiting for the launcher process to exit."
316
- ].join("\n");
317
- }
318
- /**
319
- * What the caller is told when the launch was handed off but no run has
320
- * registered inside the acknowledgement budget.
321
- *
322
- * Deliberately not an error: the launcher is slow rather than broken often
323
- * enough that failing here would strand runs that go on to start normally.
324
- * There is no run key to offer yet, so it points at the surfaces that do not
325
- * need one.
326
- */
327
- function launchHandoffSummary(workflowPath) {
328
- return [
329
- `Launch handed off for ${workflowPath}, but no run has registered yet.`,
330
- "Nothing was cancelled: the launcher is still starting, and a run that registers later still reports back here when it finishes.",
331
- "Do not launch it again. Ask the user to check the active workflow list if they need to see it sooner."
332
- ].join("\n");
333
- }
334
- /**
335
- * Escape sequences a terminal consumes rather than prints.
336
- *
337
- * Built from a string with the rule silenced, the way `doom-runner`'s own
338
- * scrubber does it, because matching the escape byte is the entire job. Not
339
- * imported from there: that package's published build is a stale artifact
340
- * missing the export, and a launch notice should not need another package's
341
- * build to be current.
342
- */
343
- const ESCAPE_SEQUENCE = new RegExp([
344
- "\\u001B\\[[0-9;?]*[ -/]*[@-~]",
345
- "\\u001B\\][^\\u0007\\u001B]*(?:\\u0007|\\u001B\\\\)",
346
- "\\u001B[@-Z\\\\-_]"
347
- ].join("|"), "g");
348
- /** Rows made only of box-drawing: separators that carry nothing once trimmed. */
349
- const RULE_ROW = /^[\s─-╿]*$/u;
350
- const NOTICE_LINE_LIMIT = 3;
351
- /**
352
- * Widest a notice row may be before it is clipped.
353
- *
354
- * A three-line cap does not bound a toast on its own: the engine announces its
355
- * delegation by echoing the whole shell command, temp script path included, and
356
- * one line of that wraps across three rows of a narrow terminal and shoves the
357
- * transcript around. Clipped at a width every terminal can hold instead.
358
- */
359
- const NOTICE_WIDTH_LIMIT = 100;
360
- /**
361
- * Cut a tool result down to something a toast can hold.
362
- *
363
- * The launch result used to reach `ui.notify` whole, and for a workflow that
364
- * executes in this process that result is the engine's entire console log:
365
- * banner, run directory, job tree, rules, colour. Pi rendered every line of it
366
- * over the transcript, which reads as a broken screen rather than as output.
367
- * Escape sequences go first: the log carries colour the notification would
368
- * render as literal text, and a stray clear or cursor-move would do to the
369
- * screen exactly what this function exists to prevent.
370
- */
371
- function launchNotice(text, limit = NOTICE_LINE_LIMIT) {
372
- const lines = text.replace(ESCAPE_SEQUENCE, "").split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0 && !RULE_ROW.test(line)).map((line) => line.length > NOTICE_WIDTH_LIMIT ? `${line.slice(0, 99)}…` : line);
373
- if (lines.length === 0) return "Workflow started.";
374
- if (lines.length <= limit) return lines.join("\n");
375
- return [...lines.slice(0, limit), `… ${lines.length - limit} more lines, in the run's own output.`].join("\n");
376
- }
377
- /** Flatten MCP tool content to text for Pi. */
378
- function toolResultText(result) {
379
- return result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n").trim();
380
- }
381
- /**
382
- * Convert an MCP tool result into a Pi tool result.
383
- *
384
- * Pi signals tool failure by throwing: the agent loop catches it and builds the
385
- * error result. So an `isError` MCP result becomes a thrown error rather than a
386
- * successful result carrying a flag Pi would ignore.
387
- */
388
- function toAgentToolResult(tool, result, errorOptions = []) {
389
- const text = toolResultText(result) || "No output.";
390
- if (result.isError) throw new Error(withOptions(text, errorOptions));
391
- return {
392
- content: [{
393
- type: "text",
394
- text
395
- }],
396
- details: { tool }
397
- };
398
- }
399
- /**
400
- * Attach a short menu of next steps to a problem.
401
- *
402
- * An error with no route forward is where an agent invents one: retrying a
403
- * capacity limit, relaunching a run that should be recovered, or silently
404
- * giving up on work the user is waiting for. Naming two or three real options
405
- * turns an error into a decision, and the decision belongs to the user.
406
- */
407
- function withOptions(problem, options) {
408
- if (options.length === 0) return problem;
409
- return [
410
- problem,
411
- "",
412
- "Options, put these to the user rather than picking one yourself:",
413
- ...options.map(bullet)
414
- ].join("\n");
415
- }
416
- function bullet(option) {
417
- return `- ${option}`;
418
- }
419
- /** The job and step a run died on, preferring the progress log to the record. */
420
- function failurePosition(record, jobs) {
421
- const failedJob = jobs.find((job) => job.status === "failed");
422
- const failedStep = failedJob?.steps.find((step) => step.status === "failed");
423
- return {
424
- job: failedJob?.name ?? record.failedJob,
425
- step: failedStep?.name,
426
- reason: failedStep?.reason ?? failedJob?.reason
427
- };
428
- }
429
- /**
430
- * Describe a finished run for the agent that launched it.
431
- *
432
- * Carries the job identifiers back so a dispatching agent can match the run to
433
- * the work item it launched it for without another lookup. When the run failed,
434
- * carries the failing step and a set of options too: the agent is being told
435
- * about this out of band, with no user question in flight, so without them it
436
- * has to guess whether to recover, relaunch, or report.
437
- */
438
- function finishedRunSummary(record, jobs = []) {
439
- const outcome = record.stage === "completed" ? "completed" : `ended in ${record.stage}`;
440
- const failure = failurePosition(record, jobs);
441
- const lines = [
442
- `Workflow run ${record.runKey} in workspace ${record.workspace} ${outcome}.`,
443
- record.workflowId ? `Workflow: ${record.workflowId}` : void 0,
444
- failure.job ? `Failed job: ${failure.job}${failure.step ? ` (step: ${failure.step})` : ""}` : void 0,
445
- record.errorMessage ? `Error: ${record.errorMessage}` : void 0,
446
- failure.reason && failure.reason !== record.errorMessage ? `Step reported: ${failure.reason}` : void 0
447
- ];
448
- const jobId = record.env?.AGIFLOW_JOB_ID;
449
- if (jobId) lines.push(`Agiflow job: ${record.env?.AGIFLOW_JOB_KIND ?? "unknown"} ${jobId}`);
450
- const summary = lines.filter((line) => Boolean(line)).join("\n");
451
- if (record.stage === "completed") return summary;
452
- if (record.outcome === "interrupted") return withOptions(summary, [`workflow_run {"action":"recover","runKey":${JSON.stringify(record.runKey)}}: pick it up where it stopped.`, "Leave it stopped and move on."]);
453
- const diagnosticText = [record.errorMessage, failure.reason].filter(Boolean).join("\n");
454
- if (diagnosticText.includes("JOB_ALREADY_CLAIMED")) return withOptions(summary, ["Report contention: another worker owns this job. Do not unlock, release, or retry the claim automatically."]);
455
- if (diagnosticText.includes("WORKFLOW_NOT_OWNED") || /release failed/i.test(diagnosticText)) return withOptions(summary, ["Inspect the current workflow ownership and ask the user before any release or unlock. Never force another worker’s lock."]);
456
- if (/running workflow not found/i.test(diagnosticText)) return withOptions(summary, [`workflow_run {"action":"status","runKey":${JSON.stringify(record.runKey)}}: re-check current state before retrying.`, "Report the recorded terminal state when no running owner remains."]);
457
- return withOptions(summary, [
458
- `workflow_run {"action":"tail","runKey":${JSON.stringify(record.runKey)}}: read the run's own output first, when the error above does not explain the failure.`,
459
- `workflow_run {"action":"recover","runKey":${JSON.stringify(record.runKey)}}: resume from the failed job, keeping the work already done. Fits a transient or external cause.`,
460
- "Report the failure and stop, when it needs a code change or a decision you cannot make."
461
- ]);
462
- }
463
- //#endregion
464
- //#region src/services/workflowExecution/index.ts
465
- const AGIFLOW_JOB_ID_ENV = "AGIFLOW_JOB_ID";
466
- const AGIFLOW_JOB_KIND_ENV = "AGIFLOW_JOB_KIND";
467
- const AGIFLOW_PROJECT_ID_ENV = "AGIFLOW_PROJECT_ID";
468
- /**
469
- * What workflow mode does to the tool surface: nothing but hide its own tools.
470
- *
471
- * The tools are registered for the whole session because Pi has no other way to
472
- * add one later, so the mode is expressed as a restriction rather than as a
473
- * registration.
474
- */
475
- function workflowToolRestriction(enabled) {
476
- const hidden = new Set(WORKFLOW_PI_TOOL_NAMES);
477
- return (incoming) => enabled ? incoming : incoming.filter((name) => !hidden.has(name));
478
- }
479
- /** How often the registry is asked whether the launch has produced a run. */
480
- const LAUNCH_ACK_POLL_MS = 500;
481
- /**
482
- * How long to wait for that before answering without a run key.
483
- *
484
- * Long enough to cover a launcher that is merely slow, short enough that a
485
- * caller is never left holding a turn open for a launcher that will not answer.
486
- */
487
- const LAUNCH_ACK_TIMEOUT_MS = 15e3;
488
- function reportProgress(onUpdate, tool, message) {
489
- onUpdate?.({
490
- content: [{
491
- type: "text",
492
- text: message
493
- }],
494
- details: { tool }
495
- });
496
- }
497
- function appendGuidance(result, guidance) {
498
- return {
499
- ...result,
500
- content: [...result.content, {
501
- type: "text",
502
- text: guidance
503
- }]
504
- };
505
- }
506
- function statusAction(runKey) {
507
- return `workflow_run {"action":"status","runKey":${JSON.stringify(runKey)}}`;
508
- }
509
- function actionGuidance(action, runKey) {
510
- const status = statusAction(runKey);
511
- if (action === "stop") return [`Next: call ${status} to confirm the run reached a terminal stage.`];
512
- if (action === "recover") return [`Report the recovery failure and do not retry it blindly: call ${status} first.`, "Ask the user before launching fresh work when the recorded recovery is no longer valid."];
513
- return [`Next: call ${status} to verify the recorded execution state.`];
514
- }
515
- const launchFailureOptions = [
516
- "Report the failure to the user with the error above. A launch failure is usually a bad workflow path, a missing launcher, or a workflow whose own pre-conditions refused.",
517
- "list_workflows: confirm the workflow path exists and its description matches the work, if the path may be wrong.",
518
- "workflow_run with action status: check whether an earlier attempt is already running before launching again."
519
- ];
520
- /**
521
- * An Error, whatever the launch rejected with.
522
- *
523
- * A rejection reaches here as `unknown` because it crossed a promise boundary,
524
- * and rethrowing that as-is hands Pi's tool layer something it cannot read a
525
- * message or a stack off. The original is kept as the cause rather than
526
- * flattened into a string.
527
- */
528
- function asError(value) {
529
- return value instanceof Error ? value : new Error(String(value), { cause: value });
530
- }
531
- /** Resolves to undefined, so a race against it reads as "nothing settled yet". */
532
- function sleep(ms) {
533
- return new Promise((settle) => {
534
- setTimeout(() => settle(void 0), ms).unref?.();
535
- });
536
- }
537
- /**
538
- * Wait for whichever answer arrives first: the launch settling, the run
539
- * appearing in the registry, or the budget running out.
540
- *
541
- * A launch that delegates to a terminal launcher settles when that launcher's
542
- * whole process chain closes, which on a loaded machine has been measured
543
- * minutes after the run itself was registered and working. The registry is the
544
- * earlier and more truthful signal: a run recorded there is a run that started.
545
- */
546
- async function awaitLaunchAck(settled, dependencies, query) {
547
- const findLaunchedRun = dependencies.findLaunchedRun;
548
- if (!findLaunchedRun) return {
549
- kind: "settled",
550
- outcome: await settled
551
- };
552
- const pollMs = dependencies.launchAckPollMs ?? LAUNCH_ACK_POLL_MS;
553
- const deadline = Date.now() + (dependencies.launchAckTimeoutMs ?? LAUNCH_ACK_TIMEOUT_MS);
554
- for (;;) {
555
- const outcome = await Promise.race([settled, sleep(pollMs)]);
556
- if (outcome) return {
557
- kind: "settled",
558
- outcome
559
- };
560
- const record = await findLaunchedRun(query).catch(() => void 0);
561
- if (record) return {
562
- kind: "record",
563
- record
564
- };
565
- if (Date.now() >= deadline) return { kind: "handoff" };
566
- }
567
- }
568
- /**
569
- * Carry a failure that lands after the caller was told the run started.
570
- *
571
- * Answering early trades the launcher's exit code for timeliness. Dropping that
572
- * code entirely is not part of the trade: a launch that fails a minute later
573
- * would otherwise leave a user waiting on a run that never existed.
574
- */
575
- function reportLateFailure(settled, dependencies, ctx) {
576
- settled.then((outcome) => {
577
- if (outcome.kind === "error") {
578
- dependencies.onLateFailure?.(asError(outcome.error), ctx);
579
- return;
580
- }
581
- if (outcome.result.isError) dependencies.onLateFailure?.(new Error(toolResultText(outcome.result) || "Workflow launch failed."), ctx);
582
- });
583
- }
584
- function createWorkflowLaunchExecutor(dependencies) {
585
- return { async execute(input, ctx, onUpdate) {
586
- const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId(), dependencies.environment);
587
- dependencies.observeSession?.(sessionId);
588
- const agiflowJobKind = input.env?.[AGIFLOW_JOB_KIND_ENV]?.trim();
589
- const agiflowJobId = input.env?.[AGIFLOW_JOB_ID_ENV]?.trim();
590
- const explicitProjectId = input.env?.[AGIFLOW_PROJECT_ID_ENV]?.trim();
591
- if (Boolean(agiflowJobKind) !== Boolean(agiflowJobId)) throw new Error("Agiflow workflow launches require AGIFLOW_JOB_KIND and AGIFLOW_JOB_ID together.");
592
- if (agiflowJobKind && agiflowJobKind !== "task" && agiflowJobKind !== "work-unit") throw new Error("Agiflow workflow launches require AGIFLOW_JOB_KIND to be task or work-unit.");
593
- if (agiflowJobKind && !explicitProjectId) throw new Error("Agiflow workflow launches require AGIFLOW_PROJECT_ID with the job identity.");
594
- if (agiflowJobKind && !input.prompt?.trim()) throw new Error("Agiflow workflow launches require a non-empty prompt so user_prompt runs do not wait for terminal input.");
595
- reportProgress(onUpdate, LAUNCH_WORKFLOW_TOOL_NAME, "Checking workflow capacity...");
596
- const maxConcurrent = resolveMaxConcurrent(dependencies.environment);
597
- const active = await dependencies.activeRunCount?.() ?? 0;
598
- if (active >= maxConcurrent) throw new Error(withOptions(`This session is at capacity: ${active}/${maxConcurrent} workflows running.`, [
599
- "Wait for a running workflow to finish, then launch again. The extension reports each one as it ends.",
600
- "workflow_run with action status: show what is running, so the user can judge whether something is stuck.",
601
- "workflow_run with action stop: free a slot by stopping a run the user no longer wants."
602
- ]));
603
- if (input.runner) {
604
- const rejection = dependencies.rejectRunner?.(input.workflowPath, input.runner);
605
- if (rejection) throw new Error(rejection);
606
- }
607
- reportProgress(onUpdate, LAUNCH_WORKFLOW_TOOL_NAME, `Launching workflow ${input.workflowPath}...`);
608
- const workflowEnv = { ...input.env };
609
- const dispatcherContextFile = dependencies.environment.AGIFLOW_DISPATCH_CONTEXT_FILE;
610
- const dispatcherProjectId = dependencies.environment[AGIFLOW_PROJECT_ID_ENV]?.trim();
611
- if (dispatcherContextFile && agiflowJobKind && dispatcherProjectId && explicitProjectId && dispatcherProjectId !== explicitProjectId) throw new Error(`Agiflow workflow project identity conflicts with the dispatcher context (${explicitProjectId} versus ${dispatcherProjectId}).`);
612
- if (dispatcherContextFile) for (const key of [
613
- "AGIFLOW_ORGANIZATION_ID",
614
- "AGIFLOW_PROJECT_ID",
615
- "AGIFLOW_DEVICE_ID",
616
- "BACKEND_AGIFLOW_API_ENDPOINT",
617
- "AGIFLOW_DISPATCH_SECRET_FILE"
618
- ]) {
619
- const hostValue = dependencies.environment[key];
620
- if (hostValue) workflowEnv[key] = hostValue;
621
- }
622
- const since = Date.now();
623
- const settled = dependencies.trackPendingRun(dependencies.runTool.execute({
624
- ...input,
625
- env: {
626
- ...workflowEnv,
627
- [PI_SESSION_ENV]: sessionId
628
- }
629
- })).then((value) => ({
630
- kind: "value",
631
- result: value
632
- }), (error) => ({
633
- kind: "error",
634
- error
635
- }));
636
- const ack = await awaitLaunchAck(settled, dependencies, {
637
- sessionId,
638
- since,
639
- workflowPath: input.workflowPath
640
- });
641
- if (ack.kind === "settled") {
642
- const outcome = ack.outcome;
643
- if (outcome.kind === "error") throw asError(outcome.error);
644
- const result = outcome.result;
645
- if (result.isError) throw new Error(withOptions(toolResultText(result) || "Workflow launch failed.", launchFailureOptions));
646
- await dependencies.onLaunch?.(ctx);
647
- return result;
648
- }
649
- reportLateFailure(settled, dependencies, ctx);
650
- await dependencies.onLaunch?.(ctx);
651
- return { content: [{
652
- type: "text",
653
- text: ack.kind === "record" ? launchedRunSummary(ack.record) : launchHandoffSummary(input.workflowPath)
654
- }] };
655
- } };
656
- }
657
- //#endregion
658
92
  //#region src/services/workflowTools/index.ts
659
93
  const PI_LAUNCH_FIELDS = {
660
94
  workflowPath: true,
@@ -719,11 +153,12 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
719
153
  async execute(_toolCallId, params, _signal, onUpdate, ctx) {
720
154
  const input = params;
721
155
  const result = await launchExecutor.execute(input, ctx, onUpdate);
722
- return appendGuidance(toAgentToolResult(LAUNCH_WORKFLOW_TOOL_NAME, result, launchFailureOptions), "Next: call workflow_run with action status and verify the recorded stage before reporting the workflow outcome.");
156
+ const adapted = toAgentToolResult(LAUNCH_WORKFLOW_TOOL_NAME, result, launchFailureOptions);
157
+ return appendGuidance(adapted, "Next: call workflow_run with action status and verify the recorded stage before reporting the workflow outcome.");
723
158
  }
724
159
  },
725
160
  {
726
- name: WORKFLOW_RUN_TOOL_NAME$1,
161
+ name: WORKFLOW_RUN_TOOL_NAME,
727
162
  label: "Workflow Run",
728
163
  description: "Inspect or control one workflow run. Use action status, tail, recovery-evidence, follow, open, pause, resume, stop, or recover. Every action requires an exact run key.",
729
164
  promptSnippet: "Inspect or control a workflow run",
@@ -738,7 +173,7 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
738
173
  ],
739
174
  parameters: z.toJSONSchema(workflowRunInputSchema),
740
175
  renderShell: "self",
741
- ...renderers(WORKFLOW_RUN_TOOL_NAME$1),
176
+ ...renderers(WORKFLOW_RUN_TOOL_NAME),
742
177
  async execute(_toolCallId, rawParams, _signal, onUpdate, ctx) {
743
178
  const input = workflowRunInputSchema.parse(rawParams);
744
179
  const sessionId = resolveRootSessionId(ctx.sessionManager.getSessionId(), dependencies.environment);
@@ -758,13 +193,13 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
758
193
  type: "text",
759
194
  text: JSON.stringify(record, null, 2)
760
195
  }],
761
- details: { tool: WORKFLOW_RUN_TOOL_NAME$1 }
196
+ details: { tool: WORKFLOW_RUN_TOOL_NAME }
762
197
  };
763
198
  }
764
199
  if (input.action === "tail") {
765
200
  if (!dependencies.tailRun) throw new Error("Reading workflow output is not available in this session.");
766
201
  const record = await requireOwnedRun();
767
- reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME$1, `Reading output for workflow ${input.runKey}...`);
202
+ reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Reading output for workflow ${input.runKey}...`);
768
203
  await dependencies.tailRun(selector, ctx);
769
204
  return {
770
205
  content: [{
@@ -775,42 +210,42 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
775
210
  "Raw launcher output is hidden from chat. Use action follow or open to view it."
776
211
  ].join("\n")
777
212
  }],
778
- details: { tool: WORKFLOW_RUN_TOOL_NAME$1 }
213
+ details: { tool: WORKFLOW_RUN_TOOL_NAME }
779
214
  };
780
215
  }
781
216
  if (input.action === "recovery-evidence") {
782
217
  if (!dependencies.readRecoveryEvidence) throw new Error("Reading workflow recovery evidence is not available in this session.");
783
- reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME$1, `Reading recovery evidence for workflow ${input.runKey}...`);
218
+ reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Reading recovery evidence for workflow ${input.runKey}...`);
784
219
  return {
785
220
  content: [{
786
221
  type: "text",
787
222
  text: await dependencies.readRecoveryEvidence(selector, ctx)
788
223
  }],
789
- details: { tool: WORKFLOW_RUN_TOOL_NAME$1 }
224
+ details: { tool: WORKFLOW_RUN_TOOL_NAME }
790
225
  };
791
226
  }
792
227
  if (input.action === "follow") {
793
228
  if (!dependencies.followRun) throw new Error("Following workflow output is not available in this session.");
794
229
  await requireOwnedRun();
795
- reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME$1, `Following workflow ${input.runKey}...`);
230
+ reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Following workflow ${input.runKey}...`);
796
231
  return {
797
232
  content: [{
798
233
  type: "text",
799
234
  text: await dependencies.followRun(selector, ctx)
800
235
  }],
801
- details: { tool: WORKFLOW_RUN_TOOL_NAME$1 }
236
+ details: { tool: WORKFLOW_RUN_TOOL_NAME }
802
237
  };
803
238
  }
804
239
  if (input.action === "open") {
805
240
  if (!dependencies.openRun) throw new Error("Opening a workflow launcher is not available in this session.");
806
241
  await requireOwnedRun();
807
- reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME$1, `Opening workflow ${input.runKey}...`);
242
+ reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Opening workflow ${input.runKey}...`);
808
243
  return {
809
244
  content: [{
810
245
  type: "text",
811
246
  text: await dependencies.openRun(selector, ctx)
812
247
  }],
813
- details: { tool: WORKFLOW_RUN_TOOL_NAME$1 }
248
+ details: { tool: WORKFLOW_RUN_TOOL_NAME }
814
249
  };
815
250
  }
816
251
  if (input.action === "recover") {
@@ -820,7 +255,7 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
820
255
  runKey: recoverable.runKey,
821
256
  workspace: recoverable.workspace
822
257
  };
823
- reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME$1, `Recovering workflow ${input.runKey}...`);
258
+ reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Recovering workflow ${input.runKey}...`);
824
259
  if (!input.dryRun) {
825
260
  const delegated = await dependencies.delegateRecovery?.({
826
261
  ...recoverySelector,
@@ -834,7 +269,7 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
834
269
  type: "text",
835
270
  text: [delegated, guidance[0]].join("\n\n")
836
271
  }],
837
- details: { tool: WORKFLOW_RUN_TOOL_NAME$1 }
272
+ details: { tool: WORKFLOW_RUN_TOOL_NAME }
838
273
  };
839
274
  }
840
275
  }
@@ -847,13 +282,13 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
847
282
  };
848
283
  const result = await trackPendingRun(recoverTool.execute(recoverInput));
849
284
  const guidance = actionGuidance(input.action, input.runKey);
850
- const adapted = toAgentToolResult(WORKFLOW_RUN_TOOL_NAME$1, result, guidance);
285
+ const adapted = toAgentToolResult(WORKFLOW_RUN_TOOL_NAME, result, guidance);
851
286
  await dependencies.onLaunch?.(ctx);
852
287
  return appendGuidance(adapted, guidance[0]);
853
288
  }
854
289
  const ownedRun = await requireOwnedRun();
855
290
  if (!ownedRun.runId) throw new Error(`Workflow ${input.runKey} has no controllable run generation.`);
856
- reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME$1, `Requesting ${input.action} for workflow ${input.runKey}...`);
291
+ reportProgress(onUpdate, WORKFLOW_RUN_TOOL_NAME, `Requesting ${input.action} for workflow ${input.runKey}...`);
857
292
  const result = await controlTool.execute({
858
293
  action: input.action,
859
294
  expectedRunId: ownedRun.runId,
@@ -862,7 +297,8 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
862
297
  workspace: input.workspace
863
298
  });
864
299
  const guidance = actionGuidance(input.action, input.runKey);
865
- return appendGuidance(toAgentToolResult(WORKFLOW_RUN_TOOL_NAME$1, result, guidance), guidance[0]);
300
+ const adapted = toAgentToolResult(WORKFLOW_RUN_TOOL_NAME, result, guidance);
301
+ return appendGuidance(adapted, guidance[0]);
866
302
  }
867
303
  }
868
304
  ];
@@ -1569,11 +1005,15 @@ function summarizeWorkflowFile(workflowPath, options) {
1569
1005
  try {
1570
1006
  const workflow = options.parseWorkflow(workflowPath);
1571
1007
  const runners = options.compatibleRunners(workflow);
1008
+ const commands = compatibleCommands(workflow);
1009
+ const choices = Object.keys(workflow.choices ?? {});
1572
1010
  return {
1573
1011
  triggers: Object.keys(workflowTriggerBlock(workflow)),
1574
1012
  inputs: catalogInputs(workflow),
1575
1013
  jobs: catalogJobs(workflow),
1576
- ...runners ? { runners } : {}
1014
+ ...runners ? { runners } : {},
1015
+ ...commands ? { commands } : {},
1016
+ ...choices.length > 0 ? { choices } : {}
1577
1017
  };
1578
1018
  } catch (cause) {
1579
1019
  return {
@@ -1593,11 +1033,25 @@ async function launchWorkflowEntry(options, selected, ctx) {
1593
1033
  runner = await options.ui.select("Select a workflow runner", runners);
1594
1034
  if (runner === void 0) return void 0;
1595
1035
  }
1036
+ const commands = compatibleCommands(workflow) ?? [];
1037
+ let command;
1038
+ if (commands.length > 1) {
1039
+ command = await options.ui.select("Select the command agent steps run through", commands);
1040
+ if (command === void 0) return void 0;
1041
+ }
1042
+ const choices = Object.keys(workflow.choices ?? {});
1043
+ let choice;
1044
+ if (choices.length > 1) {
1045
+ choice = await options.ui.select("Select a model choice", choices);
1046
+ if (choice === void 0) return void 0;
1047
+ }
1596
1048
  const triggerInputs = await collectWorkflowInputs(workflow, options.ui);
1597
1049
  if (triggerInputs === void 0) return void 0;
1598
1050
  const launchInput = {
1599
1051
  workflowPath: selected.path,
1600
1052
  ...runner === void 0 ? {} : { runner },
1053
+ ...command === void 0 ? {} : { command },
1054
+ ...choice === void 0 ? {} : { choice },
1601
1055
  ...typeof workflow.workspace === "string" ? { workspace: workflow.workspace } : {},
1602
1056
  ...triggerInputs
1603
1057
  };
@@ -3879,10 +3333,12 @@ function createWorkflowPiRuntime(pi, options) {
3879
3333
  trackPendingRun,
3880
3334
  ...options.launchAckPollMs === void 0 ? {} : { launchAckPollMs: options.launchAckPollMs },
3881
3335
  ...options.launchAckTimeoutMs === void 0 ? {} : { launchAckTimeoutMs: options.launchAckTimeoutMs },
3882
- findLaunchedRun: async ({ sessionId, since, workflowPath }) => {
3336
+ findLaunchedRun: async ({ launchId, sessionId, since, workflowPath }) => {
3883
3337
  const expectedId = workflowIdFor(workflowPath);
3884
3338
  const matches = (record) => {
3885
3339
  if (!isSessionRun(record, sessionId) || record.stale) return false;
3340
+ const carried = record.env?.[WORKFLOW_LAUNCH_ID_ENV];
3341
+ if (carried !== void 0) return carried === launchId;
3886
3342
  if (record.workflowId && record.workflowId !== expectedId) return false;
3887
3343
  const startedAt = Date.parse(record.startedAt);
3888
3344
  return Number.isFinite(startedAt) && startedAt >= since - LAUNCH_ACK_CLOCK_SKEW_MS;
@@ -4721,6 +4177,8 @@ function createWorkflowPiRuntime(pi, options) {
4721
4177
  const input = {
4722
4178
  workflowPath: entry.path,
4723
4179
  ...parsed.runner === void 0 ? {} : { runner: parsed.runner },
4180
+ ...parsed.command === void 0 ? {} : { command: parsed.command },
4181
+ ...parsed.choice === void 0 ? {} : { choice: parsed.choice },
4724
4182
  ...typeof workflow.workspace === "string" ? { workspace: workflow.workspace } : {},
4725
4183
  ...parsed.prompt === void 0 ? {} : { prompt: parsed.prompt },
4726
4184
  ...Object.keys(parsed.inputs).length === 0 ? {} : { inputs: parsed.inputs }
@@ -4729,7 +4187,7 @@ function createWorkflowPiRuntime(pi, options) {
4729
4187
  ctx.ui.notify(launchNotice(toolResultText(result)), "info");
4730
4188
  };
4731
4189
  const commands = [[WORKFLOW_LAUNCH_COMMAND, {
4732
- description: "Launch a workflow: /workflow-launch <workflow> [runner=x] [key=value …] [prompt]",
4190
+ description: "Launch a workflow: /workflow-launch <workflow> [runner=x] [command=x] [choice=x] [key=value …] [prompt]",
4733
4191
  handler: async (args, ctx) => {
4734
4192
  await waitForReadiness();
4735
4193
  try {