pi-subagents 0.35.0 → 0.36.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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +132 -24
  3. package/agents/advisor.md +73 -0
  4. package/package.json +8 -12
  5. package/skills/pi-subagents/SKILL.md +22 -9
  6. package/src/agents/agents.ts +22 -5
  7. package/src/api/delegation.ts +125 -0
  8. package/src/extension/config.ts +7 -1
  9. package/src/extension/index.ts +50 -38
  10. package/src/extension/rpc.ts +27 -2
  11. package/src/extension/schemas.ts +22 -2
  12. package/src/extension/tool-description.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +1 -1
  14. package/src/intercom/native-supervisor-channel.ts +45 -6
  15. package/src/intercom/result-intercom.ts +7 -0
  16. package/src/runs/background/async-execution.ts +34 -4
  17. package/src/runs/background/async-job-tracker.ts +4 -0
  18. package/src/runs/background/async-resume.ts +27 -5
  19. package/src/runs/background/async-status.ts +76 -3
  20. package/src/runs/background/chain-append.ts +2 -0
  21. package/src/runs/background/completion-batcher.ts +6 -4
  22. package/src/runs/background/completion-dedupe.ts +2 -11
  23. package/src/runs/background/fleet-view.ts +9 -4
  24. package/src/runs/background/notify.ts +132 -120
  25. package/src/runs/background/result-watcher.ts +138 -78
  26. package/src/runs/background/run-status.ts +3 -1
  27. package/src/runs/background/subagent-runner.ts +225 -43
  28. package/src/runs/background/subagent-wait.ts +130 -4
  29. package/src/runs/background/wait-tool.ts +2 -2
  30. package/src/runs/foreground/chain-execution.ts +176 -111
  31. package/src/runs/foreground/execution.ts +90 -36
  32. package/src/runs/foreground/foreground-control.ts +90 -0
  33. package/src/runs/foreground/subagent-executor.ts +394 -163
  34. package/src/runs/shared/acceptance.ts +55 -13
  35. package/src/runs/shared/agent-contract.ts +38 -0
  36. package/src/runs/shared/child-protocol.ts +1 -1
  37. package/src/runs/shared/completion-guard.ts +36 -5
  38. package/src/runs/shared/context-mode.ts +44 -0
  39. package/src/runs/shared/dynamic-fanout.ts +4 -4
  40. package/src/runs/shared/long-running-guard.ts +4 -0
  41. package/src/runs/shared/nested-events.ts +27 -2
  42. package/src/runs/shared/parallel-handoff.ts +154 -0
  43. package/src/runs/shared/parallel-utils.ts +6 -0
  44. package/src/runs/shared/pi-args.ts +23 -14
  45. package/src/runs/shared/run-history.ts +90 -5
  46. package/src/runs/shared/structured-output.ts +112 -7
  47. package/src/runs/shared/subagent-control.ts +4 -0
  48. package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
  49. package/src/runs/shared/task-intent.ts +10 -5
  50. package/src/runs/shared/tool-availability.ts +3 -1
  51. package/src/runs/shared/tool-budget.ts +11 -5
  52. package/src/runs/shared/turn-budget.ts +2 -1
  53. package/src/runs/shared/worktree.ts +63 -14
  54. package/src/shared/accessible-dir.ts +25 -0
  55. package/src/shared/artifacts.ts +37 -7
  56. package/src/shared/atomic-json.ts +14 -42
  57. package/src/shared/child-transcript.ts +52 -0
  58. package/src/shared/file-system-retry.ts +47 -0
  59. package/src/shared/settings.ts +9 -1
  60. package/src/shared/types.ts +211 -22
  61. package/src/slash/delegation-adapters.ts +152 -5
  62. package/src/slash/delegation-json.ts +108 -0
  63. package/src/slash/delegation-request.ts +182 -36
  64. package/src/slash/prompt-template-bridge.ts +222 -37
  65. package/src/slash/selector.ts +147 -0
  66. package/src/slash/slash-commands.ts +14 -5
  67. package/src/slash/slash-live-state.ts +2 -2
  68. package/src/slash/subagents-admin.ts +42 -42
  69. package/src/tui/fleet-status.ts +362 -0
  70. package/src/tui/fleet-transcript.ts +472 -0
  71. package/src/tui/fleet.ts +318 -59
  72. package/src/tui/render.ts +25 -15
  73. package/src/watchdog/change-signature.ts +105 -12
  74. package/src/watchdog/review.ts +7 -2
  75. package/src/watchdog/runtime.ts +5 -3
  76. 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,10 @@ 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";
