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.
- package/CHANGELOG.md +71 -0
- package/README.md +224 -31
- package/agents/advisor.md +73 -0
- package/agents/delegate.md +2 -0
- package/agents/worker.md +2 -0
- package/package.json +11 -13
- package/skills/pi-subagents/SKILL.md +42 -13
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +121 -17
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +127 -0
- package/src/api/preflight.ts +399 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +52 -38
- package/src/extension/rpc.ts +31 -2
- package/src/extension/schemas.ts +29 -3
- package/src/extension/tool-description.ts +4 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +158 -17
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +47 -8
- package/src/runs/background/async-status.ts +94 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/process-terminal.ts +280 -0
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +10 -2
- package/src/runs/background/scheduled-runs.ts +6 -1
- package/src/runs/background/stale-run-reconciler.ts +6 -0
- package/src/runs/background/subagent-runner.ts +406 -54
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +181 -111
- package/src/runs/foreground/execution.ts +147 -47
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +426 -165
- package/src/runs/shared/acceptance.ts +94 -41
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/capability-ceiling.ts +177 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +5 -5
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/src/runs/shared/nested-events.ts +35 -3
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +11 -0
- package/src/runs/shared/pi-args.ts +148 -56
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/session-lease.ts +25 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +21 -3
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/launch-contract.ts +123 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +314 -28
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +153 -6
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +15 -6
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +17 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- 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
|
|
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
|
|
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
|
|
199
|
-
const names:
|
|
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
|
-
|
|
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
|
-
|
|
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;
|