@osolmaz/pi-workflows 0.11.2 → 0.12.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.
Files changed (177) hide show
  1. package/README.md +15 -10
  2. package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
  3. package/dist/builtins/autoimplement-command-batches.js +258 -0
  4. package/dist/builtins/autoimplement-command-batches.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +509 -124
  6. package/dist/builtins/autoimplement.workflow.js +884 -327
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +5 -3
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +4 -1
  11. package/dist/builtins/index.js +3 -1
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/monitor.workflow.d.ts +2 -4
  14. package/dist/builtins/monitor.workflow.js +26 -128
  15. package/dist/builtins/monitor.workflow.js.map +1 -1
  16. package/dist/builtins/pi-agent-group.d.ts +72 -0
  17. package/dist/builtins/pi-agent-group.js +1087 -0
  18. package/dist/builtins/pi-agent-group.js.map +1 -0
  19. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  20. package/dist/builtins/plan-approval.workflow.js +92 -14
  21. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  22. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  23. package/dist/builtins/plan-change.workflow.js +256 -0
  24. package/dist/builtins/plan-change.workflow.js.map +1 -0
  25. package/dist/builtins/plan-presentation.js +2 -2
  26. package/dist/builtins/plan-presentation.js.map +1 -1
  27. package/dist/builtins/sanity-check.workflow.d.ts +85 -0
  28. package/dist/builtins/sanity-check.workflow.js +482 -0
  29. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  30. package/dist/controllers/sqlite.d.ts +90 -2
  31. package/dist/controllers/sqlite.js +380 -5
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.d.ts +2 -2
  34. package/dist/extension/decision-channels.js +29 -35
  35. package/dist/extension/decision-channels.js.map +1 -1
  36. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  37. package/dist/extension/deferred-turn-coordinator.js +143 -0
  38. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  39. package/dist/extension/deferred-turn.d.ts +44 -0
  40. package/dist/extension/deferred-turn.js +110 -0
  41. package/dist/extension/deferred-turn.js.map +1 -0
  42. package/dist/extension/index.js +348 -80
  43. package/dist/extension/index.js.map +1 -1
  44. package/dist/extension/session-events.d.ts +2 -2
  45. package/dist/extension/step-message.d.ts +1 -0
  46. package/dist/extension/step-message.js.map +1 -1
  47. package/dist/extension/widget.js +23 -3
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/herdr/setup.js +4 -4
  50. package/dist/render/graph-render.js +1 -2
  51. package/dist/render/graph-render.js.map +1 -1
  52. package/dist/viewer/render.js +7 -6
  53. package/dist/viewer/render.js.map +1 -1
  54. package/dist/workflows/catalog.js +7 -2
  55. package/dist/workflows/catalog.js.map +1 -1
  56. package/dist/workflows/command-batch.d.ts +38 -0
  57. package/dist/workflows/command-batch.js +176 -0
  58. package/dist/workflows/command-batch.js.map +1 -0
  59. package/dist/workflows/composition.js +8 -0
  60. package/dist/workflows/composition.js.map +1 -1
  61. package/dist/workflows/decision-presentation.d.ts +1 -1
  62. package/dist/workflows/decision-presentation.js +51 -38
  63. package/dist/workflows/decision-presentation.js.map +1 -1
  64. package/dist/workflows/engine.d.ts +2 -2
  65. package/dist/workflows/engine.js +22 -17
  66. package/dist/workflows/engine.js.map +1 -1
  67. package/dist/workflows/errors.d.ts +13 -0
  68. package/dist/workflows/errors.js +15 -0
  69. package/dist/workflows/errors.js.map +1 -1
  70. package/dist/workflows/human-decision.d.ts +16 -4
  71. package/dist/workflows/human-decision.js +175 -72
  72. package/dist/workflows/human-decision.js.map +1 -1
  73. package/dist/workflows/index.d.ts +3 -2
  74. package/dist/workflows/index.js +2 -1
  75. package/dist/workflows/index.js.map +1 -1
  76. package/dist/workflows/progress.d.ts +1 -0
  77. package/dist/workflows/progress.js +15 -3
  78. package/dist/workflows/progress.js.map +1 -1
  79. package/dist/workflows/schema.js +12 -1
  80. package/dist/workflows/schema.js.map +1 -1
  81. package/dist/workflows/shell.d.ts +4 -0
  82. package/dist/workflows/shell.js +6 -0
  83. package/dist/workflows/shell.js.map +1 -1
  84. package/dist/workflows/store.js +8 -1
  85. package/dist/workflows/store.js.map +1 -1
  86. package/dist/workflows/types.d.ts +37 -48
  87. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  88. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  89. package/docs/CONTROLLERS.md +2 -2
  90. package/docs/DEFERRED_TURNS.md +298 -0
  91. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  92. package/docs/HUMAN_DECISIONS.md +30 -40
  93. package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
  94. package/docs/MONITOR.md +7 -13
  95. package/docs/WORKFLOW_COMPOSITION.md +10 -9
  96. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  97. package/docs/WORKFLOW_UPDATES.md +28 -5
  98. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  99. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  100. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  101. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  102. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  103. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  104. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  105. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  106. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  107. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  108. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  109. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  110. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  111. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  112. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  113. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  114. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  115. package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
  116. package/docs/run-bundles.md +8 -6
  117. package/docs/workflows.md +85 -22
  118. package/examples/workflows/approved-plan.workflow.ts +19 -46
  119. package/examples/workflows/command-batch.workflow.ts +50 -0
  120. package/examples/workflows/sanity-check.workflow.ts +1 -0
  121. package/herdr-plugin.toml +3 -3
  122. package/package.json +7 -7
  123. package/schemas/decision-presentation-v1.schema.json +1 -1
  124. package/schemas/human-decision-accepted-v1.schema.json +16 -4
  125. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  126. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  127. package/schemas/human-decision-continuation-v1.schema.json +11 -2
  128. package/schemas/human-decision-delivery-v1.schema.json +9 -1
  129. package/schemas/human-decision-receipt-v1.schema.json +9 -1
  130. package/schemas/human-decision-request-v1.schema.json +25 -5
  131. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  132. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  133. package/skills/autodoc/SKILL.md +33 -2
  134. package/skills/autoimplement/SKILL.md +93 -12
  135. package/skills/autoplan/SKILL.md +26 -2
  136. package/skills/monitor/SKILL.md +66 -24
  137. package/skills/pi-workflows/SKILL.md +21 -6
  138. package/skills/sanity-check/SKILL.md +44 -0
  139. package/src/builtins/autoimplement-command-batches.ts +364 -0
  140. package/src/builtins/autoimplement.workflow.ts +1075 -370
  141. package/src/builtins/catalog.ts +5 -3
  142. package/src/builtins/index.ts +24 -0
  143. package/src/builtins/monitor.workflow.ts +27 -150
  144. package/src/builtins/pi-agent-group.ts +1407 -0
  145. package/src/builtins/plan-approval.workflow.ts +157 -24
  146. package/src/builtins/plan-change.workflow.ts +321 -0
  147. package/src/builtins/plan-presentation.ts +2 -2
  148. package/src/builtins/sanity-check.workflow.ts +769 -0
  149. package/src/controllers/sqlite.ts +580 -7
  150. package/src/extension/decision-channels.ts +36 -66
  151. package/src/extension/deferred-turn-coordinator.ts +171 -0
  152. package/src/extension/deferred-turn.ts +166 -0
  153. package/src/extension/index.ts +438 -90
  154. package/src/extension/session-events.ts +2 -2
  155. package/src/extension/step-message.ts +1 -0
  156. package/src/extension/widget.ts +24 -5
  157. package/src/herdr/setup.ts +4 -4
  158. package/src/render/graph-render.ts +1 -2
  159. package/src/viewer/render.ts +7 -6
  160. package/src/workflows/catalog.ts +7 -2
  161. package/src/workflows/command-batch.ts +254 -0
  162. package/src/workflows/composition.ts +9 -0
  163. package/src/workflows/decision-presentation.ts +56 -43
  164. package/src/workflows/engine.ts +25 -20
  165. package/src/workflows/errors.ts +24 -0
  166. package/src/workflows/human-decision.ts +218 -101
  167. package/src/workflows/index.ts +20 -11
  168. package/src/workflows/progress.ts +18 -3
  169. package/src/workflows/schema.ts +19 -1
  170. package/src/workflows/shell.ts +10 -0
  171. package/src/workflows/store.ts +8 -1
  172. package/src/workflows/types.ts +43 -59
  173. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  174. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  175. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  176. package/schemas/human-decision-request-v2.schema.json +0 -69
  177. package/schemas/human-decision-resolution-v2.schema.json +0 -27
