@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
@@ -24,6 +24,8 @@ import type {
24
24
  HumanDecisionSettlementRecord,
25
25
  HumanDecisionSubmission,
26
26
  HumanDecisionTextInput,
27
+ HumanDecisionTimeout,
28
+ ResolvedHumanDecision,
27
29
  WorkflowNodeCommon,
28
30
  WorkflowNodeContext,
29
31
  } from "./types.js";
@@ -43,6 +45,14 @@ export type HumanDecisionDefinition<TChoices extends HumanDecisionChoiceMap> =
43
45
  audience: HumanDecisionAudience;
44
46
  choices: TChoices;
45
47
  request: (context: WorkflowNodeContext) => HumanDecisionPrompt | Promise<HumanDecisionPrompt>;
48
+ onTimeout?:
49
+ | { afterMs: number; response: HumanDecisionResponseFor<TChoices> }
50
+ | ((
51
+ context: WorkflowNodeContext,
52
+ ) =>
53
+ | { afterMs: number; response: HumanDecisionResponseFor<TChoices> }
54
+ | undefined
55
+ | Promise<{ afterMs: number; response: HumanDecisionResponseFor<TChoices> } | undefined>);
46
56
  };
47
57
 
48
58
  export type HumanDecisionResponseFor<TChoices extends HumanDecisionChoiceMap> = {
@@ -98,11 +108,25 @@ export function humanDecision<const TChoices extends HumanDecisionChoiceMap>(
98
108
  if (typeof definition.request !== "function") {
99
109
  throw new Error("Human decision request must be a function");
100
110
  }
101
- const { choices, request, ...common } = definition;
111
+ const { choices, request, onTimeout, ...common } = definition;
112
+ if (
113
+ onTimeout !== undefined &&
114
+ typeof onTimeout !== "function" &&
115
+ (typeof onTimeout.afterMs !== "number" ||
116
+ !Number.isFinite(onTimeout.afterMs) ||
117
+ onTimeout.afterMs <= 0)
118
+ ) {
119
+ throw new Error("Human decision onTimeout afterMs must be a finite positive number");
120
+ }
102
121
  return checkpoint({
103
122
  ...common,
104
123
  summary: typeof audience === "string" ? `human decision for ${audience}` : "human decision",
105
- humanDecision: { audience, choices, request },
124
+ humanDecision: {
125
+ audience,
126
+ choices,
127
+ request,
128
+ ...(onTimeout !== undefined ? { onTimeout } : {}),
129
+ },
106
130
  }) as CheckpointNodeDefinition & { readonly __humanChoices?: TChoices };
107
131
  }
108
132
 
@@ -136,11 +160,31 @@ export function createHumanDecisionRequest(input: {
136
160
  attemptId: string;
137
161
  contract: { audience: string; choices: HumanDecisionChoiceMap };
138
162
  prompt: HumanDecisionPrompt;
163
+ timeout?: HumanDecisionTimeout;
139
164
  createdAt?: string;
140
165
  }): HumanDecisionRequest {
141
166
  validateChoices(input.contract.choices);
167
+ const createdAt = input.createdAt ?? new Date().toISOString();
142
168
  const title = requireString(input.prompt.title, "Human decision title");
143
- const expiresAt = validateExpiry(input.prompt.expiresAt, input.createdAt);
169
+ if (input.timeout !== undefined && input.prompt.expiresAt !== undefined) {
170
+ throw new Error("Human decision request cannot combine expiresAt with onTimeout");
171
+ }
172
+ if (
173
+ input.timeout !== undefined &&
174
+ (typeof input.timeout.afterMs !== "number" ||
175
+ !Number.isFinite(input.timeout.afterMs) ||
176
+ input.timeout.afterMs <= 0)
177
+ ) {
178
+ throw new Error("Human decision onTimeout afterMs must be a finite positive number");
179
+ }
180
+ const defaultResponse =
181
+ input.timeout === undefined
182
+ ? undefined
183
+ : validateResponseForChoices(input.contract.choices, input.timeout.response);
184
+ const expiresAt =
185
+ input.timeout === undefined
186
+ ? validateExpiry(input.prompt.expiresAt, createdAt)
187
+ : new Date(Date.parse(createdAt) + input.timeout.afterMs).toISOString();
144
188
  const common = {
145
189
  runId: input.runId,
146
190
  workflowName: input.workflowName,
@@ -150,58 +194,35 @@ export function createHumanDecisionRequest(input: {
150
194
  title,
151
195
  choices: input.contract.choices,
152
196
  ...(expiresAt !== undefined ? { expiresAt } : {}),
197
+ ...(defaultResponse !== undefined ? { defaultResponse } : {}),
153
198
  } as const;
154
- const presented =
155
- Object.hasOwn(input.prompt, "subject") || Object.hasOwn(input.prompt, "presentation");
156
- if (presented) {
157
- validatePresentedText(title, "Human decision title");
158
- validatePresentedChoices(input.contract.choices);
159
- if (Object.hasOwn(input.prompt, "body")) {
160
- throw new Error("Presented human decision requests must not contain a legacy body");
161
- }
162
- if (!Object.hasOwn(input.prompt, "subject") || !Object.hasOwn(input.prompt, "presentation")) {
163
- throw new Error("Presented human decision requests require subject and presentation");
164
- }
165
- const prompt = input.prompt as Extract<HumanDecisionPrompt, { subject: unknown }>;
166
- assertJsonValue(prompt.subject, "Human decision subject");
167
- const presentation = normalizeDecisionPresentation(prompt.presentation);
168
- const revision = prompt.revision ?? 1;
169
- if (!Number.isInteger(revision) || revision < 1) {
170
- throw new Error("Human decision revision must be a positive integer");
171
- }
172
- const basis = {
173
- schema: "pi-workflows.human-decision-request.v2" as const,
174
- ...common,
175
- subject: prompt.subject,
176
- presentation,
177
- revision,
178
- };
179
- const subjectDigest = digestCanonical(prompt.subject);
180
- const presentationDigest = digestCanonical(presentation);
181
- const requestDigest = digestCanonical(basis);
182
- const decisionId = decisionIdFor(input, requestDigest);
183
- return {
184
- ...basis,
185
- decisionId,
186
- requestDigest,
187
- subjectDigest,
188
- presentationDigest,
189
- createdAt: input.createdAt ?? new Date().toISOString(),
190
- };
199
+ validatePresentedText(title, "Human decision title");
200
+ validatePresentedChoices(input.contract.choices);
201
+ const prompt = input.prompt;
202
+ assertJsonValue(prompt.subject, "Human decision subject");
203
+ const presentation = normalizeDecisionPresentation(prompt.presentation);
204
+ const revision = prompt.revision ?? 1;
205
+ if (!Number.isInteger(revision) || revision < 1) {
206
+ throw new Error("Human decision revision must be a positive integer");
191
207
  }
192
- const prompt = input.prompt as Extract<HumanDecisionPrompt, { body: unknown }>;
193
- assertJsonValue(prompt.body, "Human decision body");
194
208
  const basis = {
195
209
  schema: "pi-workflows.human-decision-request.v1" as const,
196
210
  ...common,
197
- body: prompt.body,
211
+ subject: prompt.subject,
212
+ presentation,
213
+ revision,
198
214
  };
199
- const requestDigest = digest(basis);
215
+ const subjectDigest = digestCanonical(prompt.subject);
216
+ const presentationDigest = digestCanonical(presentation);
217
+ const requestDigest = digestCanonical(basis);
218
+ const decisionId = decisionIdFor(input, requestDigest);
200
219
  return {
201
220
  ...basis,
202
- decisionId: decisionIdFor(input, requestDigest),
221
+ decisionId,
203
222
  requestDigest,
204
- createdAt: input.createdAt ?? new Date().toISOString(),
223
+ subjectDigest,
224
+ presentationDigest,
225
+ createdAt,
205
226
  };
206
227
  }
207
228
 
@@ -220,13 +241,20 @@ function decisionIdFor(
220
241
  export function validateHumanDecisionResponse(
221
242
  request: HumanDecisionRequest,
222
243
  value: unknown,
244
+ ): HumanDecisionResponse {
245
+ return validateResponseForChoices(request.choices, value);
246
+ }
247
+
248
+ function validateResponseForChoices(
249
+ choices: HumanDecisionChoiceMap,
250
+ value: unknown,
223
251
  ): HumanDecisionResponse {
224
252
  const response = requireRecord(value, "Human decision response");
225
253
  const selected = response.choice;
226
- if (typeof selected !== "string" || !Object.hasOwn(request.choices, selected)) {
254
+ if (typeof selected !== "string" || !Object.hasOwn(choices, selected)) {
227
255
  throw new Error(`Human decision choice ${JSON.stringify(selected)} is not available`);
228
256
  }
229
- const selectedChoice = request.choices[selected];
257
+ const selectedChoice = choices[selected];
230
258
  if (!selectedChoice) throw new Error("Human decision choice contract is missing");
231
259
  if (selectedChoice.input === undefined) {
232
260
  if (response.input !== undefined) {
@@ -288,34 +316,65 @@ export function humanDecisionStateRoot(runsRoot: string): string {
288
316
  }
289
317
 
290
318
  export type HumanDecisionAcceptance =
291
- | { status: "accepted" | "adopted"; decision: AcceptedHumanDecision }
292
- | { status: "conflict"; decision: AcceptedHumanDecision };
319
+ | { status: "accepted" | "adopted"; decision: ResolvedHumanDecision }
320
+ | { status: "conflict"; decision: ResolvedHumanDecision };
293
321
 
294
322
  type HumanDecisionResolution =
295
323
  | {
296
324
  schema: "pi-workflows.human-decision-resolution.v1";
297
325
  outcome: "accepted";
298
- decision: Extract<
299
- AcceptedHumanDecision,
300
- { schema: "pi-workflows.human-decision-accepted.v1" }
301
- >;
326
+ decision: ResolvedHumanDecision;
302
327
  }
303
328
  | {
304
- schema: "pi-workflows.human-decision-resolution.v2";
305
- outcome: "accepted";
306
- decision: Extract<
307
- AcceptedHumanDecision,
308
- { schema: "pi-workflows.human-decision-accepted.v2" }
309
- >;
310
- }
311
- | {
312
- schema:
313
- | "pi-workflows.human-decision-resolution.v1"
314
- | "pi-workflows.human-decision-resolution.v2";
329
+ schema: "pi-workflows.human-decision-resolution.v1";
315
330
  outcome: "cancelled";
316
331
  cancellation: HumanDecisionCancellationRecord;
317
332
  };
318
333
 
334
+ const INCOMPATIBLE_HUMAN_DECISION_STATE =
335
+ "Human decision state uses an incompatible alpha contract; reset the affected workflow run and decision state.";
336
+
337
+ function requireCurrentDecision(value: unknown): ResolvedHumanDecision {
338
+ const decision = requireRecord(value, "Resolved human decision");
339
+ if (
340
+ decision.schema !== "pi-workflows.human-decision-accepted.v1" ||
341
+ !Object.hasOwn(decision, "subjectDigest") ||
342
+ !Object.hasOwn(decision, "presentationDigest") ||
343
+ !Object.hasOwn(decision, "revision")
344
+ ) {
345
+ throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
346
+ }
347
+ return value as ResolvedHumanDecision;
348
+ }
349
+
350
+ function requireCurrentResolution(value: unknown): HumanDecisionResolution | null {
351
+ if (value === null) return null;
352
+ const resolution = requireRecord(value, "Human decision resolution");
353
+ if (resolution.schema !== "pi-workflows.human-decision-resolution.v1") {
354
+ throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
355
+ }
356
+ if (resolution.outcome === "accepted") {
357
+ return {
358
+ ...resolution,
359
+ decision: requireCurrentDecision(resolution.decision),
360
+ } as HumanDecisionResolution;
361
+ }
362
+ if (resolution.outcome === "cancelled") return value as HumanDecisionResolution;
363
+ throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
364
+ }
365
+
366
+ function requireCurrentDelivery(value: unknown): HumanDecisionDeliveryRecord {
367
+ const delivery = requireRecord(value, "Human decision delivery");
368
+ if (
369
+ delivery.schema !== "pi-workflows.human-decision-delivery.v1" ||
370
+ !Object.hasOwn(delivery, "presentationDigest") ||
371
+ !Object.hasOwn(delivery, "phase")
372
+ ) {
373
+ throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
374
+ }
375
+ return value as HumanDecisionDeliveryRecord;
376
+ }
377
+
319
378
  export class HumanDecisionStore {
320
379
  readonly root: string;
321
380
 
@@ -412,36 +471,35 @@ export class HumanDecisionStore {
412
471
  decisionId: request.decisionId,
413
472
  requestDigest: request.requestDigest,
414
473
  response,
474
+ provenance: "human" as const,
415
475
  source: normalized.source,
416
476
  idempotencyKey: normalized.idempotencyKey,
417
477
  acceptedAt: attemptedAt,
418
478
  answerDigest: digest({ response, source: normalized.source }),
419
479
  };
420
- const decision: AcceptedHumanDecision =
421
- request.schema === "pi-workflows.human-decision-request.v2"
422
- ? {
423
- schema: "pi-workflows.human-decision-accepted.v2",
424
- ...commonDecision,
425
- subjectDigest: request.subjectDigest,
426
- presentationDigest: request.presentationDigest,
427
- revision: request.revision,
428
- }
429
- : { schema: "pi-workflows.human-decision-accepted.v1", ...commonDecision };
480
+ const decision: AcceptedHumanDecision = {
481
+ schema: "pi-workflows.human-decision-accepted.v1",
482
+ ...commonDecision,
483
+ subjectDigest: request.subjectDigest,
484
+ presentationDigest: request.presentationDigest,
485
+ revision: request.revision,
486
+ };
430
487
  const resolutionPath = path.join(this.decisionDir(request.decisionId), "resolution.json");
431
- const resolution = {
432
- schema:
433
- decision.schema === "pi-workflows.human-decision-accepted.v2"
434
- ? ("pi-workflows.human-decision-resolution.v2" as const)
435
- : ("pi-workflows.human-decision-resolution.v1" as const),
436
- outcome: "accepted" as const,
488
+ const resolution: HumanDecisionResolution = {
489
+ schema: "pi-workflows.human-decision-resolution.v1",
490
+ outcome: "accepted",
437
491
  decision,
438
- } as HumanDecisionResolution;
492
+ };
439
493
  const result = await writeImmutableJson(resolutionPath, resolution, false);
440
494
  const winner =
441
495
  result === "created"
442
496
  ? resolution
443
497
  : ((await readJson(resolutionPath)) as HumanDecisionResolution | null);
444
498
  if (winner === null) throw new Error("Human decision resolution became unreadable");
499
+ const winningCancellation = await this.readCancellation(request.decisionId);
500
+ if (winningCancellation !== null) {
501
+ throw new Error(`Human decision request was ${winningCancellation.reason}`);
502
+ }
445
503
  if (winner.outcome === "cancelled") {
446
504
  throw new Error(`Human decision request was ${winner.cancellation.reason}`);
447
505
  }
@@ -455,6 +513,7 @@ export class HumanDecisionStore {
455
513
  return { status: "adopted", decision: existing };
456
514
  }
457
515
  if (
516
+ existing.provenance === "human" &&
458
517
  existing.idempotencyKey === decision.idempotencyKey &&
459
518
  canonicalJson(existing.response) === canonicalJson(decision.response) &&
460
519
  canonicalJson(existing.source) === canonicalJson(decision.source)
@@ -464,13 +523,74 @@ export class HumanDecisionStore {
464
523
  return { status: "conflict", decision: existing };
465
524
  }
466
525
 
526
+ async resolveTimeout(
527
+ request: HumanDecisionRequest,
528
+ now = new Date(),
529
+ ): Promise<HumanDecisionAcceptance> {
530
+ validateHumanDecisionRequestIntegrity(request);
531
+ if (request.expiresAt === undefined || request.defaultResponse === undefined) {
532
+ throw new Error("Human decision request has no timeout default");
533
+ }
534
+ if (Date.parse(request.expiresAt) > now.getTime()) {
535
+ throw new Error("Human decision timeout default is not eligible yet");
536
+ }
537
+ const response = validateHumanDecisionResponse(request, request.defaultResponse);
538
+ const commonDecision = {
539
+ decisionId: request.decisionId,
540
+ requestDigest: request.requestDigest,
541
+ response,
542
+ provenance: "timeout" as const,
543
+ acceptedAt: now.toISOString(),
544
+ answerDigest: digest({
545
+ provenance: "timeout",
546
+ requestDigest: request.requestDigest,
547
+ response,
548
+ }),
549
+ };
550
+ const decision: ResolvedHumanDecision = {
551
+ schema: "pi-workflows.human-decision-accepted.v1",
552
+ ...commonDecision,
553
+ subjectDigest: request.subjectDigest,
554
+ presentationDigest: request.presentationDigest,
555
+ revision: request.revision,
556
+ };
557
+ const resolutionPath = path.join(this.decisionDir(request.decisionId), "resolution.json");
558
+ const resolution: HumanDecisionResolution = {
559
+ schema: "pi-workflows.human-decision-resolution.v1",
560
+ outcome: "accepted",
561
+ decision,
562
+ };
563
+ const result = await writeImmutableJson(resolutionPath, resolution, false);
564
+ const winner =
565
+ result === "created"
566
+ ? resolution
567
+ : ((await readJson(resolutionPath)) as HumanDecisionResolution | null);
568
+ if (winner === null) throw new Error("Human decision resolution became unreadable");
569
+ const winningCancellation = await this.readCancellation(request.decisionId);
570
+ if (winningCancellation !== null) {
571
+ throw new Error(`Human decision request was ${winningCancellation.reason}`);
572
+ }
573
+ if (winner.outcome === "cancelled") {
574
+ throw new Error(`Human decision request was ${winner.cancellation.reason}`);
575
+ }
576
+ const existing = winner.decision;
577
+ await writeImmutableJson(
578
+ path.join(this.decisionDir(request.decisionId), "accepted.json"),
579
+ existing,
580
+ );
581
+ if (result === "created") return { status: "accepted", decision: existing };
582
+ return canonicalJson(existing) === canonicalJson(decision)
583
+ ? { status: "adopted", decision: existing }
584
+ : { status: "conflict", decision: existing };
585
+ }
586
+
467
587
  async listDeliveries(
468
588
  decisionId: string,
469
589
  channel: string,
470
590
  ): Promise<HumanDecisionDeliveryRecord[]> {
471
591
  const safeChannel = requireSimpleId(channel, "Human decision channel");
472
592
  const directory = path.join(this.decisionDir(decisionId), "deliveries", safeChannel);
473
- return (await readJsonDirectory(directory)) as HumanDecisionDeliveryRecord[];
593
+ return (await readJsonDirectory(directory)).map(requireCurrentDelivery);
474
594
  }
475
595
 
476
596
  async recordSettlement(
@@ -505,8 +625,8 @@ export class HumanDecisionStore {
505
625
  reason: HumanDecisionCancellationRecord["reason"],
506
626
  ): Promise<"created" | "adopted"> {
507
627
  validateHumanDecisionRequestIntegrity(request);
508
- if ((await this.readAccepted(request.decisionId)) !== null) {
509
- throw new Error("Accepted human decision cannot be cancelled");
628
+ if ((await this.readResolved(request.decisionId)) !== null) {
629
+ throw new Error("Resolved human decision cannot be cancelled");
510
630
  }
511
631
  const filePath = path.join(this.decisionDir(request.decisionId), "cancelled.json");
512
632
  const record: HumanDecisionCancellationRecord = {
@@ -518,10 +638,7 @@ export class HumanDecisionStore {
518
638
  };
519
639
  const resolutionPath = path.join(this.decisionDir(request.decisionId), "resolution.json");
520
640
  const resolution: HumanDecisionResolution = {
521
- schema:
522
- request.schema === "pi-workflows.human-decision-request.v2"
523
- ? "pi-workflows.human-decision-resolution.v2"
524
- : "pi-workflows.human-decision-resolution.v1",
641
+ schema: "pi-workflows.human-decision-resolution.v1",
525
642
  outcome: "cancelled",
526
643
  cancellation: record,
527
644
  };
@@ -532,7 +649,7 @@ export class HumanDecisionStore {
532
649
  : ((await readJson(resolutionPath)) as HumanDecisionResolution | null);
533
650
  if (winner === null) throw new Error("Human decision resolution became unreadable");
534
651
  if (winner.outcome === "accepted") {
535
- throw new Error("Accepted human decision cannot be cancelled");
652
+ throw new Error("Resolved human decision cannot be cancelled");
536
653
  }
537
654
  const existing = winner.cancellation;
538
655
  if (
@@ -551,9 +668,9 @@ export class HumanDecisionStore {
551
668
  path.join(this.decisionDir(decisionId), "cancelled.json"),
552
669
  )) as HumanDecisionCancellationRecord | null;
553
670
  if (stored !== null) return stored;
554
- const resolution = (await readJson(
555
- path.join(this.decisionDir(decisionId), "resolution.json"),
556
- )) as HumanDecisionResolution | null;
671
+ const resolution = requireCurrentResolution(
672
+ await readJson(path.join(this.decisionDir(decisionId), "resolution.json")),
673
+ );
557
674
  if (resolution?.outcome !== "cancelled") return null;
558
675
  await writeImmutableJson(
559
676
  path.join(this.decisionDir(decisionId), "cancelled.json"),
@@ -562,14 +679,14 @@ export class HumanDecisionStore {
562
679
  return resolution.cancellation;
563
680
  }
564
681
 
565
- async readAccepted(decisionId: string): Promise<AcceptedHumanDecision | null> {
566
- const stored = (await readJson(
567
- path.join(this.decisionDir(decisionId), "accepted.json"),
568
- )) as AcceptedHumanDecision | null;
569
- if (stored !== null) return stored;
570
- const resolution = (await readJson(
571
- path.join(this.decisionDir(decisionId), "resolution.json"),
572
- )) as HumanDecisionResolution | null;
682
+ async readResolved(decisionId: string): Promise<ResolvedHumanDecision | null> {
683
+ const cancellation = await readJson(path.join(this.decisionDir(decisionId), "cancelled.json"));
684
+ if (cancellation !== null) return null;
685
+ const stored = await readJson(path.join(this.decisionDir(decisionId), "accepted.json"));
686
+ if (stored !== null) return requireCurrentDecision(stored);
687
+ const resolution = requireCurrentResolution(
688
+ await readJson(path.join(this.decisionDir(decisionId), "resolution.json")),
689
+ );
573
690
  if (resolution?.outcome !== "accepted") return null;
574
691
  await writeImmutableJson(
575
692
  path.join(this.decisionDir(decisionId), "accepted.json"),
@@ -23,8 +23,8 @@ export {
23
23
  decisionPresentationFingerprint,
24
24
  digestCanonical,
25
25
  humanDecisionChannelRequest,
26
- legacyDecisionPresentation,
27
26
  normalizeDecisionPresentation,
27
+ valueDecisionPresentation,
28
28
  validateHumanDecisionRequestIntegrity,
29
29
  type DecisionDocumentSegment,
30
30
  } from "./decision-presentation.js";
@@ -74,6 +74,21 @@ export {
74
74
  type WorkflowSearchPaths,
75
75
  } from "./loader.js";
76
76
  export { renderShellCommand, runShellAction } from "./shell.js";
77
+ export {
78
+ COMMAND_BATCH_RESULT_SCHEMA,
79
+ MAX_COMMAND_BATCH_CONCURRENCY,
80
+ MAX_COMMAND_BATCH_ITEMS,
81
+ MAX_COMMAND_BATCH_OUTPUT_CHARS,
82
+ MAX_COMMAND_BATCH_TIMEOUT_MS,
83
+ runCommandBatch,
84
+ validateCommandBatchRequest,
85
+ type CommandBatchItem,
86
+ type CommandBatchItemOutcome,
87
+ type CommandBatchItemResult,
88
+ type CommandBatchRequest,
89
+ type CommandBatchResult,
90
+ type RunCommandBatchOptions,
91
+ } from "./command-batch.js";
77
92
  export {
78
93
  estimateProgress,
79
94
  formatProgressLine,
@@ -122,8 +137,8 @@ export {
122
137
  } from "./store.js";
123
138
  export type {
124
139
  AcceptedHumanDecision,
125
- AcceptedHumanDecisionV1,
126
- AcceptedHumanDecisionV2,
140
+ DefaultedHumanDecision,
141
+ ResolvedHumanDecision,
127
142
  AgentNodeDefinition,
128
143
  AgentStepContract,
129
144
  AgentStepExecutor,
@@ -152,24 +167,18 @@ export type {
152
167
  HumanDecisionChoiceMap,
153
168
  HumanDecisionContinuationRecord,
154
169
  HumanDecisionDeliveryRecord,
155
- HumanDecisionDeliveryRecordV1,
156
- HumanDecisionDeliveryRecordV2,
157
170
  HumanDecisionNodeContract,
158
171
  HumanDecisionPrompt,
159
172
  HumanDecisionReceipt,
160
- HumanDecisionReceiptV1,
161
- HumanDecisionReceiptV2,
162
173
  HumanDecisionRequest,
163
- HumanDecisionRequestV1,
164
- HumanDecisionRequestV2,
165
174
  HumanDecisionResponse,
166
175
  HumanDecisionSettlementRecord,
167
176
  HumanDecisionSubmission,
168
177
  HumanDecisionTextInput,
169
- LegacyHumanDecisionPrompt,
178
+ HumanDecisionTimeout,
179
+ HumanDecisionTimeoutPolicy,
170
180
  MaybePromise,
171
181
  NotifyNodeDefinition,
172
- PresentedHumanDecisionPrompt,
173
182
  ShellActionExecution,
174
183
  ShellActionNodeDefinition,
175
184
  ShellActionResult,
@@ -18,6 +18,7 @@ export type ProgressEstimate = {
18
18
  confidence?: ProgressConfidence;
19
19
  sourceEstimatedFinishAt?: string;
20
20
  unavailableReason?: string;
21
+ elapsedMs?: number;
21
22
  };
22
23
 
23
24
  export type ProgressSample = {
@@ -42,7 +43,16 @@ export function estimateProgress(
42
43
  for (const sample of samples) validateProgressData(sample.data as Record<string, unknown>);
43
44
  const latest = samples.at(-1) as ProgressSample;
44
45
  const data = latest.data;
45
- const base: ProgressEstimate = { key, data, sampleCount: 1 };
46
+ const firstAt = Date.parse(samples[0]!.at);
47
+ const endAt = TERMINAL.has(data.status) ? Date.parse(latest.at) : now.getTime();
48
+ const elapsedMs =
49
+ Number.isFinite(firstAt) && Number.isFinite(endAt) ? Math.max(0, endAt - firstAt) : undefined;
50
+ const base: ProgressEstimate = {
51
+ key,
52
+ data,
53
+ sampleCount: 1,
54
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
55
+ };
46
56
  if (TERMINAL.has(data.status)) return base;
47
57
  const sourceFinish = validSourceFinish(latest, now);
48
58
  if (sourceFinish !== undefined) base.sourceEstimatedFinishAt = sourceFinish;
@@ -212,10 +222,15 @@ export function formatProgressLine(estimate: ProgressEstimate, now = new Date())
212
222
  ? `${formatRemaining(estimate.remainingLowMs)}–${formatRemaining(estimate.remainingHighMs)}`
213
223
  : formatRemaining(estimate.remainingMedianMs);
214
224
  eta = `ETA ${range}`;
215
- } else if (!TERMINAL.has(data.status)) {
225
+ } else if (!TERMINAL.has(data.status) && data.phase === undefined) {
216
226
  eta = `ETA unavailable${estimate.unavailableReason ? ` (${estimate.unavailableReason})` : ""}`;
217
227
  }
218
- return [label, count, eta].filter(Boolean).join(" ");
228
+ const phase = data.phase === undefined ? "" : sanitizeText(data.phase);
229
+ const elapsed =
230
+ data.phase === undefined || estimate.elapsedMs === undefined
231
+ ? ""
232
+ : `elapsed ${formatRemaining(estimate.elapsedMs)}`;
233
+ return [label, count, phase, elapsed, eta].filter(Boolean).join(" ");
219
234
  }
220
235
 
221
236
  export function formatProgressReport(
@@ -35,10 +35,11 @@ function assertOptionalFunction(value: unknown, description: string): void {
35
35
  function assertCommonNodeFields(node: WorkflowNodeDefinition, nodeId: string): void {
36
36
  if (
37
37
  node.timeoutMs !== undefined &&
38
+ node.timeoutMs !== null &&
38
39
  typeof node.timeoutMs !== "function" &&
39
40
  (typeof node.timeoutMs !== "number" || !Number.isFinite(node.timeoutMs) || node.timeoutMs <= 0)
40
41
  ) {
41
- fail(`node ${nodeId} timeoutMs must be a finite positive number or function`);
42
+ fail(`node ${nodeId} timeoutMs must be null, a finite positive number, or a function`);
42
43
  }
43
44
  if (node.statusDetail !== undefined && typeof node.statusDetail !== "string") {
44
45
  fail(`node ${nodeId} statusDetail must be a string`);
@@ -150,6 +151,23 @@ export function assertValidCheckpointNode(
150
151
  if (typeof node.humanDecision.request !== "function") {
151
152
  fail(`node ${nodeId} humanDecision request must be a function`);
152
153
  }
154
+ if (
155
+ node.humanDecision.onTimeout !== undefined &&
156
+ typeof node.humanDecision.onTimeout !== "function"
157
+ ) {
158
+ assertRecord(node.humanDecision.onTimeout, `node ${nodeId} humanDecision onTimeout`);
159
+ if (
160
+ typeof node.humanDecision.onTimeout.afterMs !== "number" ||
161
+ !Number.isFinite(node.humanDecision.onTimeout.afterMs) ||
162
+ node.humanDecision.onTimeout.afterMs <= 0
163
+ ) {
164
+ fail(`node ${nodeId} humanDecision onTimeout afterMs must be a finite positive number`);
165
+ }
166
+ assertRecord(
167
+ node.humanDecision.onTimeout.response,
168
+ `node ${nodeId} humanDecision onTimeout response`,
169
+ );
170
+ }
153
171
  }
154
172
  assertCommonNodeFields(node, nodeId);
155
173
  }
@@ -26,6 +26,16 @@ export function renderShellCommand(command: string, args: string[]): string {
26
26
  return renderedArgs.length > 0 ? `${command} ${renderedArgs}` : command;
27
27
  }
28
28
 
29
+ export function shellOutputTruncation(result: ShellActionResult): {
30
+ stdout: boolean;
31
+ stderr: boolean;
32
+ } {
33
+ return {
34
+ stdout: result.stdout.endsWith(TRUNCATION_MARKER),
35
+ stderr: result.stderr.endsWith(TRUNCATION_MARKER),
36
+ };
37
+ }
38
+
29
39
  function shellFailure(
30
40
  spec: ShellActionExecution,
31
41
  args: string[],
@@ -1417,7 +1417,9 @@ function snapshotNode(
1417
1417
  : exit !== undefined
1418
1418
  ? { includeTransition: "exit" as const }
1419
1419
  : {}),
1420
- ...(typeof node.timeoutMs === "number" ? { timeoutMs: node.timeoutMs } : {}),
1420
+ ...(typeof node.timeoutMs === "number" || node.timeoutMs === null
1421
+ ? { timeoutMs: node.timeoutMs }
1422
+ : {}),
1421
1423
  ...(node.statusDetail !== undefined ? { statusDetail: node.statusDetail } : {}),
1422
1424
  };
1423
1425
  if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
@@ -1435,6 +1437,11 @@ function snapshotNode(
1435
1437
  typeof node.humanDecision.audience === "string" ? node.humanDecision.audience : "<dynamic>",
1436
1438
  ...(typeof node.humanDecision.audience === "function" ? { dynamicAudience: true } : {}),
1437
1439
  choices: structuredClone(node.humanDecision.choices),
1440
+ ...(node.humanDecision.onTimeout !== undefined &&
1441
+ typeof node.humanDecision.onTimeout !== "function"
1442
+ ? { onTimeout: structuredClone(node.humanDecision.onTimeout) }
1443
+ : {}),
1444
+ ...(typeof node.humanDecision.onTimeout === "function" ? { dynamicTimeout: true } : {}),
1438
1445
  };
1439
1446
  }
1440
1447
  if (node.nodeType === "action") {