76
82
 
77
83
  interface ChainExecutionDetailsInput {
78
84
  results: SingleResult[];
@@ -89,6 +95,7 @@ interface ChainExecutionDetailsInput {
89
95
  currentFlatIndex?: number;
90
96
  dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
91
97
  dynamicGroupStatuses?: Record<number, { status: "pending" | "running" | "completed" | "failed" | "paused" | "stopped" | "detached"; error?: string; acceptance?: SingleResult["acceptance"] }>;
98
+ parallelHandoff?: Details["parallelHandoff"];
92
99
  }
93
100
 
94
101
  interface ParallelChainRunInput {
@@ -111,6 +118,7 @@ interface ParallelChainRunInput {
111
118
  sessionFileForIndex?: (idx?: number) => string | undefined;
112
119
  sessionFileForTask?: (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
113
120
  thinkingOverrideForTask?: (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
121
+ contextForAgent?: (agentName: string) => ContextMode;
114
122
  shareEnabled: boolean;
115
123
  artifactConfig: ArtifactConfig;
116
124
  artifactsDir: string;
@@ -118,22 +126,10 @@ interface ParallelChainRunInput {
118
126
  onUpdate?: (r: AgentToolResult<Details>) => void;
119
127
  onControlEvent?: (event: ControlEvent) => void;
120
128
  controlConfig: ResolvedControlConfig;
129
+ agentContract?: AgentContract;
121
130
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
122
131
  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
- };
132
+ foregroundControl?: ForegroundRunControl;
137
133
  results: SingleResult[];
138
134
  allProgress: AgentProgress[];
139
135
  outputs: ChainOutputMap;
@@ -167,6 +163,7 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
167
163
  outputs: input.outputs,
168
164
  totalChildUsage: sumResultsUsage(input.results),
169
165
  totalCost: sumResultsCost(input.results),
166
+ ...(input.parallelHandoff ? { parallelHandoff: input.parallelHandoff } : {}),
170
167
  workflowGraph: buildWorkflowGraphSnapshot({
171
168
  runId: input.runId,
172
169
  mode: "chain",
@@ -200,17 +197,48 @@ function ensureParallelProgressFile(
200
197
  return true;
201
198
  }
202
199
 
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);
200
+ function finalizeParallelWorktreeHandoff(input: {
201
+ output: string;
202
+ worktreeSetup: WorktreeSetup;
203
+ artifactsDir: string;
204
+ runId: string;
205
+ cwd: string;
206
+ stepIndex: number;
207
+ flatStartIndex: number;
208
+ agents: string[];
209
+ results: SingleResult[];
210
+ }): { output: string; reference?: NonNullable<Details["parallelHandoff"]> } {
211
+ const diffs = diffWorktrees(input.worktreeSetup, input.agents, path.join(input.artifactsDir, "worktree-diffs", input.runId, `step-${input.stepIndex}`));
212
+ const cleanup = cleanupWorktrees(input.worktreeSetup);
211
213
  const diffSummary = formatWorktreeDiffSummary(diffs);
212
- if (!diffSummary) return output;
213
- return `${output}\n\n${diffSummary}`;
214
+ try {
215
+ const reference = writeParallelHandoffGroup({
216
+ manifestPath: parallelHandoffPath(input.artifactsDir, input.runId),
217
+ runId: input.runId,
218
+ mode: "chain",
219
+ source: "foreground",
220
+ cwd: input.cwd,
221
+ stepIndex: input.stepIndex,
222
+ flatStartIndex: input.flatStartIndex,
223
+ setup: input.worktreeSetup,
224
+ diffs,
225
+ cleanup,
226
+ results: input.results.map((result) => ({
227
+ agent: result.agent,
228
+ status: result.stopped ? "stopped" : result.detached ? "detached" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
229
+ summary: getSingleResultOutput(result) || result.error || "(no output)",
230
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
231
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
232
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
233
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
234
+ })),
235
+ });
236
+ const suffix = [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n");
237
+ return { output: suffix ? `${input.output}\n\n${suffix}` : input.output, reference };
238
+ } catch (error) {
239
+ const suffix = [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
240
+ return { output: suffix ? `${input.output}\n\n${suffix}` : input.output };
241
+ }
214
242
  }
215
243
 
216
244
  function resolveChainToolBudget(input: { stepBudget?: ToolBudgetConfig; runBudget?: ResolvedToolBudget; agentBudget?: ToolBudgetConfig; configBudget?: ToolBudgetConfig }): { toolBudget?: ResolvedToolBudget; error?: string } {
@@ -294,36 +322,38 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
294
322
  : undefined;
295
323
  taskStr = injectSingleOutputInstruction(taskStr, outputPath, taskAgentConfig);
296
324
  const interruptController = new AbortController();
325
+ const childIndex = input.globalTaskIndex + taskIndex;
297
326
  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
- };
327
+ beginForegroundChild(input.foregroundControl, {
328
+ index: childIndex,
329
+ agent: task.agent,
330
+ description: cleanTask.trim(),
331
+ interrupt: () => {
332
+ if (interruptController.signal.aborted) return false;
333
+ interruptController.abort();
334
+ return true;
335
+ },
336
+ });
309
337
  }
310
338
 
311
339
  const structuredRuntime = task.outputSchema
312
340
  ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
313
341
  : undefined;
342
+ const agentContract = task.agentContract ?? input.step.agentContract ?? input.agentContract;
314
343
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
315
344
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
345
+ context: input.contextForAgent?.(task.agent),
316
346
  cwd: taskCwd,
317
347
  signal: input.signal,
318
348
  interruptSignal: interruptController.signal,
319
349
  allowIntercomDetach: taskAgentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
320
350
  intercomEvents: input.intercomEvents,
321
351
  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),
352
+ index: childIndex,
353
+ sessionDir: input.sessionDirForIndex(childIndex),
354
+ sessionFile: input.sessionFileForTask?.(task.agent, childIndex, effectiveModel)
355
+ ?? input.sessionFileForIndex?.(childIndex),
356
+ thinkingOverride: input.thinkingOverrideForTask?.(task.agent, childIndex, effectiveModel),
327
357
  share: input.shareEnabled,
328
358
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
329
359
  artifactConfig: input.artifactConfig,
@@ -332,7 +362,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
332
362
  maxSubagentDepth,
333
363
  controlConfig: input.controlConfig,
334
364
  onControlEvent: input.onControlEvent,
335
- intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
365
+ intercomSessionName: input.childIntercomTarget?.(task.agent, childIndex),
336
366
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
337
367
  nestedRoute: input.nestedRoute,
338
368
  modelOverride: effectiveModel,
@@ -341,13 +371,14 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
341
371
  modelScope: input.modelScope,
342
372
  skills: behavior.skills === false ? [] : behavior.skills,
343
373
  structuredOutput: structuredRuntime,
374
+ agentContract,
344
375
  acceptance: task.acceptance,
345
376
  acceptanceContext: { mode: "chain", dynamic: input.dynamic && task.acceptance === undefined },
346
377
  timeoutMs: input.timeoutMs,
347
378
  deadlineAt: input.deadlineAt,
348
379
  turnBudget: input.turnBudget,
349
380
  onDetachedExit: input.onDetachedExit
350
- ? (result) => input.onDetachedExit?.(input.globalTaskIndex + taskIndex, result)
381
+ ? (result) => input.onDetachedExit?.(childIndex, result)
351
382
  : undefined,
352
383
  toolBudget: toolBudget.toolBudget,
353
384
  onUpdate: input.onUpdate
@@ -355,18 +386,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
355
386
  const stepResults = progressUpdate.details?.results || [];
356
387
  const stepProgress = progressUpdate.details?.progress || [];
357
388
  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();
389
+ updateForegroundChild(input.foregroundControl, childIndex, stepProgress[0]);
370
390
  }
371
391
  input.onUpdate?.({
372
392
  ...progressUpdate,
@@ -385,7 +405,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
385
405
  steps: input.chainSteps,
386
406
  results: input.results.concat(stepResults),
387
407
  currentStepIndex: input.stepIndex,
388
- currentFlatIndex: input.globalTaskIndex + taskIndex,
408
+ currentFlatIndex: childIndex,
389
409
  dynamicChildren: input.dynamicChildren,
390
410
  dynamicGroupStatuses: input.dynamicGroupStatuses,
391
411
  }),
@@ -394,10 +414,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
394
414
  }
395
415
  : undefined,
396
416
  });
397
- if (input.foregroundControl?.currentIndex === input.globalTaskIndex + taskIndex) {
398
- input.foregroundControl.interrupt = undefined;
399
- input.foregroundControl.updatedAt = Date.now();
400
- }
417
+ if (input.foregroundControl) finishForegroundChild(input.foregroundControl, childIndex);
401
418
 
402
419
  if (result.exitCode !== 0 && failFast) {
403
420
  aborted = true;
@@ -425,6 +442,8 @@ interface ChainExecutionParams {
425
442
  sessionFileForIndex?: (idx?: number) => string | undefined;
426
443
  sessionFileForTask?: (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
427
444
  thinkingOverrideForTask?: (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
445
+ contextForAgent?: (agentName: string) => ContextMode;
446
+ modelScope?: ModelScopeConfig;
428
447
  artifactsDir: string;
429
448
  artifactConfig: ArtifactConfig;
430
449
  includeProgress?: boolean;
@@ -432,22 +451,10 @@ interface ChainExecutionParams {
432
451
  onUpdate?: (r: AgentToolResult<Details>) => void;
433
452
  onControlEvent?: (event: ControlEvent) => void;
434
453
  controlConfig: ResolvedControlConfig;
454
+ agentContract?: AgentContract;
435
455
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
436
456
  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
- };
457
+ foregroundControl?: ForegroundRunControl;
451
458
  chainSkills?: string[];
452
459
  chainDir?: string;
453
460
  dynamicFanoutMaxItems?: number;
@@ -517,6 +524,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
517
524
  const dynamicGroupStatuses: ChainExecutionDetailsInput["dynamicGroupStatuses"] = {};
518
525
  const allProgress: AgentProgress[] = [];
519
526
  const allArtifactPaths: ArtifactPaths[] = [];
527
+ let parallelHandoff: Details["parallelHandoff"];
520
528
 
521
529
  const chainAgents: string[] = chainSteps.map((step) =>
522
530
  isParallelStep(step)
@@ -540,6 +548,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
540
548
  outputs,
541
549
  dynamicChildren,
542
550
  dynamicGroupStatuses,
551
+ ...(parallelHandoff ? { parallelHandoff } : {}),
543
552
  ...overrides,
544
553
  });
545
554
 
@@ -671,6 +680,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
671
680
  const parallelTemplates = stepTemplates as string[];
672
681
  const parallelCwd = resolveChildCwd(cwd ?? ctx.cwd, step.cwd);
673
682
  let worktreeSetup: WorktreeSetup | undefined;
683
+ let worktreeFinalized = false;
674
684
  if (step.worktree) {
675
685
  const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(step.parallel, parallelCwd);
676
686
  if (worktreeTaskCwdConflict) {
@@ -728,6 +738,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
728
738
  sessionFileForIndex,
729
739
  sessionFileForTask,
730
740
  thinkingOverrideForTask,
741
+ contextForAgent: params.contextForAgent,
731
742
  shareEnabled,
732
743
  artifactConfig,
733
744
  artifactsDir,
@@ -742,6 +753,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
742
753
  dynamicChildren,
743
754
  dynamicGroupStatuses,
744
755
  controlConfig,
756
+ agentContract: params.agentContract,
745
757
  onControlEvent,
746
758
  childIntercomTarget,
747
759
  orchestratorIntercomTarget,
@@ -764,6 +776,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
764
776
  if (result.progress) allProgress.push(result.progress);
765
777
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
766
778
  }
779
+ let worktreeSuffix = "";
780
+ if (worktreeSetup) {
781
+ worktreeFinalized = true;
782
+ const finalized = finalizeParallelWorktreeHandoff({
783
+ output: "",
784
+ worktreeSetup,
785
+ artifactsDir,
786
+ runId,
787
+ cwd: parallelCwd,
788
+ stepIndex,
789
+ flatStartIndex: globalTaskIndex - step.parallel.length,
790
+ agents: agentNames,
791
+ results: parallelResults,
792
+ });
793
+ if (finalized.reference) parallelHandoff = finalized.reference;
794
+ worktreeSuffix = finalized.output.trim();
795
+ }
767
796
  const interruptedIndexInStep = parallelResults.findIndex((result) => result.interrupted);
768
797
  const interrupted = interruptedIndexInStep >= 0 ? parallelResults[interruptedIndexInStep] : undefined;
769
798
  if (interrupted) {
@@ -808,6 +837,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
808
837
  })),
809
838
  };
810
839
  }
840
+ const acceptanceFailures = parallelResults
841
+ .map((result, originalIndex) => ({ result, originalIndex, task: step.parallel[originalIndex] }))
842
+ .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract ?? params.agentContract) && (task?.gateOn ?? step.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
843
+ if (acceptanceFailures.length > 0) {
844
+ const acceptanceSummary = acceptanceFailures
845
+ .map(({ result, originalIndex }) => `- Task ${originalIndex + 1} (${result.agent}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
846
+ .join("\n");
847
+ const errorMsg = `Parallel step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
848
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: errorMsg });
849
+ return {
850
+ content: [{ type: "text", text: summary }],
851
+ isError: true,
852
+ details: buildChainExecutionDetails(makeDetailsInput({
853
+ currentStepIndex: stepIndex,
854
+ currentFlatIndex: globalTaskIndex - step.parallel.length + acceptanceFailures[0]!.originalIndex,
855
+ })),
856
+ };
857
+ }
811
858
 
812
859
  for (let taskIndex = 0; taskIndex < parallelResults.length; taskIndex++) {
813
860
  const outputName = step.parallel[taskIndex]?.as;
@@ -831,14 +878,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
831
878
  };
832
879
  });
