@moxxy/cli 0.13.1 → 0.13.2

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
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { z as z$1, createMutex, defineTunnelProvider, isCliTunnelAvailable, definePlugin, defineProvider, defineTool, MoxxyError, writeFileAtomic, asTurnId, defineMode, asPluginId, defineCommand, defineChannel, spawnCliTunnel, defineWorkflowExecutor, toFriendlyError, estimateTextTokens, classifyHttpStatus, createStuckLoopDetector, runCompactionIfNeeded, runElisionIfNeeded, collectProviderStream, usageEventFields, isContextOverflowError, emitRequestsAndDetectStuck, executeToolUses, buildSystemPromptWithSkills, projectMessages, defineCompactor, defineCacheStrategy, denyByDefaultResolver, createAllowListResolver, moxxyPath, moxxyHome, zodToJsonSchema, fileDiffSummary, runSingleShotTurn, bearerTokenMatches, resolveChannelToken, rotateChannelToken, defineSurface, estimateContextTokens as estimateContextTokens$1, readRequestBody, isFileDiffDisplay, MOXXY_WS_SUBPROTOCOL, renderFrontmatter, defineEmbedder, migrateModeName, bearerGuard, tokenFromWsProtocolHeader, skillFrontmatterSchema, asSkillId, getInstallHint, parseFrontmatterFile, createDeferredPermissionResolver, writeFileAtomicSync, encodeLoginPrompt, defineTranscriber, summarizeTokensByModel, countNodes, moxxyPackageSchema, classifyNetworkError, addModelTotals, createJsonFileStore, ISOLATION_RANK, fileDiffVerb, parseFrontmatter, createCallbackResolver, autoAllowResolver, asSessionId, asToolCallId, defineViewRenderer, DEFAULT_VIEW_TAGS, assertNever, isSafeViewUrl, evaluateToolRule, summarizeSessionTokensFromEvents, toDiffRows, diffGutterNo, computeElisionState, toolResultStubbed, toolResultStub, toolResultBytes, conversationalStubbed, conversationalStub, asEventId } from '@moxxy/sdk';
3
+ import { z as z$1, createMutex, defineTunnelProvider, definePlugin, defineProvider, defineTool, MoxxyError, asTurnId, defineMode, asPluginId, defineCommand, defineChannel, defineWorkflowExecutor, toFriendlyError, estimateTextTokens, classifyHttpStatus, createStuckLoopDetector, runCompactionIfNeeded, runElisionIfNeeded, collectProviderStream, usageEventFields, isContextOverflowError, emitRequestsAndDetectStuck, executeToolUses, buildSystemPromptWithSkills, projectMessages, defineCompactor, defineCacheStrategy, denyByDefaultResolver, createAllowListResolver, zodToJsonSchema, fileDiffSummary, runSingleShotTurn, defineSurface, runManualCompaction, isFileDiffDisplay, renderFrontmatter, defineEmbedder, migrateModeName, skillFrontmatterSchema, asSkillId, getInstallHint, parseFrontmatterFile, createDeferredPermissionResolver, encodeLoginPrompt, defineTranscriber, summarizeTokensByModel, countNodes, moxxyPackageSchema, classifyNetworkError, addModelTotals, createJsonFileStore, ISOLATION_RANK, MOXXY_PCM16_24KHZ_MIME, fileDiffVerb, parseFrontmatter, createCallbackResolver, autoAllowResolver, asSessionId, asToolCallId, defineViewRenderer, DEFAULT_VIEW_TAGS, assertNever, isSafeViewUrl, evaluateToolRule, summarizeSessionTokensFromEvents, toDiffRows, diffGutterNo, computeElisionState, toolResultStubbed, toolResultStub, toolResultBytes, conversationalStubbed, conversationalStub, asEventId } from '@moxxy/sdk';
4
4
  import * as fs32 from 'fs';
