@moxxy/cli 0.7.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
@@ -142435,16 +142603,54 @@ function findCycle(steps) {
142435
142603
  }
142436
142604
  return null;
142437
142605
  }
142438
- function formatIssues(error2) {
142606
+ function humanizeIssue(iss) {
142607
+ switch (iss.code) {
142608
+ case "too_small": {
142609
+ const min = Number(iss.minimum);
142610
+ if (iss.type === "string")
142611
+ return min === 1 ? "must not be empty" : `must be at least ${min} characters`;
142612
+ if (iss.type === "array")
142613
+ return min === 1 ? "needs at least one entry" : `needs at least ${min} entries`;
142614
+ return `must be at least ${min}`;
142615
+ }
142616
+ case "too_big": {
142617
+ const max = Number(iss.maximum);
142618
+ if (iss.type === "string")
142619
+ return `must be at most ${max} characters`;
142620
+ if (iss.type === "array")
142621
+ return `can have at most ${max} entries`;
142622
+ return `must be at most ${max}`;
142623
+ }
142624
+ case "invalid_type":
142625
+ return iss.received === "undefined" ? "is required" : `should be a ${iss.expected} (got ${iss.received})`;
142626
+ case "invalid_enum_value":
142627
+ return `must be one of: ${iss.options.map((o2) => `"${String(o2)}"`).join(", ")}`;
142628
+ default:
142629
+ return iss.message;
142630
+ }
142631
+ }
142632
+ function formatIssues(error2, raw) {
142633
+ const steps = raw !== null && typeof raw === "object" && Array.isArray(raw.steps) ? raw.steps : [];
142439
142634
  return error2.issues.map((iss) => {
142440
- const path61 = iss.path.join(".") || "(root)";
142441
- return `${path61}: ${iss.message}`;
142635
+ if (iss.code === "custom")
142636
+ return iss.message;
142637
+ const msg = humanizeIssue(iss);
142638
+ const [head, idx, ...rest] = iss.path;
142639
+ if (head === "steps" && typeof idx === "number") {
142640
+ const step = steps[idx];
142641
+ const id = step !== null && typeof step === "object" && typeof step.id === "string" ? step.id : "";
142642
+ const where = id ? `step "${id}"` : `step ${idx + 1}`;
142643
+ const field = rest.join(".");
142644
+ return field ? `${where}: ${field} ${msg}` : `${where} ${msg}`;
142645
+ }
142646
+ const path61 = iss.path.join(".");
142647
+ return path61 ? `${path61} ${msg}` : `workflow ${msg}`;
142442
142648
  });
142443
142649
  }
142444
142650
  function validateWorkflow(raw) {
142445
142651
  const parsed = workflowSchema.safeParse(raw);
142446
142652
  if (!parsed.success)
142447
- return { ok: false, errors: formatIssues(parsed.error) };
142653
+ return { ok: false, errors: formatIssues(parsed.error, raw) };
142448
142654
  return { ok: true, workflow: parsed.data, errors: [] };
142449
142655
  }
142450
142656
  function parseWorkflowYaml(text) {
@@ -142800,6 +143006,7 @@ function stepsToSkipForBranch(step, selected) {
142800
143006
  // ../plugin-workflows/dist/executor/dag.js
142801
143007
  var DAG_EXECUTOR_NAME = "dag";
142802
143008
  var MAX_NESTING_DEPTH = 5;
143009
+ 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
143010
  function collectLoopBodyIds(workflow) {
142804
143011
  const ids = /* @__PURE__ */ new Set();
142805
143012
  for (const step of workflow.steps) {
@@ -142876,6 +143083,19 @@ function serializeStates(states) {
142876
143083
  }
142877
143084
  return out;
142878
143085
  }
143086
+ function restoreStates(raw) {
143087
+ const states = /* @__PURE__ */ new Map();
143088
+ for (const [id, st3] of Object.entries(raw)) {
143089
+ states.set(id, {
143090
+ status: st3.status,
143091
+ output: st3.output,
143092
+ ...st3.error ? { error: st3.error } : {},
143093
+ startedAt: st3.startedAt,
143094
+ endedAt: st3.endedAt
143095
+ });
143096
+ }
143097
+ return states;
143098
+ }
142879
143099
  function buildStepResults(workflow, states) {
142880
143100
  return workflow.steps.map((step) => {
142881
143101
  const st3 = states.get(step.id);
@@ -143005,7 +143225,13 @@ async function runExecutorLoop(ctx) {
143005
143225
  await deps.emit?.("workflow_paused", {
143006
143226
  runId,
143007
143227
  stepId: step.id,
143008
- childSessionId: outcome.interactionAgentId
143228
+ childSessionId: outcome.interactionAgentId,
143229
+ // Carry the human-facing question so the operator UI is self-contained
143230
+ // (no separate event correlation needed): the workflow name, the step
143231
+ // label, and the prompt/question the paused step asked.
143232
+ workflow: workflow.name,
143233
+ label: step.label ?? step.id,
143234
+ prompt: outcome.output.slice(0, 2e3)
143009
143235
  });
143010
143236
  return buildRunResult(ctx, "paused", true, {
143011
143237
  runId,
@@ -143062,6 +143288,78 @@ async function runExecutor(workflow, deps) {
143062
143288
  }
143063
143289
  return runExecutorLoop(ctx);
143064
143290
  }
143291
+ async function resumeWorkflowRun(runId, userMessage, deps, store = defaultWorkflowRunStore) {
143292
+ const checkpoint = await store.load(runId);
143293
+ if (!checkpoint) {
143294
+ return {
143295
+ ok: false,
143296
+ status: "failed",
143297
+ steps: [],
143298
+ output: "",
143299
+ error: `no paused workflow run "${runId}"`
143300
+ };
143301
+ }
143302
+ const depsWithStore = { ...deps, runStore: store };
143303
+ const step = checkpoint.workflow.steps.find((s2) => s2.id === checkpoint.pendingStepId);
143304
+ if (!step) {
143305
+ return {
143306
+ ok: false,
143307
+ status: "failed",
143308
+ steps: buildStepResults(checkpoint.workflow, restoreStates(checkpoint.states)),
143309
+ output: "",
143310
+ error: `paused step "${checkpoint.pendingStepId}" not found`
143311
+ };
143312
+ }
143313
+ const restoredVars = {};
143314
+ const ctx = {
143315
+ workflow: checkpoint.workflow,
143316
+ deps: depsWithStore,
143317
+ inputs: checkpoint.inputs,
143318
+ vars: restoredVars,
143319
+ states: restoreStates(checkpoint.states),
143320
+ now: nowFn(deps),
143321
+ loopBodyIds: collectLoopBodyIds(checkpoint.workflow)
143322
+ };
143323
+ mergeVars(ctx, checkpoint.vars);
143324
+ const st3 = ctx.states.get(step.id);
143325
+ await deps.emit?.("workflow_resumed", { runId, stepId: step.id });
143326
+ if (typeof deps.spawner.continue !== "function") {
143327
+ st3.status = "failed";
143328
+ st3.error = "subagent spawner does not support resume (continue)";
143329
+ st3.endedAt = ctx.now();
143330
+ await deps.emit?.("workflow_step_failed", { id: step.id, error: st3.error });
143331
+ await store.remove(runId);
143332
+ return buildRunResult(ctx, "failed", false, { error: st3.error });
143333
+ }
143334
+ const finalizePrompt = `Operator reply:
143335
+ ${userMessage.trim()}${FINALIZE_REPLY_SUFFIX}`;
143336
+ try {
143337
+ const child = await deps.spawner.continue({
143338
+ childSessionId: checkpoint.interactionAgentId,
143339
+ prompt: finalizePrompt,
143340
+ label: step.label ?? step.id
143341
+ });
143342
+ if (child.error)
143343
+ throw new Error(child.error.message);
143344
+ st3.status = "completed";
143345
+ st3.output = child.text;
143346
+ st3.endedAt = ctx.now();
143347
+ await deps.emit?.("workflow_step_completed", {
143348
+ id: step.id,
143349
+ preview: child.text.slice(0, 280)
143350
+ });
143351
+ } catch (err) {
143352
+ const message = err instanceof Error ? err.message : String(err);
143353
+ st3.status = "failed";
143354
+ st3.error = message;
143355
+ st3.endedAt = ctx.now();
143356
+ await deps.emit?.("workflow_step_failed", { id: step.id, error: message });
143357
+ await store.remove(runId);
143358
+ return buildRunResult(ctx, "failed", false, { error: message });
143359
+ }
143360
+ await store.remove(runId);
143361
+ return runExecutorLoop(ctx);
143362
+ }
143065
143363
  function sinkOutput(workflow, states) {
143066
143364
  const needed = /* @__PURE__ */ new Set();
143067
143365
  for (const step of workflow.steps)
@@ -143414,7 +143712,7 @@ A workflow is a DAG of steps. Schema:
143414
143712
  - when (optional, legacy): simple guards only \u2014 '{{ steps.x.output }} is not empty'. Do NOT use when for semantic decisions (use condition/switch).
143415
143713
  - onError (optional): fail | continue | retry ; retries (optional, 0-3)
143416
143714
 
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".
143715
+ 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
143716
 
143419
143717
  Templating: {{ steps.<id>.output }}, {{ inputs.<name> }}, {{ vars.<name> }}, {{ trigger }}, {{ now }}.
143420
143718
 
@@ -143424,8 +143722,8 @@ Ordering: steps whose \`needs\` are all satisfied run in parallel \u2014 chain w
143424
143722
 
143425
143723
  Authoring rules:
143426
143724
  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.
143725
+ 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> }}\`.
143726
+ 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
143727
  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
143728
  5. Use ONLY skill/tool names from the catalogs below \u2014 never placeholders like "<< skill-name >>", "TBD", or empty skill/tool fields.
143431
143729
  6. Prefer a listed skill when its description fits; otherwise use a detailed \`prompt\` step.
@@ -143541,6 +143839,31 @@ steps:
143541
143839
  Improve the current draft (start from {{ steps.first_draft.output }} or {{ vars.draft }}).
143542
143840
  Return JSON with vars.draft set to the improved text.
143543
143841
  \`\`\`
143842
+
143843
+ Example shape for a mid-run question (awaitInput pause \u2192 operator reply \u2192 continue):
143844
+ \`\`\`yaml
143845
+ name: draft-with-approval
143846
+ description: Draft an announcement, ask the operator to approve or tweak it, then publish.
143847
+ enabled: true
143848
+ steps:
143849
+ - id: draft
143850
+ label: Draft announcement
143851
+ prompt: |
143852
+ Write a short product announcement.
143853
+ - id: approve
143854
+ needs: [draft]
143855
+ label: Approve or tweak
143856
+ awaitInput: true
143857
+ prompt: |
143858
+ Here is the draft announcement:
143859
+ {{ steps.draft.output }}
143860
+ Reply with "ship it" to approve, or describe any changes you want.
143861
+ - id: publish
143862
+ needs: [approve]
143863
+ label: Publish
143864
+ prompt: |
143865
+ Apply the operator's decision ({{ steps.approve.output }}) and produce the final announcement.
143866
+ \`\`\`
143544
143867
  (Replace skill/tool names with ones from the catalog when drafting.)`;
143545
143868
  }
143546
143869
  function formatCatalog(entries, emptyLabel) {
@@ -144195,6 +144518,49 @@ function buildWorkflowsIntegration(args) {
144195
144518
  inFlight.delete(input.name);
144196
144519
  }
144197
144520
  }
144521
+ async function resumeNow(runId, reply2) {
144522
+ const checkpoint = await defaultWorkflowRunStore.load(runId);
144523
+ const turnId = session.startTurn().turnId;
144524
+ const spawner = createSubagentSpawner({
144525
+ parentSession: session,
144526
+ parentTurnId: turnId,
144527
+ parentSignal: session.signal,
144528
+ parentModel: activeModel(session)
144529
+ });
144530
+ const result = await resumeWorkflowRun(
144531
+ runId,
144532
+ reply2,
144533
+ {
144534
+ spawner,
144535
+ tools: session.tools,
144536
+ lookup: {
144537
+ skill: (n2) => session.skills.byName(n2),
144538
+ workflow: (n2) => store.lookup(n2)
144539
+ },
144540
+ signal: session.signal,
144541
+ now: () => Date.now(),
144542
+ emit: (subtype, payload) => void session.log.append({
144543
+ type: "plugin_event",
144544
+ sessionId: session.id,
144545
+ turnId,
144546
+ source: "plugin",
144547
+ pluginId: PLUGIN_ID3,
144548
+ subtype,
144549
+ payload
144550
+ }),
144551
+ ...logger ? { logger } : {}
144552
+ },
144553
+ defaultWorkflowRunStore
144554
+ );
144555
+ if (result.status === "paused") {
144556
+ logger?.warn?.("workflows: run paused again awaiting operator input; not delivering to inbox", {
144557
+ runId: result.runId
144558
+ });
144559
+ return result;
144560
+ }
144561
+ if (checkpoint?.workflow) await deliverToInbox(checkpoint.workflow, result, logger);
144562
+ return result;
144563
+ }
144198
144564
  const view = {
144199
144565
  list: async () => (await store.list()).map((w4) => ({
144200
144566
  name: w4.workflow.name,
@@ -144214,7 +144580,9 @@ function buildWorkflowsIntegration(args) {
144214
144580
  ok: r2.ok,
144215
144581
  output: r2.output,
144216
144582
  ...r2.error ? { error: r2.error } : {},
144217
- steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.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 } : {}
144218
144586
  };
144219
144587
  },
144220
144588
  // Builder-facing additions (phase 2 GUI): validate a draft YAML, persist a
@@ -144242,6 +144610,18 @@ function buildWorkflowsIntegration(args) {
144242
144610
  path: entry.path,
144243
144611
  yaml: serializeWorkflow(entry.workflow)
144244
144612
  };
144613
+ },
144614
+ // Human-in-the-loop: answer a paused run's awaitInput question and resume.
144615
+ resume: async (runId, reply2) => {
144616
+ const r2 = await resumeNow(runId, reply2);
144617
+ return {
144618
+ ok: r2.ok,
144619
+ output: r2.output,
144620
+ ...r2.error ? { error: r2.error } : {},
144621
+ steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.error } : {} })),
144622
+ status: r2.status,
144623
+ ...r2.runId ? { runId: r2.runId } : {}
144624
+ };
144245
144625
  }
144246
144626
  };
144247
144627
  async function syncSchedules() {