833
880
  prev = aggregateParallelOutputs(taskResults);
834
- prev = appendParallelWorktreeSummary(
835
- prev,
836
- worktreeSetup,
837
- path.join(chainDir, "worktree-diffs", `step-${stepIndex}`),
838
- agentNames,
839
- );
881
+ if (worktreeSuffix) prev = `${prev}\n\n${worktreeSuffix}`;
840
882
  } finally {
841
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
883
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
842
884
  }
843
885
  } else if (isDynamicParallelStep(step)) {
844
886
  const dynamicStartIndex = globalTaskIndex;
@@ -863,7 +905,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
863
905
  if (materialized.parallel.length === 0) {
864
906
  const collection: DynamicCollectedResult[] = [];
865
907
  try {
866
- validateDynamicCollection(step.collect.outputSchema, collection);
908
+ await validateDynamicCollection(step.collect.outputSchema, collection);
867
909
  } catch (error) {
868
910
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
869
911
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
@@ -884,6 +926,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
884
926
  task: (step.parallel.task ?? originalTask ?? "").replace(/\{task\}/g, originalTask ?? ""),
885
927
  mode: "chain",
886
928
  dynamicGroup: true,
929
+ agentContract: step.agentContract ?? params.agentContract,
887
930
  });
888
931
  const groupAcceptance = await evaluateAcceptance({
889
932
  acceptance: effectiveGroupAcceptance,
@@ -893,9 +936,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
893
936
  notes: "Dynamic fanout produced 0 results.",
894
937
  }),
895
938
  cwd: cwd ?? ctx.cwd,
939
+ reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
896
940
  });
