pi-subagents 0.58.0 → 0.59.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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -2,6 +2,10 @@ import { readFileSync } 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";
5
+ import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
6
+ import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
7
+ import type { HostStepNodeV1, SingleResult } from "../shared/types.ts";
8
+ import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
5
9
 
6
10
  const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7
11
  const requireFromPackage = createRequire(import.meta.url);
@@ -252,11 +256,11 @@ function hostCall(method, args, observation) {
252
256
  : promise;
253
257
  }
254
258
 
255
- function runHostCall(key, params, collectFailure, batch) {
259
+ function runHostCall(key, params, collectFailure, batch, generatedLaneKey) {
256
260
  const callId = ++nextCallId;
257
261
  const promise = new Promise((resolve, reject) => {
258
262
  pending.set(callId, { resolve, reject });
259
- parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}) } });
263
+ parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}), ...(generatedLaneKey ? { generatedLaneKey } : {}) } });
260
264
  });
261
265
  return { key, callId, promise };
262
266
  }
@@ -269,6 +273,15 @@ function isArrayIndexProperty(prop) {
269
273
 
270
274
  const runsAllResultTargets = new WeakMap();
271
275
 
276
+ const MAX_LANES = 32;
277
+ const MAX_LANE_STAGES = 16;
278
+ const MAX_LANE_STAGE_COUNT = 64;
279
+ const MAX_LANE_SPEC_BYTES = 64 * 1024;
280
+ const MAX_LANE_TASK_BYTES = 1024 * 1024;
281
+ const MAX_LANE_PATH_BYTES = 32 * 1024;
282
+ const MAX_LANE_BOARD_TEXT_BYTES = 256;
283
+ const LANE_PATH_FIELDS = new Set(["cwd", "output", "sessionDir"]);
284
+
272
285
  function runsAllKeyAccessError(prop) {
273
286
  return new Error("Cannot read runs.all result property '" + prop + "'. runs.all resolves to an ordered array, not a key map. Use results[0], array destructuring, or results.map((result) => result.output), not results." + prop + ".");
274
287
  }
@@ -288,6 +301,224 @@ function wrapRunsAllResults(results, keys) {
288
301
  return proxy;
289
302
  }
290
303
 
304
+ function laneByteLength(value) {
305
+ return new TextEncoder().encode(value).byteLength;
306
+ }
307
+
308
+ function laneBoardText(value) {
309
+ if (typeof value !== "string" || !value) return undefined;
310
+ if (laneByteLength(value) <= MAX_LANE_BOARD_TEXT_BYTES) return value;
311
+ const bytes = new TextEncoder().encode(value).subarray(0, MAX_LANE_BOARD_TEXT_BYTES - 3);
312
+ return new TextDecoder().decode(bytes) + "...";
313
+ }
314
+
315
+ function laneBoardPath(value) {
316
+ if (typeof value !== "string" || !value || laneByteLength(value) > MAX_LANE_BOARD_TEXT_BYTES) return undefined;
317
+ return value;
318
+ }
319
+
320
+ function validateLaneStageBounds(params, label) {
321
+ if (typeof params.task === "string" && laneByteLength(params.task) > MAX_LANE_TASK_BYTES) throw new Error(label + " task exceeds 1 MiB when UTF-8 encoded.");
322
+ for (const field of LANE_PATH_FIELDS) if (typeof params[field] === "string" && laneByteLength(params[field]) > MAX_LANE_PATH_BYTES) throw new Error(label + " " + field + " exceeds 32 KiB when UTF-8 encoded.");
323
+ }
324
+
325
+ function validateLaneKey(value, owner) {
326
+ if (typeof value !== "string" || !runKeyPattern.test(value)) throw new Error(owner + " key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.");
327
+ return value;
328
+ }
329
+
330
+ function laneStageParams(stage, previous) {
331
+ const resume = stage.resume;
332
+ const params = { ...stage.params };
333
+ if (resume !== "previous") return params;
334
+ if (!previous || typeof previous.runId !== "string" || !previous.runId.trim()) return undefined;
335
+ return { ...params, resume: previous.runId.trim() };
336
+ }
337
+
338
+ function laneStageVerdict(result) {
339
+ const structured = result && typeof result === "object" && !Array.isArray(result) ? result.structuredOutput : undefined;
340
+ const verdict = structured && typeof structured === "object" && !Array.isArray(structured) ? structured.verdict : undefined;
341
+ return typeof verdict === "string" && verdict.trim() ? laneBoardText(verdict.trim()) : undefined;
342
+ }
343
+
344
+ function laneStageIsBlocked(result) {
345
+ const structured = result && typeof result === "object" && !Array.isArray(result) ? result.structuredOutput : undefined;
346
+ return structured && typeof structured === "object" && !Array.isArray(structured) && structured.verdict === "blocked";
347
+ }
348
+
349
+ function laneStageRecord(stageKey, child, forcedState) {
350
+ const result = child && typeof child === "object" && !Array.isArray(child) ? child : undefined;
351
+ const ok = result?.ok === true;
352
+ const verdict = laneStageVerdict(result);
353
+ const blocked = laneStageIsBlocked(result);
354
+ const state = forcedState ?? (result?.stopped ? "stopped" : result?.detached ? "detached" : ok ? blocked ? "blocked" : "completed" : "failed");
355
+ const error = state !== "completed"
356
+ ? state === "blocked" && blocked
357
+ ? "Stage returned a blocked verdict."
358
+ : typeof result?.error === "string"
359
+ ? result.error
360
+ : typeof result?.output === "string"
361
+ ? result.output
362
+ : "Stage did not complete successfully."
363
+ : undefined;
364
+ return {
365
+ key: stageKey,
366
+ ...(typeof result?.runId === "string" && result.runId.trim() ? { runId: laneBoardText(result.runId.trim()) } : {}),
367
+ ...(result ? { ok } : {}),
368
+ state,
369
+ ...(typeof result?.outputReference === "string" ? (laneBoardPath(result.outputReference) ? { outputReference: laneBoardPath(result.outputReference) } : {}) : {}),
370
+ ...(verdict ? { verdict } : {}),
371
+ ...(error ? { error: laneBoardText(error) } : {}),
372
+ };
373
+ }
374
+
375
+ function laneFailure(stageKey, error) {
376
+ const text = error instanceof Error ? error.message : String(error);
377
+ return { key: stageKey, ok: false, output: text, error: text, artifactPaths: [] };
378
+ }
379
+
380
+ function runCollected(key, params, observe, generatedLaneKey) {
381
+ validateRunCall(key, params, "runs.lanes stage", runFingerprints);
382
+ const launched = runHostCall(key, params, true, undefined, generatedLaneKey);
383
+ observe([{ key, operation: "run", callId: launched.callId }]);
384
+ return launched.promise.then(decorateWorkflowChildResult);
385
+ }
386
+
387
+ function validateLaneSpecs(laneSpecs) {
388
+ if (!Array.isArray(laneSpecs)) throw new Error("runs.lanes(lanes) requires an array.");
389
+ if (laneSpecs.length === 0) throw new Error("runs.lanes(lanes) requires at least one lane.");
390
+ if (laneSpecs.length > MAX_LANES) throw new Error("runs.lanes supports at most " + MAX_LANES + " lanes.");
391
+ assertJsonValue(laneSpecs, "runs.lanes lanes");
392
+ if (laneByteLength(stableRunJson(laneSpecs)) > MAX_LANE_SPEC_BYTES) throw new Error("runs.lanes canonical JSON exceeds 64 KiB.");
393
+ const generatedKeys = new Set();
394
+ const validationFingerprints = new Map();
395
+ let stageCount = 0;
396
+ const normalized = [];
397
+ for (let laneIndex = 0; laneIndex < laneSpecs.length; laneIndex++) {
398
+ const lane = laneSpecs[laneIndex];
399
+ const laneLabel = "runs.lanes lane " + laneIndex;
400
+ if (!lane || typeof lane !== "object" || Array.isArray(lane)) throw new Error(laneLabel + " must be an object.");
401
+ const laneFields = Object.keys(lane);
402
+ if (laneFields.some((field) => field !== "key" && field !== "stages")) throw new Error(laneLabel + " contains unsupported fields.");
403
+ const laneKey = validateLaneKey(lane.key, laneLabel);
404
+ if (!Array.isArray(lane.stages) || lane.stages.length === 0) throw new Error(laneLabel + " stages must contain at least one stage.");
405
+ if (lane.stages.length > MAX_LANE_STAGES) throw new Error(laneLabel + " supports at most " + MAX_LANE_STAGES + " stages.");
406
+ const stageKeys = new Set();
407
+ const stages = [];
408
+ for (let stageIndex = 0; stageIndex < lane.stages.length; stageIndex++) {
409
+ stageCount++;
410
+ if (stageCount > MAX_LANE_STAGE_COUNT) throw new Error("runs.lanes supports at most " + MAX_LANE_STAGE_COUNT + " total stages.");
411
+ const stage = lane.stages[stageIndex];
412
+ const stageLabel = laneLabel + " stage " + stageIndex;
413
+ if (!stage || typeof stage !== "object" || Array.isArray(stage)) throw new Error(stageLabel + " must be an object.");
414
+ const stageKey = validateLaneKey(stage.key, stageLabel);
415
+ if (stageKeys.has(stageKey)) throw new Error(laneLabel + " contains duplicate stage key '" + stageKey + "'.");
416
+ stageKeys.add(stageKey);
417
+ const generatedKey = laneKey + "." + stageKey;
418
+ validateLaneKey(generatedKey, stageLabel + " generated");
419
+ if (generatedKeys.has(generatedKey)) throw new Error("runs.lanes generated child key '" + generatedKey + "' is duplicated.");
420
+ generatedKeys.add(generatedKey);
421
+ const resume = stage.resume;
422
+ if (resume !== undefined && resume !== "previous") {
423
+ if (stageIndex === 0 && typeof resume === "string") {
424
+ throw new Error(stageLabel + " cannot resume a retained run id in runs.lanes; use runs.run(key, { resume: id }) outside lanes, or start the lane with an agent stage and use resume: \"previous\" later.");
425
+ }
426
+ throw new Error(stageLabel + " resume must be 'previous'.");
427
+ }
428
+ if (stageIndex === 0 && resume === "previous") throw new Error(stageLabel + " cannot resume previous without a predecessor stage.");
429
+ const { key: _stageKey, resume: _resume, ...params } = stage;
430
+ const validationParams = resume === "previous" ? { ...params, resume: "retained-run-placeholder" } : params;
431
+ validateLaneStageBounds(validationParams, stageLabel);
432
+ validateRunCall(generatedKey, validationParams, stageLabel, validationFingerprints);
433
+ const existingFingerprint = runFingerprints.get(generatedKey);
434
+ if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(params))) {
435
+ throw new Error("runs.lanes generated child key '" + generatedKey + "' is already used with incompatible launch params.");
436
+ }
437
+ stages.push({ key: stageKey, generatedKey, resume, params });
438
+ }
439
+ normalized.push({ key: laneKey, stages });
440
+ }
441
+ return normalized;
442
+ }
443
+
444
+ function runLane(lane, firstResult, observe) {
445
+ const records = [];
446
+ const appendSkipped = (start) => {
447
+ for (let index = start; index < lane.stages.length; index++) records.push({ key: lane.stages[index].key, state: "skipped" });
448
+ };
449
+ const finish = (state, failedStage) => ({ key: lane.key, state, ...(failedStage ? { failedStage } : {}), stages: records });
450
+ const visit = (index, previous) => {
451
+ if (index >= lane.stages.length) return Promise.resolve(finish("complete"));
452
+ const stage = lane.stages[index];
453
+ if (index === 0) {
454
+ const record = laneStageRecord(stage.key, previous);
455
+ records.push(record);
456
+ if (record.state !== "completed") {
457
+ appendSkipped(index + 1);
458
+ return Promise.resolve(finish("blocked", stage.key));
459
+ }
460
+ return visit(index + 1, previous);
461
+ }
462
+ if (stage.resume === "previous" && (!previous || typeof previous.runId !== "string" || !previous.runId.trim())) {
463
+ records.push({ key: stage.key, state: "blocked", error: "Previous stage did not return a retained run id." });
464
+ appendSkipped(index + 1);
465
+ return Promise.resolve(finish("blocked", stage.key));
466
+ }
467
+ if (!previous || previous.ok !== true || laneStageIsBlocked(previous)) {
468
+ records.push({ key: stage.key, state: "blocked", error: "Previous stage did not complete successfully." });
469
+ appendSkipped(index + 1);
470
+ return Promise.resolve(finish("blocked", stage.key));
471
+ }
472
+ const params = laneStageParams(stage, previous);
473
+ if (!params) {
474
+ records.push({ key: stage.key, state: "blocked", error: "Previous stage did not return a retained run id." });
475
+ appendSkipped(index + 1);
476
+ return Promise.resolve(finish("blocked", stage.key));
477
+ }
478
+ let launched;
479
+ try {
480
+ launched = runCollected(stage.generatedKey, params, observe, lane.key);
481
+ } catch (error) {
482
+ const failed = laneFailure(stage.generatedKey, error);
483
+ records.push(laneStageRecord(stage.key, failed));
484
+ appendSkipped(index + 1);
485
+ return Promise.resolve(finish("blocked", stage.key));
486
+ }
487
+ return launched.then((result) => {
488
+ const record = laneStageRecord(stage.key, result);
489
+ records.push(record);
490
+ if (record.state === "completed") return visit(index + 1, result);
491
+ appendSkipped(index + 1);
492
+ return finish("blocked", stage.key);
493
+ }, (error) => {
494
+ const failed = laneFailure(stage.generatedKey, error);
495
+ records.push(laneStageRecord(stage.key, failed));
496
+ appendSkipped(index + 1);
497
+ return finish("blocked", stage.key);
498
+ });
499
+ };
500
+ return visit(0, firstResult);
501
+ }
502
+
503
+ function runLanes(laneSpecs) {
504
+ const lanes = validateLaneSpecs(laneSpecs);
505
+ const firstItems = lanes.map((lane) => {
506
+ const first = lane.stages[0];
507
+ return { key: first.generatedKey, ...first.params };
508
+ });
509
+ // Share the runs.all batch launcher while allowing each lane to advance independently.
510
+ const firstBatch = launchRunsAll(firstItems, lanes.map((lane) => lane.key));
511
+ const firstResults = firstBatch.launched.map(({ promise }) => promise.then(decorateWorkflowChildResult));
512
+ let trackedAggregate;
513
+ const observe = (observations) => trackRunObservation(observations, trackedAggregate);
514
+ const laneAggregate = Promise.all(lanes.map((lane, index) => firstResults[index].then(
515
+ (result) => runLane(lane, result, observe),
516
+ (error) => runLane(lane, laneFailure(lane.stages[0].generatedKey, error), observe),
517
+ )));
518
+ trackedAggregate = trackRunObservation(firstBatch.launched.map(({ key, callId }) => ({ key, operation: "run", callId })), laneAggregate);
519
+ return trackedAggregate;
520
+ }
521
+
291
522
  function formatRef(result) {
292
523
  if (!result || typeof result !== "object") throw new Error("runs.ref(result) requires a run result object.");
293
524
  const parts = ["run " + (result.key || "unknown")];
@@ -332,8 +563,28 @@ function validateExtensionBindings(value, label) {
332
563
  if (new TextEncoder().encode(stableRunJson(value)).byteLength > 16384) throw new Error(label + " extensionBindings canonical JSON exceeds 16384 bytes.");
333
564
  }
334
565
 
566
+ function validateLaneMetadata(value, label, workflowKey) {
567
+ if (value === undefined) return;
568
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be a plain JSON object.");
569
+ const fields = Object.keys(value);
570
+ const allowed = ["version", "key", "mode", "sourceRef", "claims", "outputPaths"];
571
+ const unknown = fields.filter((field) => !allowed.includes(field));
572
+ if (unknown.length > 0) throw new Error(label + " has unsupported fields: " + unknown.join(", ") + ".");
573
+ if (value.version !== 1) throw new Error(label + ".version must be 1.");
574
+ if (typeof value.key !== "string" || !value.key.trim() || !runKeyPattern.test(value.key.trim())) throw new Error(label + ".key is invalid.");
575
+ if (workflowKey !== undefined && value.key.trim() !== workflowKey) throw new Error(label + ".key must match workflow key '" + workflowKey + "'.");
576
+ if (value.mode !== undefined && !["mutation", "review", "scout", "gate"].includes(value.mode)) throw new Error(label + ".mode is invalid.");
577
+ const bounded = (entry, maxBytes) => typeof entry === "string" && entry.trim() && new TextEncoder().encode(entry.trim()).byteLength <= maxBytes && !/[\r\n\u0000]/.test(entry);
578
+ if (value.sourceRef !== undefined && !bounded(value.sourceRef, 128)) throw new Error(label + ".sourceRef is invalid.");
579
+ for (const [name, maxItems, maxLength] of [["claims", 20, 160], ["outputPaths", 10, 256]]) {
580
+ if (value[name] === undefined) continue;
581
+ if (!Array.isArray(value[name]) || value[name].length > maxItems || value[name].some((entry) => !bounded(entry, maxLength))) throw new Error(label + "." + name + " is invalid.");
582
+ }
583
+ }
584
+
335
585
  function validateRunCall(key, params, label, fingerprints) {
336
586
  if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
587
+ if (hostKeys.has(key)) throw new Error("Workflow key '" + key + "' is already used by runs.host.");
337
588
  if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error(label + " requires a params object.");
338
589
  if (Object.prototype.hasOwnProperty.call(params, "action") || Object.prototype.hasOwnProperty.call(params, "workflowScript") || Object.prototype.hasOwnProperty.call(params, "tasks") || Object.prototype.hasOwnProperty.call(params, "chain") || Object.prototype.hasOwnProperty.call(params, "parallel") || Object.prototype.hasOwnProperty.call(params, "concurrency") || Object.prototype.hasOwnProperty.call(params, "chainDir")) {
339
590
  const hint = label === "runs.run" ? "; use runs.all(...) and JavaScript control flow for orchestration." : ".";
@@ -341,6 +592,7 @@ function validateRunCall(key, params, label, fingerprints) {
341
592
  }
342
593
  if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
343
594
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
595
+ validateLaneMetadata(params.lane, label + " lane", key);
344
596
  if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
345
597
  if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
346
598
  if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
@@ -365,6 +617,44 @@ function validateRunCall(key, params, label, fingerprints) {
365
617
  fingerprints.set(key, fingerprint);
366
618
  }
367
619
 
620
+ const hostKeys = new Set();
621
+
622
+ function validateHostCommand(key, params) {
623
+ validateLaneKey(key, "runs.host");
624
+ if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("runs.host('" + key + "') params must be an object.");
625
+ const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
626
+ const unknown = Object.keys(params).filter((field) => !allowed.has(field));
627
+ if (unknown.length) throw new Error("runs.host('" + key + "') params have unsupported fields: " + unknown.join(", ") + ".");
628
+ if (params.kind !== "command") throw new Error("runs.host('" + key + "') kind must be 'command'.");
629
+ if (typeof params.command !== "string" || !params.command.trim() || params.command.includes("\u0000") || new TextEncoder().encode(params.command.trim()).byteLength > 16384) throw new Error("runs.host('" + key + "') command must be a non-empty string of at most 16384 bytes without NUL.");
630
+ if (!Number.isInteger(params.timeoutMs) || params.timeoutMs < 1 || params.timeoutMs > 86400000) throw new Error("runs.host('" + key + "') timeoutMs must be an integer from 1 to 86400000.");
631
+ if (params.output !== undefined && (typeof params.output !== "string" || !params.output.trim() || /^[/\\]|(?:^|[/\\])\.\.(?:[/\\]|$)/.test(params.output) || /[\u0000-\u001f\u007f]/.test(params.output) || new TextEncoder().encode(params.output.trim()).byteLength > 240)) throw new Error("runs.host('" + key + "') output must be a bounded relative path without traversal or control characters.");
632
+ if (params.role !== undefined && params.role !== "ci" && params.role !== "gate") throw new Error("runs.host('" + key + "') role must be 'ci' or 'gate'.");
633
+ if (params.provider !== undefined && (typeof params.provider !== "string" || !params.provider.trim() || /[\r\n\u0000]/.test(params.provider) || new TextEncoder().encode(params.provider.trim()).byteLength > 64)) throw new Error("runs.host('" + key + "') provider must be a non-empty single-line string of at most 64 bytes.");
634
+ assertJsonValue(params, "runs.host('" + key + "') params");
635
+ if (hostKeys.has(key) || runFingerprints.has(key)) throw new Error("Workflow key '" + key + "' is already in use.");
636
+ if (hostKeys.size >= 32) throw new Error("workflowScript supports at most 32 runs.host calls.");
637
+ hostKeys.add(key);
638
+ }
639
+
640
+ function launchRunsAll(items, generatedLaneKeys) {
641
+ if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
642
+ const fingerprints = new Map(runFingerprints);
643
+ const calls = [];
644
+ for (let index = 0; index < items.length; index++) {
645
+ if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
646
+ const item = items[index];
647
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object.");
648
+ const { key, ...params } = item;
649
+ validateRunCall(key, params, "runs.all item " + index, fingerprints);
650
+ calls.push({ key, params });
651
+ }
652
+ runFingerprints = fingerprints;
653
+ const batch = { id: "batch-" + (++nextCallId), calls };
654
+ const launched = calls.map(({ key, params }, index) => runHostCall(key, params, true, batch, generatedLaneKeys?.[index]));
655
+ return { calls, launched };
656
+ }
657
+
368
658
  const runs = Object.freeze({
369
659
  run(key, params) {
370
660
  validateRunCall(key, params, "runs.run", runFingerprints);
@@ -372,22 +662,16 @@ const runs = Object.freeze({
372
662
  return trackRunObservation([{ key, operation: "run", callId: launched.callId }], launched.promise.then(decorateWorkflowChildResult));
373
663
  },
374
664
  all(items) {
375
- if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
376
- const fingerprints = new Map(runFingerprints);
377
- const calls = [];
378
- for (let index = 0; index < items.length; index++) {
379
- if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
380
- const item = items[index];
381
- if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object.");
382
- const { key, ...params } = item;
383
- validateRunCall(key, params, "runs.all item " + index, fingerprints);
384
- calls.push({ key, params });
385
- }
386
- runFingerprints = fingerprints;
387
- const batch = { id: "batch-" + (++nextCallId), calls };
388
- const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
665
+ const { calls, launched } = launchRunsAll(items);
389
666
  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))));
390
667
  },
668
+ lanes(laneSpecs) {
669
+ return runLanes(laneSpecs);
670
+ },
671
+ host(key, params) {
672
+ validateHostCommand(key, params);
673
+ return hostCall("host", { key, params }, { key, operation: "host" });
674
+ },
391
675
  steer(key, message, options = {}) {
392
676
  if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error("runs.steer has an invalid key.");
393
677
  if (typeof message !== "string" || !message.trim()) throw new Error("runs.steer message must be a non-empty string.");
@@ -654,6 +938,7 @@ parentPort.on("message", async (message) => {
654
938
  export interface WorkflowScriptChildResult {
655
939
  key: string;
656
940
  ok: boolean;
941
+ lane?: import("../shared/types.ts").WorkflowLaneMetadata;
657
942
  terminalOutcome?: import("../shared/types.ts").WorkflowTerminalOutcome;
658
943
  stopped?: boolean;
659
944
  /** Canonical child agent name when launch resolution produced one. */
@@ -672,11 +957,11 @@ export interface WorkflowScriptChildResult {
672
957
  resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
673
958
  continuation?: { runIds: string[] };
674
959
  artifactPaths: string[];
675
- results?: unknown[];
960
+ results?: SingleResult[];
676
961
  }
677
962
 
678
963
  export interface WorkflowScriptTraceEntry {
679
- operation: "run" | "status" | "steer";
964
+ operation: "run" | "status" | "steer" | "host";
680
965
  key: string;
681
966
  state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused" | "queued" | "delivered" | "missed";
682
967
  /** Canonical child agent name when resolved launch or result data is available. */
@@ -686,6 +971,10 @@ export interface WorkflowScriptTraceEntry {
686
971
  phase?: string;
687
972
  label?: string;
688
973
  error?: string;
974
+ /** Internal provenance for a generated runs.lanes child key. */
975
+ generatedLaneKey?: string;
976
+ lane?: import("../shared/types.ts").WorkflowLaneMetadata;
977
+ warning?: string;
689
978
  }
690
979
 
691
980
  export interface WorkflowSteerOptions {
@@ -740,11 +1029,15 @@ export interface RunWorkflowScriptOptions {
740
1029
  oneUsePermit?: { claim: (key: string) => string | undefined };
741
1030
  timeoutMs?: number;
742
1031
  signal?: AbortSignal;
1032
+ /** Maximum children executing concurrently within this workflow. Defaults to 20. */
1033
+ globalConcurrencyLimit?: number;
743
1034
  admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
744
1035
  launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
745
1036
  resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
746
1037
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
747
1038
  steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
1039
+ host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
1040
+ onHostStep?: (hostStep: HostStepNodeV1) => void;
748
1041
  state?: {
749
1042
  get: (key: string) => unknown | Promise<unknown>;
750
1043
  set: (key: string, value: unknown) => void | Promise<void>;
@@ -916,12 +1209,47 @@ function literalString(node: unknown): string | undefined {
916
1209
  return undefined;
917
1210
  }
918
1211
 
919
- function directRunsCall(node: unknown, method: "run" | "all"): node is AstNode {
1212
+ function directRunsCall(node: unknown, method: "run" | "all" | "host"): node is AstNode {
920
1213
  if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
921
1214
  const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
922
1215
  return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
923
1216
  }
924
1217
 
1218
+ function validateStaticHostCall(call: AstNode): WorkflowScriptValidationError[] {
1219
+ const args = Array.isArray(call.arguments) ? call.arguments : [];
1220
+ const keyNode = astNode(args[0]) ? args[0] : undefined;
1221
+ const params = astNode(args[1]) ? args[1] : undefined;
1222
+ const errors: WorkflowScriptValidationError[] = [];
1223
+ const key = literalString(keyNode);
1224
+ if (keyNode && key !== undefined && !KEY_PATTERN.test(key)) errors.push({ message: "runs.host key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
1225
+ if (!params || params.type !== "ObjectExpression" || !Array.isArray(params.properties) || params.properties.some((property) => !astNode(property) || property.type !== "Property")) return errors;
1226
+ const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
1227
+ for (const property of params.properties) {
1228
+ if (!astNode(property)) continue;
1229
+ const name = staticPropertyKey(property);
1230
+ if (name !== undefined && !allowed.has(name)) errors.push({ message: `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
1231
+ }
1232
+ const kindNode = directObjectPropertyValue(params, "kind");
1233
+ const commandNode = directObjectPropertyValue(params, "command");
1234
+ const timeoutNode = directObjectPropertyValue(params, "timeoutMs");
1235
+ const kind = literalString(kindNode);
1236
+ const command = literalString(commandNode);
1237
+ const timeout = astNode(timeoutNode) && timeoutNode.type === "Literal" ? timeoutNode.value : undefined;
1238
+ if (!kindNode) errors.push({ message: "runs.host params require kind: 'command'.", ...nodeLocation(params) });
1239
+ else if (kind !== undefined && kind !== "command") errors.push({ message: "runs.host params kind must be 'command'.", ...nodeLocation(kindNode) });
1240
+ if (!commandNode) errors.push({ message: "runs.host params require command.", ...nodeLocation(params) });
1241
+ else if (command !== undefined && !command.trim()) errors.push({ message: "runs.host params command must be non-empty.", ...nodeLocation(commandNode) });
1242
+ if (!timeoutNode) errors.push({ message: "runs.host params require timeoutMs.", ...nodeLocation(params) });
1243
+ else if (typeof timeout === "number" && (!Number.isInteger(timeout) || timeout < 1)) errors.push({ message: "runs.host params timeoutMs must be a positive integer.", ...nodeLocation(timeoutNode) });
1244
+ const outputNode = directObjectPropertyValue(params, "output");
1245
+ const output = literalString(outputNode);
1246
+ if (outputNode && output !== undefined && (!output.trim() || /^[/\\]|(?:^|[/\\])\.\.(?:[/\\]|$)/.test(output))) errors.push({ message: "runs.host params output must be a non-empty relative path without traversal.", ...nodeLocation(outputNode) });
1247
+ const roleNode = directObjectPropertyValue(params, "role");
1248
+ const role = literalString(roleNode);
1249
+ if (roleNode && role !== undefined && role !== "ci" && role !== "gate") errors.push({ message: "runs.host params role must be 'ci' or 'gate'.", ...nodeLocation(roleNode) });
1250
+ return errors;
1251
+ }
1252
+
925
1253
  function nodeLocation(node: AstNode): Pick<WorkflowScriptValidationError, "line" | "column"> {
926
1254
  return node.loc ? { line: Math.max(1, node.loc.start.line - 1), column: node.loc.start.column + 1 } : {};
927
1255
  }
@@ -1042,6 +1370,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
1042
1370
  }
1043
1371
  }
1044
1372
  }
1373
+ if (directRunsCall(node, "host")) errors.push(...validateStaticHostCall(node));
1045
1374
  const boundaryValue = node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "Identifier" && node.callee.name === "emit" && Array.isArray(node.arguments) && astNode(node.arguments[0])
1046
1375
  ? node.arguments[0]
1047
1376
  : node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "MemberExpression" && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "state" && astNode(node.callee.property) && node.callee.property.type === "Identifier" && node.callee.property.name === "set" && Array.isArray(node.arguments) && astNode(node.arguments[1])
@@ -1111,7 +1440,7 @@ function resolveWorkflowParserEntry(): string {
1111
1440
  }
1112
1441
  }
1113
1442
 
1114
- const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "turnBudget", "worktree"] as const;
1443
+ const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "lane", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "worktree"] as const;
1115
1444
 
1116
1445
  function isZeroUsage(usage: unknown): boolean {
1117
1446
  if (!isRecord(usage)) return false;
@@ -1149,6 +1478,10 @@ function setupAbortResumeParams(params: Record<string, unknown>, result: Workflo
1149
1478
  export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
1150
1479
  if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
1151
1480
  if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
1481
+ if (options.globalConcurrencyLimit !== undefined && (!Number.isInteger(options.globalConcurrencyLimit) || options.globalConcurrencyLimit < 1)) {
1482
+ throw new Error("workflow script global concurrency limit must be a positive integer.");
1483
+ }
1484
+ const launchSemaphore = new Semaphore(options.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
1152
1485
 
1153
1486
  let acornPath: string;
1154
1487
  try {
@@ -1162,13 +1495,15 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1162
1495
  const trace: WorkflowScriptTraceEntry[] = [];
1163
1496
  const children = new Map<string, WorkflowScriptChildResult>();
1164
1497
  const childOrder: string[] = [];
1165
- const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
1498
+ const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean; generatedLaneKey?: string }>();
1166
1499
  const steers = new Map<number, { key: string; promise: Promise<WorkflowSteerResult>; observed: boolean }>();
1500
+ const hostCalls = new Map<number, { key: string; promise: Promise<WorkflowHostCommandResult>; observed: boolean }>();
1167
1501
  const stoppedLaunches = new Set<string>();
1168
1502
  const childStopControllers = new Map<string, AbortController>();
1169
1503
  const batchAdmissions = new Map<string, Promise<void>>();
1170
1504
  const observedRunCalls = new Set<number>();
1171
1505
  const observedSteerCalls = new Set<number>();
1506
+ const observedHostCalls = new Set<number>();
1172
1507
  const childController = new AbortController();
1173
1508
  let settled = false;
1174
1509
  let finishing = false;
@@ -1189,7 +1524,18 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1189
1524
  console.error("Workflow onTrace callback failed:", error);
1190
1525
  }
1191
1526
  };
1527
+ const hostStepChanged = (hostStep: HostStepNodeV1) => {
1528
+ try {
1529
+ options.onHostStep?.(hostStep);
1530
+ } catch (error) {
1531
+ console.error("Workflow onHostStep callback failed:", error);
1532
+ }
1533
+ };
1192
1534
  const stoppedChildResult = (key: string, message: string): WorkflowScriptChildResult => ({ key, ok: false, stopped: true, output: message, error: message, artifactPaths: [] });
1535
+ const responseBoundaryFailure = (key: string, error: unknown): WorkflowScriptChildResult => {
1536
+ const text = error instanceof Error ? error.message : String(error);
1537
+ return { key, ok: false, output: text, error: text, artifactPaths: [] };
1538
+ };
1193
1539
  const stopChild = (key: string, message = `Workflow child '${key}' stopped by user.`): boolean => {
1194
1540
  if (!launches.has(key) || children.has(key)) return false;
1195
1541
  stoppedLaunches.add(key);
@@ -1203,6 +1549,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1203
1549
  ...(started?.agent ? { agent: started.agent } : {}),
1204
1550
  ...(started?.phase ? { phase: started.phase } : {}),
1205
1551
  ...(started?.label ? { label: started.label } : {}),
1552
+ ...(started?.generatedLaneKey ? { generatedLaneKey: started.generatedLaneKey } : {}),
1206
1553
  error: message,
1207
1554
  });
1208
1555
  traceChanged();
@@ -1215,7 +1562,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1215
1562
  if (settled || finishing) return;
1216
1563
  finishing = true;
1217
1564
  childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
1218
- void Promise.allSettled([...steers.values()].map(({ promise }) => promise)).then(() => {
1565
+ void Promise.allSettled([...steers.values(), ...hostCalls.values()].map(({ promise }) => promise)).then(() => {
1219
1566
  if (settled) return;
1220
1567
  settled = true;
1221
1568
  options.registerStopChild?.(undefined);
@@ -1228,7 +1575,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1228
1575
  : "value" in outcome
1229
1576
  ? (() => {
1230
1577
  const unobservedSteers = [...steers.values()].filter((steer) => !steer.observed).map((steer) => steer.key);
1231
- return unobservedSteers.length > 0 ? new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
1578
+ if (unobservedSteers.length > 0) return new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`);
1579
+ const unobservedHosts = [...hostCalls.values()].filter((host) => !host.observed).map((host) => host.key);
1580
+ return unobservedHosts.length > 0 ? new Error(`workflowScript completed with unawaited runs.host call(s): ${unobservedHosts.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
1232
1581
  })()
1233
1582
  : undefined;
1234
1583
  if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" || outcome.error.workflowErrorKind === "timeout" ? outcome.error.workflowErrorKind : undefined));
@@ -1254,6 +1603,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1254
1603
  ...(started?.agent ? { agent: started.agent } : {}),
1255
1604
  ...(started?.phase ? { phase: started.phase } : {}),
1256
1605
  ...(started?.label ? { label: started.label } : {}),
1606
+ ...(started?.generatedLaneKey ? { generatedLaneKey: started.generatedLaneKey } : {}),
1257
1607
  error: error.message,
1258
1608
  });
1259
1609
  }
@@ -1322,12 +1672,16 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1322
1672
  const steer = steers.get(message.callId);
1323
1673
  if (steer) steer.observed = true;
1324
1674
  else observedSteerCalls.add(message.callId);
1675
+ } else if (message.operation === "host") {
1676
+ const host = hostCalls.get(message.callId);
1677
+ if (host) host.observed = true;
1678
+ else observedHostCalls.add(message.callId);
1325
1679
  }
1326
1680
  return;
1327
1681
  }
1328
1682
  if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
1329
1683
 
1330
- const respond = (promise: Promise<unknown>, responsePath?: string) => {
1684
+ const respond = (promise: Promise<unknown>, responsePath?: string, onBoundaryError?: (error: unknown) => void) => {
1331
1685
  void promise.then(
1332
1686
  (value) => {
1333
1687
  if (settled) return;
@@ -1340,6 +1694,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1340
1694
  assertWorkflowJsonValue(normalized, responsePath);
1341
1695
  worker.postMessage({ type: "response", callId: message.callId, ok: true, value: normalized });
1342
1696
  } catch (error) {
1697
+ onBoundaryError?.(error);
1343
1698
  worker.postMessage({ type: "response", callId: message.callId, ok: false, error: `${responsePath} must contain only JSON data before it can be returned from workflowScript. Return a plain projection such as { runId, ok, output }. ${error instanceof Error ? error.message : String(error)}` });
1344
1699
  }
1345
1700
  },
@@ -1414,6 +1769,52 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1414
1769
  respond(promise);
1415
1770
  return;
1416
1771
  }
1772
+ if (message.method === "host") {
1773
+ let key: string;
1774
+ let params: WorkflowHostCommandParams;
1775
+ try {
1776
+ key = validateKey(message.args.key, "runs.host");
1777
+ params = normalizeWorkflowHostCommandParams(message.args.params, `runs.host('${key}') params`);
1778
+ } catch (error) {
1779
+ return respond(Promise.reject(error));
1780
+ }
1781
+ if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
1782
+ if (hostCalls.size >= HOST_STEP_MAX_COUNT) return respond(Promise.reject(new Error(`workflowScript supports at most ${HOST_STEP_MAX_COUNT} runs.host calls.`)));
1783
+ const startedAt = Date.now();
1784
+ const startedStep: HostStepNodeV1 = {
1785
+ version: 1,
1786
+ kind: "host-step",
1787
+ monitorKind: "command",
1788
+ id: key,
1789
+ label: key,
1790
+ ...(params.role ? { role: params.role } : {}),
1791
+ ...(params.provider ? { provider: params.provider } : {}),
1792
+ state: "running",
1793
+ updatedAt: startedAt,
1794
+ deadlineAt: startedAt + params.timeoutMs,
1795
+ };
1796
+ hostStepChanged(startedStep);
1797
+ trace.push({ operation: "host", key, state: "started" });
1798
+ traceChanged();
1799
+ const promise = Promise.resolve().then(() => options.host!(key, params, childController.signal)).then((result) => {
1800
+ const state = result.state === "stopped" ? "cancelled" : result.ok ? "done" : "error";
1801
+ const detail = [result.error, result.stderr.trim() || result.stdout.trim()].filter(Boolean).join(" ").replace(/\s+/g, " ").trim().slice(0, 200);
1802
+ hostStepChanged({ ...startedStep, state, ...(state === "done" ? { verdict: "pass" as const } : {}), ...(!result.ok ? { reasonCode: result.state === "timed-out" ? "timed_out" : result.state === "stopped" ? "aborted" : "command_failed" } : {}), ...(detail ? { detail } : {}), reportPath: params.output ?? result.outputPath.split(/[\\/]/).at(-1), exitCode: result.exitCode, updatedAt: Date.now() });
1803
+ trace.push({ operation: "host", key, state: result.ok ? "completed" : result.state === "stopped" ? "stopped" : "failed", durationMs: result.durationMs, ...(!result.ok ? { error: result.error ?? "Host command failed." } : {}) });
1804
+ traceChanged();
1805
+ if (!result.ok) throw new Error(`Host command '${key}' failed: ${detail || result.error || `exit code ${result.exitCode ?? "unknown"}`}`);
1806
+ return result;
1807
+ }, (error: unknown) => {
1808
+ const text = error instanceof Error ? error.message : String(error);
1809
+ hostStepChanged({ ...startedStep, state: "error", reasonCode: "execution_failed", detail: text.replace(/\s+/g, " ").slice(0, 200), updatedAt: Date.now() });
1810
+ trace.push({ operation: "host", key, state: "failed", durationMs: Date.now() - startedAt, error: text });
1811
+ traceChanged();
1812
+ throw error;
1813
+ });
1814
+ hostCalls.set(message.callId, { key, promise, observed: observedHostCalls.delete(message.callId) });
1815
+ respond(promise, `runs.host('${key}') result`);
1816
+ return;
1817
+ }
1417
1818
  if (message.method !== "run") return respond(Promise.reject(new Error(`Unknown runs API method '${message.method}'.`)));
1418
1819
 
1419
1820
  let key: string;
@@ -1424,6 +1825,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1424
1825
  }
1425
1826
  const params = message.args.params;
1426
1827
  if (!isRecord(params)) return respond(Promise.reject(new Error(`runs.run('${key}', params) requires a params object.`)));
1828
+ const generatedLaneKey = typeof message.args.generatedLaneKey === "string" && KEY_PATTERN.test(message.args.generatedLaneKey) && key.startsWith(`${message.args.generatedLaneKey}.`)
1829
+ ? message.args.generatedLaneKey
1830
+ : undefined;
1427
1831
  const collectFailure = message.args.collectFailure === true;
1428
1832
  const callObserved = observedRunCalls.delete(message.callId);
1429
1833
  const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
@@ -1441,9 +1845,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1441
1845
  if (existing) {
1442
1846
  if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
1443
1847
  if (callObserved) existing.observed = true;
1444
- trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
1848
+ trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params), ...(existing.generatedLaneKey ? { generatedLaneKey: existing.generatedLaneKey } : {}) });
1445
1849
  traceChanged();
1446
- return respond(deliver(existing.promise), `runs.run('${key}') result`);
1850
+ return respond(deliver(existing.promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
1447
1851
  }
1448
1852
  const permitError = options.oneUsePermit?.claim(key);
1449
1853
  if (permitError) return respond(Promise.reject(new Error(permitError)));
@@ -1528,13 +1932,23 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1528
1932
  if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
1529
1933
  }
1530
1934
  const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
1531
- const result = await options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
1532
- const autoResumeParams = setupAbortResumeParams(params, result, childSignal);
1533
- if (!autoResumeParams) return result;
1534
- resolvedResumeLineage = [...new Set([...(resolvedResumeLineage ?? []), result.runId!])];
1535
- trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(autoResumeParams), phase: "auto-resume", runId: result.runId });
1536
- traceChanged();
1537
- return options.launch(key, autoResumeParams, childSignal, { admitted: true, batch: batch !== undefined });
1935
+ await launchSemaphore.acquire();
1936
+ try {
1937
+ if (settled || finishing || stoppedLaunches.has(key) || childSignal.aborted) {
1938
+ const reason = childSignal.reason;
1939
+ const text = children.get(key)?.error ?? (reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.");
1940
+ return stoppedChildResult(key, text);
1941
+ }
1942
+ const result = await options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
1943
+ const autoResumeParams = setupAbortResumeParams(params, result, childSignal);
1944
+ if (!autoResumeParams) return result;
1945
+ resolvedResumeLineage = [...new Set([...(resolvedResumeLineage ?? []), result.runId!])];
1946
+ trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(autoResumeParams), ...(generatedLaneKey ? { generatedLaneKey } : {}), phase: "auto-resume", runId: result.runId });
1947
+ traceChanged();
1948
+ return options.launch(key, autoResumeParams, childSignal, { admitted: true, batch: batch !== undefined });
1949
+ } finally {
1950
+ launchSemaphore.release();
1951
+ }
1538
1952
  }).then((result) => {
1539
1953
  let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
1540
1954
  if (resolvedResumeLineage?.length && normalized.runId) {
@@ -1544,7 +1958,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1544
1958
  if (stoppedLaunches.has(key)) return children.get(key) ?? normalized;
1545
1959
  children.set(key, normalized);
1546
1960
  const state = normalized.ok ? "completed" : normalized.stopped ? "stopped" : normalized.detached ? "detached" : "failed";
1547
- trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
1961
+ 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 } : {}) });
1548
1962
  traceChanged();
1549
1963
  return normalized;
1550
1964
  }, (error: unknown) => {
@@ -1553,15 +1967,15 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
1553
1967
  childStopControllers.delete(key);
1554
1968
  if (stoppedLaunches.has(key)) return children.get(key) ?? { ...failure, stopped: true };
1555
1969
  children.set(key, failure);
1556
- trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), error: text });
1970
+ trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), error: text });
1557
1971
  traceChanged();
1558
1972
  return failure;
1559
1973
  });
1560
- launches.set(key, { fingerprint, promise, observed: callObserved });
1974
+ launches.set(key, { fingerprint, promise, observed: callObserved, ...(generatedLaneKey ? { generatedLaneKey } : {}) });
1561
1975
  childOrder.push(key);
1562
- trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
1976
+ trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}) });
1563
1977
  traceChanged();
1564
- respond(deliver(promise), `runs.run('${key}') result`);
1978
+ respond(deliver(promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
1565
1979
  });
1566
1980
 
1567
1981
  worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });