@moxxy/cli 0.7.3 → 0.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moxxy (moxxy.ai)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/bin.js CHANGED
@@ -88109,6 +88109,8 @@ var init_WorkflowsPanel = __esm({
88109
88109
  const [loading, setLoading] = import_react55.default.useState(true);
88110
88110
  const [busy, setBusy] = import_react55.default.useState(false);
88111
88111
  const [status, setStatus] = import_react55.default.useState(null);
88112
+ const [pending, setPending] = import_react55.default.useState(null);
88113
+ const [reply2, setReply] = import_react55.default.useState("");
88112
88114
  const reload = import_react55.default.useCallback(async () => {
88113
88115
  if (!view) {
88114
88116
  setLoading(false);
@@ -88126,7 +88128,7 @@ var init_WorkflowsPanel = __esm({
88126
88128
  import_react55.default.useEffect(() => {
88127
88129
  void reload();
88128
88130
  }, [reload]);
88129
- const active = !busy && !!view && rows.length > 0;
88131
+ const active = !busy && !pending && !!view && rows.length > 0;
88130
88132
  const run2 = import_react55.default.useCallback(async (wf) => {
88131
88133
  if (!view || busy)
88132
88134
  return;
@@ -88138,6 +88140,17 @@ var init_WorkflowsPanel = __esm({
88138
88140
  setStatus(`running "${wf.name}"\u2026`);
88139
88141
  try {
88140
88142
  const result = await view.run(wf.name);
88143
+ if (result.status === "paused" && result.runId) {
88144
+ const askStep = result.steps.find((s2) => s2.status === "awaiting_input");
88145
+ setPending({
88146
+ runId: result.runId,
88147
+ label: askStep?.id ?? wf.name,
88148
+ prompt: result.output || "The workflow is waiting for your input."
88149
+ });
88150
+ setReply("");
88151
+ setStatus(`\u23F8 "${wf.name}" is waiting for your reply \u2014 type it and press Enter (Esc cancels).`);
88152
+ return;
88153
+ }
88141
88154
  const marks = result.steps.map((s2) => `${stepMark(s2.status)}${s2.id}`).join(" ");
88142
88155
  setStatus(result.ok ? `\u2713 ${wf.name} completed \u2014 ${marks}` : `\u2717 ${wf.name} failed: ${result.error ?? ""} \u2014 ${marks}`);
88143
88156
  } catch (err) {
@@ -88147,6 +88160,35 @@ var init_WorkflowsPanel = __esm({
88147
88160
  void reload();
88148
88161
  }
88149
88162
  }, [view, busy, reload]);
88163
+ const submitReply = import_react55.default.useCallback(async (pendingRun, text) => {
88164
+ if (!view?.resume) {
88165
+ setStatus("resume not supported by this session");
88166
+ return;
88167
+ }
88168
+ const answer = text.trim();
88169
+ if (!answer)
88170
+ return;
88171
+ setPending(null);
88172
+ setReply("");
88173
+ setBusy(true);
88174
+ setStatus(`resuming "${pendingRun.label}"\u2026`);
88175
+ try {
88176
+ const result = await view.resume(pendingRun.runId, answer);
88177
+ if (result.status === "paused" && result.runId) {
88178
+ const askStep = result.steps.find((s2) => s2.status === "awaiting_input");
88179
+ setPending({ runId: result.runId, label: askStep?.id ?? pendingRun.label, prompt: result.output });
88180
+ setStatus("\u23F8 waiting again \u2014 type your reply and press Enter (Esc cancels).");
88181
+ } else {
88182
+ const marks = result.steps.map((s2) => `${stepMark(s2.status)}${s2.id}`).join(" ");
88183
+ setStatus(result.ok ? `\u2713 resumed & completed \u2014 ${marks}` : `\u2717 resume failed: ${result.error ?? ""} \u2014 ${marks}`);
88184
+ }
88185
+ } catch (err) {
88186
+ setStatus(`\u2717 resume errored: ${err instanceof Error ? err.message : String(err)}`);
88187
+ } finally {
88188
+ setBusy(false);
88189
+ void reload();
88190
+ }
88191
+ }, [view, reload]);
88150
88192
  const scroll = useScrollableList({
88151
88193
  total: rows.length,
88152
88194
  windowSize: WINDOW4,
@@ -88172,6 +88214,26 @@ var init_WorkflowsPanel = __esm({
88172
88214
  void run2(wf);
88173
88215
  }
88174
88216
  }, { isActive: active });
88217
+ use_input_default((input, key) => {
88218
+ if (!pending)
88219
+ return;
88220
+ if (key.escape) {
88221
+ setPending(null);
88222
+ setReply("");
88223
+ setStatus("reply cancelled \u2014 the run stays paused (re-run to resume).");
88224
+ return;
88225
+ }
88226
+ if (key.return) {
88227
+ void submitReply(pending, reply2);
88228
+ return;
88229
+ }
88230
+ if (key.backspace || key.delete) {
88231
+ setReply((r2) => r2.slice(0, -1));
88232
+ return;
88233
+ }
88234
+ if (input && !key.ctrl && !key.meta)
88235
+ setReply((r2) => r2 + input);
88236
+ }, { isActive: !busy && !!pending });
88175
88237
  const termWidth = process.stdout.columns ?? 80;
88176
88238
  const descWidth = Math.max(16, termWidth - NAME_COL3 - SCOPE_COL2 - TRIG_COL - 12);
88177
88239
  const slice = rows.slice(scroll.visible.start, scroll.visible.end);
@@ -88181,7 +88243,7 @@ var init_WorkflowsPanel = __esm({
88181
88243
  const absoluteIndex = scroll.visible.start + i2;
88182
88244
  const focused = absoluteIndex === scroll.cursor;
88183
88245
  return (0, import_jsx_runtime27.jsxs)(Box_default, { children: [(0, import_jsx_runtime27.jsx)(Text, { ...focused ? {} : { dimColor: true }, children: focused ? "\u203A " : " " }), (0, import_jsx_runtime27.jsx)(Text, { color: wf.enabled ? Colors.active : void 0, dimColor: !wf.enabled, children: wf.enabled ? "\u25CF " : "\u25CB " }), (0, import_jsx_runtime27.jsx)(Box_default, { width: NAME_COL3, children: (0, import_jsx_runtime27.jsx)(Text, { bold: focused, children: truncate8(wf.name, NAME_COL3 - 1) }) }), (0, import_jsx_runtime27.jsx)(Box_default, { width: SCOPE_COL2, children: (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: wf.scope }) }), (0, import_jsx_runtime27.jsx)(Box_default, { width: TRIG_COL, children: (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, wrap: "truncate", children: wf.triggers }) }), (0, import_jsx_runtime27.jsx)(Box_default, { width: descWidth, children: (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, wrap: "truncate", children: oneLine5(wf.description) }) })] }, wf.name);
88184
- }), scroll.canScrollDown ? (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: ` \u2193 ${rows.length - scroll.visible.end} more below` }) : null, status ? (0, import_jsx_runtime27.jsx)(Box_default, { marginTop: 1, children: (0, import_jsx_runtime27.jsx)(Text, { wrap: "truncate-end", children: status }) }) : null] });
88246
+ }), scroll.canScrollDown ? (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: ` \u2193 ${rows.length - scroll.visible.end} more below` }) : null, pending ? (0, import_jsx_runtime27.jsxs)(Box_default, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: Colors.active, paddingX: 1, children: [(0, import_jsx_runtime27.jsxs)(Text, { color: Colors.active, children: ["\u23F8 Workflow waiting \xB7 ", pending.label] }), pending.prompt ? (0, import_jsx_runtime27.jsx)(Text, { wrap: "wrap", children: oneLine5(pending.prompt).slice(0, 280) }) : null, (0, import_jsx_runtime27.jsxs)(Box_default, { marginTop: 1, children: [(0, import_jsx_runtime27.jsx)(Text, { children: "reply \u203A " }), (0, import_jsx_runtime27.jsx)(Text, { children: reply2 || " " })] }), (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: "Enter send \xB7 Esc cancel" })] }) : null, status ? (0, import_jsx_runtime27.jsx)(Box_default, { marginTop: 1, children: (0, import_jsx_runtime27.jsx)(Text, { wrap: "truncate-end", children: status }) }) : null] });
88185
88247
  };