897
941
  dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
898
- const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
942
+ const groupAcceptanceFailure = !isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance" ? acceptanceFailureMessage(groupAcceptance) : undefined;
899
943
  if (groupAcceptanceFailure) {
900
944
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
901
945
  return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
@@ -910,6 +954,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
910
954
  parallel: materialized.parallel,
911
955
  concurrency: step.concurrency,
912
956
  failFast: step.failFast,
957
+ agentContract: step.agentContract,
958
+ gateOn: step.gateOn,
913
959
  };
914
960
  const parallelTemplates = materialized.parallel.map((task) => task.task ?? "{previous}");
915
961
  const parallelBehaviors = resolveParallelBehaviors(dynamicParallelStep.parallel, agents, stepIndex, chainSkills)
@@ -949,6 +995,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
949
995
  sessionFileForIndex,
950
996
  sessionFileForTask,
951
997
  thinkingOverrideForTask,
998
+ contextForAgent: params.contextForAgent,
952
999
  shareEnabled,
953
1000
  artifactConfig,
954
1001
  artifactsDir,
@@ -963,6 +1010,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
963
1010
  dynamicChildren,
964
1011
  dynamicGroupStatuses,
965
1012
  controlConfig,
1013
+ agentContract: params.agentContract,
966
1014
  onControlEvent,