@@ -9,7 +9,7 @@ export type PublicAssistantMessageEvent =
9
9
  | { type: "toolcall_start"; contentIndex: number; partial: unknown }
10
10
  | { type: "toolcall_delta"; contentIndex: number; delta: string; partial: unknown }
11
11
  | { type: "toolcall_end"; contentIndex: number; toolCall: unknown; partial: unknown }
12
- | { type: "done"; reason: "stop" | "length" | "toolUse"; message: unknown }
12
+ | { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred"; message: unknown }
13
13
  | { type: "error"; reason: "aborted" | "error"; error: unknown };
14
14
 
15
15
  export type TurnStartEventLike = { turnIndex: number };
@@ -44,7 +44,7 @@ export type NormalizedAssistantEvent =
44
44
  | { type: "toolcall_start"; contentIndex: number }
45
45
  | { type: "toolcall_delta"; contentIndex: number; delta: string }
46
46
  | { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
47
- | { type: "done"; reason: "stop" | "length" | "toolUse" }
47
+ | { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred" }
48
48
  | { type: "error"; reason: "aborted" | "error" };
49
49
 
50
50
  /** Remove cumulative snapshots while preserving every semantic stream event. */
@@ -12,6 +12,7 @@ export type WorkflowAgentStepMessageDetails = {
12
12
  kind: PromptDeliveryKind;
13
13
  contract: AgentStepContract;
14
14
  presentation?: AgentStepPresentation;
15
+ turnIntentId?: string;
15
16
  };
16
17
 
17
18
  type WorkflowAgentStepMessage = {
@@ -162,9 +162,11 @@ function progressLines(
162
162
  : progressTracksFromRecords(updateHistory, now).map((track) => track.estimate);
163
163
  const projected = mergeProgressEstimates(latestProgressEstimates(state, now), measured ?? []);
164
164
  const estimates = mergeProgressEstimates(projected, monitorEstimates(state) ?? []);
165
- const lines = prioritizeProgressEstimates(estimates).map((estimate) =>
166
- formatProgressLine(estimate, now),
167
- );
165
+ const lines = prioritizeWidgetProgress(prioritizeProgressEstimates(estimates)).map((estimate) => {
166
+ const indentation =
167
+ estimate.key.startsWith("agents/") && estimate.key.split("/").length > 2 ? " " : "";
168
+ return `${indentation}${formatProgressLine(estimate, now)}`;
169
+ });
168
170
  const schedule = (state.updates ?? []).find(
169
171
  (record) => record.type === "monitor.schedule" && record.key === "next-check",
170
172
  );
@@ -180,6 +182,24 @@ function progressLines(
180
182
  return lines;
181
183
  }
182
184
 
185
+ function prioritizeWidgetProgress(estimates: ProgressEstimate[]): ProgressEstimate[] {
186
+ const weight = (estimate: ProgressEstimate) => {
187
+ if (!estimate.key.startsWith("agents/")) return 0;
188
+ const child = estimate.key.split("/").length > 2;
189
+ if (!child) return -4;
190
+ if (estimate.data.status === "failed" || estimate.data.status === "blocked") return -3;
191
+ if (
192
+ estimate.data.status === "running" ||
193
+ estimate.data.status === "pending" ||
194
+ estimate.data.status === "waiting"
195
+ ) {
196
+ return -2;
197
+ }
198
+ return 2;
199
+ };
200
+ return [...estimates].sort((left, right) => weight(left) - weight(right));
201
+ }
202
+
183
203
  function monitorEstimates(state: WorkflowRunState): ProgressEstimate[] | undefined {
184
204
  const output = state.outputs.estimate;
185
205
  if (output === null || typeof output !== "object" || Array.isArray(output)) return undefined;
@@ -309,8 +329,7 @@ function nodeRuntimeSegments(
309
329
  })
310
330
  : undefined;
311
331
  const requestAudience =
312
- (request?.schema === "pi-workflows.human-decision-request.v1" ||
313
- request?.schema === "pi-workflows.human-decision-request.v2") &&
332
+ request?.schema === "pi-workflows.human-decision-request.v1" &&
314
333
  typeof request.audience === "string"
315
334
  ? request.audience
316
335
  : human.audience;
@@ -115,20 +115,20 @@ function preflightPackage(packageRoot: string): PluginPackage {
115
115
  try {
116
116
  root = fs.realpathSync(requestedRoot);
117
117
  } catch (error) {
118
- throw new Error(`Pi Workflows package root is missing: ${requestedRoot}`, { cause: error });
118
+ throw new Error(`pi-workflows package root is missing: ${requestedRoot}`, { cause: error });
119
119
  }
120
120
  if (!fs.statSync(root).isDirectory()) {
121
- throw new Error(`Pi Workflows package root is not a directory: ${requestedRoot}`);
121
+ throw new Error(`pi-workflows package root is not a directory: ${requestedRoot}`);
122
122
  }
123
123
  const packagePath = checkedRegularFile(root, "package.json");
124
124
  const manifestPath = checkedRegularFile(root, MANIFEST_NAME);
125
125
  const packageJson = parseJsonObject(fs.readFileSync(packagePath, "utf8"), "package.json");
126
126
  if (packageJson["name"] !== PACKAGE_NAME) {
127
- throw new Error(`Unexpected Pi Workflows package name in ${packagePath}.`);
127
+ throw new Error(`Unexpected pi-workflows package name in ${packagePath}.`);
128
128
  }
129
129
  const packageVersion = packageJson["version"];
130
130
  if (typeof packageVersion !== "string" || packageVersion.length === 0) {
131
- throw new Error(`Pi Workflows package version is missing in ${packagePath}.`);
131
+ throw new Error(`pi-workflows package version is missing in ${packagePath}.`);
132
132
  }
133
133
 
134
134
  const manifest = fs.readFileSync(manifestPath, "utf8");
@@ -313,8 +313,7 @@ function renderCellText(
313
313
  ? (state.finalOutput as { schema?: unknown }).schema
314
314
  : undefined;
315
315
  const waitingRequest =
316
- waitingSchema === "pi-workflows.human-decision-request.v1" ||
317
- waitingSchema === "pi-workflows.human-decision-request.v2"
316
+ waitingSchema === "pi-workflows.human-decision-request.v1"
318
317
  ? (state.finalOutput as {
319
318
  audience?: unknown;
320
319
  presentationDigest?: unknown;
@@ -147,13 +147,13 @@ function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number,
147
147
  suffix = ansi.yellow(
148
148
  human === undefined
149
149
  ? " waiting"
150
- : ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request?.schema === "pi-workflows.human-decision-request.v2" ? ` · ${sanitizeText(request.presentation.summary)}` : ""} · ${Object.values(
150
+ : ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request === null ? "" : ` · ${sanitizeText(request.presentation.summary)}`} · ${Object.values(
151
151
  human.choices,
152
152
  )
153
153
  .map((choice) => sanitizeText(choice.label))
154
154
  .join(
155
155
  " / ",
156
- )}${request?.schema === "pi-workflows.human-decision-request.v2" ? ` · ${request.presentationDigest.slice(7, 19)}` : ""}`,
156
+ )}${request === null ? "" : ` · ${request.presentationDigest.slice(7, 19)}`}`,
157
157
  );
158
158
  } else if (result) {
159
159
  glyph = result.outcome === "ok" ? ansi.green("✓") : ansi.red("✗");
@@ -210,8 +210,7 @@ function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
210
210
  function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
211
211
  if (value === null || typeof value !== "object") return null;
212
212
  const schema = (value as { schema?: unknown }).schema;
213
- return schema === "pi-workflows.human-decision-request.v1" ||
214
- schema === "pi-workflows.human-decision-request.v2"
213
+ return schema === "pi-workflows.human-decision-request.v1"
215
214
  ? (value as HumanDecisionRequest)
216
215
  : null;
217
216
  }
@@ -271,12 +270,14 @@ export function renderRunDetailLines(
271
270
  lines.push("");
272
271
  lines.push(ansi.bold("progress"));
273
272
  for (const track of progress) {
274
- lines.push(fitWidth(formatProgressLine(track.estimate, now), size.width));
273
+ const depth = track.key.startsWith("agents/") ? track.key.split("/").length - 2 : 0;
274
+ const indentation = " ".repeat(Math.max(0, depth));
275
+ lines.push(fitWidth(`${indentation}${formatProgressLine(track.estimate, now)}`, size.width));
275
276
  const latest = track.samples.at(-1);
276
277
  lines.push(
277
278
  fitWidth(
278
279
  ansi.dim(
279
- ` ${track.estimate.sampleCount} samples · ${track.estimate.confidence ?? "no"} confidence · updated ${latest?.at ?? "unknown"}`,
280
+ `${indentation} ${track.estimate.sampleCount} samples · ${track.estimate.confidence ?? "no"} confidence · updated ${latest?.at ?? "unknown"}`,
280
281
  ),
281
282
  size.width,
282
283
  ),
@@ -1,5 +1,5 @@
1
1
  import { isWorkflowDefinition } from "./definition.js";
2
- import { WorkflowSourceChangedError } from "./errors.js";
2
+ import { BuiltinWorkflowRevisionChangedError } from "./errors.js";
3
3
  import type { WorkflowDefinition, WorkflowSource } from "./types.js";
4
4
 
5
5
  const BUILTIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
@@ -102,7 +102,12 @@ export class BuiltinWorkflowCatalog {
102
102
  throw new Error(`Unknown built-in workflow: ${source.id}`);
103
103
  }
104
104
  if (entry.revision !== source.revision) {
105
- throw new WorkflowSourceChangedError(runId);
105
+ throw new BuiltinWorkflowRevisionChangedError({
106
+ runId,
107
+ workflowId: entry.id,
108
+ previousRevision: source.revision,
109
+ currentRevision: entry.revision,
110
+ });
106
111
  }
107
112
  return entry.definition;
108
113
  }
@@ -0,0 +1,254 @@
1
+ import path from "node:path";
2
+ import { CancelledError, TimeoutError, errorMessage, isAbortLikeError } from "./errors.js";
3
+ import { runShellAction, shellOutputTruncation, shellResultFromError } from "./shell.js";
4
+ import type { MaybePromise, ShellActionResult } from "./types.js";
5
+
6
+ export const COMMAND_BATCH_RESULT_SCHEMA = "pi-workflows.command-batch-result.v1";
7
+ export const MAX_COMMAND_BATCH_ITEMS = 64;
8
+ export const MAX_COMMAND_BATCH_CONCURRENCY = 8;
9
+ export const MAX_COMMAND_BATCH_TIMEOUT_MS = 60 * 60_000;
10
+ export const MAX_COMMAND_BATCH_OUTPUT_CHARS = 1_000_000;
11
+
12
+ const MAX_ERROR_CHARS = 2_000;
13
+ const ITEM_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
14
+
15
+ export type CommandBatchItem = {
16
+ id: string;
17
+ command: string;
18
+ args: string[];
19
+ cwd: string;
20
+ timeoutMs: number;
21
+ maxOutputChars: number;
22
+ };
23
+
24
+ export type CommandBatchRequest = {
25
+ items: CommandBatchItem[];
26
+ maxConcurrency: number;
27
+ };
28
+
29
+ export type CommandBatchItemOutcome = "succeeded" | "failed" | "timedOut" | "cancelled";
30
+
31
+ export type CommandBatchItemResult = ShellActionResult & {
32
+ id: string;
33
+ outcome: CommandBatchItemOutcome;
34
+ stdoutTruncated: boolean;
35
+ stderrTruncated: boolean;
36
+ error?: string;
37
+ };
38
+
39
+ export type CommandBatchResult = {
40
+ schema: typeof COMMAND_BATCH_RESULT_SCHEMA;
41
+ items: CommandBatchItemResult[];
42
+ completed: number;
43
+ total: number;
44
+ };
45
+
46
+ export type RunCommandBatchOptions = {
47
+ signal?: AbortSignal;
48
+ onItemSettled?: (
49
+ result: CommandBatchItemResult,
50
+ completed: number,
51
+ total: number,
52
+ ) => MaybePromise<void>;
53
+ };
54
+
55
+ export function validateCommandBatchRequest(value: unknown): CommandBatchRequest {
56
+ const request = requireRecord(value, "command batch request");
57
+ for (const key of Object.keys(request)) {
58
+ if (key !== "items" && key !== "maxConcurrency") {
59
+ throw new Error(`command batch request.${key} is not supported`);
60
+ }
61
+ }
62
+ if (!Array.isArray(request.items)) {
63
+ throw new Error("command batch items must be an array");
64
+ }
65
+ if (request.items.length > MAX_COMMAND_BATCH_ITEMS) {
66
+ throw new Error(`command batch items must contain at most ${MAX_COMMAND_BATCH_ITEMS} entries`);
67
+ }
68
+ const ids = new Set<string>();
69
+ const items = request.items.map((item, index) => {
70
+ const normalized = validateItem(item, index);
71
+ if (ids.has(normalized.id)) {
72
+ throw new Error(`command batch item id is duplicated: ${normalized.id}`);
73
+ }
74
+ ids.add(normalized.id);
75
+ return normalized;
76
+ });
77
+ const maxConcurrency = positiveInteger(
78
+ request.maxConcurrency,
79
+ "command batch maxConcurrency",
80
+ MAX_COMMAND_BATCH_CONCURRENCY,
81
+ );
82
+ return { items, maxConcurrency };
83
+ }
84
+
85
+ export async function runCommandBatch(
86
+ input: CommandBatchRequest,
87
+ options: RunCommandBatchOptions = {},
88
+ ): Promise<CommandBatchResult> {
89
+ const request = validateCommandBatchRequest(input);
90
+ const total = request.items.length;
91
+ if (total === 0) {
92
+ return { schema: COMMAND_BATCH_RESULT_SCHEMA, items: [], completed: 0, total: 0 };
93
+ }
94
+
95
+ const results = Array.from<CommandBatchItemResult | undefined>({ length: total });
96
+ const signal = options.signal;
97
+ let nextIndex = 0;
98
+ let completed = 0;
99
+
100
+ const worker = async () => {
101
+ while (!signal?.aborted) {
102
+ const index = nextIndex;
103
+ if (index >= total) return;
104
+ nextIndex += 1;
105
+ if (signal?.aborted) return;
106
+
107
+ const item = request.items[index];
108
+ if (item === undefined) return;
109
+ const result = await runItem(item, signal);
110
+ results[index] = result;
111
+ completed += 1;
112
+ try {
113
+ await options.onItemSettled?.(result, completed, total);
114
+ } catch {
115
+ // Completion callbacks are observational and cannot change batch execution.
116
+ }
117
+ }
118
+ };
119
+
120
+ const workerCount = Math.min(request.maxConcurrency, total);
121
+ await Promise.all(Array.from({ length: workerCount }, worker));
122
+
123
+ for (let index = 0; index < total; index += 1) {
124
+ if (results[index] !== undefined) continue;
125
+ const item = request.items[index];
126
+ if (item !== undefined) results[index] = cancelledResult(item);
127
+ }
128
+
129
+ return {
130
+ schema: COMMAND_BATCH_RESULT_SCHEMA,
131
+ items: results as CommandBatchItemResult[],
132
+ completed,
133
+ total,
134
+ };
135
+ }
136
+
137
+ function validateItem(value: unknown, index: number): CommandBatchItem {
138
+ const item = requireRecord(value, `command batch items[${index}]`);
139
+ const allowed = new Set(["id", "command", "args", "cwd", "timeoutMs", "maxOutputChars"]);
140
+ for (const key of Object.keys(item)) {
141
+ if (!allowed.has(key)) throw new Error(`command batch items[${index}].${key} is not supported`);
142
+ }
143
+ const id = requireString(item.id, `command batch items[${index}].id`);
144
+ if (!ITEM_ID_PATTERN.test(id)) {
145
+ throw new Error(`command batch items[${index}].id is invalid`);
146
+ }
147
+ const command = requireString(item.command, `command batch items[${index}].command`);
148
+ if (!Array.isArray(item.args) || item.args.some((arg) => typeof arg !== "string")) {
149
+ throw new Error(`command batch items[${index}].args must be an array of strings`);
150
+ }
151
+ const cwd = requireString(item.cwd, `command batch items[${index}].cwd`);
152
+ if (!path.isAbsolute(cwd)) {
153
+ throw new Error(`command batch items[${index}].cwd must be absolute`);
154
+ }
155
+ const timeoutMs = positiveInteger(
156
+ item.timeoutMs,
157
+ `command batch items[${index}].timeoutMs`,
158
+ MAX_COMMAND_BATCH_TIMEOUT_MS,
159
+ );
160
+ const maxOutputChars = positiveInteger(
161
+ item.maxOutputChars,
162
+ `command batch items[${index}].maxOutputChars`,
163
+ MAX_COMMAND_BATCH_OUTPUT_CHARS,
164
+ );
165
+ return { id, command, args: [...item.args] as string[], cwd, timeoutMs, maxOutputChars };
166
+ }
167
+
168
+ async function runItem(
169
+ item: CommandBatchItem,
170
+ signal?: AbortSignal,
171
+ ): Promise<CommandBatchItemResult> {
172
+ try {
173
+ const result = await runShellAction(
174
+ {
175
+ command: item.command,
176
+ args: item.args,
177
+ cwd: item.cwd,
178
+ timeoutMs: item.timeoutMs,
179
+ maxOutputChars: item.maxOutputChars,
180
+ },
181
+ signal,
182
+ );
183
+ return itemResult(item.id, "succeeded", result);
184
+ } catch (error) {
185
+ const result = shellResultFromError(error) ?? emptyShellResult(item);
186
+ const outcome =
187
+ error instanceof TimeoutError
188
+ ? "timedOut"
189
+ : error instanceof CancelledError || isAbortLikeError(error)
190
+ ? "cancelled"
191
+ : "failed";
192
+ return itemResult(item.id, outcome, result, boundedError(error));
193
+ }
194
+ }
195
+
196
+ function itemResult(
197
+ id: string,
198
+ outcome: CommandBatchItemOutcome,
199
+ result: ShellActionResult,
200
+ error?: string,
201
+ ): CommandBatchItemResult {
202
+ const truncation = shellOutputTruncation(result);
203
+ return {
204
+ id,
205
+ outcome,
206
+ ...result,
207
+ stdoutTruncated: truncation.stdout,
208
+ stderrTruncated: truncation.stderr,
209
+ ...(error !== undefined ? { error } : {}),
210
+ };
211
+ }
212
+
213
+ function cancelledResult(item: CommandBatchItem): CommandBatchItemResult {
214
+ return itemResult(item.id, "cancelled", emptyShellResult(item), "Command was not started");
215
+ }
216
+
217
+ function emptyShellResult(item: CommandBatchItem): ShellActionResult {
218
+ return {
219
+ command: item.command,
220
+ args: [...item.args],
221
+ cwd: item.cwd,
222
+ stdout: "",
223
+ stderr: "",
224
+ exitCode: null,
225
+ signal: null,
226
+ durationMs: 0,
227
+ };
228
+ }
229
+
230
+ function boundedError(error: unknown): string {
231
+ const message = errorMessage(error);
232
+ return message.length <= MAX_ERROR_CHARS ? message : `${message.slice(0, MAX_ERROR_CHARS)}…`;
233
+ }
234
+
235
+ function positiveInteger(value: unknown, label: string, maximum: number): number {
236
+ if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) {
237
+ throw new Error(`${label} must be an integer from 1 through ${maximum}`);
238
+ }
239
+ return value as number;
240
+ }
241
+
242
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
243
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
244
+ throw new Error(`${label} must be an object`);
245
+ }
246
+ return value as Record<string, unknown>;
247
+ }
248
+
249
+ function requireString(value: unknown, label: string): string {
250
+ if (typeof value !== "string" || value.trim().length === 0) {
251
+ throw new Error(`${label} must be a non-empty string`);
252
+ }
253
+ return value;
254
+ }
@@ -480,6 +480,7 @@ function wrapNode(
480
480
  case "checkpoint": {
481
481
  const human = node.humanDecision;
482
482
  const audience = human?.audience;
483
+ const onTimeout = human?.onTimeout;
483
484
  return {
484
485
  ...common,
485
486
  nodeType: "checkpoint",
@@ -496,6 +497,14 @@ function wrapNode(
496
497
  : audience!,
497
498
  choices: human.choices,
498
499
  request: (context: WorkflowNodeContext) => human.request(project(context)),
500
+ ...(onTimeout !== undefined
501
+ ? {
502
+ onTimeout:
503
+ typeof onTimeout === "function"
504
+ ? (context: WorkflowNodeContext) => onTimeout(project(context))
505
+ : onTimeout,
506
+ }
507
+ : {}),
499
508
  },
500
509
  }
501
510
  : {}),