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
@@ -42,15 +42,15 @@ const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
42
42
  const ITEM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
43
43
  const ITEM_REF_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)(?:\.([^{}]+))?\}/g;
44
44
  const RESERVED_TEMPLATE_NAMES = new Set(["task", "previous", "chain_dir", "outputs"]);
45
- const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency", "failFast", "phase", "label", "acceptance"]);
45
+ const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency", "failFast", "phase", "label", "acceptance", "agentContract", "gateOn"]);
46
46
  const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "sessionFiles", "thinkingOverrides"]);
47
47
  const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
48
48
  const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
49
- const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "toolBudget", "acceptance"]);
49
+ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "toolBudget", "acceptance", "agentContract", "gateOn"]);
50
50
  const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
51
51
  ...DYNAMIC_PARALLEL_KEYS,
52
52
  "outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "waitToolEnabled",
53
- "structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "completionGuard", "systemPrompt",
53
+ "structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "capabilityCeiling", "completionGuard", "systemPrompt",
54
54
  "systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId",
55
55
  ]);
56
56
  const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
@@ -288,9 +288,9 @@ export function collectDynamicResults(
288
288
  });
289
289
  }
290
290
 
291
- export function validateDynamicCollection(schema: JsonSchemaObject | undefined, value: DynamicCollectedResult[]): void {
291
+ export async function validateDynamicCollection(schema: JsonSchemaObject | undefined, value: DynamicCollectedResult[]): Promise<void> {
292
292
  if (!schema) return;
293
- const validation = validateStructuredOutputValue(schema, value);
293
+ const validation = await validateStructuredOutputValue(schema, value);
294
294
  if (validation.status === "invalid") {
295
295
  throw new DynamicFanoutError(`Collected output validation failed: ${validation.message}`);
296
296
  }
@@ -103,6 +103,10 @@ export function isMutatingBashCommand(command: string): boolean {
103
103
  export function isMutatingTool(toolName: string | undefined, args: Record<string, unknown> | undefined): boolean {
104
104
  if (!toolName) return false;
105
105
  if (toolName === "edit" || toolName === "write") return true;
106
+ if (toolName === "cursor") {
107
+ const activityTitle = typeof args?.activityTitle === "string" ? args.activityTitle : "";
108
+ return /^Cursor (?:edit|write)\b/i.test(activityTitle);
109
+ }
106
110
  if (toolName !== "bash") return false;
107
111
  const command = typeof args?.command === "string" ? args.command : "";
108
112
  if (!command.trim()) return false;
@@ -69,14 +69,16 @@ interface MetadataCache {
69
69
  servers: Record<string, ServerCacheEntry>;
70
70
  }
71
71
 
72
- export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {
72
+ export interface ResolvedMcpDirectToolSelection { name: string; selector: string }
73
+
74
+ export function resolveMcpDirectToolSelections(mcpDirectTools: string[] | undefined, cwd = process.cwd()): ResolvedMcpDirectToolSelection[] {
73
75
  if (!mcpDirectTools?.length) return [];
74
76
 
75
77
  try {
76
78
  const config = loadMcpConfig(cwd);
77
79
  const cache = loadMetadataCache();
78
80
  if (!cache) return [];
79
- return resolveDirectToolNames(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools);
81
+ return resolveDirectToolSelections(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools);
80
82
  } catch {
81
83
  return [];
82
84
  }
@@ -195,8 +197,8 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
195
197
  return servers && typeof servers === "object" && !Array.isArray(servers) ? servers as Record<string, ServerEntry> : {};
196
198
  }
197
199
 
198
- function resolveDirectToolNames(config: McpConfig, cache: MetadataCache, prefix: ToolPrefix, envOverride: string[]): string[] {
199
- const names: string[] = [];
200
+ function resolveDirectToolSelections(config: McpConfig, cache: MetadataCache, prefix: ToolPrefix, envOverride: string[]): ResolvedMcpDirectToolSelection[] {
201
+ const names: ResolvedMcpDirectToolSelection[] = [];
200
202
  const seenNames = new Set<string>();
201
203
  const { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);
202
204
 
@@ -216,7 +218,7 @@ function resolveDirectToolNames(config: McpConfig, cache: MetadataCache, prefix:
216
218
  const prefixedName = formatToolName(tool.name, serverName, prefix);
217
219
  if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
218
220
  seenNames.add(prefixedName);
219
- names.push(prefixedName);
221
+ names.push({ name: prefixedName, selector: `${serverName}/${tool.name}` });
220
222
  }
221
223
 
222
224
  if (definition.exposeResources === false) continue;
@@ -228,13 +230,17 @@ function resolveDirectToolNames(config: McpConfig, cache: MetadataCache, prefix:
228
230
  const prefixedName = formatToolName(baseName, serverName, prefix);
229
231
  if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
230
232
  seenNames.add(prefixedName);
231
- names.push(prefixedName);
233
+ names.push({ name: prefixedName, selector: `${serverName}/${baseName}` });
232
234
  }
233
235
  }
234
236
 
235
237
  return names;
236
238
  }
237
239
 
240
+ export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {
241
+ return resolveMcpDirectToolSelections(mcpDirectTools, cwd).map((selection) => selection.name);
242
+ }
243
+
238
244
  function parseSelections(selections: string[]): { servers: Set<string>; tools: Map<string, Set<string>> } {
239
245
  const servers = new Set<string>();
240
246
  const tools = new Map<string, Set<string>>();
@@ -27,6 +27,7 @@ import {
27
27
  SUBAGENT_PARENT_RUN_ID_ENV,
28
28
  } from "./pi-args.ts";
29
29
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
30
+ import { sanitizeProcessTerminal } from "../background/process-terminal.ts";
30
31
 
31
32
  export const NESTED_EVENTS_DIR = path.join(TEMP_ROOT_DIR, "nested-subagent-events");
32
33
  const ROUTE_FILE = "route.json";
@@ -612,7 +613,9 @@ export function projectNestedEvents(route: NestedRoute): NestedRegistry {
612
613
  } catch {
613
614
  continue;
614
615
  }
615
- for (const event of parseNestedEventRecords(content, route)) {
616
+ const records = parseNestedEventRecords(content, route);
617
+ if (records.length === 0) continue;
618
+ for (const event of records) {
616
619
  registry = applyNestedEvent(registry, event);
617
620
  changed = true;
618
621
  }
@@ -620,11 +623,34 @@ export function projectNestedEvents(route: NestedRoute): NestedRegistry {
620
623
  changed = true;
621
624
  }
622
625
  if (changed) {
623
- registry = { ...registry, processedEvents: [...seen].slice(-1000) };
626
+ const processedEvents = [...seen];
627
+ const retainedEvents = processedEvents.slice(-1000);
628
+ const evictedEvents = processedEvents.slice(0, -1000);
629
+ registry = { ...registry, processedEvents: retainedEvents };
624
630
  // Parent projection is the only writer to this sidecar registry. Child and
625
631
  // runner processes only create immutable event files, so parent status.json
626
632
  // remains owned by the existing runner writer and is never rewritten here.
627
633
  writeAtomicJson(registryPath(route), registry);
634
+
635
+ // The registry retains only a bounded filename cursor. Remove the old
636
+ // immutable status records after the cursor is durable; otherwise an evicted
637
+ // filename is rediscovered on the next projection and replayed forever.
638
+ for (const entry of evictedEvents) {
639
+ const eventPath = path.join(route.eventSink, entry);
640
+ if (!containedPath(route.eventSink, eventPath)) continue;
641
+ try {
642
+ const stat = fs.statSync(eventPath);
643
+ if (!stat.isFile() || stat.size > MAX_EVENT_BYTES) continue;
644
+ const content = fs.readFileSync(eventPath, "utf-8");
645
+ const hasStatusRecord = parseNestedEventRecords(content, route).length > 0;
646
+ const hasControlResult = content
647
+ .split("\n")
648
+ .some((line) => line.trim() && parseControlResult(line.trim(), route));
649
+ if (hasStatusRecord && !hasControlResult) fs.unlinkSync(eventPath);
650
+ } catch {
651
+ // A cleanup failure must not make a successfully projected status fail.
652
+ }
653
+ }
628
654
  }
629
655
  return registry;
630
656
  }
@@ -848,6 +874,9 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
848
874
  ...(status.pid ? { pid: status.pid } : {}),
849
875
  ...(status.sessionId ? { sessionId: status.sessionId } : {}),
850
876
  mode: status.mode ?? fallback.mode,
877
+ ...(status.processTerminal ? { processTerminal: sanitizeProcessTerminal(status.processTerminal, { runId: status.runId || fallback.id, runnerProcessInstanceId: status.processTerminal.runnerProcessInstanceId }, `${asyncDir}/status.json`) } : {}),
878
+ ...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
879
+ ...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
851
880
  state: status.state,
852
881
  ...(status.currentStep !== undefined ? { currentStep: status.currentStep } : {}),
853
882
  ...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
@@ -871,7 +900,7 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
871
900
  ...(status.endedAt !== undefined ? { endedAt: status.endedAt } : {}),
872
901
  lastUpdate: status.lastUpdate ?? fallback.ts,
873
902
  ...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
874
- ...(status.steps?.length ? { steps: status.steps.map((step) => ({
903
+ ...(status.steps?.length ? { steps: status.steps.map((step, index) => ({
875
904
  agent: step.agent,
876
905
  status: step.status,
877
906
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
@@ -890,6 +919,9 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
890
919
  ...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
891
920
  ...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
892
921
  ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
922
+ ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId || fallback.id, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${asyncDir}/status.json step ${index}`) } : {}),
923
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
924
+ ...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
893
925
  })).slice(0, MAX_STEPS) } : {}),
894
926
  };
895
927
  }
@@ -0,0 +1,154 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
4
+ import type {
5
+ ParallelHandoffGroup,
6
+ ParallelHandoffManifest,
7
+ ParallelHandoffReference,
8
+ SubagentResultStatus,
9
+ } from "../../shared/types.ts";
10
+ import type {
11
+ WorktreeCleanupReport,
12
+ WorktreeDiff,
13
+ WorktreeSetup,
14
+ } from "./worktree.ts";
15
+
16
+ export interface ParallelHandoffResult {
17
+ agent: string;
18
+ status: SubagentResultStatus;
19
+ summary: string;
20
+ outputPath?: string;
21
+ structuredOutput?: unknown;
22
+ structuredOutputPath?: string;
23
+ sessionPath?: string;
24
+ }
25
+
26
+ function readManifest(manifestPath: string): ParallelHandoffManifest | undefined {
27
+ if (!fs.existsSync(manifestPath)) return undefined;
28
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as ParallelHandoffManifest;
29
+ if (parsed.version !== 1 || !Array.isArray(parsed.groups)) {
30
+ throw new Error(`Invalid parallel handoff manifest: ${manifestPath}`);
31
+ }
32
+ return parsed;
33
+ }
34
+
35
+ function referenceFor(manifestPath: string, manifest: ParallelHandoffManifest): ParallelHandoffReference {
36
+ const children = manifest.groups.flatMap((group) => group.children);
37
+ return {
38
+ version: 1,
39
+ path: manifestPath,
40
+ groupCount: manifest.groups.length,
41
+ childCount: children.length,
42
+ changedPatches: children.filter((child) => child.patch.changed).length,
43
+ cleanupState: manifest.groups.every((group) => group.cleanup.state === "complete") ? "complete" : "partial",
44
+ };
45
+ }
46
+
47
+ function safeHandoffAgentName(agent: string): string {
48
+ return agent.replace(/[^\w.-]/g, "_");
49
+ }
50
+
51
+ function missingDiff(input: { manifestPath: string; stepIndex: number; taskIndex: number; agent: string; branch?: string }): WorktreeDiff {
52
+ const patchPath = path.join(path.dirname(input.manifestPath), `missing-diff-step-${input.stepIndex}-task-${input.taskIndex}-${safeHandoffAgentName(input.agent)}.patch`);
53
+ try {
54
+ fs.mkdirSync(path.dirname(patchPath), { recursive: true });
55
+ fs.writeFileSync(patchPath, "", "utf-8");
56
+ } catch {
57
+ // Handoff records the artifact failure below; patch creation remains best-effort.
58
+ }
59
+ return {
60
+ index: input.taskIndex,
61
+ agent: input.agent,
62
+ branch: input.branch ?? "",
63
+ diffStat: "",
64
+ filesChanged: 0,
65
+ insertions: 0,
66
+ deletions: 0,
67
+ patchPath,
68
+ error: "diff artifact unavailable; no patch was captured",
69
+ };
70
+ }
71
+
72
+ export function writeParallelHandoffGroup(input: {
73
+ manifestPath: string;
74
+ runId: string;
75
+ mode: "parallel" | "chain";
76
+ source: "foreground" | "async";
77
+ cwd: string;
78
+ stepIndex: number;
79
+ flatStartIndex: number;
80
+ setup: WorktreeSetup;
81
+ diffs: WorktreeDiff[];
82
+ cleanup: WorktreeCleanupReport;
83
+ results: ParallelHandoffResult[];
84
+ now?: number;
85
+ }): ParallelHandoffReference {
86
+ const now = input.now ?? Date.now();
87
+ const existing = readManifest(input.manifestPath);
88
+ if (existing && (existing.runId !== input.runId || existing.mode !== input.mode || existing.source !== input.source)) {
89
+ throw new Error(`Parallel handoff manifest belongs to a different run: ${input.manifestPath}`);
90
+ }
91
+ const group: ParallelHandoffGroup = {
92
+ stepIndex: input.stepIndex,
93
+ baseCommit: input.setup.baseCommit,
94
+ repoRoot: input.setup.cwd,
95
+ children: input.results.map((result, taskIndex) => {
96
+ const diff = input.diffs[taskIndex] ?? missingDiff({
97
+ manifestPath: input.manifestPath,
98
+ stepIndex: input.stepIndex,
99
+ taskIndex,
100
+ agent: result.agent,
101
+ branch: input.setup.worktrees[taskIndex]?.branch,
102
+ });
103
+ return {
104
+ index: input.flatStartIndex + taskIndex,
105
+ taskIndex,
106
+ agent: result.agent,
107
+ status: result.status,
108
+ summary: result.summary,
109
+ ...(result.outputPath ? { outputPath: result.outputPath } : {}),
110
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
111
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
112
+ ...(result.sessionPath ? { sessionPath: result.sessionPath } : {}),
113
+ patch: {
114
+ path: diff.patchPath,
115
+ branch: diff.branch,
116
+ changed: diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0,
117
+ diffStat: diff.diffStat,
118
+ filesChanged: diff.filesChanged,
119
+ insertions: diff.insertions,
120
+ deletions: diff.deletions,
121
+ ...(diff.error ? { error: diff.error } : {}),
122
+ },
123
+ };
124
+ }),
125
+ cleanup: input.cleanup,
126
+ };
127
+ const groups = existing?.groups.filter((candidate) => candidate.stepIndex !== input.stepIndex) ?? [];
128
+ groups.push(group);
129
+ groups.sort((left, right) => left.stepIndex - right.stepIndex);
130
+ const manifest: ParallelHandoffManifest = {
131
+ version: 1,
132
+ runId: input.runId,
133
+ mode: input.mode,
134
+ source: input.source,
135
+ cwd: input.cwd,
136
+ createdAt: existing?.createdAt ?? now,
137
+ updatedAt: now,
138
+ groups,
139
+ };
140
+ writeAtomicJson(input.manifestPath, manifest);
141
+ return referenceFor(input.manifestPath, manifest);
142
+ }
143
+
144
+ export function parallelHandoffPath(baseDir: string, runId?: string): string {
145
+ return runId ? path.join(baseDir, "handoffs", `${runId}.json`) : path.join(baseDir, "handoff.json");
146
+ }
147
+
148
+ export function formatParallelHandoffReference(reference: ParallelHandoffReference): string {
149
+ return `Parallel handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
150
+ }
151
+
152
+ export function formatParallelHandoffError(error: unknown): string {
153
+ return `Parallel handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
154
+ }
@@ -3,6 +3,8 @@ export interface RunnerSubagentStep {
3
3
  parentSessionId?: string;
4
4
  agent: string;
5
5
  task: string;
6
+ /** Resolved launch context for this child. */
7
+ context?: "fresh" | "fork";
6
8
  importAsyncRoot?: {
7
9
  runId: string;
8
10
  asyncDir: string;
@@ -40,10 +42,17 @@ export interface RunnerSubagentStep {
40
42
  outputPath: string;
41
43
  };
42
44
  structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
45
+ agentContract?: import("../../shared/types.ts").AgentContract;
46
+ definitionDigest?: string;
47
+ launchBindingTask?: string;
48
+ launchContractDigest?: string;
43
49
  effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
44
50
  acceptanceInput?: import("../../shared/types.ts").AcceptanceInput;
45
51
  acceptanceRole?: import("../../shared/types.ts").AcceptanceRole;
52
+ gateOn?: import("../../shared/types.ts").ChainGateLayer;
46
53
  toolBudget?: import("../../shared/types.ts").ResolvedToolBudget;
54
+ capabilityCeiling?: import("./capability-ceiling.ts").ResolvedSubagentCapabilityCeiling;
55
+ capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
47
56
  }
48
57
 
49
58
  export interface ParallelStepGroup {
@@ -66,6 +75,8 @@ export interface DynamicRunnerGroup {
66
75
  effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
67
76
  acceptanceInput?: import("../../shared/types.ts").AcceptanceInput;
68
77
  acceptanceRole?: import("../../shared/types.ts").AcceptanceRole;
78
+ agentContract?: import("../../shared/types.ts").AgentContract;
79
+ gateOn?: import("../../shared/types.ts").ChainGateLayer;
69
80
  }
70
81
 
71
82
  export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;