pi-subagents 0.58.0 → 0.60.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 +102 -0
- package/docs/agents.md +5 -3
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +1 -1
- package/docs/models.md +24 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +93 -5
- package/docs/workflows.md +94 -3
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +87 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/skills/pi-subagents/references/execution-controls.md +52 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +159 -42
- package/src/agents/agent-serializer.ts +4 -2
- package/src/agents/agents.ts +67 -26
- package/src/agents/runtime-agent-registry.ts +9 -16
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +6 -9
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +51 -23
- package/src/extension/public-execution.ts +16 -2
- package/src/extension/schemas.ts +36 -10
- package/src/extension/tool-description.ts +24 -5
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +52 -37
- package/src/runs/background/async-job-tracker.ts +17 -12
- package/src/runs/background/async-resume.ts +23 -25
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +110 -8
- package/src/runs/background/chain-append.ts +6 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +158 -6
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +2 -0
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +30 -9
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +351 -358
- package/src/runs/background/subagent-wait.ts +68 -12
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +25 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +125 -122
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +576 -240
- package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +597 -0
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/mcp-config-sources.ts +42 -6
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
- package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
- package/src/runs/shared/model-fallback.ts +9 -2
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +67 -3
- package/src/runs/shared/single-output.ts +72 -22
- package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +5 -2
- package/src/shared/launch-contract.ts +1 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +232 -30
- package/src/shared/utils.ts +35 -55
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/tui/fleet-status.ts +134 -75
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +895 -121
- package/src/watchdog/change-signature.ts +40 -1
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +235 -0
- package/src/workflows/scripted-workflow.ts +503 -38
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +43 -4
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import { stableJsonDigest } from "../../shared/launch-contract.ts";
|
|
4
5
|
import type {
|
|
6
|
+
CleanupEligibility,
|
|
5
7
|
ParallelHandoffGroup,
|
|
6
8
|
ParallelHandoffManifest,
|
|
7
9
|
ParallelHandoffReference,
|
|
10
|
+
ParallelHandoffLaneBinding,
|
|
11
|
+
ParallelHandoffMergeEvidence,
|
|
12
|
+
ParallelHandoffSupersessionEvidence,
|
|
8
13
|
SubagentResultStatus,
|
|
14
|
+
WorkflowLaneMetadata,
|
|
9
15
|
} from "../../shared/types.ts";
|
|
10
16
|
import type {
|
|
11
17
|
WorktreeCleanupReport,
|
|
@@ -14,6 +20,7 @@ import type {
|
|
|
14
20
|
WorktreeCleanupIntent,
|
|
15
21
|
} from "./worktree.ts";
|
|
16
22
|
import { cleanupWorktrees } from "./worktree.ts";
|
|
23
|
+
import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "./lane-metadata.ts";
|
|
17
24
|
|
|
18
25
|
export interface ParallelHandoffResult {
|
|
19
26
|
agent: string;
|
|
@@ -23,17 +30,121 @@ export interface ParallelHandoffResult {
|
|
|
23
30
|
structuredOutput?: unknown;
|
|
24
31
|
structuredOutputPath?: string;
|
|
25
32
|
sessionPath?: string;
|
|
33
|
+
workflowKey?: string;
|
|
34
|
+
runId?: string;
|
|
35
|
+
lane?: WorkflowLaneMetadata;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
function
|
|
29
|
-
if (
|
|
30
|
-
|
|
38
|
+
function optionalIdentity(value: unknown, label: string): string | undefined {
|
|
39
|
+
if (value === undefined) return undefined;
|
|
40
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string.`);
|
|
41
|
+
return value.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nonNegativeIndex(value: unknown, label: string): number {
|
|
45
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error(`${label} must be a non-negative integer.`);
|
|
46
|
+
return value as number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeLaneBinding(value: unknown, label: string): ParallelHandoffLaneBinding {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
51
|
+
const binding = value as Record<string, unknown>;
|
|
52
|
+
const unknown = Object.keys(binding).filter((field) => !["index", "taskIndex", "workflowKey", "runId", "lane"].includes(field));
|
|
53
|
+
if (unknown.length > 0) throw new Error(`${label} has unsupported fields: ${unknown.join(", ")}.`);
|
|
54
|
+
const index = nonNegativeIndex(binding.index, `${label}.index`);
|
|
55
|
+
const taskIndex = nonNegativeIndex(binding.taskIndex, `${label}.taskIndex`);
|
|
56
|
+
const workflowKey = optionalIdentity(binding.workflowKey, `${label}.workflowKey`);
|
|
57
|
+
const runId = optionalIdentity(binding.runId, `${label}.runId`);
|
|
58
|
+
const lane = normalizeWorkflowLaneMetadata(binding.lane, `${label}.lane`);
|
|
59
|
+
assertWorkflowLaneKey(lane, workflowKey, `${label}.lane`);
|
|
60
|
+
return {
|
|
61
|
+
index,
|
|
62
|
+
taskIndex,
|
|
63
|
+
...(workflowKey ? { workflowKey } : {}),
|
|
64
|
+
...(runId ? { runId } : {}),
|
|
65
|
+
...(lane ? { lane } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateManifestIdentity(parsed: ParallelHandoffManifest, manifestPath: string): ParallelHandoffManifest {
|
|
31
70
|
if (parsed.version !== 1 || !Array.isArray(parsed.groups)) {
|
|
32
71
|
throw new Error(`Invalid parallel handoff manifest: ${manifestPath}`);
|
|
33
72
|
}
|
|
73
|
+
if (typeof parsed.runId !== "string" || !parsed.runId.trim()) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': runId must be a non-empty string.`);
|
|
74
|
+
parsed.runId = parsed.runId.trim();
|
|
75
|
+
const workflowKeys = new Set<string>();
|
|
76
|
+
const childRunIds = new Set<string>();
|
|
77
|
+
const childIndexes = new Set<number>();
|
|
78
|
+
for (const [groupIndex, group] of parsed.groups.entries()) {
|
|
79
|
+
if (!group || typeof group !== "object" || !Array.isArray(group.children) || !group.cleanup || !Array.isArray(group.cleanup.tasks)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] is malformed.`);
|
|
80
|
+
if (group.laneBindings !== undefined) {
|
|
81
|
+
if (!Array.isArray(group.laneBindings)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings must be an array.`);
|
|
82
|
+
const taskIndexes = new Set<number>();
|
|
83
|
+
const cleanupIndexes = new Set(group.cleanup.tasks.map((task) => task.index));
|
|
84
|
+
group.laneBindings = group.laneBindings.map((binding, bindingIndex) => {
|
|
85
|
+
const normalized = normalizeLaneBinding(binding, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings[${bindingIndex}]`);
|
|
86
|
+
if (taskIndexes.has(normalized.taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] has duplicate lane binding taskIndex ${normalized.taskIndex}.`);
|
|
87
|
+
if (!cleanupIndexes.has(normalized.taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].laneBindings[${bindingIndex}] has no cleanup task for taskIndex ${normalized.taskIndex}.`);
|
|
88
|
+
taskIndexes.add(normalized.taskIndex);
|
|
89
|
+
return normalized;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const taskIndexes = new Set<number>();
|
|
93
|
+
for (const [childIndex, child] of group.children.entries()) {
|
|
94
|
+
if (!child || typeof child !== "object" || !child.patch || typeof child.patch !== "object") throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}] is malformed.`);
|
|
95
|
+
const childRecord = child as unknown as Record<string, unknown>;
|
|
96
|
+
const index = nonNegativeIndex(childRecord.index, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].index`);
|
|
97
|
+
const taskIndex = nonNegativeIndex(childRecord.taskIndex, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].taskIndex`);
|
|
98
|
+
if (taskIndexes.has(taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}] has duplicate child taskIndex ${taskIndex}.`);
|
|
99
|
+
taskIndexes.add(taskIndex);
|
|
100
|
+
if (childIndexes.has(index)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate child index ${index}.`);
|
|
101
|
+
childIndexes.add(index);
|
|
102
|
+
if (!group.cleanup.tasks.some((task) => task.index === taskIndex)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}] has no cleanup task for taskIndex ${taskIndex}.`);
|
|
103
|
+
const workflowKey = optionalIdentity(childRecord.workflowKey, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].workflowKey`);
|
|
104
|
+
const runId = optionalIdentity(childRecord.runId, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].runId`);
|
|
105
|
+
const lane = normalizeWorkflowLaneMetadata(childRecord.lane, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].lane`);
|
|
106
|
+
assertWorkflowLaneKey(lane, workflowKey, `Invalid parallel handoff manifest '${manifestPath}': groups[${groupIndex}].children[${childIndex}].lane`);
|
|
107
|
+
if (workflowKey && workflowKeys.has(workflowKey)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate workflow key '${workflowKey}'.`);
|
|
108
|
+
if (workflowKey) workflowKeys.add(workflowKey);
|
|
109
|
+
if (runId && childRunIds.has(runId)) throw new Error(`Invalid parallel handoff manifest '${manifestPath}': duplicate child run id '${runId}'.`);
|
|
110
|
+
if (runId) childRunIds.add(runId);
|
|
111
|
+
if (lane) childRecord.lane = lane;
|
|
112
|
+
if (workflowKey) childRecord.workflowKey = workflowKey;
|
|
113
|
+
if (runId) childRecord.runId = runId;
|
|
114
|
+
childRecord.index = index;
|
|
115
|
+
childRecord.taskIndex = taskIndex;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
34
118
|
return parsed;
|
|
35
119
|
}
|
|
36
120
|
|
|
121
|
+
export function readParallelHandoffManifest(manifestPath: string): ParallelHandoffManifest | undefined {
|
|
122
|
+
if (!fs.existsSync(manifestPath)) return undefined;
|
|
123
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as ParallelHandoffManifest;
|
|
124
|
+
return validateManifestIdentity(parsed, manifestPath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveParallelHandoffChild(input: {
|
|
128
|
+
manifestPath: string;
|
|
129
|
+
runId: string;
|
|
130
|
+
workflowKey?: string;
|
|
131
|
+
childRunId?: string;
|
|
132
|
+
}): { group: ParallelHandoffGroup; child: ParallelHandoffGroup["children"][number] } | undefined {
|
|
133
|
+
const manifest = readParallelHandoffManifest(input.manifestPath);
|
|
134
|
+
if (!manifest) return undefined;
|
|
135
|
+
if (manifest.runId !== input.runId) throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${input.runId}'.`);
|
|
136
|
+
const workflowKey = input.workflowKey?.trim() || undefined;
|
|
137
|
+
const childRunId = input.childRunId?.trim() || undefined;
|
|
138
|
+
if (!workflowKey && !childRunId) throw new Error("Parallel handoff child resolution requires workflowKey or childRunId.");
|
|
139
|
+
const matches = manifest.groups.flatMap((group) => group.children.map((child) => ({ group, child }))).filter(({ child }) => {
|
|
140
|
+
if (workflowKey && child.workflowKey !== workflowKey) return false;
|
|
141
|
+
if (childRunId && child.runId !== childRunId) return false;
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
if (matches.length > 1) throw new Error(`Parallel handoff has multiple children matching workflow identity${workflowKey ? ` '${workflowKey}'` : ` '${childRunId}'`}.`);
|
|
145
|
+
return matches[0];
|
|
146
|
+
}
|
|
147
|
+
|
|
37
148
|
function resolveExistingPath(candidate: string): string {
|
|
38
149
|
try {
|
|
39
150
|
return fs.realpathSync(candidate);
|
|
@@ -48,7 +159,7 @@ function pathInside(root: string, candidate: string): boolean {
|
|
|
48
159
|
}
|
|
49
160
|
|
|
50
161
|
export function resolveRetainedWorktreeCwd(manifestPath: string, runId: string, childIndex: number): string | undefined {
|
|
51
|
-
const manifest =
|
|
162
|
+
const manifest = readParallelHandoffManifest(manifestPath);
|
|
52
163
|
if (!manifest) return undefined;
|
|
53
164
|
if (manifest.runId !== runId) throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${runId}'.`);
|
|
54
165
|
const match = manifest.groups
|
|
@@ -85,9 +196,278 @@ function referenceFor(manifestPath: string, manifest: ParallelHandoffManifest):
|
|
|
85
196
|
childCount: children.length,
|
|
86
197
|
changedPatches: children.filter((child) => child.patch.changed).length,
|
|
87
198
|
cleanupState: manifest.groups.every((group) => group.cleanup.state === "complete") ? "complete" : "partial",
|
|
199
|
+
...(Object.prototype.hasOwnProperty.call(manifest, "cleanupEligibility")
|
|
200
|
+
? { cleanupEligibility: trustedStoredCleanupEligibility(manifest) }
|
|
201
|
+
: {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
|
|
206
|
+
const MANIFEST_DIGEST_PATTERN = /^[0-9a-f]{64}$/i;
|
|
207
|
+
const MAX_ATTESTATION_ACTOR_LENGTH = 128;
|
|
208
|
+
const MAX_ATTESTATION_TIMESTAMP_LENGTH = 64;
|
|
209
|
+
const MAX_SUPERSESSION_ID_LENGTH = 128;
|
|
210
|
+
|
|
211
|
+
function boundedString(value: unknown, label: string, maxBytes: number): string {
|
|
212
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string.`);
|
|
213
|
+
const normalized = value.trim();
|
|
214
|
+
if (normalized.includes("\n") || normalized.includes("\r")) throw new Error(`${label} must not contain newlines.`);
|
|
215
|
+
if (Buffer.byteLength(normalized, "utf-8") > maxBytes) throw new Error(`${label} must be at most ${maxBytes} bytes.`);
|
|
216
|
+
return normalized;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function commitString(value: unknown, label: string): string {
|
|
220
|
+
const normalized = boundedString(value, label, 64).toLowerCase();
|
|
221
|
+
if (!COMMIT_PATTERN.test(normalized)) throw new Error(`${label} must be a full 40-character commit SHA.`);
|
|
222
|
+
return normalized;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function manifestDigestString(value: unknown, label: string): string {
|
|
226
|
+
const normalized = boundedString(value, label, 64).toLowerCase();
|
|
227
|
+
if (!MANIFEST_DIGEST_PATTERN.test(normalized)) throw new Error(`${label} must be a full 64-character SHA-256 digest.`);
|
|
228
|
+
return normalized;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function manifestFactsDigest(manifest: ParallelHandoffManifest): string {
|
|
232
|
+
return stableJsonDigest({
|
|
233
|
+
version: manifest.version,
|
|
234
|
+
runId: manifest.runId,
|
|
235
|
+
mode: manifest.mode,
|
|
236
|
+
source: manifest.source,
|
|
237
|
+
cwd: manifest.cwd,
|
|
238
|
+
groups: manifest.groups,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function attestationTimestamp(value: unknown): string {
|
|
243
|
+
const normalized = boundedString(value, "attestedAt", MAX_ATTESTATION_TIMESTAMP_LENGTH);
|
|
244
|
+
if (!Number.isFinite(Date.parse(normalized))) throw new Error("attestedAt must be a valid timestamp.");
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeMergeEvidence(value: unknown): ParallelHandoffMergeEvidence {
|
|
249
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("merge must be an object.");
|
|
250
|
+
const merge = value as Record<string, unknown>;
|
|
251
|
+
const prNumber = merge.prNumber;
|
|
252
|
+
if (typeof prNumber !== "number" || !Number.isInteger(prNumber) || prNumber < 1) throw new Error("merge.prNumber must be a positive integer.");
|
|
253
|
+
const treeEquivalent = merge.treeEquivalent;
|
|
254
|
+
if (treeEquivalent !== true && treeEquivalent !== false && treeEquivalent !== "unknown") throw new Error("merge.treeEquivalent must be true, false, or 'unknown'.");
|
|
255
|
+
const postMergeChecks = merge.postMergeChecks;
|
|
256
|
+
if (postMergeChecks !== "recorded" && postMergeChecks !== "unknown") throw new Error("merge.postMergeChecks must be 'recorded' or 'unknown'.");
|
|
257
|
+
return {
|
|
258
|
+
prNumber,
|
|
259
|
+
reviewedHead: commitString(merge.reviewedHead, "merge.reviewedHead"),
|
|
260
|
+
mergeCommit: commitString(merge.mergeCommit, "merge.mergeCommit"),
|
|
261
|
+
treeEquivalent,
|
|
262
|
+
postMergeChecks,
|
|
263
|
+
attestedBy: boundedString(merge.attestedBy, "merge.attestedBy", MAX_ATTESTATION_ACTOR_LENGTH),
|
|
264
|
+
attestedAt: attestationTimestamp(merge.attestedAt),
|
|
265
|
+
...(merge.manifestDigest === undefined ? {} : { manifestDigest: manifestDigestString(merge.manifestDigest, "merge.manifestDigest") }),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeSupersessionEvidence(value: unknown): ParallelHandoffSupersessionEvidence {
|
|
270
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("supersession must be an object.");
|
|
271
|
+
const supersession = value as Record<string, unknown>;
|
|
272
|
+
return {
|
|
273
|
+
supersededBy: boundedString(supersession.supersededBy, "supersession.supersededBy", MAX_SUPERSESSION_ID_LENGTH),
|
|
274
|
+
attestedBy: boundedString(supersession.attestedBy, "supersession.attestedBy", MAX_ATTESTATION_ACTOR_LENGTH),
|
|
275
|
+
attestedAt: attestationTimestamp(supersession.attestedAt),
|
|
276
|
+
...(supersession.manifestDigest === undefined ? {} : { manifestDigest: manifestDigestString(supersession.manifestDigest, "supersession.manifestDigest") }),
|
|
88
277
|
};
|
|
89
278
|
}
|
|
90
279
|
|
|
280
|
+
function normalizeStoredCleanupEligibility(value: unknown): CleanupEligibility {
|
|
281
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { state: "unknown" };
|
|
282
|
+
const eligibility = value as Record<string, unknown>;
|
|
283
|
+
if (eligibility.state === "active" || eligibility.state === "terminal-eligible" || eligibility.state === "superseded-eligible" || eligibility.state === "unknown") {
|
|
284
|
+
return { state: eligibility.state };
|
|
285
|
+
}
|
|
286
|
+
if (eligibility.state === "terminal-blocked" && typeof eligibility.reason === "string" && eligibility.reason.trim() && !eligibility.reason.includes("\n") && !eligibility.reason.includes("\r")) {
|
|
287
|
+
return { state: "terminal-blocked", reason: eligibility.reason.trim().slice(0, 256) };
|
|
288
|
+
}
|
|
289
|
+
return { state: "unknown" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const STORED_CHILD_STATUSES = new Set(["pending", "running", "completed", "complete", "failed", "paused", "stopped", "detached", "rejected"]);
|
|
293
|
+
|
|
294
|
+
export function isTerminalParallelHandoffChildStatus(status: unknown): boolean {
|
|
295
|
+
return status === "completed" || status === "complete" || status === "failed" || status === "paused" || status === "stopped" || status === "rejected";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function hasValidStoredLaneShape(manifest: ParallelHandoffManifest): boolean {
|
|
299
|
+
if (!manifest || typeof manifest !== "object" || manifest.version !== 1 || typeof manifest.runId !== "string" || !manifest.runId.trim() || !Array.isArray(manifest.groups) || manifest.groups.length === 0) return false;
|
|
300
|
+
let hasManagedTask = false;
|
|
301
|
+
const valid = manifest.groups.every((group) => {
|
|
302
|
+
if (!group || typeof group !== "object" || typeof group.baseCommit !== "string" || !group.baseCommit.trim() || typeof group.repoRoot !== "string" || !group.repoRoot.trim()) return false;
|
|
303
|
+
const cleanup = group.cleanup;
|
|
304
|
+
if (!cleanup || typeof cleanup !== "object" || (cleanup.state !== "complete" && cleanup.state !== "partial") || !Array.isArray(cleanup.tasks) || !Array.isArray(group.children)) return false;
|
|
305
|
+
if (cleanup.tasks.some((task) => !task || typeof task !== "object" || !Number.isSafeInteger(task.index) || task.index < 0 || typeof task.path !== "string" || !task.path.trim() || typeof task.branch !== "string" || !task.branch.trim())) return false;
|
|
306
|
+
if (cleanup.tasks.length > 0) hasManagedTask = true;
|
|
307
|
+
return group.children.every((child) => Boolean(child) && typeof child === "object" && typeof child.status === "string" && STORED_CHILD_STATUSES.has(child.status));
|
|
308
|
+
});
|
|
309
|
+
return valid && hasManagedTask;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function trustedStoredCleanupEligibility(manifest: ParallelHandoffManifest): CleanupEligibility {
|
|
313
|
+
const eligibility = normalizeStoredCleanupEligibility(manifest.cleanupEligibility);
|
|
314
|
+
if (!hasValidStoredLaneShape(manifest)) return { state: "unknown" };
|
|
315
|
+
if (hasActiveChildren(manifest)) return { state: "active" };
|
|
316
|
+
if (eligibility.state === "active") return cleanupEligibilityForEvidence(manifest);
|
|
317
|
+
if (eligibility.state === "terminal-eligible" || eligibility.state === "superseded-eligible") {
|
|
318
|
+
const current = cleanupEligibilityForEvidence(manifest);
|
|
319
|
+
if (current.state === eligibility.state || (current.state === "terminal-blocked" && / evidence is stale$/.test(current.reason))) return current;
|
|
320
|
+
return { state: "unknown" };
|
|
321
|
+
}
|
|
322
|
+
return eligibility;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hasActiveChildren(manifest: ParallelHandoffManifest): boolean {
|
|
326
|
+
return Array.isArray(manifest.groups) && manifest.groups.some((group) => {
|
|
327
|
+
if (!group || typeof group !== "object") return false;
|
|
328
|
+
const children = Array.isArray(group.children) ? group.children : [];
|
|
329
|
+
if (children.length === 0 && Array.isArray(group.cleanup?.tasks) && group.cleanup.tasks.length > 0) return true;
|
|
330
|
+
return children.some((child) => !child || typeof child !== "object" || !isTerminalParallelHandoffChildStatus(child.status));
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function hasManagedWorktreeTasks(manifest: ParallelHandoffManifest): boolean {
|
|
335
|
+
return manifest.groups.length > 0 && manifest.groups.some((group) => Array.isArray(group.cleanup?.tasks) && group.cleanup.tasks.length > 0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function validateManifestForLaneEvidence(manifest: ParallelHandoffManifest, laneId: string): void {
|
|
339
|
+
if (manifest.runId !== laneId) throw new Error(`Lane '${laneId}' does not match manifest run '${manifest.runId}'.`);
|
|
340
|
+
if (!hasValidStoredLaneShape(manifest)) throw new Error("Lane manifest is malformed.");
|
|
341
|
+
if (!manifest.groups.length || !hasManagedWorktreeTasks(manifest)) throw new Error("Lane manifest has no managed worktree tasks.");
|
|
342
|
+
if (hasActiveChildren(manifest)) throw new Error("Lane has an active child owner; merge evidence must be recorded after local reconciliation.");
|
|
343
|
+
for (const group of manifest.groups) {
|
|
344
|
+
if (typeof group.repoRoot !== "string" || !group.repoRoot.trim()) throw new Error("Lane manifest has no valid repository root.");
|
|
345
|
+
if (group.cleanup.state !== "complete" && group.cleanup.state !== "partial") throw new Error("Lane manifest has an invalid cleanup state.");
|
|
346
|
+
if (!Array.isArray(group.cleanup.tasks) || group.cleanup.tasks.some((task) => typeof task.path !== "string" || !task.path.trim() || typeof task.branch !== "string" || !task.branch.trim())) {
|
|
347
|
+
throw new Error("Lane manifest has an invalid cleanup task.");
|
|
348
|
+
}
|
|
349
|
+
if (!Array.isArray(group.children)) throw new Error("Lane manifest has invalid child records.");
|
|
350
|
+
for (const child of group.children) {
|
|
351
|
+
if (!STORED_CHILD_STATUSES.has(child.status)) {
|
|
352
|
+
throw new Error("Lane manifest has an invalid child status.");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function cleanupEligibilityForEvidence(manifest: ParallelHandoffManifest): CleanupEligibility {
|
|
359
|
+
if (hasActiveChildren(manifest)) return { state: "active" };
|
|
360
|
+
let merge: ParallelHandoffMergeEvidence | undefined;
|
|
361
|
+
if (manifest.merge) {
|
|
362
|
+
try {
|
|
363
|
+
merge = normalizeMergeEvidence(manifest.merge);
|
|
364
|
+
} catch {
|
|
365
|
+
return { state: "terminal-blocked", reason: "stored merge evidence is invalid" };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (manifest.supersession) {
|
|
369
|
+
try {
|
|
370
|
+
const supersession = normalizeSupersessionEvidence(manifest.supersession);
|
|
371
|
+
if (supersession.manifestDigest !== manifestFactsDigest(manifest)) return { state: "terminal-blocked", reason: "stored supersession evidence is stale" };
|
|
372
|
+
} catch {
|
|
373
|
+
return { state: "terminal-blocked", reason: "stored supersession evidence is invalid" };
|
|
374
|
+
}
|
|
375
|
+
return { state: "superseded-eligible" };
|
|
376
|
+
}
|
|
377
|
+
if (!merge) return { state: "terminal-blocked", reason: "no merge or supersession evidence recorded" };
|
|
378
|
+
if (merge.manifestDigest !== manifestFactsDigest(manifest)) return { state: "terminal-blocked", reason: "stored merge evidence is stale" };
|
|
379
|
+
if (merge.treeEquivalent !== true) {
|
|
380
|
+
return { state: "terminal-blocked", reason: merge.treeEquivalent === "unknown" ? "merged tree equivalence was not attested" : "merged tree is not equivalent to the reviewed head" };
|
|
381
|
+
}
|
|
382
|
+
if (merge.postMergeChecks !== "recorded") return { state: "terminal-blocked", reason: "required post-merge checks were not recorded" };
|
|
383
|
+
return { state: "terminal-eligible" };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function sameMergeEvidence(left: ParallelHandoffMergeEvidence, right: ParallelHandoffMergeEvidence): boolean {
|
|
387
|
+
return left.prNumber === right.prNumber
|
|
388
|
+
&& left.reviewedHead === right.reviewedHead
|
|
389
|
+
&& left.mergeCommit === right.mergeCommit
|
|
390
|
+
&& left.treeEquivalent === right.treeEquivalent
|
|
391
|
+
&& left.postMergeChecks === right.postMergeChecks
|
|
392
|
+
&& left.attestedBy === right.attestedBy
|
|
393
|
+
&& left.attestedAt === right.attestedAt;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function sameSupersessionEvidence(left: ParallelHandoffSupersessionEvidence, right: ParallelHandoffSupersessionEvidence): boolean {
|
|
397
|
+
return left.supersededBy === right.supersededBy && left.attestedBy === right.attestedBy && left.attestedAt === right.attestedAt;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function firstRepositoryRoot(manifest: ParallelHandoffManifest): string | undefined {
|
|
401
|
+
const repoRoot = Array.isArray(manifest.groups) ? manifest.groups[0]?.repoRoot : undefined;
|
|
402
|
+
return typeof repoRoot === "string" && repoRoot.trim() ? repoRoot.trim() : undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function formatCleanupCommand(manifestPath: string, manifest: ParallelHandoffManifest): string | undefined {
|
|
406
|
+
const repoRoot = firstRepositoryRoot(manifest);
|
|
407
|
+
return repoRoot ? `subagent({ action: "worktree.cleanup", repo: ${JSON.stringify(repoRoot)}, handoffPath: ${JSON.stringify(manifestPath)}, mode: "plan" })` : undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function formatStoredParallelHandoffCleanup(manifestPath: string, manifest?: ParallelHandoffManifest): string {
|
|
411
|
+
let stored = manifest;
|
|
412
|
+
if (!stored) {
|
|
413
|
+
try {
|
|
414
|
+
stored = readParallelHandoffManifest(manifestPath);
|
|
415
|
+
} catch {
|
|
416
|
+
stored = undefined;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!stored) return ["Cleanup eligibility: unknown", "Reason: lane manifest is missing or invalid; removal is not safe.", `Plan command: subagent({ action: "worktree.cleanup", handoffPath: ${JSON.stringify(manifestPath)}, mode: "plan" })`].join("\n");
|
|
420
|
+
const eligibility = trustedStoredCleanupEligibility(stored);
|
|
421
|
+
const lines = [`Cleanup eligibility: ${eligibility.state}`];
|
|
422
|
+
if (eligibility.state === "terminal-blocked") lines.push(`Reason: ${eligibility.reason}`);
|
|
423
|
+
if (eligibility.state === "unknown") lines.push("Reason: stored eligibility is missing or invalid; removal is not safe.");
|
|
424
|
+
if (eligibility.state === "active") lines.push("Reason: a child owner is still active; removal is not safe.");
|
|
425
|
+
const command = formatCleanupCommand(manifestPath, stored);
|
|
426
|
+
if (command) lines.push(`Plan command: ${command}`);
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export interface ParallelHandoffEvidenceResult {
|
|
431
|
+
manifest: ParallelHandoffManifest;
|
|
432
|
+
reference: ParallelHandoffReference;
|
|
433
|
+
text: string;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function recordParallelHandoffMerge(input: { manifestPath: string; laneId: string; merge: unknown; now?: number }): ParallelHandoffEvidenceResult {
|
|
437
|
+
const manifest = readParallelHandoffManifest(input.manifestPath);
|
|
438
|
+
if (!manifest) throw new Error(`Parallel handoff manifest not found: ${input.manifestPath}`);
|
|
439
|
+
validateManifestForLaneEvidence(manifest, boundedString(input.laneId, "laneId", MAX_SUPERSESSION_ID_LENGTH));
|
|
440
|
+
if (hasActiveChildren(manifest)) throw new Error("Lane has an active child owner; merge evidence must be recorded after local reconciliation.");
|
|
441
|
+
const merge = { ...normalizeMergeEvidence(input.merge), manifestDigest: manifestFactsDigest(manifest) };
|
|
442
|
+
if (manifest.merge) {
|
|
443
|
+
const existing = normalizeMergeEvidence(manifest.merge);
|
|
444
|
+
if (existing.reviewedHead !== merge.reviewedHead) throw new Error(`Lane manifest is stale: reviewed head is already recorded as ${existing.reviewedHead}.`);
|
|
445
|
+
if (!sameMergeEvidence(existing, merge)) throw new Error("Lane manifest already contains different merge evidence for this reviewed head.");
|
|
446
|
+
}
|
|
447
|
+
const reconciled: ParallelHandoffManifest = { ...manifest, merge };
|
|
448
|
+
delete reconciled.supersession;
|
|
449
|
+
const updated: ParallelHandoffManifest = { ...reconciled, cleanupEligibility: cleanupEligibilityForEvidence(reconciled), updatedAt: input.now ?? Date.now() };
|
|
450
|
+
writeAtomicJson(input.manifestPath, updated);
|
|
451
|
+
return { manifest: updated, reference: referenceFor(input.manifestPath, updated), text: formatStoredParallelHandoffCleanup(input.manifestPath, updated) };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function recordParallelHandoffSupersession(input: { manifestPath: string; laneId: string; supersession: unknown; now?: number }): ParallelHandoffEvidenceResult {
|
|
455
|
+
const manifest = readParallelHandoffManifest(input.manifestPath);
|
|
456
|
+
if (!manifest) throw new Error(`Parallel handoff manifest not found: ${input.manifestPath}`);
|
|
457
|
+
const laneId = boundedString(input.laneId, "laneId", MAX_SUPERSESSION_ID_LENGTH);
|
|
458
|
+
validateManifestForLaneEvidence(manifest, laneId);
|
|
459
|
+
if (hasActiveChildren(manifest)) throw new Error("Lane has an active child owner; supersession must be recorded after local reconciliation.");
|
|
460
|
+
const supersession = { ...normalizeSupersessionEvidence(input.supersession), manifestDigest: manifestFactsDigest(manifest) };
|
|
461
|
+
if (supersession.supersededBy === laneId) throw new Error("supersession.supersededBy must identify a different replacement lane.");
|
|
462
|
+
if (manifest.supersession) {
|
|
463
|
+
const existing = normalizeSupersessionEvidence(manifest.supersession);
|
|
464
|
+
if (!sameSupersessionEvidence(existing, supersession)) throw new Error("Lane manifest already contains different supersession evidence.");
|
|
465
|
+
}
|
|
466
|
+
const updated: ParallelHandoffManifest = { ...manifest, supersession, cleanupEligibility: cleanupEligibilityForEvidence({ ...manifest, supersession }), updatedAt: input.now ?? Date.now() };
|
|
467
|
+
writeAtomicJson(input.manifestPath, updated);
|
|
468
|
+
return { manifest: updated, reference: referenceFor(input.manifestPath, updated), text: formatStoredParallelHandoffCleanup(input.manifestPath, updated) };
|
|
469
|
+
}
|
|
470
|
+
|
|
91
471
|
function safeHandoffAgentName(agent: string): string {
|
|
92
472
|
return agent.replace(/[^\w.-]/g, "_");
|
|
93
473
|
}
|
|
@@ -125,18 +505,35 @@ export function writeParallelHandoffGroup(input: {
|
|
|
125
505
|
diffs: WorktreeDiff[];
|
|
126
506
|
cleanup?: WorktreeCleanupReport;
|
|
127
507
|
results: ParallelHandoffResult[];
|
|
508
|
+
laneBindings?: ParallelHandoffLaneBinding[];
|
|
128
509
|
now?: number;
|
|
129
510
|
}): ParallelHandoffReference {
|
|
130
511
|
const now = input.now ?? Date.now();
|
|
131
|
-
const existing =
|
|
512
|
+
const existing = readParallelHandoffManifest(input.manifestPath);
|
|
132
513
|
if (existing && (existing.runId !== input.runId || existing.mode !== input.mode || existing.source !== input.source)) {
|
|
133
514
|
throw new Error(`Parallel handoff manifest belongs to a different run: ${input.manifestPath}`);
|
|
134
515
|
}
|
|
516
|
+
const laneBindings = input.laneBindings?.map((binding, index) => normalizeLaneBinding(binding, `parallel handoff lane binding ${index}`));
|
|
517
|
+
if (laneBindings) {
|
|
518
|
+
const taskIndexes = new Set<number>();
|
|
519
|
+
for (const binding of laneBindings) {
|
|
520
|
+
if (taskIndexes.has(binding.taskIndex)) throw new Error(`Parallel handoff has duplicate lane binding taskIndex ${binding.taskIndex}.`);
|
|
521
|
+
if (!input.setup.worktrees.some((worktree) => worktree.index === binding.taskIndex)) throw new Error(`Parallel handoff lane binding taskIndex ${binding.taskIndex} has no managed worktree.`);
|
|
522
|
+
if (binding.index !== input.flatStartIndex + binding.taskIndex) throw new Error(`Parallel handoff lane binding index ${binding.index} does not match taskIndex ${binding.taskIndex}.`);
|
|
523
|
+
taskIndexes.add(binding.taskIndex);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const bindingForTask = (taskIndex: number): ParallelHandoffLaneBinding | undefined => laneBindings?.find((binding) => binding.taskIndex === taskIndex);
|
|
135
527
|
const group: ParallelHandoffGroup = {
|
|
136
528
|
stepIndex: input.stepIndex,
|
|
137
529
|
baseCommit: input.setup.baseCommit,
|
|
138
530
|
repoRoot: input.setup.cwd,
|
|
139
531
|
children: input.results.map((result, taskIndex) => {
|
|
532
|
+
const binding = bindingForTask(taskIndex);
|
|
533
|
+
const runId = optionalIdentity(result.runId ?? binding?.runId, `parallel handoff child ${taskIndex}.runId`);
|
|
534
|
+
const lane = normalizeWorkflowLaneMetadata(result.lane ?? binding?.lane, `parallel handoff child ${taskIndex}.lane`);
|
|
535
|
+
const workflowKey = optionalIdentity(result.workflowKey ?? binding?.workflowKey, `parallel handoff child ${taskIndex}.workflowKey`);
|
|
536
|
+
assertWorkflowLaneKey(lane, workflowKey, `parallel handoff child ${taskIndex}.lane`);
|
|
140
537
|
const diff = input.diffs[taskIndex] ?? missingDiff({
|
|
141
538
|
manifestPath: input.manifestPath,
|
|
142
539
|
stepIndex: input.stepIndex,
|
|
@@ -148,6 +545,9 @@ export function writeParallelHandoffGroup(input: {
|
|
|
148
545
|
index: input.flatStartIndex + taskIndex,
|
|
149
546
|
taskIndex,
|
|
150
547
|
agent: result.agent,
|
|
548
|
+
...(workflowKey ? { workflowKey } : {}),
|
|
549
|
+
...(runId ? { runId } : {}),
|
|
550
|
+
...(lane ? { lane } : {}),
|
|
151
551
|
status: result.status,
|
|
152
552
|
summary: result.summary,
|
|
153
553
|
...(result.outputPath ? { outputPath: result.outputPath } : {}),
|
|
@@ -166,6 +566,7 @@ export function writeParallelHandoffGroup(input: {
|
|
|
166
566
|
},
|
|
167
567
|
};
|
|
168
568
|
}),
|
|
569
|
+
...(laneBindings && laneBindings.length > 0 && input.results.length === 0 ? { laneBindings } : {}),
|
|
169
570
|
cleanup: input.cleanup ?? {
|
|
170
571
|
state: "partial",
|
|
171
572
|
pruned: false,
|
|
@@ -183,7 +584,7 @@ export function writeParallelHandoffGroup(input: {
|
|
|
183
584
|
const groups = existing?.groups.filter((candidate) => candidate.stepIndex !== input.stepIndex) ?? [];
|
|
184
585
|
groups.push(group);
|
|
185
586
|
groups.sort((left, right) => left.stepIndex - right.stepIndex);
|
|
186
|
-
const
|
|
587
|
+
const manifestWithoutEligibility: ParallelHandoffManifest = {
|
|
187
588
|
version: 1,
|
|
188
589
|
runId: input.runId,
|
|
189
590
|
mode: input.mode,
|
|
@@ -192,6 +593,16 @@ export function writeParallelHandoffGroup(input: {
|
|
|
192
593
|
createdAt: existing?.createdAt ?? now,
|
|
193
594
|
updatedAt: now,
|
|
194
595
|
groups,
|
|
596
|
+
...(existing && Object.hasOwn(existing, "merge") ? { merge: existing.merge } : {}),
|
|
597
|
+
...(existing && Object.hasOwn(existing, "supersession") ? { supersession: existing.supersession } : {}),
|
|
598
|
+
};
|
|
599
|
+
const carriesCleanupMetadata = existing !== undefined
|
|
600
|
+
&& (Object.hasOwn(existing, "merge") || Object.hasOwn(existing, "supersession") || Object.hasOwn(existing, "cleanupEligibility"));
|
|
601
|
+
const manifest: ParallelHandoffManifest = {
|
|
602
|
+
...manifestWithoutEligibility,
|
|
603
|
+
...(carriesCleanupMetadata
|
|
604
|
+
? { cleanupEligibility: cleanupEligibilityForEvidence(manifestWithoutEligibility) }
|
|
605
|
+
: {}),
|
|
195
606
|
};
|
|
196
607
|
writeAtomicJson(input.manifestPath, manifest);
|
|
197
608
|
return referenceFor(input.manifestPath, manifest);
|
|
@@ -210,6 +621,7 @@ export function writePendingParallelHandoff(input: {
|
|
|
210
621
|
stepIndex: number;
|
|
211
622
|
flatStartIndex: number;
|
|
212
623
|
setup: WorktreeSetup;
|
|
624
|
+
laneBindings?: ParallelHandoffLaneBinding[];
|
|
213
625
|
}): ParallelHandoffReference {
|
|
214
626
|
return writeParallelHandoffGroup({ ...input, diffs: [], results: [] });
|
|
215
627
|
}
|
|
@@ -227,7 +639,7 @@ export function discardPreservedWorktrees(
|
|
|
227
639
|
authorization: Extract<WorktreeCleanupIntent, { kind: "discard" }>["authorization"],
|
|
228
640
|
): { manifest: ParallelHandoffManifest; text: string } {
|
|
229
641
|
const resolvedPath = path.resolve(manifestPath);
|
|
230
|
-
const manifest =
|
|
642
|
+
const manifest = readParallelHandoffManifest(resolvedPath);
|
|
231
643
|
if (!manifest) throw new Error(`Parallel handoff manifest not found: ${resolvedPath}`);
|
|
232
644
|
let attempted = 0;
|
|
233
645
|
for (const group of manifest.groups) {
|
|
@@ -6,6 +6,8 @@ export interface RunnerSubagentStep {
|
|
|
6
6
|
/** Resolved opt-in rules for native Pi child tool calls. */
|
|
7
7
|
permissionRules?: import("./permissions.ts").PermissionRules;
|
|
8
8
|
agent: string;
|
|
9
|
+
/** Human-readable display name for the child session, derived internally at launch. */
|
|
10
|
+
sessionName?: string;
|
|
9
11
|
task: string;
|
|
10
12
|
runner?: ResolvedRunnerConfig;
|
|
11
13
|
externalJobFollowUp?: {
|
|
@@ -40,6 +42,7 @@ export interface RunnerSubagentStep {
|
|
|
40
42
|
skipPrimaryModelVerification?: boolean;
|
|
41
43
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
42
44
|
tools?: string[];
|
|
45
|
+
allowNestedSubagents?: boolean;
|
|
43
46
|
extensions?: string[];
|
|
44
47
|
subagentOnlyExtensions?: string[];
|
|
45
48
|
mcpDirectTools?: string[];
|
|
@@ -54,6 +57,7 @@ export interface RunnerSubagentStep {
|
|
|
54
57
|
inheritSkills: boolean;
|
|
55
58
|
skills?: string[];
|
|
56
59
|
outputPath?: string;
|
|
60
|
+
outputClaimPath?: string;
|
|
57
61
|
/** Defer the authoritative output instruction until a dynamic fanout item is materialized. */
|
|
58
62
|
namespaceOutputPath?: boolean;
|
|
59
63
|
outputMode?: "inline" | "file-only";
|
|
@@ -63,6 +67,7 @@ export interface RunnerSubagentStep {
|
|
|
63
67
|
/** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
|
|
64
68
|
toolTimeoutMs?: number;
|
|
65
69
|
waitToolEnabled?: boolean;
|
|
70
|
+
waitToolDefaultTimeoutMs?: number;
|
|
66
71
|
structuredOutput?: {
|
|
67
72
|
schema: import("../../shared/types.ts").JsonSchemaObject;
|
|
68
73
|
schemaPath: string;
|
|
@@ -88,6 +93,8 @@ export interface RunnerSubagentStep {
|
|
|
88
93
|
runFanoutPath?: string;
|
|
89
94
|
/** Run this single child in one managed worktree. */
|
|
90
95
|
worktree?: boolean;
|
|
96
|
+
/** Bounded launch-declared lane metadata; display/triage only. */
|
|
97
|
+
lane?: import("../../shared/types.ts").WorkflowLaneMetadata;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
export interface ParallelStepGroup {
|