967
1015
  childIntercomTarget,
968
1016
  orchestratorIntercomTarget,
@@ -1030,8 +1078,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1030
1078
  })),
1031
1079
  };
1032
1080
  }
1081
+ const acceptanceFailures = parallelResults
1082
+ .map((result, originalIndex) => ({ result, originalIndex, task: dynamicParallelStep.parallel[originalIndex] }))
1083
+ .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? dynamicParallelStep.agentContract ?? params.agentContract) && (task?.gateOn ?? dynamicParallelStep.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
1084
+ if (acceptanceFailures.length > 0) {
1085
+ const acceptanceSummary = acceptanceFailures
1086
+ .map(({ result, originalIndex }) => `- Item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
1087
+ .join("\n");
1088
+ const errorMsg = `Dynamic step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
1089
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
1090
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: errorMsg });
1091
+ return {
1092
+ content: [{ type: "text", text: summary }],
1093
+ isError: true,
1094
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: dynamicStartIndex + acceptanceFailures[0]!.originalIndex })),
1095
+ };
1096
+ }
1033
1097
  try {
1034
- validateDynamicCollection(step.collect.outputSchema, collected);
1098
+ await validateDynamicCollection(step.collect.outputSchema, collected);
1035
1099
  } catch (error) {
1036
1100
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1037
1101
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
@@ -1053,6 +1117,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1053
1117
  .join("\n"),
1054
1118
  mode: "chain",
1055
1119
  dynamicGroup: true,
1120
+ agentContract: step.agentContract ?? params.agentContract,
1056
1121
  });
