pi-subagents 0.66.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, resolve as resolvePath } from "node:path";
4
4
  import { Worker } from "node:worker_threads";
@@ -24,11 +24,22 @@ export interface WorkflowScriptValidationError {
24
24
  message: string;
25
25
  line?: number;
26
26
  column?: number;
27
+ kind?: "spawn-budget";
28
+ }
29
+
30
+ export interface WorkflowScriptValidationWarning {
31
+ message: string;
32
+ kind: "dynamic-spawn-count";
27
33
  }
28
34
 
29
35
  export interface WorkflowScriptValidationResult {
30
36
  ok: boolean;
31
37
  errors: WorkflowScriptValidationError[];
38
+ warnings?: WorkflowScriptValidationWarning[];
39
+ }
40
+
41
+ export interface WorkflowScriptValidationOptions {
42
+ maxSubagentSpawnsPerRun?: number;
32
43
  }
33
44
 
34
45
  const WORKER_SOURCE = String.raw`
@@ -539,7 +550,7 @@ function runLanes(laneSpecs) {
539
550
  generatedKey: stage.generatedKey,
540
551
  ...workflowPlanStringMetadata(stage.params),
541
552
  ...(typeof stage.params.as === "string" && stage.params.as.trim() ? { outputName: stage.params.as.trim() } : {}),
542
- ...(stage.params.outputSchema !== undefined ? { structured: true } : {}),
553
+ ...(stage.params.outputSchema ? { structured: true } : {}),
543
554
  })),
544
555
  })) });
545
556
  const firstItems = lanes.map((lane) => {
@@ -696,8 +707,26 @@ function validateHostCommand(key, params) {
696
707
  hostKeys.add(key);
697
708
  }
698
709
 
710
+ const RUNS_ALL_PERMISSIVE_WARNING = "runs.all received runs.run(...) launch promise(s) or thenable(s). Those children were already launched individually, so runs.all cannot apply batch fingerprint validation, batch grouping in the fleet view, or collectFailure semantics; a failed child will throw at the runs.all boundary instead of returning as { ok: false }. To get full runs.all semantics, pass config objects: runs.all(items.map((item) => ({ key: item.key, agent: item.agent, task: item.task }))).";
711
+
712
+ function runsAllItemThenableInfo(item, index) {
713
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
714
+ if (typeof item.then !== "function") return null;
715
+ const tracker = promiseObservationTracker(item);
716
+ const observation = tracker && Array.isArray(tracker.observations) ? tracker.observations.find((entry) => entry && entry.operation === "run") : undefined;
717
+ return {
718
+ key: observation && typeof observation.key === "string" ? observation.key : ("runs.all[" + index + "]"),
719
+ callId: observation && typeof observation.callId === "number" ? observation.callId : null,
720
+ promise: item,
721
+ };
722
+ }
723
+
724
+ let warnedPermissiveRunsAll = false;
725
+
699
726
  function launchRunsAll(items, generatedLaneKeys) {
700
727
  if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
728
+ const anyThenable = items.some((item, index) => runsAllItemThenableInfo(item, index) !== null);
729
+ if (anyThenable) return launchRunsAllPermissive(items);
701
730
  const fingerprints = new Map(runFingerprints);
702
731
  const calls = [];
703
732
  for (let index = 0; index < items.length; index++) {
@@ -714,6 +743,33 @@ function launchRunsAll(items, generatedLaneKeys) {
714
743
  return { calls, launched };
715
744
  }
716
745
 
746
+ function launchRunsAllPermissive(items) {
747
+ if (!warnedPermissiveRunsAll) {
748
+ warnedPermissiveRunsAll = true;
749
+ try { capturedConsole.warn(RUNS_ALL_PERMISSIVE_WARNING); } catch { /* console emission is best-effort */ }
750
+ }
751
+ const fingerprints = new Map(runFingerprints);
752
+ const calls = [];
753
+ const launched = [];
754
+ for (let index = 0; index < items.length; index++) {
755
+ if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
756
+ const item = items[index];
757
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object or a runs.run(...) launch promise.");
758
+ const thenable = runsAllItemThenableInfo(item, index);
759
+ if (thenable) {
760
+ calls.push({ key: thenable.key, params: {} });
761
+ launched.push(thenable);
762
+ continue;
763
+ }
764
+ const { key, ...params } = item;
765
+ validateRunCall(key, params, "runs.all item " + index, fingerprints);
766
+ calls.push({ key, params });
767
+ launched.push(null);
768
+ }
769
+ runFingerprints = fingerprints;
770
+ return { calls, launched: launched.map((entry, index) => entry ?? runHostCall(calls[index].key, calls[index].params, false, undefined)) };
771
+ }
772
+
717
773
  const runs = Object.freeze({
718
774
  run(key, params) {
719
775
  validateRunCall(key, params, "runs.run", runFingerprints);
@@ -722,7 +778,7 @@ const runs = Object.freeze({
722
778
  },
723
779
  all(items) {
724
780
  const { calls, launched } = launchRunsAll(items);
725
- return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), Promise.all(launched.map(({ promise }) => promise)).then((results) => wrapRunsAllResults(results.map(decorateWorkflowChildResult), calls.map(({ key }) => key))));
781
+ return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), trackPromiseCombinator(launched.map(({ promise }) => promise), (values) => Promise.all(values).then((results) => wrapRunsAllResults(results.map(decorateWorkflowChildResult), calls.map(({ key }) => key)))));
726
782
  },
727
783
  lanes(laneSpecs) {
728
784
  return runLanes(laneSpecs);
@@ -896,6 +952,12 @@ function omitUndefinedWorkflowValues(value, seen = new Set()) {
896
952
  return normalized;
897
953
  }
898
954
 
955
+ function deepFreezeWorkflowArgs(value) {
956
+ if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
957
+ for (const entry of Object.values(value)) deepFreezeWorkflowArgs(entry);
958
+ return Object.freeze(value);
959
+ }
960
+
899
961
  parentPort.on("message", async (message) => {
900
962
  if (message.type === "response") {
901
963
  const entry = pending.get(message.callId);
@@ -915,6 +977,9 @@ parentPort.on("message", async (message) => {
915
977
  if (message.stateEnabled) sandbox.state = state;
916
978
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
917
979
  contextObjectPrototype = vm.runInContext("Object.prototype", context);
980
+ // Rebuild args inside the VM realm: a worker-realm object would expose the worker's unrestricted
981
+ // Function through args.constructor.constructor, bypassing codeGeneration.strings: false.
982
+ sandbox.args = deepFreezeWorkflowArgs(vm.runInContext("JSON.parse", context)(JSON.stringify(message.args ?? {})));
918
983
  let compiled;
919
984
  try {
920
985
  assertPortableWorkflowScript(message.script);
@@ -1099,8 +1164,26 @@ export class WorkflowScriptError extends Error {
1099
1164
  }
1100
1165
  }
1101
1166
 
1167
+ export type WorkflowChildSettledOutcome = "completed" | "failed" | "paused" | "stopped";
1168
+
1169
+ export interface WorkflowChildSettledNotification {
1170
+ workflowRunId: string;
1171
+ childKey: string;
1172
+ childRunId?: string;
1173
+ outcome: WorkflowChildSettledOutcome;
1174
+ outputReference?: string;
1175
+ error?: string;
1176
+ workflowRunning: boolean;
1177
+ }
1178
+
1102
1179
  export interface RunWorkflowScriptOptions {
1103
1180
  script: string;
1181
+ /** Normalized raw-script input exposed as the deeply frozen sandbox global `args`. */
1182
+ args?: Readonly<Record<string, unknown>>;
1183
+ /** Parent-session cwd used to recover a stale process cwd. */
1184
+ processCwd?: string;
1185
+ /** Workflow run ID for notifications. Required when onChildSettled is provided. */
1186
+ workflowRunId?: string;
1104
1187
  /** Host-only first-slice admission context. It is never sent to the workflow worker. */
1105
1188
  oneUsePermit?: { claim: (key: string) => string | undefined };
1106
1189
  timeoutMs?: number;
@@ -1109,7 +1192,7 @@ export interface RunWorkflowScriptOptions {
1109
1192
  continueAfterAbortWhenChildrenSettled?: (abortError: Error) => boolean;
1110
1193
  /** Maximum children executing concurrently within this workflow. Defaults to 20. */
1111
1194
  globalConcurrencyLimit?: number;
1112
- admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
1195
+ admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>, signal: AbortSignal) => void | Promise<void>;
1113
1196
  launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
1114
1197
  resolveResume?: (reference: WorkflowReceiptResumeReference | string, signal: AbortSignal, index?: number) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
1115
1198
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
@@ -1124,6 +1207,7 @@ export interface RunWorkflowScriptOptions {
1124
1207
  onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
1125
1208
  onLanePlan?: (lanes: WorkflowLanePlan[]) => void;
1126
1209
  onEmit?: (emits: unknown[]) => void;
1210
+ onChildSettled?: (notification: WorkflowChildSettledNotification) => void;
1127
1211
  }
1128
1212
 
1129
1213
  function combinedAbortSignal(signals: AbortSignal[]): AbortSignal {
@@ -1426,7 +1510,7 @@ function literalString(node: unknown): string | undefined {
1426
1510
  return undefined;
1427
1511
  }
1428
1512
 
1429
- function directRunsCall(node: unknown, method: "run" | "all" | "host"): node is AstNode {
1513
+ function directRunsCall(node: unknown, method: "run" | "all" | "host" | "lanes"): boolean {
1430
1514
  if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
1431
1515
  const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
1432
1516
  return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
@@ -1563,8 +1647,121 @@ function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }>
1563
1647
  });
1564
1648
  }
1565
1649
 
1650
+ function containsWorkflowLaunch(node: unknown): boolean {
1651
+ let found = false;
1652
+ walkAst(node, (candidate) => {
1653
+ if (directRunsCall(candidate, "run") || directRunsCall(candidate, "all") || directRunsCall(candidate, "lanes")) found = true;
1654
+ });
1655
+ return found;
1656
+ }
1657
+
1658
+ function containsRunsIdentifier(node: unknown): boolean {
1659
+ let found = false;
1660
+ walkAst(node, (candidate) => {
1661
+ if (candidate.type === "Identifier" && candidate.name === "runs") found = true;
1662
+ });
1663
+ return found;
1664
+ }
1665
+
1666
+ function invalidatesRunsBinding(workflowBody: AstNode): boolean {
1667
+ let invalidated = false;
1668
+ walkAst(workflowBody, (node) => {
1669
+ if ((node.type === "VariableDeclarator" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression")
1670
+ && containsRunsIdentifier(node.id)) invalidated = true;
1671
+ if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && Array.isArray(node.params)
1672
+ && node.params.some(containsRunsIdentifier)) invalidated = true;
1673
+ if (node.type === "CatchClause" && containsRunsIdentifier(node.param)) invalidated = true;
1674
+ if ((node.type === "AssignmentExpression" || node.type === "UpdateExpression") && containsRunsIdentifier(node.left ?? node.argument)) invalidated = true;
1675
+ });
1676
+ return invalidated;
1677
+ }
1678
+
1679
+ function containsAbruptControl(node: AstNode): boolean {
1680
+ let found = false;
1681
+ walkAst(node, (candidate) => {
1682
+ if (candidate !== node && (candidate.type === "ReturnStatement" || candidate.type === "ThrowStatement" || candidate.type === "BreakStatement" || candidate.type === "ContinueStatement")) found = true;
1683
+ }, false);
1684
+ return found;
1685
+ }
1686
+
1687
+ function directLaunchCall(expression: unknown): AstNode | undefined {
1688
+ if (!astNode(expression)) return undefined;
1689
+ const candidate = expression.type === "AwaitExpression" && astNode(expression.argument) ? expression.argument : expression;
1690
+ return directRunsCall(candidate, "run") || directRunsCall(candidate, "all") ? candidate : undefined;
1691
+ }
1692
+
1693
+ function staticWorkflowLaunchPlan(workflowBody: AstNode): { keys: string[]; dynamic: boolean } {
1694
+ if (workflowBody.type !== "BlockStatement" || !Array.isArray(workflowBody.body) || invalidatesRunsBinding(workflowBody)) {
1695
+ return { keys: [], dynamic: containsWorkflowLaunch(workflowBody) };
1696
+ }
1697
+ const keys = new Set<string>();
1698
+ let dynamic = false;
1699
+ let remainderUncertain = false;
1700
+ const addCall = (call: AstNode): void => {
1701
+ const args = Array.isArray(call.arguments) ? call.arguments : [];
1702
+ let nestedLaunch = false;
1703
+ for (const argument of args) walkAst(argument, (node) => {
1704
+ if (node !== call && (directRunsCall(node, "run") || directRunsCall(node, "all") || directRunsCall(node, "lanes"))) nestedLaunch = true;
1705
+ });
1706
+ if (nestedLaunch) {
1707
+ dynamic = true;
1708
+ return;
1709
+ }
1710
+ if (directRunsCall(call, "run")) {
1711
+ const key = literalString(args[0]);
1712
+ if (key === undefined) dynamic = true;
1713
+ else keys.add(key);
1714
+ return;
1715
+ }
1716
+ const array = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0] : undefined;
1717
+ if (!array) {
1718
+ dynamic = true;
1719
+ return;
1720
+ }
1721
+ const batchKeys: string[] = [];
1722
+ for (const item of array.elements as unknown[]) {
1723
+ if (!astNode(item) || item.type !== "ObjectExpression" || !Array.isArray(item.properties)
1724
+ || item.properties.some((property) => !astNode(property) || property.type !== "Property" || staticPropertyKey(property) === undefined)) {
1725
+ dynamic = true;
1726
+ return;
1727
+ }
1728
+ const key = literalString(directObjectPropertyValue(item, "key"));
1729
+ if (key === undefined) {
1730
+ dynamic = true;
1731
+ return;
1732
+ }
1733
+ batchKeys.push(key);
1734
+ }
1735
+ for (const key of batchKeys) keys.add(key);
1736
+ };
1737
+ for (const statement of workflowBody.body) {
1738
+ if (!astNode(statement)) continue;
1739
+ if (remainderUncertain) {
1740
+ if (containsWorkflowLaunch(statement)) dynamic = true;
1741
+ continue;
1742
+ }
1743
+ const expressions: unknown[] = [];
1744
+ if (statement.type === "VariableDeclaration" && Array.isArray(statement.declarations)) {
1745
+ for (const declaration of statement.declarations) if (astNode(declaration)) expressions.push(declaration.init);
1746
+ } else if (statement.type === "ExpressionStatement") expressions.push(statement.expression);
1747
+ else if (statement.type === "ReturnStatement" || statement.type === "ThrowStatement") expressions.push(statement.argument);
1748
+ let handledLaunch = false;
1749
+ for (const expression of expressions) {
1750
+ const call = directLaunchCall(expression);
1751
+ if (call) {
1752
+ addCall(call);
1753
+ handledLaunch = true;
1754
+ } else if (containsWorkflowLaunch(expression)) dynamic = true;
1755
+ }
1756
+ if (!handledLaunch && expressions.length === 0 && containsWorkflowLaunch(statement)) dynamic = true;
1757
+ if (statement.type === "ReturnStatement" || statement.type === "ThrowStatement") remainderUncertain = true;
1758
+ else if (containsAbruptControl(statement)) remainderUncertain = true;
1759
+ }
1760
+ return { keys: [...keys], dynamic };
1761
+ }
1762
+
1566
1763
  /** Parse a workflowScript and apply only rules that are decidable from its local syntax. */
1567
- export function validateWorkflowScript(script: string): WorkflowScriptValidationResult {
1764
+ export function validateWorkflowScript(script: string, options: WorkflowScriptValidationOptions = {}): WorkflowScriptValidationResult {
1568
1765
  const errors: WorkflowScriptValidationError[] = [];
1569
1766
  if (!script.trim()) return { ok: false, errors: [{ message: "workflowScript must not be empty." }] };
1570
1767
  let root: AstNode;
@@ -1631,7 +1828,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
1631
1828
  const statement = workflowBody.body[statementIndex];
1632
1829
  if (!astNode(statement) || statement.type !== "VariableDeclaration" || !Array.isArray(statement.declarations)) continue;
1633
1830
  for (const declaration of statement.declarations) {
1634
- if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !directRunsCall(declaration.init.argument, "all")) continue;
1831
+ if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !astNode(declaration.init.argument) || !directRunsCall(declaration.init.argument, "all")) continue;
1635
1832
  const name = declaration.id.name as string;
1636
1833
  const keys = new Set(directRunsAllKeys(declaration.init.argument).map((entry) => entry.key));
1637
1834
  if (keys.size === 0) continue;
@@ -1647,8 +1844,24 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
1647
1844
  }
1648
1845
  }
1649
1846
 
1847
+ const warnings: WorkflowScriptValidationWarning[] = [];
1848
+ if (options.maxSubagentSpawnsPerRun !== undefined) {
1849
+ const plan = staticWorkflowLaunchPlan(workflowBody);
1850
+ if (plan.keys.length > options.maxSubagentSpawnsPerRun) {
1851
+ errors.push({
1852
+ kind: "spawn-budget",
1853
+ message: `workflowScript statically requires child launches ${plan.keys.map((key) => `'${key}'`).join(", ")}; minimum required: ${plan.keys.length}; configured: ${options.maxSubagentSpawnsPerRun}.`,
1854
+ });
1855
+ }
1856
+ if (plan.dynamic) {
1857
+ warnings.push({
1858
+ kind: "dynamic-spawn-count",
1859
+ message: `workflowScript contains dynamic child launches; static validation proved ${plan.keys.length} launch(es), so runtime fan-out enforcement remains authoritative for the configured budget of ${options.maxSubagentSpawnsPerRun}.`,
1860
+ });
1861
+ }
1862
+ }
1650
1863
  const unique = errors.filter((error, index) => errors.findIndex((candidate) => candidate.message === error.message && candidate.line === error.line && candidate.column === error.column) === index);
1651
- return { ok: unique.length === 0, errors: unique };
1864
+ return { ok: unique.length === 0, errors: unique, ...(warnings.length > 0 ? { warnings } : {}) };
1652
1865
  }
1653
1866
  function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
1654
1867
  return {
@@ -1722,6 +1935,32 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1722
1935
  }
1723
1936
  const launchSemaphore = new Semaphore(options.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
1724
1937
 
1938
+ if (options.processCwd !== undefined) {
1939
+ let staleCwd = false;
1940
+ try {
1941
+ realpathSync(process.cwd());
1942
+ } catch (error) {
1943
+ const code = typeof error === "object" && error !== null && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
1944
+ if (code !== "ENOENT") throw new Error("Workflow current cwd could not be validated.", { cause: error });
1945
+ staleCwd = true;
1946
+ }
1947
+ try {
1948
+ if (staleCwd) process.chdir(options.processCwd);
1949
+ else {
1950
+ const target = realpathSync(options.processCwd);
1951
+ if (!statSync(target).isDirectory()) {
1952
+ const error = new Error(`ENOTDIR: not a directory, access '${target}'`) as NodeJS.ErrnoException;
1953
+ error.code = "ENOTDIR";
1954
+ throw error;
1955
+ }
1956
+ accessSync(target, constants.X_OK);
1957
+ }
1958
+ } catch (error) {
1959
+ const detail = error instanceof Error ? error.message : String(error);
1960
+ throw new Error(`Workflow process cwd is unavailable: ${options.processCwd}: ${detail}`, { cause: error });
1961
+ }
1962
+ }
1963
+
1725
1964
  let acornPath: string;
1726
1965
  try {
1727
1966
  acornPath = resolveWorkflowParserEntry();
@@ -1812,6 +2051,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1812
2051
  traceChanged();
1813
2052
  return true;
1814
2053
  };
2054
+ const notifyChildSettled = (key: string, result: WorkflowScriptChildResult): void => {
2055
+ if (!options.onChildSettled || !options.workflowRunId) return;
2056
+ const outcome: WorkflowChildSettledOutcome = result.ok
2057
+ ? "completed"
2058
+ : result.stopped
2059
+ ? "stopped"
2060
+ : result.detached
2061
+ ? "paused"
2062
+ : "failed";
2063
+ const outputReference = result.outputReference ?? result.artifactPaths[0];
2064
+ try {
2065
+ options.onChildSettled({
2066
+ workflowRunId: options.workflowRunId,
2067
+ childKey: key,
2068
+ ...(result.runId ? { childRunId: result.runId } : {}),
2069
+ outcome,
2070
+ ...(outputReference ? { outputReference } : {}),
2071
+ ...(!result.ok && result.error ? { error: result.error } : {}),
2072
+ workflowRunning: !settled && !finishing,
2073
+ });
2074
+ } catch (error) {
2075
+ console.error("Workflow onChildSettled callback failed:", error);
2076
+ }
2077
+ };
1815
2078
  options.registerStopChild?.(stopChild);
1816
2079
 
1817
2080
  return await new Promise<WorkflowScriptResult>((resolve, reject) => {
@@ -2192,7 +2455,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2192
2455
  admission = Promise.resolve().then(() => {
2193
2456
  if (settled || finishing) return;
2194
2457
  for (const call of calls) assertRecoveryBarrierAllowsRun(call.key, call.params);
2195
- return options.admit?.(calls);
2458
+ return options.admit?.(calls, childController.signal);
2196
2459
  });
2197
2460
  if (batch) batchAdmissions.set(batch.id, admission);
2198
2461
  }
@@ -2249,6 +2512,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2249
2512
  } finally {
2250
2513
  launchSemaphore.release();
2251
2514
  }
2515
+ }, (error: unknown) => {
2516
+ if (!childController.signal.aborted) throw error;
2517
+ const reason = childController.signal.reason;
2518
+ return stoppedChildResult(key, reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.");
2252
2519
  }).then((result) => {
2253
2520
  let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
2254
2521
  if (resolvedResumeLineage?.length && normalized.runId) {
@@ -2261,6 +2528,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2261
2528
  const state = normalized.ok ? "completed" : normalized.stopped ? "stopped" : normalized.detached ? "detached" : "failed";
2262
2529
  trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
2263
2530
  traceChanged();
2531
+ notifyChildSettled(key, normalized);
2264
2532
  return normalized;
2265
2533
  }, (error: unknown) => {
2266
2534
  const text = error instanceof Error ? error.message : String(error);
@@ -2270,6 +2538,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2270
2538
  children.set(key, failure);
2271
2539
  trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), error: text });
2272
2540
  traceChanged();
2541
+ notifyChildSettled(key, failure);
2273
2542
  return failure;
2274
2543
  });
2275
2544
  launches.set(key, { fingerprint, promise, observed: callObserved, ...(generatedLaneKey ? { generatedLaneKey } : {}) });
@@ -2279,6 +2548,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
2279
2548
  respond(deliver(promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
2280
2549
  });
2281
2550
 
2282
- worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });
2551
+ worker.postMessage({ type: "start", script: options.script, ...(options.args ? { args: options.args } : {}), stateEnabled: options.state !== undefined });
2283
2552
  });
2284
2553
  }
@@ -186,7 +186,7 @@ function duration(step: Pick<WorkflowChecklistStep, "durationMs" | "startedAt" |
186
186
  const startedAt = finite(step.startedAt);
187
187
  if (explicit !== undefined) return Math.max(0, explicit);
188
188
  if (startedAt === undefined) return undefined;
189
- const end = state === "running" ? now : finite(step.endedAt) ?? now;
189
+ const end = state === "running" ? now : finite(step.endedAt);
190
190
  return end === undefined ? undefined : Math.max(0, end - startedAt);
191
191
  }
192
192
 
@@ -296,7 +296,7 @@ function priority(item: WorkflowChecklistItem): number {
296
296
  }
297
297
 
298
298
  function applyNow(item: WorkflowChecklistItem, now: number | undefined): WorkflowChecklistItem {
299
- return item.durationMs === undefined && item.startedAt !== undefined && now !== undefined ? { ...item, durationMs: Math.max(0, now - item.startedAt) } : item;
299
+ return item.state === "running" && item.startedAt !== undefined && now !== undefined ? { ...item, durationMs: Math.max(0, now - item.startedAt) } : item;
300
300
  }
301
301
 
302
302
  function finalize(phase: WorkflowChecklistPhase): void {
@@ -37,6 +37,7 @@ export function buildWorkflowReceipt(input: {
37
37
  workflowRunId: string;
38
38
  state: WorkflowReceiptState;
39
39
  children: WorkflowScriptChildResult[];
40
+ argsDigest?: string;
40
41
  hostSteps?: WorkflowReceipt["hostSteps"];
41
42
  workflowChildren?: WorkflowReceipt["workflowChildren"];
42
43
  resource?: WorkflowResourceProvenance;
@@ -44,6 +45,7 @@ export function buildWorkflowReceipt(input: {
44
45
  createdAt?: number;
45
46
  }): WorkflowReceipt {
46
47
  const workflowRunId = assertSafeRunId(input.workflowRunId, "workflowRunId");
48
+ if (input.argsDigest !== undefined && !/^[a-f0-9]{64}$/u.test(input.argsDigest)) throw new Error("workflow receipt argsDigest must be a lowercase SHA-256 digest.");
47
49
  if (input.workflowChildren?.workflowRunId !== undefined && input.workflowChildren.workflowRunId !== workflowRunId) throw new Error("workflowChildren workflowRunId does not match its receipt.");
48
50
  const entries: Record<string, WorkflowReceiptEntry> = Object.create(null) as Record<string, WorkflowReceiptEntry>;
49
51
  for (const child of input.children) {
@@ -75,7 +77,7 @@ export function buildWorkflowReceipt(input: {
75
77
  const hostSteps = input.hostSteps?.map((hostStep, index) => parseHostStepNode(hostStep, `workflow receipt hostSteps[${index}]`));
76
78
  if (hostSteps) assertUniqueHostStepIds(hostSteps, "workflow receipt");
77
79
  const resource = parseWorkflowResource(input.resource, "workflow receipt");
78
- return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}), ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}) };
80
+ return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(input.argsDigest ? { argsDigest: input.argsDigest } : {}), ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}), ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}) };
79
81
  }
80
82
 
81
83
  export function writeWorkflowReceipt(asyncDir: string, receipt: WorkflowReceipt): string {
@@ -115,12 +117,26 @@ const EXTERNAL_CLI_CAPABILITIES = {
115
117
  extensionBindings: false,
116
118
  } as const;
117
119
 
120
+ function parseExternalCliMachine(value: unknown, label: string): void {
121
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label}.machine must be an object.`);
122
+ const machine = value as Record<string, unknown>;
123
+ const unknownMachine = Object.keys(machine).filter((field) => !["provider", "id", "label", "target", "session", "cwd", "remoteGit"].includes(field));
124
+ if (unknownMachine.length > 0) throw new Error(`${label}.machine has unsupported fields: ${unknownMachine.join(", ")}.`);
125
+ if (machine.provider !== "herdr" || typeof machine.id !== "string" || !machine.id.trim() || typeof machine.target !== "string" || !machine.target.trim() || typeof machine.cwd !== "string" || !machine.cwd.trim()) throw new Error(`${label}.machine is invalid.`);
126
+ for (const field of ["label", "session"] as const) if (machine[field] !== undefined && (typeof machine[field] !== "string" || !machine[field].trim())) throw new Error(`${label}.machine.${field} is invalid.`);
127
+ if (machine.remoteGit === undefined) return;
128
+ if (!machine.remoteGit || typeof machine.remoteGit !== "object" || Array.isArray(machine.remoteGit)) throw new Error(`${label}.machine.remoteGit must be an object.`);
129
+ const remoteGit = machine.remoteGit as Record<string, unknown>;
130
+ const unknownRemoteGit = Object.keys(remoteGit).filter((field) => !["head", "branch", "dirty"].includes(field));
131
+ if (unknownRemoteGit.length > 0 || (remoteGit.head !== undefined && (typeof remoteGit.head !== "string" || !remoteGit.head.trim())) || (remoteGit.branch !== undefined && (typeof remoteGit.branch !== "string" || !remoteGit.branch.trim())) || (remoteGit.dirty !== undefined && typeof remoteGit.dirty !== "boolean")) throw new Error(`${label}.machine.remoteGit is invalid.`);
132
+ }
133
+
118
134
  function parseExternalCliReceiptMetadata(value: unknown, key: string, source: string): ExternalCliReceiptMetadata | undefined {
119
135
  if (value === undefined) return undefined;
120
136
  const label = `Invalid workflow receipt '${source}': entry '${key}' externalAdapter`;
121
137
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
122
138
  const metadata = value as Record<string, unknown>;
123
- const unknownMetadata = Object.keys(metadata).filter((field) => !["adapter", "capabilities", "safety", "outputArtifacts", "handoff", "supervisor", "nonResumableReason"].includes(field));
139
+ const unknownMetadata = Object.keys(metadata).filter((field) => !["adapter", "capabilities", "machine", "safety", "outputArtifacts", "handoff", "supervisor", "nonResumableReason"].includes(field));
124
140
  if (unknownMetadata.length > 0) throw new Error(`${label} has unsupported fields: ${unknownMetadata.join(", ")}.`);
125
141
  const adapter = metadata.adapter;
126
142
  if (!adapter || typeof adapter !== "object" || Array.isArray(adapter)) throw new Error(`${label}.adapter must be an object.`);
@@ -136,6 +152,7 @@ function parseExternalCliReceiptMetadata(value: unknown, key: string, source: st
136
152
  for (const [capability, expected] of Object.entries(EXTERNAL_CLI_CAPABILITIES)) {
137
153
  if (capabilityRecord[capability] !== expected) throw new Error(`${label}.capabilities.${capability} is invalid.`);
138
154
  }
155
+ if (metadata.machine !== undefined) parseExternalCliMachine(metadata.machine, label);
139
156
  const safety = metadata.safety;
140
157
  if (adapterRecord.id === "codex-exec") {
141
158
  if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
@@ -330,10 +347,11 @@ export function readWorkflowReceipt(asyncDirRoot: string, workflowRunId: string)
330
347
  assertUniqueHostStepIds(hostSteps, receiptPath);
331
348
  }
332
349
  const workflowResolution = parseWorkflowResolution(receipt.workflowResolution, receiptPath);
350
+ if (receipt.argsDigest !== undefined && (typeof receipt.argsDigest !== "string" || !/^[a-f0-9]{64}$/u.test(receipt.argsDigest))) throw new Error(`Invalid workflow receipt '${receiptPath}': argsDigest is invalid.`);
333
351
  const resource = parseWorkflowResource(receipt.resource, receiptPath);
334
352
  const terminalOutcome = parseTerminalOutcome(receipt.terminalOutcome, `Invalid workflow receipt '${receiptPath}': terminalOutcome`);
335
353
  const recovery = parseRecovery(receipt.recovery, workflowRunId, entries, receiptPath);
336
- return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(workflowResolution ? { workflowResolution } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(recovery ? { recovery } : {}) };
354
+ return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(receipt.argsDigest ? { argsDigest: receipt.argsDigest } : {}), ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(workflowResolution ? { workflowResolution } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(recovery ? { recovery } : {}) };
337
355
  }
338
356
 
339
357
  export function resolveWorkflowReceiptResumeEntry(input: {
@@ -125,7 +125,7 @@ function validatePlainJson(value: unknown, path: string, depth = 0): void {
125
125
  }
126
126
  }
127
127
 
128
- function normalizeArgs(value: unknown): { args: Record<string, unknown> } | { error: string } {
128
+ export function normalizeWorkflowArgs(value: unknown): { args: Record<string, unknown> } | { error: string } {
129
129
  if (value === undefined) return { args: {} };
130
130
  if (!isPlainRecord(value)) return { error: "workflow args must be a plain JSON object." };
131
131
  try {
@@ -137,6 +137,17 @@ function normalizeArgs(value: unknown): { args: Record<string, unknown> } | { er
137
137
  }
138
138
  }
139
139
 
140
+ export function deepFreezeWorkflowArgs<T extends Record<string, unknown>>(args: T): Readonly<T> {
141
+ deepFreezeWorkflowValue(args);
142
+ return args;
143
+ }
144
+
145
+ function deepFreezeWorkflowValue(value: unknown): void {
146
+ if (!Array.isArray(value) && !isPlainRecord(value)) return;
147
+ for (const entry of Object.values(value)) deepFreezeWorkflowValue(entry);
148
+ Object.freeze(value);
149
+ }
150
+
140
151
  function resolveRunCi(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
141
152
  const allowed = new Set(["command", "timeoutMs"]);
142
153
  const unsupported = Object.keys(args).filter((key) => !allowed.has(key));
@@ -191,7 +202,7 @@ function resolveResource(nameValue: unknown, argsValue?: unknown, sessionId?: st
191
202
  if (!RESOURCE_NAME_PATTERN.test(name)) return { ok: false, error: "workflow must use a safe resource name." };
192
203
  const resource = findWorkflowResource(name) ?? (sessionId ? registry().bySession.get(sessionId)?.get(name) : undefined);
193
204
  if (!resource) return { ok: false, error: `Unknown workflow resource '${name}'. Available resources: ${listWorkflowResourceNames().join(", ")}.` };
194
- const normalizedArgs = normalizeArgs(argsValue);
205
+ const normalizedArgs = normalizeWorkflowArgs(argsValue);
195
206
  if ("error" in normalizedArgs) return { ok: false, error: normalizedArgs.error };
196
207
  const resolved = resource.resolve(normalizedArgs.args);
197
208
  if (resolved && typeof (resolved as unknown as { then?: unknown }).then === "function") {
@@ -1,13 +0,0 @@
1
- // Only loaded when the parent supplies Pi 0.85.0's missing server exports.
2
- import { registerHooks } from "node:module";
3
- import { pathToFileURL } from "node:url";
4
-
5
- const aliases = JSON.parse(process.env.JITI_ALIAS);
6
- registerHooks({
7
- resolve(specifier, context, nextResolve) {
8
- if (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix") {
9
- return nextResolve(pathToFileURL(aliases[specifier]).href, context);
10
- }
11
- return nextResolve(specifier, context);
12
- },
13
- });