5
- import fs32__default, { existsSync, promises, ReadStream, mkdirSync, statSync, readdirSync, writeFileSync, readFileSync, chmodSync, unlinkSync, watch, createReadStream } from 'fs';
5
+ import fs32__default, { existsSync, promises, ReadStream, mkdirSync, statSync, readdirSync, writeFileSync, readFileSync, unlinkSync, chmodSync, watch, createReadStream } from 'fs';
6
6
  import * as path3 from 'path';
7
7
  import path3__default, { join, dirname, basename } from 'path';
8
+ import { isCliTunnelAvailable, writeFileAtomic, spawnCliTunnel, moxxyPath, moxxyHome, bearerTokenMatches, resolveChannelToken, rotateChannelToken, readRequestBody, MOXXY_WS_SUBPROTOCOL, bearerGuard, tokenFromWsProtocolHeader, writeFileAtomicSync } from '@moxxy/sdk/server';
8
9
  import { z } from 'zod';
9
10
  import * as os5 from 'os';
10
11
  import os5__default, { homedir, tmpdir, userInfo } from 'os';
@@ -85505,12 +85506,9 @@ function handleCollabEvent(e3, ref, root) {
85505
85506
  root.push(block2);
85506
85507
  return;
85507
85508
  }
85508
- let block = ref.current;
85509
- if (!block) {
85510
- block = newCollabBlock(e3.id, atMs);
85511
- ref.current = block;
85512
- root.push(block);
85513
- }
85509
+ const block = ref.current;
85510
+ if (!block)
85511
+ return;
85514
85512
  switch (e3.subtype) {
85515
85513
  case "collab_fallback_sequential":
85516
85514
  block.fallbackReason = String(p3.reason ?? "");
@@ -87762,7 +87760,7 @@ function useVoiceInput(opts) {
87762
87760
  const pcm = await recording.stop();
87763
87761
  const transcriber = resolveCodexTranscriber(session);
87764
87762
  const result = await transcriber.transcribe(pcm, {
87765
- mimeType: MOXXY_PCM16_24KHZ_MIME2
87763
+ mimeType: MOXXY_PCM16_24KHZ_MIME
87766
87764
  });
87767
87765
  const text = result.text.trim();
87768
87766
  if (!text) {
@@ -87869,13 +87867,12 @@ function formatVoiceError(err) {
87869
87867
  return `voice: ${message}`;
87870
87868
  return `voice: ${message || "failed"}`;
87871
87869
  }
87872
- var import_react51, CODEX_TRANSCRIBER_NAME, MOXXY_PCM16_24KHZ_MIME2;
87870
+ var import_react51, CODEX_TRANSCRIBER_NAME;
87873
87871
  var init_use_voice_input = __esm({
87874
87872
  "../plugin-cli/dist/session/use-voice-input.js"() {
87875
87873
  import_react51 = __toESM(require_react());
87876
87874
  init_voice_input();
87877
87875
  CODEX_TRANSCRIBER_NAME = "openai-codex-transcribe";
87878
- MOXXY_PCM16_24KHZ_MIME2 = "audio/x-moxxy-pcm16-24khz";
87879
87876
  }
87880
87877
  });
87881
87878
 
@@ -88165,13 +88162,17 @@ function startCollab(deps, arg) {
88165
88162
  deps.submitPrompt(objective);
88166
88163
  }
88167
88164
  function openModePicker(deps, arg = "") {
88168
- const modes = deps.session.modes.list();
88165
+ const modes = deps.session.modes.list().filter((m3) => !COLLAB_HIDDEN_MODES.has(m3.name));
88169
88166
  if (modes.length === 0) {
88170
88167
  deps.setSystemNotice("no modes registered");
88171
88168
  return;
88172
88169
  }
88173
88170
  const target = arg.trim().toLowerCase();
88174
88171
  if (target) {
88172
+ if (COLLAB_HIDDEN_MODES.has(target)) {
88173
+ deps.setSystemNotice("Use /collab <goal> to run a collaborative team (only one runs at a time).");
88174
+ return;
88175
+ }
88175
88176
  const match = modes.find((m3) => m3.name.toLowerCase() === target);
88176
88177
  if (match) {
88177
88178
  try {
@@ -88197,7 +88198,7 @@ function openModePicker(deps, arg = "") {
88197
88198
  function truncate5(s2, n2) {
88198
88199
  return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "\u2026";
88199
88200
  }
88200
- var PLUGIN_KIND_TAB, OTHER_TAB, PLUGIN_TAB_ORDER;
88201
+ var PLUGIN_KIND_TAB, OTHER_TAB, PLUGIN_TAB_ORDER, COLLAB_HIDDEN_MODES;
88201
88202
  var init_run_slash = __esm({
88202
88203
  async "../plugin-cli/dist/session/run-slash.js"() {
88203
88204
  init_dist();
@@ -88212,6 +88213,11 @@ var init_run_slash = __esm({
88212
88213
  };
88213
88214
  OTHER_TAB = { id: "others", label: "Others" };
88214
88215
  PLUGIN_TAB_ORDER = ["providers", "modes", "channels", "tools", "others"];
88216
+ COLLAB_HIDDEN_MODES = /* @__PURE__ */ new Set([
88217
+ "collaborative",
88218
+ "collab-architect",
88219
+ "collab-peer"
88220
+ ]);
88215
88221
  }
88216
88222
  });
88217
88223
 
@@ -133481,9 +133487,6 @@ var localPlugin = definePlugin({
133481
133487
  version: "0.0.0",
133482
133488
  providers: [localProviderDef]
133483
133489
  });
133484
-
133485
- // ../plugin-stt-whisper/dist/audio.js
133486
- var MOXXY_PCM16_24KHZ_MIME = "audio/x-moxxy-pcm16-24khz";
133487
133490
  var WHISPER_FILENAME_BY_MIME = {
133488
133491
  "audio/ogg": "audio.ogg",
133489
133492
  "audio/opus": "audio.opus",
@@ -139079,6 +139082,57 @@ async function integrate(input) {
139079
139082
  }
139080
139083
  return { merged, conflicts, resolvedByOwnership, stagingBranch: branchName, promoted };
139081
139084
  }
139085
+ var COLLAB_LOCK_PATH = join(homedir(), ".moxxy", "collab", "active.lock");
139086
+ function collabLockPath() {
139087
+ return process.env.MOXXY_COLLAB_LOCK || COLLAB_LOCK_PATH;
139088
+ }
139089
+ function readRaw() {
139090
+ try {
139091
+ return JSON.parse(readFileSync(collabLockPath(), "utf8"));
139092
+ } catch {
139093
+ return null;
139094
+ }
139095
+ }
139096
+ function isAlive(pid) {
139097
+ try {
139098
+ process.kill(pid, 0);
139099
+ return true;
139100
+ } catch (err) {
139101
+ return err.code === "EPERM";
139102
+ }
139103
+ }
139104
+ function readActiveCollab() {
139105
+ const info = readRaw();
139106
+ if (!info)
139107
+ return null;
139108
+ if (!isAlive(info.pid)) {
139109
+ try {
139110
+ unlinkSync(collabLockPath());
139111
+ } catch {
139112
+ }
139113
+ return null;
139114
+ }
139115
+ return info;
139116
+ }
139117
+ function tryAcquireCollabLock(args) {
139118
+ mkdirSync(dirname(collabLockPath()), { recursive: true });
139119
+ const existing = readActiveCollab();
139120
+ if (existing && existing.sessionId !== args.sessionId) {
139121
+ return { ok: false, holder: existing };
139122
+ }
139123
+ const info = { pid: process.pid, ...args };
139124
+ writeFileSync(collabLockPath(), JSON.stringify(info));
139125
+ return { ok: true };
139126
+ }
139127
+ function releaseCollabLock(sessionId) {
139128
+ const info = readRaw();
139129
+ if (info && info.sessionId === sessionId) {
139130
+ try {
139131
+ unlinkSync(collabLockPath());
139132
+ } catch {
139133
+ }
139134
+ }
139135
+ }
139082
139136
 
139083
139137
  // ../mode-collaborative/dist/collab-loop.js
139084
139138
  var POLL_MS = 500;
@@ -139097,47 +139151,56 @@ async function* runCollaborative(ctx, deps) {
139097
139151
  yield await ctx.emit(assistant(ctx, "Collaborative mode needs a task to work on."));
139098
139152
  return;
139099
139153
  }
139100
- const runId = collabRunId(String(ctx.sessionId), String(ctx.turnId));
139101
- mkdirSync(collabRunDir(runId), { recursive: true });
139102
- const { installed: gitInstalled2, repo: gitRepo } = await detectGit(cwd2);
139103
- const parallel = cfg.concurrency === "parallel" && gitRepo;
139104
- if (!parallel) {
139105
- yield await ctx.emit(plugin4(ctx, "collab_fallback_sequential", {
139106
- reason: !gitInstalled2 ? "git is not installed \u2014 running agents sequentially in your workspace" : !gitRepo ? "this folder is not a git repository \u2014 running agents sequentially in your workspace" : "sequential mode selected"
139107
- }));
139108
- }
139109
- let baseSha = "";
139110
- if (parallel) {
139111
- const base2 = await resolveBase(cwd2);
139112
- baseSha = base2.baseSha;
139154
+ const lock = tryAcquireCollabLock({ sessionId: String(ctx.sessionId), task, startedAtMs: Date.now() });
139155
+ if (!lock.ok) {
139156
+ yield await ctx.emit(plugin4(ctx, "collab_blocked", { reason: "already-running", holderTask: lock.holder.task }));
139157
+ yield await ctx.emit(assistant(ctx, `A collaboration is already running ("${lock.holder.task}"). Only one runs at a time to save resources \u2014 stop it first, then start again.`));
139158
+ return;
139113
139159
  }
139160
+ const runId = collabRunId(String(ctx.sessionId), String(ctx.turnId));
139114
139161
  const worktrees = /* @__PURE__ */ new Map();
139115
- const architectEntry = {
139116
- id: ARCHITECT_AGENT_ID,
139117
- name: "Architect",
139118
- role: "architect",
139119
- subtask: task
139120
- };
139121
- const hub = await createCollaborationHub({
139122
- socketPath: hubSocketPath(runId),
139123
- task,
139124
- roster: [architectEntry],
139125
- peerReader: peerReaderFor(worktrees, baseSha)
139126
- });
139127
- registerActiveHub(String(ctx.sessionId), hub);
139128
- const unsubscribe = hub.subscribe((e3) => {
139129
- void ctx.emit(toCollabEvent(ctx, e3));
139130
- });
139131
- const supervisorOpts = {
139132
- runId,
139133
- hubSocket: hub.socketPath,
139134
- coordinatorSessionId: String(ctx.sessionId),
139135
- parentTask: task,
139136
- ...cfg.defaultModel ? { defaultModel: cfg.defaultModel } : {},
139137
- signal: ctx.signal
139138
- };
139139
- const supervisor = (deps.createSupervisor ?? ((o2) => new PeerSupervisor(o2)))(supervisorOpts, hub);
139162
+ let hub = null;
139163
+ let supervisor = null;
139164
+ let unsubscribe = null;
139140
139165
  try {
139166
+ mkdirSync(collabRunDir(runId), { recursive: true });
139167
+ const { installed: gitInstalled2, repo: gitRepo } = await detectGit(cwd2);
139168
+ const parallel = cfg.concurrency === "parallel" && gitRepo;
139169
+ if (!parallel) {
139170
+ yield await ctx.emit(plugin4(ctx, "collab_fallback_sequential", {
139171
+ reason: !gitInstalled2 ? "git is not installed \u2014 running agents sequentially in your workspace" : !gitRepo ? "this folder is not a git repository \u2014 running agents sequentially in your workspace" : "sequential mode selected"
139172
+ }));
139173
+ }
139174
+ let baseSha = "";
139175
+ if (parallel) {
139176
+ const base2 = await resolveBase(cwd2, { snapshotDirty: true });
139177
+ baseSha = base2.baseSha;
139178
+ }
139179
+ const architectEntry = {
139180
+ id: ARCHITECT_AGENT_ID,
139181
+ name: "Architect",
139182
+ role: "architect",
139183
+ subtask: task
139184
+ };
139185
+ hub = await createCollaborationHub({
139186
+ socketPath: hubSocketPath(runId),
139187
+ task,
139188
+ roster: [architectEntry],
139189
+ peerReader: peerReaderFor(worktrees, baseSha)
139190
+ });
139191
+ registerActiveHub(String(ctx.sessionId), hub);
139192
+ unsubscribe = hub.subscribe((e3) => {
139193
+ void ctx.emit(toCollabEvent(ctx, e3));
139194
+ });
139195
+ const supervisorOpts = {
139196
+ runId,
139197
+ hubSocket: hub.socketPath,
139198
+ coordinatorSessionId: String(ctx.sessionId),
139199
+ parentTask: task,
139200
+ ...cfg.defaultModel ? { defaultModel: cfg.defaultModel } : {},
139201
+ signal: ctx.signal
139202
+ };
139203
+ supervisor = (deps.createSupervisor ?? ((o2) => new PeerSupervisor(o2)))(supervisorOpts, hub);
139141
139204
  yield await ctx.emit(plugin4(ctx, "collab_started", { task, parallel, gitInstalled: gitInstalled2, gitRepo }));
139142
139205
  supervisor.spawn({ entry: architectEntry, cwd: cwd2, mode: COLLAB_ARCHITECT_MODE_NAME });
139143
139206
  yield await ctx.emit(plugin4(ctx, "collab_agent_spawned", { id: ARCHITECT_AGENT_ID, role: "architect" }));
@@ -139235,10 +139298,14 @@ ${summaryBlock}${mergeNote ? `
139235
139298
  ${mergeNote}` : ""}`));
139236
139299
  yield await ctx.emit(plugin4(ctx, "collab_completed", { done: doneIds, total: roster.length }));
139237
139300
  } finally {
139238
- await supervisor.shutdownAll("collaboration complete");
139239
- unsubscribe();
139301
+ if (supervisor)
139302
+ await supervisor.shutdownAll("collaboration complete");
139303
+ if (unsubscribe)
139304
+ unsubscribe();
139240
139305
  unregisterActiveHub(String(ctx.sessionId));
139241
- await hub.close();
139306
+ if (hub)
139307
+ await hub.close();
139308
+ releaseCollabLock(String(ctx.sessionId));
139242
139309
  }
139243
139310
  }
139244
139311
  function lastUserPromptText(ctx) {
@@ -144062,6 +144129,7 @@ var optionalWorkspace = z.string().min(1).max(256).optional();
144062
144129
  var MAX_AUDIO_BASE64 = 4e7;
144063
144130
  var MAX_INLINE_ATTACHMENT_CONTENT = 12e6;
144064
144131
  var commandName = z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "invalid command name");
144132
+ var appId = z.string().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "invalid app id");
144065
144133
  var workflowName = z.string().min(1).max(200).refine((s2) => !s2.includes("..") && !s2.includes("/") && !s2.includes("\\"), "invalid workflow name");
144066
144134
  var ipcInputSchemas = {
144067
144135
  // No-arg, but spawns a child process (npm install) — pin the payload to
@@ -144171,6 +144239,20 @@ var ipcInputSchemas = {
144171
144239
  "settings.writeSkill": z.object({ name: skillName, body: z.string().max(1e6) }),
144172
144240
  "settings.readSkill": z.object({ name: skillName }),
144173
144241
  "settings.deleteSkill": z.object({ name: skillName }),
144242
+ // Desktop apps: appId keys the per-app install dir + a network download, so
144243
+ // pin it to a non-traversing slug. (pickDocument is no-arg → see below.)
144244
+ "apps.status": z.object({ appId }),
144245
+ "apps.install": z.object({ appId }),
144246
+ "apps.uninstall": z.object({ appId }),
144247
+ // Anonymizer: parseDocument reads a file (bound the path), saveRedacted writes
144248
+ // one (bound name + cap content so a renderer can't OOM main). pickDocument
144249
+ // takes nothing — pin it to "nothing" so no args can be smuggled across.
144250
+ "anonymizer.pickDocument": z.undefined(),
144251
+ "anonymizer.parseDocument": z.object({ path: z.string().min(1).max(4096) }),
144252
+ "anonymizer.saveRedacted": z.object({
144253
+ suggestedName: z.string().min(1).max(255),
144254
+ content: z.string().max(2e7)
144255
+ }),
144174
144256
  "desks.create": z.object({ name: z.string().min(1).max(200), cwd: z.string().min(1).max(4096) }),
144175
144257
  // Mirror desks.create's name bounds — rename writes the name into the desks
144176
144258
  // JSON, so an unbounded string would let a renderer bloat the state file.
@@ -146562,37 +146644,25 @@ async function compactSession(session) {
146562
146644
  if (events.length === 0) {
146563
146645
  return { kind: "text", text: "nothing to compact: event log is empty" };
146564
146646
  }
146565
- const providerCtxWindow = resolveActiveContextWindow(s2);
146566
146647
  const provider = safe(() => s2.providers?.getActive()) ?? void 0;
146567
146648
  const model = provider?.models[0]?.id;
146649
+ const contextWindow = provider?.models[0]?.contextWindow;
146568
146650
  try {
146569
- const result = await compactor.compact(events, {
146570
- log: s2.log.asReader ? s2.log.asReader() : s2.log,
146571
- budget: {
146572
- contextWindow: providerCtxWindow,
146573
- estimatedTokens: estimateContextTokens$1(s2.log.asReader ? s2.log.asReader() : s2.log),
146574
- reserveForOutput: 0
146575
- },
146576
- signal: s2.signal ?? new AbortController().signal,
146651
+ const result = await runManualCompaction({
146652
+ compactor,
146653
+ log: s2.log,
146654
+ signal: s2.signal,
146577
146655
  ...provider ? { provider } : {},
146578
- ...model ? { model } : {}
146656
+ ...model !== void 0 ? { model } : {},
146657
+ ...contextWindow !== void 0 ? { contextWindow } : {},
146658
+ ...s2.id !== void 0 ? { sessionId: s2.id } : {}
146579
146659
  });
146580
- if (result.tokensSaved <= 0 || result.summary.trim().length === 0) {
146660
+ if (!result.compacted) {
146581
146661
  return { kind: "text", text: "nothing to compact yet" };
146582
146662
  }
146583
- const lastEvent = events[events.length - 1];
146584
- const emittable = {
146585
- sessionId: s2.id ?? lastEvent?.sessionId,
146586
- turnId: lastEvent?.turnId,
146587
- source: "compactor",
146588
- ...result
146589
- };
146590
- await s2.log.append(emittable);
146591
- const [fromSeq, toSeq] = result.replacedRange;
146592
- const compactedEvents = events.filter((e3) => e3.seq >= fromSeq && e3.seq <= toSeq).length;
146593
146663
  return {
146594
146664
  kind: "text",
146595
- text: `context compacted: ${formatCount2(compactedEvents)} ${plural2(compactedEvents, "event")}, ~${formatTokenCount2(result.tokensSaved)} tokens saved`
146665
+ text: `context compacted: ${formatCount2(result.eventsCompacted)} ${plural2(result.eventsCompacted, "event")}, ~${formatTokenCount2(result.tokensSaved)} tokens saved`
146596
146666
  };
146597
146667
  } catch (err) {
146598
146668
  return {
@@ -146601,17 +146671,6 @@ async function compactSession(session) {
146601
146671
  };
146602
146672
  }
146603
146673
  }
146604
- function resolveActiveContextWindow(s2) {
146605
- try {
146606
- const provider = s2.providers?.getActive();
146607
- if (!provider)
146608
- return Number.MAX_SAFE_INTEGER;
146609
- const window2 = provider.models[0]?.contextWindow;
146610
- return window2 && window2 > 0 ? window2 : Number.MAX_SAFE_INTEGER;
146611
- } catch {
146612
- return Number.MAX_SAFE_INTEGER;
146613
- }
146614
- }
146615
146674
  function formatCount2(value) {
146616
146675
  return new Intl.NumberFormat("en-US").format(value);
146617
146676
  }
@@ -147610,6 +147669,8 @@ var stepSchema = z.object({
147610
147669
  needs: z.array(z.string().min(1)).default([]),
147611
147670
  when: z.string().min(1).optional(),
147612
147671
  onError: z.enum(["fail", "continue", "retry"]).default("fail"),
147672
+ // `retries` only takes effect when `onError: 'retry'`; with 'fail'/'continue'
147673
+ // the step runs exactly one attempt (see runStep in executor/steps.ts).
147613
147674
  retries: z.number().int().min(0).max(3).default(0),
147614
147675
  label: z.string().max(60).optional(),
147615
147676
  format: z.enum(["json", "plain"]).optional(),
@@ -148519,7 +148580,7 @@ function buildRunResult(ctx, status, ok, extra) {
148519
148580
  };
148520
148581
  }
148521
148582
  async function runStep(step, scope, ctx) {
148522
- const attempts = 1 + Math.max(0, step.retries);
148583
+ const attempts = step.onError === "retry" ? 1 + Math.max(0, step.retries) : 1;
148523
148584
  let lastError = "";
148524
148585
  for (let attempt = 0; attempt < attempts; attempt++) {
148525
148586
  if (ctx.deps.signal.aborted)
@@ -149040,7 +149101,7 @@ ${userMessage.trim()}${FINALIZE_REPLY_SUFFIX}`;
149040
149101
  var DAG_EXECUTOR_NAME = "dag";
149041
149102
  var dagExecutor = defineWorkflowExecutor({
149042
149103
  name: DAG_EXECUTOR_NAME,
149043
- description: "DAG runner: steps with settled dependencies are scheduled in waves of up to `concurrency` ready steps, then executed sequentially within each wave (no overlap yet \u2014 `concurrency` caps the batch size, not wall-clock latency).",
149104
+ description: "DAG runner: steps with settled dependencies are scheduled in waves of up to `concurrency` ready steps, then executed sequentially within each wave (no overlap \u2014 `concurrency` caps the batch size drained per pass, not wall-clock latency).",
149044
149105
  run: runExecutor
149045
149106
  });
149046
149107
 
@@ -149107,7 +149168,7 @@ A workflow is a DAG of steps. Schema:
149107
149168
  - args: templated args object for tool/workflow steps
149108
149169
  - needs: [ <upstream step ids> ] (defines the DAG; omit only for true sources)
149109
149170
  - when (optional, legacy): simple guards only \u2014 '{{ steps.x.output }} is not empty'. Do NOT use when for semantic decisions (use condition/switch).
149110
- - onError (optional): fail | continue | retry ; retries (optional, 0-3)
149171
+ - onError (optional): fail | continue | retry ; retries (optional, 0-3 \u2014 only applies when onError is retry; fail/continue always run exactly one attempt)
149111
149172
 
149112
149173
  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.
149113
149174