1057
1122
  const groupAcceptance = await evaluateAcceptance({
1058
1123
  acceptance: effectiveGroupAcceptance,
@@ -1062,9 +1127,10 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1062
1127
  notes: `Dynamic fanout collected ${collected.length} result(s) into ${step.collect.as}.`,
1063
1128
  }),
1064
1129
  cwd: cwd ?? ctx.cwd,
1130
+ reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
1065
1131
  });
1066
1132
  dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
1067
- const groupAcceptanceFailure = effectiveGroupAcceptance.explicit ? acceptanceFailureMessage(groupAcceptance) : undefined;
1133
+ const groupAcceptanceFailure = effectiveGroupAcceptance.explicit && (!isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(groupAcceptance) : undefined;
1068
1134
  if (groupAcceptanceFailure) {
1069
1135
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
1070
1136
  return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
@@ -1147,22 +1213,22 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1147
1213
  const childIndex = globalTaskIndex;
1148
1214
  const interruptController = new AbortController();
1149
1215
  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
- };
1216
+ beginForegroundChild(foregroundControl, {
1217
+ index: childIndex,
1218
+ agent: seqStep.agent,
1219
+ description: cleanTask.trim(),
1220
+ interrupt: () => {
1221
+ if (interruptController.signal.aborted) return false;
1222
+ interruptController.abort();
1223
+ return true;
1224
+ },
1225
+ });
1161
1226
  }
