pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -33,9 +33,11 @@ import {
33
33
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
34
34
  import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
35
35
  import { runSync } from "./execution.ts";
36
+ import { beginForegroundChild, finishForegroundChild, updateForegroundChild } from "./foreground-control.ts";
36
37
  import { buildChainSummary } from "../../shared/formatters.ts";
37
38
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
38
39
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
40
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
39
41
  import { recordRun } from "../shared/run-history.ts";
40
42
  import {
41
43
  cleanupWorktrees,
@@ -48,11 +50,13 @@ import {
48
50
  } from "../shared/worktree.ts";
49
51
  import {
50
52
  type ActivityState,
53
+ type AgentContract,
51
54
  type AgentProgress,
52
55
  type ArtifactConfig,
53
56
  type ArtifactPaths,
54
57
  type ControlEvent,
55
58
  type Details,
59
+ type ForegroundRunControl,
56
60
  type IntercomEventBus,
57
61
  type NestedRouteInfo,
58
62
  type ResolvedControlConfig,
@@ -71,8 +75,11 @@ import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferen
71
75
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
72
76
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
73
77
  import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
78
+ import { isAgentContractV1 } from "../shared/agent-contract.ts";
74
79
  import type { ChainOutputMap } from "../../shared/types.ts";
75
80
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
81
+ import type { ContextMode } from "../shared/context-mode.ts";
82
+ import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
76
83
 
77
84
  interface ChainExecutionDetailsInput {
78
85
  results: SingleResult[];
@@ -89,6 +96,7 @@ interface ChainExecutionDetailsInput {
89
96
  currentFlatIndex?: number;
90
97
  dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
91
98
  dynamicGroupStatuses?: Record<number, { status: "pending" | "running" | "completed" | "failed" | "paused" | "stopped" | "detached"; error?: string; acceptance?: SingleResult["acceptance"] }>;
99
+ parallelHandoff?: Details["parallelHandoff"];
92
100
  }
93
101
 
94
102
  interface ParallelChainRunInput {
@@ -111,6 +119,7 @@ interface ParallelChainRunInput {
111
119
  sessionFileForIndex?: (idx?: number) => string | undefined;
112
120
  sessionFileForTask?: (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
113
121
  thinkingOverrideForTask?: (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
122
+ contextForAgent?: (agentName: string) => ContextMode;
114
123
  shareEnabled: boolean;
115
124
  artifactConfig: ArtifactConfig;
116
125
  artifactsDir: string;
@@ -118,22 +127,10 @@ interface ParallelChainRunInput {
118
127
  onUpdate?: (r: AgentToolResult<Details>) => void;
119
128
  onControlEvent?: (event: ControlEvent) => void;
120
129
  controlConfig: ResolvedControlConfig;
130
+ agentContract?: AgentContract;
121
131
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
122
132
  orchestratorIntercomTarget?: string;
123
- foregroundControl?: {
124
- updatedAt: number;
125
- currentAgent?: string;
126
- currentIndex?: number;
127
- currentActivityState?: ActivityState;
128
- lastActivityAt?: number;
129
- currentTool?: string;
130
- currentToolStartedAt?: number;
131
- currentPath?: string;
132
- turnCount?: number;
133
- tokens?: number;
134
- toolCount?: number;
135
- interrupt?: () => boolean;
136
- };
133
+ foregroundControl?: ForegroundRunControl;
137
134
  results: SingleResult[];
138
135
  allProgress: AgentProgress[];
139
136
  outputs: ChainOutputMap;
@@ -152,6 +149,7 @@ interface ParallelChainRunInput {
152
149
  toolBudget?: ResolvedToolBudget;
153
150
  configToolBudget?: ToolBudgetConfig;
154
151
  globalSemaphore?: Semaphore;
152
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
155
153
  dynamic?: boolean;
156
154
  }
157
155
 
@@ -167,6 +165,7 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
167
165
  outputs: input.outputs,
168
166
  totalChildUsage: sumResultsUsage(input.results),
169
167
  totalCost: sumResultsCost(input.results),
168
+ ...(input.parallelHandoff ? { parallelHandoff: input.parallelHandoff } : {}),
170
169
  workflowGraph: buildWorkflowGraphSnapshot({
171
170
  runId: input.runId,
172
171
  mode: "chain",
@@ -200,17 +199,48 @@ function ensureParallelProgressFile(
200
199
  return true;
201
200
  }
202
201
 
203
- function appendParallelWorktreeSummary(
204
- output: string,
205
- worktreeSetup: WorktreeSetup | undefined,
206
- diffsDir: string,
207
- agents: string[],
208
- ): string {
209
- if (!worktreeSetup) return output;
210
- const diffs = diffWorktrees(worktreeSetup, agents, diffsDir);
202
+ function finalizeParallelWorktreeHandoff(input: {
203
+ output: string;
204
+ worktreeSetup: WorktreeSetup;
205
+ artifactsDir: string;
206
+ runId: string;
207
+ cwd: string;
208
+ stepIndex: number;
209
+ flatStartIndex: number;
210
+ agents: string[];
211
+ results: SingleResult[];
212
+ }): { output: string; reference?: NonNullable<Details["parallelHandoff"]> } {
213
+ const diffs = diffWorktrees(input.worktreeSetup, input.agents, path.join(input.artifactsDir, "worktree-diffs", input.runId, `step-${input.stepIndex}`));
214
+ const cleanup = cleanupWorktrees(input.worktreeSetup);
211
215
  const diffSummary = formatWorktreeDiffSummary(diffs);
212
- if (!diffSummary) return output;
213
- return `${output}\n\n${diffSummary}`;
216
+ try {
217
+ const reference = writeParallelHandoffGroup({
218
+ manifestPath: parallelHandoffPath(input.artifactsDir, input.runId),
219
+ runId: input.runId,
220
+ mode: "chain",
221
+ source: "foreground",
222
+ cwd: input.cwd,
223
+ stepIndex: input.stepIndex,
224
+ flatStartIndex: input.flatStartIndex,
225
+ setup: input.worktreeSetup,
226
+ diffs,
227
+ cleanup,
228
+ results: input.results.map((result) => ({
229
+ agent: result.agent,
230
+ status: result.stopped ? "stopped" : result.detached ? "detached" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
231
+ summary: getSingleResultOutput(result) || result.error || "(no output)",
232
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
233
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
234
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
235
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
236
+ })),
237
+ });
238
+ const suffix = [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n");
239
+ return { output: suffix ? `${input.output}\n\n${suffix}` : input.output, reference };
240
+ } catch (error) {
241
+ const suffix = [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
242
+ return { output: suffix ? `${input.output}\n\n${suffix}` : input.output };
243
+ }
214
244
  }
215
245
 
216
246
  function resolveChainToolBudget(input: { stepBudget?: ToolBudgetConfig; runBudget?: ResolvedToolBudget; agentBudget?: ToolBudgetConfig; configBudget?: ToolBudgetConfig }): { toolBudget?: ResolvedToolBudget; error?: string } {
@@ -294,36 +324,39 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
294
324
  : undefined;
295
325
  taskStr = injectSingleOutputInstruction(taskStr, outputPath, taskAgentConfig);
296
326
  const interruptController = new AbortController();
327
+ const childIndex = input.globalTaskIndex + taskIndex;
297
328
  if (input.foregroundControl) {
298
- input.foregroundControl.currentAgent = task.agent;
299
- input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
300
- input.foregroundControl.currentActivityState = undefined;
301
- input.foregroundControl.updatedAt = Date.now();
302
- input.foregroundControl.interrupt = () => {
303
- if (interruptController.signal.aborted) return false;
304
- interruptController.abort();
305
- input.foregroundControl!.currentActivityState = undefined;
306
- input.foregroundControl!.updatedAt = Date.now();
307
- return true;
308
- };
329
+ beginForegroundChild(input.foregroundControl, {
330
+ index: childIndex,
331
+ agent: task.agent,
332
+ description: cleanTask.trim(),
333
+ interrupt: () => {
334
+ if (interruptController.signal.aborted) return false;
335
+ interruptController.abort();
336
+ return true;
337
+ },
338
+ });
309
339
  }
310
340
 
311
341
  const structuredRuntime = task.outputSchema
312
342
  ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
313
343
  : undefined;
344
+ const agentContract = task.agentContract ?? input.step.agentContract ?? input.agentContract;
314
345
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
315
346
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
347
+ capabilityCeiling: input.capabilityCeiling,
348
+ context: input.contextForAgent?.(task.agent),
316
349
  cwd: taskCwd,
317
350
  signal: input.signal,
318
351
  interruptSignal: interruptController.signal,
319
352
  allowIntercomDetach: taskAgentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
320
353
  intercomEvents: input.intercomEvents,
321
354
  runId: input.runId,
322
- index: input.globalTaskIndex + taskIndex,
323
- sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
324
- sessionFile: input.sessionFileForTask?.(task.agent, input.globalTaskIndex + taskIndex, effectiveModel)
325
- ?? input.sessionFileForIndex?.(input.globalTaskIndex + taskIndex),
326
- thinkingOverride: input.thinkingOverrideForTask?.(task.agent, input.globalTaskIndex + taskIndex, effectiveModel),
355
+ index: childIndex,
356
+ sessionDir: input.sessionDirForIndex(childIndex),
357
+ sessionFile: input.sessionFileForTask?.(task.agent, childIndex, effectiveModel)
358
+ ?? input.sessionFileForIndex?.(childIndex),
359
+ thinkingOverride: input.thinkingOverrideForTask?.(task.agent, childIndex, effectiveModel),
327
360
  share: input.shareEnabled,
328
361
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
329
362
  artifactConfig: input.artifactConfig,
@@ -332,7 +365,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
332
365
  maxSubagentDepth,
333
366
  controlConfig: input.controlConfig,
334
367
  onControlEvent: input.onControlEvent,
335
- intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
368
+ intercomSessionName: input.childIntercomTarget?.(task.agent, childIndex),
336
369
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
337
370
  nestedRoute: input.nestedRoute,
338
371
  modelOverride: effectiveModel,
@@ -341,13 +374,14 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
341
374
  modelScope: input.modelScope,
342
375
  skills: behavior.skills === false ? [] : behavior.skills,
343
376
  structuredOutput: structuredRuntime,
377
+ agentContract,
344
378
  acceptance: task.acceptance,
345
379
  acceptanceContext: { mode: "chain", dynamic: input.dynamic && task.acceptance === undefined },
346
380
  timeoutMs: input.timeoutMs,
347
381
  deadlineAt: input.deadlineAt,
348
382
  turnBudget: input.turnBudget,
349
383
  onDetachedExit: input.onDetachedExit
350
- ? (result) => input.onDetachedExit?.(input.globalTaskIndex + taskIndex, result)
384
+ ? (result) => input.onDetachedExit?.(childIndex, result)
351
385
  : undefined,
352
386
  toolBudget: toolBudget.toolBudget,
353
387
  onUpdate: input.onUpdate
@@ -355,18 +389,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
355
389
  const stepResults = progressUpdate.details?.results || [];
356
390
  const stepProgress = progressUpdate.details?.progress || [];
357
391
  if (input.foregroundControl && stepProgress.length > 0) {
358
- const current = stepProgress[0];
359
- input.foregroundControl.currentAgent = task.agent;
360
- input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
361
- input.foregroundControl.currentActivityState = current?.activityState;
362
- input.foregroundControl.lastActivityAt = current?.lastActivityAt;
363
- input.foregroundControl.currentTool = current?.currentTool;
364
- input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
365
- input.foregroundControl.currentPath = current?.currentPath;
366
- input.foregroundControl.turnCount = current?.turnCount;
367
- input.foregroundControl.tokens = current?.tokens;
368
- input.foregroundControl.toolCount = current?.toolCount;
369
- input.foregroundControl.updatedAt = Date.now();
392
+ updateForegroundChild(input.foregroundControl, childIndex, stepProgress[0]);
370
393
  }
371
394
  input.onUpdate?.({
372
395
  ...progressUpdate,
@@ -385,7 +408,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
385
408
  steps: input.chainSteps,
386
409
  results: input.results.concat(stepResults),
387
410
  currentStepIndex: input.stepIndex,
388
- currentFlatIndex: input.globalTaskIndex + taskIndex,
411
+ currentFlatIndex: childIndex,
389
412
  dynamicChildren: input.dynamicChildren,
390
413
  dynamicGroupStatuses: input.dynamicGroupStatuses,
391
414
  }),
@@ -394,10 +417,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
394
417
  }
395
418
  : undefined,
396
419
  });
397
- if (input.foregroundControl?.currentIndex === input.globalTaskIndex + taskIndex) {
398
- input.foregroundControl.interrupt = undefined;
399
- input.foregroundControl.updatedAt = Date.now();
400
- }
420
+ if (input.foregroundControl) finishForegroundChild(input.foregroundControl, childIndex);
401
421
 
402
422
  if (result.exitCode !== 0 && failFast) {
403
423
  aborted = true;
@@ -425,6 +445,8 @@ interface ChainExecutionParams {
425
445
  sessionFileForIndex?: (idx?: number) => string | undefined;
426
446
  sessionFileForTask?: (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
427
447
  thinkingOverrideForTask?: (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
448
+ contextForAgent?: (agentName: string) => ContextMode;
449
+ modelScope?: ModelScopeConfig;
428
450
  artifactsDir: string;
429
451
  artifactConfig: ArtifactConfig;
430
452
  includeProgress?: boolean;
@@ -432,22 +454,10 @@ interface ChainExecutionParams {
432
454
  onUpdate?: (r: AgentToolResult<Details>) => void;
433
455
  onControlEvent?: (event: ControlEvent) => void;
434
456
  controlConfig: ResolvedControlConfig;
457
+ agentContract?: AgentContract;
435
458
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
436
459
  orchestratorIntercomTarget?: string;
437
- foregroundControl?: {
438
- updatedAt: number;
439
- currentAgent?: string;
440
- currentIndex?: number;
441
- currentActivityState?: ActivityState;
442
- lastActivityAt?: number;
443
- currentTool?: string;
444
- currentToolStartedAt?: number;
445
- currentPath?: string;
446
- turnCount?: number;
447
- tokens?: number;
448
- toolCount?: number;
449
- interrupt?: () => boolean;
450
- };
460
+ foregroundControl?: ForegroundRunControl;
451
461
  chainSkills?: string[];
452
462
  chainDir?: string;
453
463
  dynamicFanoutMaxItems?: number;
@@ -464,6 +474,7 @@ interface ChainExecutionParams {
464
474
  configToolBudget?: ToolBudgetConfig;
465
475
  /** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
466
476
  globalConcurrencyLimit?: number;
477
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
467
478
  }
468
479
 
469
480
  interface ChainExecutionResult {
@@ -517,6 +528,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
517
528
  const dynamicGroupStatuses: ChainExecutionDetailsInput["dynamicGroupStatuses"] = {};
518
529
  const allProgress: AgentProgress[] = [];
519
530
  const allArtifactPaths: ArtifactPaths[] = [];
531
+ let parallelHandoff: Details["parallelHandoff"];
520
532
 
521
533
  const chainAgents: string[] = chainSteps.map((step) =>
522
534
  isParallelStep(step)
@@ -540,6 +552,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
540
552
  outputs,
541
553
  dynamicChildren,
542
554
  dynamicGroupStatuses,
555
+ ...(parallelHandoff ? { parallelHandoff } : {}),
543
556
  ...overrides,
544
557
  });
545
558
 
@@ -671,6 +684,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
671
684
  const parallelTemplates = stepTemplates as string[];
672
685
  const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
673
686
  let worktreeSetup: WorktreeSetup | undefined;
687
+ let worktreeFinalized = false;
674
688
  if (step.worktree) {
675
689
  const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(step.parallel, parallelCwd);
676
690
  if (worktreeTaskCwdConflict) {
@@ -728,6 +742,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
728
742
  sessionFileForIndex,
729
743
  sessionFileForTask,
730
744
  thinkingOverrideForTask,
745
+ contextForAgent: params.contextForAgent,
731
746
  shareEnabled,
732
747
  artifactConfig,
733
748
  artifactsDir,
@@ -742,6 +757,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
742
757
  dynamicChildren,
743
758
  dynamicGroupStatuses,
744
759
  controlConfig,
760
+ agentContract: params.agentContract,
745
761
  onControlEvent,
746
762
  childIntercomTarget,
747
763
  orchestratorIntercomTarget,
@@ -764,6 +780,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
764
780
  if (result.progress) allProgress.push(result.progress);
765
781
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
766
782
  }
783
+ let worktreeSuffix = "";
784
+ if (worktreeSetup) {
785
+ worktreeFinalized = true;
786
+ const finalized = finalizeParallelWorktreeHandoff({
787
+ output: "",
788
+ worktreeSetup,
789
+ artifactsDir,
790
+ runId,
791
+ cwd: parallelCwd,
792
+ stepIndex,
793
+ flatStartIndex: globalTaskIndex - step.parallel.length,
794
+ agents: agentNames,
795
+ results: parallelResults,
796
+ });
797
+ if (finalized.reference) parallelHandoff = finalized.reference;
798
+ worktreeSuffix = finalized.output.trim();
799
+ }
767
800
  const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
768
801
  const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
769
802
  if (interrupted) {
@@ -808,6 +841,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
808
841
  })),
809
842
  };
810
843
  }
844
+ const acceptanceFailures = parallelResults
845
+ .map((result, originalIndex) => ({ result, originalIndex, task: step.parallel[originalIndex] }))
846
+ .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract ?? params.agentContract) && (task?.gateOn ?? step.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
847
+ if (acceptanceFailures.length > 0) {
848
+ const acceptanceSummary = acceptanceFailures
849
+ .map(({ result, originalIndex }) => `- Task ${originalIndex + 1} (${result.agent}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
850
+ .join("\n");
851
+ const errorMsg = `Parallel step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
852
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: errorMsg });
853
+ return {
854
+ content: [{ type: "text", text: summary }],
855
+ isError: true,
856
+ details: buildChainExecutionDetails(makeDetailsInput({
857
+ currentStepIndex: stepIndex,
858
+ currentFlatIndex: globalTaskIndex - step.parallel.length + acceptanceFailures[0]!.originalIndex,
859
+ })),
860
+ };
861
+ }
811
862
 
812
863
  for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
813
864
  const outputName = step.parallel[taskIndex]?.as;
@@ -831,14 +882,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
831
882
  };
832
883
  });
833
884
  prev = aggregateParallelOutputs(taskResults);
834
- prev = appendParallelWorktreeSummary(
835
- prev,
836
- worktreeSetup,
837
- path.join(chainDir, "worktree-diffs", `step-${stepIndex}`),
838
- agentNames,
839
- );
885
+ if (worktreeSuffix) prev = `${prev}\n\n${worktreeSuffix}`;
840
886
  } finally {
841
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
887
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
842
888
  }
843
889
  } else if (isDynamicParallelStep(step)) {
844
890
  const dynamicStartIndex = globalTaskIndex;
@@ -863,7 +909,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
863
909
  if (materialized.parallel.length === 0) {
864
910
  const collection: DynamicCollectedResult[] = [];
865
911
  try {
866
- validateDynamicCollection(step.collect.outputSchema, collection);
912
+ await validateDynamicCollection(step.collect.outputSchema, collection);
867
913
  } catch (error) {
868
914
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
869
915
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
@@ -884,6 +930,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
884
930
  task: (step.parallel.task ?? originalTask ?? "").replace(/\{task\}/g, originalTask ?? ""),
885
931
  mode: "chain",
886
932
  dynamicGroup: true,
933
+ agentContract: step.agentContract ?? params.agentContract,
887
934
  });
888
935
  const groupAcceptance = await evaluateAcceptance({
889
936
  acceptance: effectiveGroupAcceptance,
@@ -893,9 +940,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
893
940
  notes: "Dynamic fanout produced 0 results.",
894
941
  }),
895
942
  cwd: cwd ?? ctx.cwd,
943
+ reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
896
944
  });
897
945
  dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
898
- const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
946
+ const groupAcceptanceFailure = !isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance" ? acceptanceFailureMessage(groupAcceptance) : undefined;
899
947
  if (groupAcceptanceFailure) {
900
948
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
901
949
  return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
@@ -910,6 +958,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
910
958
  parallel: materialized.parallel,
911
959
  concurrency: step.concurrency,
912
960
  failFast: step.failFast,
961
+ agentContract: step.agentContract,
962
+ gateOn: step.gateOn,
913
963
  };
914
964
  const parallelTemplates = materialized.parallel.map((task) => task.task ?? "{previous}");
915
965
  const parallelBehaviors = resolveParallelBehaviors(dynamicParallelStep.parallel, agents, stepIndex, chainSkills)
@@ -949,6 +999,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
949
999
  sessionFileForIndex,
950
1000
  sessionFileForTask,
951
1001
  thinkingOverrideForTask,
1002
+ contextForAgent: params.contextForAgent,
952
1003
  shareEnabled,
953
1004
  artifactConfig,
954
1005
  artifactsDir,
@@ -963,6 +1014,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
963
1014
  dynamicChildren,
964
1015
  dynamicGroupStatuses,
965
1016
  controlConfig,
1017
+ agentContract: params.agentContract,
966
1018
  onControlEvent,
967
1019
  childIntercomTarget,
968
1020
  orchestratorIntercomTarget,
@@ -1030,8 +1082,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1030
1082
  })),
1031
1083
  };
1032
1084
  }
1085
+ const acceptanceFailures = parallelResults
1086
+ .map((result, originalIndex) => ({ result, originalIndex, task: dynamicParallelStep.parallel[originalIndex] }))
1087
+ .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? dynamicParallelStep.agentContract ?? params.agentContract) && (task?.gateOn ?? dynamicParallelStep.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
1088
+ if (acceptanceFailures.length > 0) {
1089
+ const acceptanceSummary = acceptanceFailures
1090
+ .map(({ result, originalIndex }) => `- Item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
1091
+ .join("\n");
1092
+ const errorMsg = `Dynamic step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
1093
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
1094
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: errorMsg });
1095
+ return {
1096
+ content: [{ type: "text", text: summary }],
1097
+ isError: true,
1098
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: dynamicStartIndex + acceptanceFailures[0]!.originalIndex })),
1099
+ };
1100
+ }
1033
1101
  try {
1034
- validateDynamicCollection(step.collect.outputSchema, collected);
1102
+ await validateDynamicCollection(step.collect.outputSchema, collected);
1035
1103
  } catch (error) {
1036
1104
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1037
1105
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
@@ -1053,6 +1121,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1053
1121
  .join("\n"),
1054
1122
  mode: "chain",
1055
1123
  dynamicGroup: true,
1124
+ agentContract: step.agentContract ?? params.agentContract,
1056
1125
  });
1057
1126
  const groupAcceptance = await evaluateAcceptance({
1058
1127
  acceptance: effectiveGroupAcceptance,
@@ -1062,9 +1131,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1062
1131
  notes: `Dynamic fanout collected ${collected.length} result(s) into ${step.collect.as}.`,
1063
1132
  }),
1064
1133
  cwd: cwd ?? ctx.cwd,
1134
+ reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
1065
1135
  });
1066
1136
  dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
1067
- const groupAcceptanceFailure = effectiveGroupAcceptance.explicit ? acceptanceFailureMessage(groupAcceptance) : undefined;
1137
+ const groupAcceptanceFailure = effectiveGroupAcceptance.explicit && (!isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(groupAcceptance) : undefined;
1068
1138
  if (groupAcceptanceFailure) {
1069
1139
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
1070
1140
  return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
@@ -1147,22 +1217,22 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1147
1217
  const childIndex = globalTaskIndex;
1148
1218
  const interruptController = new AbortController();
1149
1219
  if (foregroundControl) {
1150
- foregroundControl.currentAgent = seqStep.agent;
1151
- foregroundControl.currentIndex = childIndex;
1152
- foregroundControl.currentActivityState = undefined;
1153
- foregroundControl.updatedAt = Date.now();
1154
- foregroundControl.interrupt = () => {
1155
- if (interruptController.signal.aborted) return false;
1156
- interruptController.abort();
1157
- foregroundControl.currentActivityState = undefined;
1158
- foregroundControl.updatedAt = Date.now();
1159
- return true;
1160
- };
1220
+ beginForegroundChild(foregroundControl, {
1221
+ index: childIndex,
1222
+ agent: seqStep.agent,
1223
+ description: cleanTask.trim(),
1224
+ interrupt: () => {
1225
+ if (interruptController.signal.aborted) return false;
1226
+ interruptController.abort();
1227
+ return true;
1228
+ },
1229
+ });
1161
1230
  }
1162
1231
 
1163
1232
  const structuredRuntime = seqStep.outputSchema
1164
1233
  ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1165
1234
  : undefined;
1235
+ const agentContract = seqStep.agentContract ?? params.agentContract;
1166
1236
  const toolBudget = resolveChainToolBudget({ stepBudget: seqStep.toolBudget, runBudget: params.toolBudget, agentBudget: agentConfig?.toolBudget, configBudget: params.configToolBudget });
1167
1237
  if (toolBudget.error) return buildChainExecutionErrorResult(toolBudget.error, {
1168
1238
  results,
@@ -1182,6 +1252,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1182
1252
  });
1183
1253
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1184
1254
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1255
+ capabilityCeiling: params.capabilityCeiling,
1256
+ context: params.contextForAgent?.(seqStep.agent),
1185
1257
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1186
1258
  signal,
1187
1259
  interruptSignal: interruptController.signal,
@@ -1210,6 +1282,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1210
1282
  modelScope,
1211
1283
  skills: behavior.skills === false ? [] : behavior.skills,
1212
1284
  structuredOutput: structuredRuntime,
1285
+ agentContract,
1213
1286
  acceptance: seqStep.acceptance,
1214
1287
  acceptanceContext: { mode: "chain" },
1215
1288
  timeoutMs: params.timeoutMs,
@@ -1224,18 +1297,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1224
1297
  const stepResults = p.details?.results || [];
1225
1298
  const stepProgress = p.details?.progress || [];
1226
1299
  if (foregroundControl && stepProgress.length > 0) {
1227
- const current = stepProgress[0];
1228
- foregroundControl.currentAgent = seqStep.agent;
1229
- foregroundControl.currentIndex = childIndex;
1230
- foregroundControl.currentActivityState = current?.activityState;
1231
- foregroundControl.lastActivityAt = current?.lastActivityAt;
1232
- foregroundControl.currentTool = current?.currentTool;
1233
- foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
1234
- foregroundControl.currentPath = current?.currentPath;
1235
- foregroundControl.turnCount = current?.turnCount;
1236
- foregroundControl.tokens = current?.tokens;
1237
- foregroundControl.toolCount = current?.toolCount;
1238
- foregroundControl.updatedAt = Date.now();
1300
+ updateForegroundChild(foregroundControl, childIndex, stepProgress[0]);
1239
1301
  }
1240
1302
  onUpdate({
1241
1303
  ...p,
@@ -1263,10 +1325,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1263
1325
  }
1264
1326
  : undefined,
1265
1327
  });
1266
- if (foregroundControl?.currentIndex === childIndex) {
1267
- foregroundControl.interrupt = undefined;
1268
- foregroundControl.updatedAt = Date.now();
1269
- }
1328
+ if (foregroundControl) finishForegroundChild(foregroundControl, childIndex);
1270
1329
  recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
1271
1330
 
1272
1331
  globalTaskIndex++;
@@ -1298,6 +1357,17 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1298
1357
  isError: true,
1299
1358
  };
1300
1359
  }
1360
+ if (isAgentContractV1(agentContract) && seqStep.gateOn === "acceptance" && r.acceptance?.status === "rejected") {
1361
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
1362
+ index: stepIndex,
1363
+ error: acceptanceFailureMessage(r.acceptance) ?? "Chain acceptance gate rejected the step.",
1364
+ });
1365
+ return {
1366
+ content: [{ type: "text", text: summary }],
1367
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: childIndex })),
1368
+ isError: true,
1369
+ };
1370
+ }
1301
1371
 
1302
1372
  if (behavior.output) {
1303
1373
  try {