88186
88248
  }
88187
88249
  });
@@ -134642,7 +134704,8 @@ function isRunnerUp(socketPath = runnerSocketPath()) {
134642
134704
 
134643
134705
  // ../runner/dist/server.js
134644
134706
  init_dist();
134645
- var RUNNER_PROTOCOL_VERSION = 4;
134707
+ var RUNNER_PROTOCOL_VERSION = 5;
134708
+ var MIN_COMPATIBLE_PROTOCOL_VERSION = 1;
134646
134709
  var RunnerMethod = {
134647
134710
  /** client->server: handshake; returns the initial info snapshot. */
134648
134711
  Attach: "attach",
@@ -134692,6 +134755,12 @@ var RunnerMethod = {
134692
134755
  WorkflowSave: "workflow.save",
134693
134756
  /** client->server: fetch one saved workflow as canonical YAML (builder). */
134694
134757
  WorkflowGetRun: "workflow.getRun",
134758
+ /**
134759
+ * client->server: answer a paused workflow's `awaitInput` question and resume
134760
+ * the run (human-in-the-loop). v5 — the client gates this on the server's
134761
+ * reported version so an older runner returns an actionable error.
134762
+ */
134763
+ WorkflowResume: "workflow.resume",
134695
134764
  /** server->client: ask this client to decide a tool-call permission. */
134696
134765
  PermissionCheck: "permission.check",
134697
134766
  /** server->client: ask this client to confirm an approval checkpoint. */
@@ -134776,6 +134845,10 @@ var workflowSaveParamsSchema = z.object({
134776
134845
  previousName: z.string().min(1).max(120).optional()
134777
134846
  });
134778
134847
  var workflowGetRunParamsSchema = z.object({ name: z.string().min(1).max(120) });
134848
+ var workflowResumeParamsSchema = z.object({
134849
+ runId: z.string().min(1).max(120),
134850
+ reply: z.string().min(1).max(1e5)
134851
+ });
134779
134852
 
134780
134853
  // ../runner/dist/server.js
134781
134854
  var RunnerServer = class {
@@ -134855,6 +134928,7 @@ var RunnerServer = class {
134855
134928
  peer.handle(RunnerMethod.WorkflowValidateDraft, (raw) => this.handleWorkflowValidateDraft(raw));
134856
134929
  peer.handle(RunnerMethod.WorkflowSave, (raw) => this.handleWorkflowSave(raw));
134857
134930
  peer.handle(RunnerMethod.WorkflowGetRun, (raw) => this.handleWorkflowGetRun(raw));
134931
+ peer.handle(RunnerMethod.WorkflowResume, (raw) => this.handleWorkflowResume(raw));
134858
134932
  peer.onClose(() => this.onDisconnect(client));
134859
134933
  }
134860
134934
  onDisconnect(client) {
@@ -134866,7 +134940,7 @@ var RunnerServer = class {
134866
134940
  // --- request handlers ----------------------------------------------------
134867
134941
  handleAttach(client, raw) {
134868
134942
  const params = attachParamsSchema.parse(raw);
134869
- if (params.protocolVersion !== RUNNER_PROTOCOL_VERSION) {
134943
+ if (params.protocolVersion < MIN_COMPATIBLE_PROTOCOL_VERSION) {
134870
134944
  throw new Error(`runner protocol mismatch: server v${RUNNER_PROTOCOL_VERSION}, client v${params.protocolVersion}`);
134871
134945
  }
134872
134946
  client.role = params.role;
@@ -135109,6 +135183,16 @@ var RunnerServer = class {
135109
135183
  throw new Error("workflows builder not supported on this runner");
135110
135184
  return await view.getRun(params.name) ?? null;
135111
135185
  }
135186
+ // --- Workflows human-in-the-loop (resume a paused awaitInput run) ---------
135187
+ // v5. Optional on the view (older hosts lack it), so feature-check and throw a
135188
+ // clear error rather than calling undefined.
135189
+ async handleWorkflowResume(raw) {
135190
+ const params = workflowResumeParamsSchema.parse(raw);
135191
+ const view = this.session.workflows;
135192
+ if (!view?.resume)
135193
+ throw new Error("workflow resume not supported on this runner");
135194
+ return view.resume(params.runId, params.reply);
135195
+ }
135112
135196
  broadcastInfo() {
135113
135197
  this.broadcast(RunnerNotification.InfoChanged, { info: this.session.getInfo() });
135114
135198
  }
@@ -135251,6 +135335,14 @@ var RemoteSession = class {
135251
135335
  permissionResolver = null;
135252
135336
  approvalResolver = null;
135253
135337
  info = null;
135338
+ /**
135339
+ * The protocol version the SERVER reported at attach. Defaults to our own
135340
+ * version until the handshake resolves. Version-specific client methods (the
135341
+ * v4 workflow *builder* family) gate on this so a newer client attached to an
135342
+ * older runner degrades with a clear, actionable error instead of a raw
135343
+ * JSON-RPC method-not-found. Null until attached.
135344
+ */
135345
+ serverProtocolVersion = null;
135254
135346
  constructor(transport) {
135255
135347
  this.peer = new JsonRpcPeer(transport);
135256
135348
  this.peer.on(RunnerNotification.Event, (params) => {
@@ -135319,6 +135411,28 @@ var RemoteSession = class {
135319
135411
  sinceSeq
135320
135412
  });
135321
135413
  this.info = result.info;
135414
+ this.serverProtocolVersion = typeof result.protocolVersion === "number" ? result.protocolVersion : RUNNER_PROTOCOL_VERSION;
135415
+ }
135416
+ /**
135417
+ * The protocol version the attached runner speaks (its own, from the
135418
+ * handshake). Lets a capability-detecting caller (e.g. the desktop's visual
135419
+ * builder, see #146) decide whether a version-gated feature is available on
135420
+ * THIS runner before invoking it. Null until attached.
135421
+ */
135422
+ get runnerProtocolVersion() {
135423
+ return this.serverProtocolVersion;
135424
+ }
135425
+ /**
135426
+ * Guard a method that only exists on a server at/after `minVersion`. Throws a
135427
+ * clear, actionable error (not a raw JSON-RPC method-not-found) when the
135428
+ * attached runner is older — the desktop case after a JS hot-update outran
135429
+ * its bundled CLI.
135430
+ */
135431
+ requireServerProtocol(minVersion, feature) {
135432
+ const server = this.serverProtocolVersion;
135433
+ if (server !== null && server < minVersion) {
135434
+ throw new Error(`${feature} is not supported by this runner (runner protocol v${server}, needs v${minVersion}) \u2014 update the moxxy CLI to continue.`);
135435
+ }
135322
135436
  }
135323
135437
  get id() {
135324
135438
  return this.requireInfo().sessionId;
@@ -135554,12 +135668,37 @@ var RemoteSession = class {
135554
135668
  run: (name) => this.peer.request(RunnerMethod.WorkflowRun, { name }),
135555
135669
  // Builder methods (protocol v4): forward to the runner so the desktop's
135556
135670
  // RemoteSession-backed visual builder can validate/save/load drafts.
135557
- validateDraft: (yaml) => this.peer.request(RunnerMethod.WorkflowValidateDraft, { yaml }),
135558
- save: (yaml, previousName) => this.peer.request(RunnerMethod.WorkflowSave, {
135559
- yaml,
135560
- ...previousName ? { previousName } : {}
135561
- }),
135562
- getRun: (name) => this.peer.request(RunnerMethod.WorkflowGetRun, { name })
135671
+ // Gated on the SERVER's reported version so a v4 client on a v3 runner
135672
+ // (a desktop whose JS hot-update outran its bundled CLI) gets a clear
135673
+ // "update the CLI" error instead of a raw method-not-found.
135674
+ validateDraft: async (yaml) => {
135675
+ this.requireServerProtocol(4, "The workflows builder");
135676
+ return this.peer.request(RunnerMethod.WorkflowValidateDraft, {
135677
+ yaml
135678
+ });
135679
+ },
135680
+ save: async (yaml, previousName) => {
135681
+ this.requireServerProtocol(4, "Saving a workflow from the builder");
135682
+ return this.peer.request(RunnerMethod.WorkflowSave, {
135683
+ yaml,
135684
+ ...previousName ? { previousName } : {}
135685
+ });
135686
+ },
135687
+ getRun: async (name) => {
135688
+ this.requireServerProtocol(4, "Loading a workflow into the builder");
135689
+ return this.peer.request(RunnerMethod.WorkflowGetRun, { name });
135690
+ },
135691
+ // Human-in-the-loop resume (protocol v5). Gated on the SERVER's reported
135692
+ // version so a v5 client attached to a v4 runner (a desktop whose JS
135693
+ // hot-update outran its bundled CLI) gets a clear "update the CLI" error
135694
+ // rather than a raw method-not-found.
135695
+ resume: async (runId, reply2) => {
135696
+ this.requireServerProtocol(5, "Resuming a paused workflow");
135697
+ return this.peer.request(RunnerMethod.WorkflowResume, {
135698
+ runId,
135699
+ reply: reply2
135700
+ });
135701
+ }
135563
135702
  };
135564
135703
  }
135565
135704
  };
@@ -135626,11 +135765,14 @@ async function connectRemoteSession(opts = {}) {
135626
135765
  throw err;
135627
135766
  }
135628
135767
  }
135768
+ function isProtocolMismatchError(err) {
135769
+ const msg = err instanceof Error ? err.message : String(err);
135770
+ return /protocol mismatch/i.test(msg);
135771
+ }
135629
135772
  async function maybeRecoverFromMismatch(err, socketPath, opts) {
135630
135773
  if (opts.transport || opts.skipMismatchRecovery)
135631
135774
  return;
135632
- const msg = err instanceof Error ? err.message : String(err);
135633
- if (!/protocol mismatch/i.test(msg))
135775
+ if (!isProtocolMismatchError(err))
135634
135776
  return;
135635
135777
  try {
135636
135778
  await killAndUnlinkRunner(socketPath, [...DEFAULT_RUNNER_PORTS, ...opts.extraPortsToFree ?? []]);
@@ -135786,7 +135928,13 @@ var REMOTE_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
135786
135928
  // phone must not rewrite or re-enable the host's workflows.
135787
135929
  "workflows.list",
135788
135930
  "workflows.run",
135789
- "workflows.getRun"
135931
+ "workflows.getRun",
135932
+ // Answer a paused workflow's awaitInput question. This is RESPOND-only — like
135933
+ // `ask.respond`, the operator answers a question the WORKFLOW asked (the reply
135934
+ // is fed into the paused step and the run continues); it cannot create or
135935
+ // rewrite a workflow. A mobile user answering "ship it" to their own pipeline
135936
+ // is the canonical human-in-the-loop case, so it belongs on the trust surface.
135937
+ "workflows.resume"
135790
135938
  ]);
135791
135939
  var providerName = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/, "invalid provider name");
135792
135940
  var httpUrl = z.string().refine((s2) => {
@@ -135877,6 +136025,12 @@ var ipcInputSchemas = {
135877
136025
  previousName: workflowName.optional()
135878
136026
  }),
135879
136027
  "workflows.getRun": z.object({ name: workflowName }),
136028
+ // Human-in-the-loop resume: bound the run id + the operator reply (the reply
136029
+ // is forwarded into the paused step's child agent, so cap it to avoid OOM).
136030
+ "workflows.resume": z.object({
136031
+ runId: z.string().min(1).max(120),
136032
+ reply: z.string().min(1).max(1e5)
136033
+ }),
135880
136034
  // Security-sensitive: this bypasses the approval sheet, so validate it at
135881
136035
  // the boundary like the other dangerous commands.
135882
136036
  "session.setAutoApprove": z.object({ workspaceId: optionalWorkspace, enabled: z.boolean() }),
@@ -136162,6 +136316,12 @@ var MobileSessionHost = class {
136162
136316
  }
136163
136317
  return await this.session.workflows.getRun(name);
136164
136318
  });
136319
+ this.bus.handle("workflows.resume", async ({ runId, reply: reply2 }) => {
136320
+ if (!this.session.workflows?.resume) {
136321
+ throw new IpcError("not-supported", "workflow resume not supported on this session");
136322
+ }
136323
+ return await this.session.workflows.resume(runId, reply2);
136324
+ });
136165
136325
  this.bus.handle("ask.respond", async ({ requestId, response }) => {
136166
136326
  this.answerAsk(requestId, response);
136167
136327
  });
@@ -142121,10 +142281,11 @@ var stepSchema = z.object({
142121
142281
  format: z.enum(["json", "plain"]).optional(),
142122
142282
  awaitInput: z.boolean().optional()
142123
142283
  }).superRefine((step, ctx) => {
142124
- if (step.awaitInput) {
142284
+ const isLogic = step.bridge != null || step.condition != null || step.switch != null;
142285
+ if (step.awaitInput && (step.tool != null || step.workflow != null || step.loop != null || isLogic)) {
142125
142286
  ctx.addIssue({
142126
142287
  code: z.ZodIssueCode.custom,
142127
- message: `step "${step.id}": awaitInput requires the resume channel, which is not available in this build \u2014 a paused run would hang forever. Remove awaitInput (gather the input via an \`inputs\` field or a normal prompt step instead).`,
142288
+ message: `step "${step.id}": awaitInput is only allowed on prompt or skill steps`,
142128
142289
  path: ["awaitInput"]
142129
142290
  });
142130
142291
  }
@@ -142356,6 +142517,13 @@ var workflowSchema = z.object({
142356
142517
  path: ["steps"]
142357
142518
  });
142358
142519
  }
142520
+ if (body.awaitInput) {
142521
+ ctx.addIssue({
142522
+ code: z.ZodIssueCode.custom,
142523
+ message: `step "${step.id}" is a loop body of "${owner}" and cannot use awaitInput \u2014 a loop body cannot pause mid-iteration. Ask the operator before/after the loop instead.`,
142524
+ path: ["steps"]
142525
+ });
142526
+ }
142359
142527
  if (body.when != null) {
142360
142528
  ctx.addIssue({
142361
142529
  code: z.ZodIssueCode.custom,
@@ -142800,6 +142968,7 @@ function stepsToSkipForBranch(step, selected) {
142800
142968
  // ../plugin-workflows/dist/executor/dag.js
142801
142969
  var DAG_EXECUTOR_NAME = "dag";
142802
142970
  var MAX_NESTING_DEPTH = 5;
142971
+ var FINALIZE_REPLY_SUFFIX = "\n\nFinalize now: consolidate the operator's answers into a clear structured response. Include every field the step instructions require. Do not ask further questions.";
142803
142972
  function collectLoopBodyIds(workflow) {
142804
142973
  const ids = /* @__PURE__ */ new Set();
142805
142974
  for (const step of workflow.steps) {
@@ -142876,6 +143045,19 @@ function serializeStates(states) {
142876
143045
  }
142877
143046
  return out;
142878
143047
  }
143048
+ function restoreStates(raw) {
143049
+ const states = /* @__PURE__ */ new Map();
143050
+ for (const [id, st3] of Object.entries(raw)) {
143051
+ states.set(id, {
143052
+ status: st3.status,
143053
+ output: st3.output,
143054
+ ...st3.error ? { error: st3.error } : {},
143055
+ startedAt: st3.startedAt,
143056
+ endedAt: st3.endedAt
143057
+ });
143058
+ }
143059
+ return states;
143060
+ }
142879
143061
  function buildStepResults(workflow, states) {
142880
143062
  return workflow.steps.map((step) => {
142881
143063
  const st3 = states.get(step.id);
@@ -143005,7 +143187,13 @@ async function runExecutorLoop(ctx) {
143005
143187
  await deps.emit?.("workflow_paused", {
143006
143188
  runId,
143007
143189
  stepId: step.id,
143008
- childSessionId: outcome.interactionAgentId
143190
+ childSessionId: outcome.interactionAgentId,
143191
+ // Carry the human-facing question so the operator UI is self-contained
143192
+ // (no separate event correlation needed): the workflow name, the step
143193
+ // label, and the prompt/question the paused step asked.
143194
+ workflow: workflow.name,
143195
+ label: step.label ?? step.id,
143196
+ prompt: outcome.output.slice(0, 2e3)
143009
143197
  });
143010
143198
  return buildRunResult(ctx, "paused", true, {
143011
143199
  runId,
@@ -143062,6 +143250,78 @@ async function runExecutor(workflow, deps) {
143062
143250
  }
143063
143251
  return runExecutorLoop(ctx);
143064
143252
  }
143253
+ async function resumeWorkflowRun(runId, userMessage, deps, store = defaultWorkflowRunStore) {
143254
+ const checkpoint = await store.load(runId);
143255
+ if (!checkpoint) {
143256
+ return {
143257
+ ok: false,
143258
+ status: "failed",
143259
+ steps: [],
143260
+ output: "",
143261
+ error: `no paused workflow run "${runId}"`
143262
+ };
143263
+ }
143264
+ const depsWithStore = { ...deps, runStore: store };
143265
+ const step = checkpoint.workflow.steps.find((s2) => s2.id === checkpoint.pendingStepId);
143266
+ if (!step) {
143267
+ return {
143268
+ ok: false,
143269
+ status: "failed",
143270
+ steps: buildStepResults(checkpoint.workflow, restoreStates(checkpoint.states)),
143271
+ output: "",
143272
+ error: `paused step "${checkpoint.pendingStepId}" not found`
143273
+ };
143274
+ }
143275
+ const restoredVars = {};
143276
+ const ctx = {
143277
+ workflow: checkpoint.workflow,
143278
+ deps: depsWithStore,
143279
+ inputs: checkpoint.inputs,
143280
+ vars: restoredVars,
143281
+ states: restoreStates(checkpoint.states),
143282
+ now: nowFn(deps),
143283
+ loopBodyIds: collectLoopBodyIds(checkpoint.workflow)
143284
+ };
143285
+ mergeVars(ctx, checkpoint.vars);
143286
+ const st3 = ctx.states.get(step.id);
143287
+ await deps.emit?.("workflow_resumed", { runId, stepId: step.id });
143288
+ if (typeof deps.spawner.continue !== "function") {
143289
+ st3.status = "failed";
143290
+ st3.error = "subagent spawner does not support resume (continue)";
143291
+ st3.endedAt = ctx.now();
143292
+ await deps.emit?.("workflow_step_failed", { id: step.id, error: st3.error });
143293
+ await store.remove(runId);
143294
+ return buildRunResult(ctx, "failed", false, { error: st3.error });
143295
+ }
143296
+ const finalizePrompt = `Operator reply:
143297
+ ${userMessage.trim()}${FINALIZE_REPLY_SUFFIX}`;
143298
+ try {
143299
+ const child = await deps.spawner.continue({
143300
+ childSessionId: checkpoint.interactionAgentId,
143301
+ prompt: finalizePrompt,
143302
+ label: step.label ?? step.id
143303
+ });
143304
+ if (child.error)
143305
+ throw new Error(child.error.message);
143306
+ st3.status = "completed";
143307
+ st3.output = child.text;
143308
+ st3.endedAt = ctx.now();
143309
+ await deps.emit?.("workflow_step_completed", {
143310
+ id: step.id,
143311
+ preview: child.text.slice(0, 280)
143312
+ });
143313
+ } catch (err) {
143314
+ const message = err instanceof Error ? err.message : String(err);
143315
+ st3.status = "failed";
143316
+ st3.error = message;
143317
+ st3.endedAt = ctx.now();
143318
+ await deps.emit?.("workflow_step_failed", { id: step.id, error: message });
143319
+ await store.remove(runId);
143320
+ return buildRunResult(ctx, "failed", false, { error: message });
143321
+ }
143322
+ await store.remove(runId);
143323
+ return runExecutorLoop(ctx);
143324
+ }
143065
143325
  function sinkOutput(workflow, states) {
143066
143326
  const needed = /* @__PURE__ */ new Set();
143067
143327
  for (const step of workflow.steps)
@@ -143414,7 +143674,7 @@ A workflow is a DAG of steps. Schema:
143414
143674
  - when (optional, legacy): simple guards only \u2014 '{{ steps.x.output }} is not empty'. Do NOT use when for semantic decisions (use condition/switch).
143415
143675
  - onError (optional): fail | continue | retry ; retries (optional, 0-3)
143416
143676
 
143417
- IMPORTANT \u2014 no mid-run questions: this build has NO chat-resume channel, so a workflow CANNOT pause to ask the operator something mid-run (\`awaitInput\` is rejected at save time). For any value the operator must supply, declare it as an \`inputs\` field (the operator fills it in before Run); never try to "ask in chat".
143677
+ Operator data \u2014 two ways: declare a value the operator can supply UP FRONT as an \`inputs\` field (filled in before Run). To PAUSE mid-run and ask a question whose answer depends on earlier steps, set \`awaitInput: true\` on a prompt or skill step: the workflow pauses, surfaces the step's prompt to the operator, and resumes with their reply once they answer. Prefer \`inputs\` for known-up-front values; use \`awaitInput\` only for genuinely mid-run questions.
143418
143678
 
143419
143679
  Templating: {{ steps.<id>.output }}, {{ inputs.<name> }}, {{ vars.<name> }}, {{ trigger }}, {{ now }}.
143420
143680
 
@@ -143424,8 +143684,8 @@ Ordering: steps whose \`needs\` are all satisfied run in parallel \u2014 chain w
143424
143684
 
143425
143685
  Authoring rules:
143426
143686
  1. Decompose the intent into concrete steps. Multi-phase requests (collect \u2192 act \u2192 summarize \u2192 deliver) need at least 4 steps with a linear or fan-in \`needs\` chain.
143427
- 2. Values the operator must supply (search topic, recipient email, brief): declare each as an \`inputs\` field with a clear \`description\` (and a \`default\` when sensible). The operator fills them in before Run; reference them downstream via \`{{ inputs.<name> }}\`. There is NO way to pause and ask mid-run.
143428
- 3. Never use \`awaitInput\` \u2014 it is rejected at save time in this build (no resume channel). Do NOT add a prompt/skill step that only says "ask the operator"; that finishes in one turn and just stores the question as output. Use \`inputs\` instead.
143687
+ 2. Values the operator must supply (search topic, recipient email, brief): declare each as an \`inputs\` field with a clear \`description\` (and a \`default\` when sensible). The operator fills them in before Run; reference them downstream via \`{{ inputs.<name> }}\`.
143688
+ 3. To ask the operator a mid-run question whose answer depends on earlier steps, set \`awaitInput: true\` on a prompt or skill step \u2014 the run pauses, shows that step's prompt to the operator, and continues with their reply (referenced downstream via \`{{ steps.<id>.output }}\`). awaitInput is ONLY valid on prompt/skill steps (never tool/logic/loop or a loop body). Prefer \`inputs\` for values known before Run.
143429
143689
  4. Research + report + email intents: typical chain \u2014 \`web-research\` skill (over \`{{ inputs.topic }}\`) \u2192 \`write_report\` \u2192 \`send_email\` tool (to \`{{ inputs.recipient }}\`). Put \`topic\` and \`recipient\` in \`inputs\`.
143430
143690
  5. Use ONLY skill/tool names from the catalogs below \u2014 never placeholders like "<< skill-name >>", "TBD", or empty skill/tool fields.
143431
143691
  6. Prefer a listed skill when its description fits; otherwise use a detailed \`prompt\` step.
@@ -143541,6 +143801,31 @@ steps:
143541
143801
  Improve the current draft (start from {{ steps.first_draft.output }} or {{ vars.draft }}).
143542
143802
  Return JSON with vars.draft set to the improved text.
143543
143803
  \`\`\`
143804
+
143805
+ Example shape for a mid-run question (awaitInput pause \u2192 operator reply \u2192 continue):
143806
+ \`\`\`yaml
143807
+ name: draft-with-approval
143808
+ description: Draft an announcement, ask the operator to approve or tweak it, then publish.
143809
+ enabled: true
143810
+ steps:
143811
+ - id: draft
143812
+ label: Draft announcement
143813
+ prompt: |
143814
+ Write a short product announcement.
143815
+ - id: approve
143816
+ needs: [draft]
143817
+ label: Approve or tweak
143818
+ awaitInput: true
143819
+ prompt: |
143820
+ Here is the draft announcement:
143821
+ {{ steps.draft.output }}
143822
+ Reply with "ship it" to approve, or describe any changes you want.
143823
+ - id: publish
143824
+ needs: [approve]
143825
+ label: Publish
143826
+ prompt: |
143827
+ Apply the operator's decision ({{ steps.approve.output }}) and produce the final announcement.
143828
+ \`\`\`
143544
143829
  (Replace skill/tool names with ones from the catalog when drafting.)`;
143545
143830
  }
143546
143831
  function formatCatalog(entries, emptyLabel) {
@@ -144195,6 +144480,49 @@ function buildWorkflowsIntegration(args) {
144195
144480
  inFlight.delete(input.name);
144196
144481
  }
144197
144482
  }
144483
+ async function resumeNow(runId, reply2) {
144484
+ const checkpoint = await defaultWorkflowRunStore.load(runId);
144485
+ const turnId = session.startTurn().turnId;
144486
+ const spawner = createSubagentSpawner({
144487
+ parentSession: session,
144488
+ parentTurnId: turnId,
144489
+ parentSignal: session.signal,
144490
+ parentModel: activeModel(session)
144491
+ });
144492
+ const result = await resumeWorkflowRun(
144493
+ runId,
144494
+ reply2,
144495
+ {
144496
+ spawner,
144497
+ tools: session.tools,
144498
+ lookup: {
144499
+ skill: (n2) => session.skills.byName(n2),
144500
+ workflow: (n2) => store.lookup(n2)
144501
+ },
144502
+ signal: session.signal,
144503
+ now: () => Date.now(),
144504
+ emit: (subtype, payload) => void session.log.append({
144505
+ type: "plugin_event",
144506
+ sessionId: session.id,
144507
+ turnId,
144508
+ source: "plugin",
144509
+ pluginId: PLUGIN_ID3,
144510
+ subtype,
144511
+ payload
144512
+ }),
144513
+ ...logger ? { logger } : {}
144514
+ },
144515
+ defaultWorkflowRunStore
144516
+ );
144517
+ if (result.status === "paused") {
144518
+ logger?.warn?.("workflows: run paused again awaiting operator input; not delivering to inbox", {
144519
+ runId: result.runId
144520
+ });
144521
+ return result;
144522
+ }
144523
+ if (checkpoint?.workflow) await deliverToInbox(checkpoint.workflow, result, logger);
144524
+ return result;
144525
+ }
144198
144526
  const view = {
144199
144527
  list: async () => (await store.list()).map((w4) => ({
144200
144528
  name: w4.workflow.name,
@@ -144214,7 +144542,9 @@ function buildWorkflowsIntegration(args) {
144214
144542
  ok: r2.ok,
144215
144543
  output: r2.output,
144216
144544
  ...r2.error ? { error: r2.error } : {},
144217
- steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.error } : {} }))
144545
+ steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.error } : {} })),
144546
+ status: r2.status,
144547
+ ...r2.runId ? { runId: r2.runId } : {}
144218
144548
  };
144219
144549
  },
144220
144550
  // Builder-facing additions (phase 2 GUI): validate a draft YAML, persist a
@@ -144242,6 +144572,18 @@ function buildWorkflowsIntegration(args) {
144242
144572
  path: entry.path,
144243
144573
  yaml: serializeWorkflow(entry.workflow)
144244
144574
  };
144575
+ },
144576
+ // Human-in-the-loop: answer a paused run's awaitInput question and resume.
144577
+ resume: async (runId, reply2) => {
144578
+ const r2 = await resumeNow(runId, reply2);
144579
+ return {
144580
+ ok: r2.ok,
144581
+ output: r2.output,
144582
+ ...r2.error ? { error: r2.error } : {},
144583
+ steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.error } : {} })),
144584
+ status: r2.status,
144585
+ ...r2.runId ? { runId: r2.runId } : {}
144586
+ };
144245
144587
  }
144246
144588
  };
144247
144589
  async function syncSchedules() {