1162
1227
 
1163
1228
  const structuredRuntime = seqStep.outputSchema
1164
1229
  ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1165
1230
  : undefined;
1231
+ const agentContract = seqStep.agentContract ?? params.agentContract;
1166
1232
  const toolBudget = resolveChainToolBudget({ stepBudget: seqStep.toolBudget, runBudget: params.toolBudget, agentBudget: agentConfig?.toolBudget, configBudget: params.configToolBudget });
1167
1233
  if (toolBudget.error) return buildChainExecutionErrorResult(toolBudget.error, {
1168
1234
  results,
@@ -1182,6 +1248,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1182
1248
  });
1183
1249
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1184
1250
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1251
+ context: params.contextForAgent?.(seqStep.agent),
1185
1252
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1186
1253
  signal,
1187
1254
  interruptSignal: interruptController.signal,
@@ -1210,6 +1277,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1210
1277
  modelScope,
1211
1278
  skills: behavior.skills === false ? [] : behavior.skills,
1212
1279
  structuredOutput: structuredRuntime,
1280
+ agentContract,
1213
1281
  acceptance: seqStep.acceptance,
1214
1282
  acceptanceContext: { mode: "chain" },
1215
1283
  timeoutMs: params.timeoutMs,
@@ -1224,18 +1292,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1224
1292
  const stepResults = p.details?.results || [];
1225
1293
  const stepProgress = p.details?.progress || [];
1226
1294
  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();
1295
+ updateForegroundChild(foregroundControl, childIndex, stepProgress[0]);
1239
1296
  }
1240
1297
  onUpdate({
1241
1298
  ...p,
@@ -1263,10 +1320,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1263
1320
  }
1264
1321
  : undefined,
1265
1322
  });
1266
- if (foregroundControl?.currentIndex === childIndex) {
1267
- foregroundControl.interrupt = undefined;
1268
- foregroundControl.updatedAt = Date.now();
1269
- }
1323
+ if (foregroundControl) finishForegroundChild(foregroundControl, childIndex);
1270
1324
  recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
1271
1325
 
1272
1326
  globalTaskIndex++;
@@ -1298,6 +1352,17 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1298
1352
  isError: true,
1299
1353
  };
1300
1354
  }
1355
+ if (isAgentContractV1(agentContract) && seqStep.gateOn === "acceptance" && r.acceptance?.status === "rejected") {
1356
+ const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
1357
+ index: stepIndex,
1358
+ error: acceptanceFailureMessage(r.acceptance) ?? "Chain acceptance gate rejected the step.",
1359
+ });
1360
+ return {
1361
+ content: [{ type: "text", text: summary }],
1362
+ details: buildChainExecutionDetails(makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: childIndex })),
1363
+ isError: true,
1364
+ };
1365
+ }
1301
1366
 
1302
1367
  if (behavior.output) {
1303
1368
  try {