pi-herdr-agents 2.0.4 → 3.0.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/AGENTS.md +20 -14
- package/CHANGELOG.md +63 -1
- package/CONTEXT.md +86 -32
- package/README.md +262 -141
- package/RELEASING.md +4 -2
- package/config.json.example +0 -3
- package/docs/README.md +38 -15
- package/docs/adr/0001-btw-ephemeral-side-questions.md +1 -1
- package/docs/adr/0002-agent-workflow-skill-runtime-taxonomy.md +1 -1
- package/docs/adr/0003-installable-role-packs.md +21 -13
- package/docs/adr/0009-remove-workflow-subsystem.md +1 -1
- package/docs/adr/0011-explicit-worktree-cleanup.md +1 -1
- package/docs/adr/0012-adopt-maestro-seams-in-repo.md +45 -0
- package/docs/adr/0013-pack-neutral-execution-host.md +63 -0
- package/docs/adr/0014-operator-cancel-terminal-intent.md +134 -0
- package/docs/worktree-subagents.md +10 -6
- package/maestro/adapters/fake/fake-harness-adapter.ts +263 -0
- package/{pi-extension/subagents/activity.ts → maestro/adapters/pi/activity-file.ts} +15 -72
- package/{pi-extension/subagents → maestro/adapters/pi/child}/subagent-done.ts +30 -3
- package/{pi-extension/subagents → maestro/adapters/pi}/completion.ts +4 -4
- package/{pi-extension/subagents → maestro/adapters/pi}/launch.ts +388 -285
- package/maestro/adapters/pi/model-registry.ts +46 -0
- package/maestro/adapters/pi/pi-harness-adapter.ts +701 -0
- package/maestro/adapters/pi/process-identity.ts +303 -0
- package/{pi-extension/subagents → maestro/adapters/pi}/session.ts +1 -13
- package/maestro/adapters/pi/task-model-init.ts +55 -0
- package/maestro/core/activity.ts +29 -0
- package/maestro/core/config/model-config.ts +473 -0
- package/{pi-extension/subagents → maestro/core/config}/pane-config.ts +4 -6
- package/{pi-extension/subagents → maestro/core/config}/persistent-config.ts +4 -6
- package/{pi-extension/subagents → maestro/core/config}/role-config.ts +15 -10
- package/{pi-extension/subagents → maestro/core/config}/supervision-config.ts +4 -6
- package/{pi-extension/subagents → maestro/core/config}/task-model-init.ts +45 -58
- package/maestro/core/config/task-model-types.ts +22 -0
- package/maestro/core/harness-adapter.ts +117 -0
- package/maestro/core/index.ts +3 -0
- package/{pi-extension/subagents → maestro/core}/lifecycle.ts +27 -126
- package/maestro/core/roles/discovery.ts +555 -0
- package/{pi-extension/subagents/runtime-routing.ts → maestro/core/routing.ts} +36 -43
- package/maestro/core/shell.ts +3 -0
- package/{pi-extension/subagents → maestro/core}/status.ts +3 -7
- package/{pi-extension/subagents → maestro/core}/supervision.ts +1 -1
- package/maestro/core/surface-provider.ts +185 -0
- package/maestro/core/types.ts +449 -0
- package/maestro/core/worktree-cleanup.ts +374 -0
- package/maestro/core/worktree.ts +93 -0
- package/maestro/runtime/index.ts +44 -0
- package/maestro/runtime/pi-run-session.ts +1210 -0
- package/maestro/runtime/run-session.ts +801 -0
- package/maestro/runtime/surface-retention.ts +6 -0
- package/maestro/runtime/task-model-init.ts +22 -0
- package/maestro/runtime/worktree-operations.ts +627 -0
- package/maestro/surfaces/fake/fake-surface-provider.ts +311 -0
- package/maestro/surfaces/herdr/herdr-surface-provider.ts +200 -0
- package/{pi-extension/subagents → maestro/surfaces/herdr}/herdr.ts +65 -14
- package/{pi-extension/subagents → maestro/surfaces/herdr}/terminal.ts +7 -5
- package/package.json +10 -9
- package/pi-extension/subagents/config-path.ts +5 -1
- package/pi-extension/subagents/index.ts +848 -1805
- package/pi-extension/subagents/model-registry.ts +46 -0
- package/skills/pi-herdr-agents/SKILL.md +276 -0
- package/agents/adversarial-reviewer.md +0 -153
- package/agents/planner.md +0 -87
- package/agents/poteto.md +0 -32
- package/agents/reviewer.md +0 -132
- package/agents/scout.md +0 -105
- package/agents/visual-tester.md +0 -234
- package/agents/worker.md +0 -121
- package/docs/review-evaluation.md +0 -183
- package/pi-extension/subagents/model-config.ts +0 -305
- package/pi-extension/subagents/plan-skill.md +0 -280
- package/pi-extension/subagents/worktree-cleanup.ts +0 -789
- package/skills/orchestrate/SKILL.md +0 -91
- package/skills/orchestrate/adversarial-review-example.js +0 -400
- package/skills/orchestrate/adversarial-review.md +0 -87
- /package/{pi-extension/subagents → maestro/core/config}/type-guards.ts +0 -0
- /package/{pi-extension/subagents → maestro/core}/wake.ts +0 -0
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
renameSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
9
2
|
import { dirname, join } from "node:path";
|
|
10
3
|
import { homedir } from "node:os";
|
|
11
4
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { getSubagentActivityFile } from "./activity.ts";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
5
|
+
import { getSubagentActivityFile } from "./activity-file.ts";
|
|
6
|
+
import {
|
|
7
|
+
createLifecycle,
|
|
8
|
+
type SubagentLifecycle,
|
|
9
|
+
} from "../../core/lifecycle.ts";
|
|
10
|
+
import type { ResolvedRuntimePlan } from "../../core/routing.ts";
|
|
11
|
+
import type { PaneConfig } from "../../core/config/pane-config.ts";
|
|
12
|
+
import { isNonEmptyString } from "../../core/config/type-guards.ts";
|
|
13
|
+
import { shellQuote } from "../../core/shell.ts";
|
|
14
|
+
import type {
|
|
15
|
+
SurfaceProvider,
|
|
16
|
+
WorktreeSurface,
|
|
17
|
+
} from "../../core/surface-provider.ts";
|
|
18
|
+
import { WorktreeProvisioningError } from "../../core/surface-provider.ts";
|
|
19
|
+
import type {
|
|
20
|
+
FailedWorktreeManifest,
|
|
21
|
+
WorktreeLaunch,
|
|
22
|
+
WorktreeOperations,
|
|
23
|
+
} from "../../core/worktree.ts";
|
|
24
|
+
import {
|
|
25
|
+
getSubagentProcessIdentityFile,
|
|
26
|
+
linuxProcessProbe,
|
|
27
|
+
readProcessIdentityRecord,
|
|
28
|
+
verifyProcessIdentityRecord,
|
|
29
|
+
type PiProcessIdentity,
|
|
30
|
+
type ProcessIdentityProbe,
|
|
31
|
+
} from "./process-identity.ts";
|
|
18
32
|
import {
|
|
19
33
|
createWorktreeSessionFork,
|
|
20
34
|
getNewEntries,
|
|
@@ -22,61 +36,11 @@ import {
|
|
|
22
36
|
seedSubagentSessionFile,
|
|
23
37
|
writeSubagentSessionPolicy,
|
|
24
38
|
} from "./session.ts";
|
|
25
|
-
import {
|
|
26
|
-
closePane,
|
|
27
|
-
createSubagentPane,
|
|
28
|
-
createGroupedSubagentPane,
|
|
29
|
-
createSubagentWorktree,
|
|
30
|
-
splitCurrentPane,
|
|
31
|
-
runScriptInPane,
|
|
32
|
-
shellQuote,
|
|
33
|
-
waitForPiReady,
|
|
34
|
-
waitForShellReady,
|
|
35
|
-
focusWorkspace,
|
|
36
|
-
type HerdrWorktreeSurface,
|
|
37
|
-
} from "./terminal.ts";
|
|
38
39
|
|
|
39
40
|
const SUBAGENTS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
40
41
|
|
|
41
42
|
type SubagentSessionMode = "standalone" | "lineage-only" | "fork";
|
|
42
43
|
|
|
43
|
-
export interface WorktreeLaunch {
|
|
44
|
-
path: string;
|
|
45
|
-
workspaceId: string;
|
|
46
|
-
paneId: string;
|
|
47
|
-
branch: string;
|
|
48
|
-
baseRef: string;
|
|
49
|
-
baseSha: string;
|
|
50
|
-
manifestFile: string;
|
|
51
|
-
sessionFile?: string;
|
|
52
|
-
sourceSessionFile?: string;
|
|
53
|
-
handoffMessage?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface FailedWorktreeManifest extends JsonObject {
|
|
57
|
-
state: "failed";
|
|
58
|
-
id: string;
|
|
59
|
-
name: string;
|
|
60
|
-
sourceCwd: string;
|
|
61
|
-
branch: string;
|
|
62
|
-
baseRef: string;
|
|
63
|
-
baseSha: string;
|
|
64
|
-
createdAt: number;
|
|
65
|
-
path?: string;
|
|
66
|
-
workspaceId?: string;
|
|
67
|
-
error?: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface WorktreeHandoff extends WorktreeLaunch {
|
|
71
|
-
headSha: string | null;
|
|
72
|
-
commitsAhead: number | null;
|
|
73
|
-
clean: boolean | null;
|
|
74
|
-
conflicted: boolean | null;
|
|
75
|
-
changedFiles: string[] | null;
|
|
76
|
-
untrackedFiles: string[] | null;
|
|
77
|
-
gitError?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
44
|
export interface FreshPiLaunchRequest {
|
|
81
45
|
kind: "fresh";
|
|
82
46
|
id?: string;
|
|
@@ -84,7 +48,7 @@ export interface FreshPiLaunchRequest {
|
|
|
84
48
|
task: string;
|
|
85
49
|
agent?: string;
|
|
86
50
|
cwd?: string;
|
|
87
|
-
worktree?: { branch: string; base?: string };
|
|
51
|
+
worktree?: { branch: string; base?: string } | null;
|
|
88
52
|
fork?: boolean;
|
|
89
53
|
handoff?: { leafId: string };
|
|
90
54
|
surface?: string;
|
|
@@ -159,47 +123,265 @@ export interface PiRunningChild {
|
|
|
159
123
|
stopState?: "requested" | "pending" | "failed";
|
|
160
124
|
stopFailure?: string;
|
|
161
125
|
crashNotified?: boolean;
|
|
126
|
+
/** Managed worktree children: where the child records its own process identity. */
|
|
127
|
+
processIdentityFile?: string;
|
|
128
|
+
/** Verified at launch readiness; a worktree cancel's only signal and exit evidence. */
|
|
129
|
+
processIdentity?: PiProcessIdentity;
|
|
130
|
+
/** Settles when launch-readiness capture verifies or gives up; never rejects. */
|
|
131
|
+
processIdentityCapture?: Promise<PiProcessIdentity | undefined>;
|
|
132
|
+
processIdentityError?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** What a captured identity must match: this run, its session, and its pane. */
|
|
136
|
+
export interface ProcessIdentityExpectation {
|
|
137
|
+
file: string;
|
|
138
|
+
id: string;
|
|
139
|
+
sessionFile: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
143
|
+
|
|
144
|
+
interface WorktreeSurfaceForLaunch {
|
|
145
|
+
path: string;
|
|
146
|
+
branch: string;
|
|
147
|
+
workspaceId: string;
|
|
148
|
+
paneId: string;
|
|
162
149
|
}
|
|
163
150
|
|
|
164
151
|
export interface PiLaunchOperations {
|
|
165
|
-
|
|
152
|
+
/** Required for managed worktree launches; ordinary panes need no Git effects. */
|
|
153
|
+
worktree?: WorktreeOperations;
|
|
154
|
+
createPane(name: string, cwd?: string): MaybePromise<string>;
|
|
166
155
|
createWorktree(
|
|
167
156
|
name: string,
|
|
168
157
|
cwd: string,
|
|
169
158
|
branch: string,
|
|
170
159
|
base: string,
|
|
171
|
-
):
|
|
160
|
+
): MaybePromise<WorktreeSurfaceForLaunch>;
|
|
172
161
|
waitForShellReady(surface: string): Promise<void>;
|
|
173
162
|
runScript(
|
|
174
163
|
surface: string,
|
|
175
164
|
command: string,
|
|
176
165
|
options: { scriptPath: string; scriptPreamble: string },
|
|
177
|
-
): string
|
|
178
|
-
closePane(pane: string): void
|
|
166
|
+
): MaybePromise<string>;
|
|
167
|
+
closePane(pane: string): MaybePromise<void>;
|
|
179
168
|
waitForPiReady?(
|
|
180
169
|
surface: string,
|
|
181
170
|
sessionFile: string,
|
|
182
171
|
cwd: string,
|
|
183
172
|
): Promise<void>;
|
|
184
|
-
focusWorkspace?(workspaceId: string): void
|
|
173
|
+
focusWorkspace?(workspaceId: string): MaybePromise<void>;
|
|
174
|
+
/** Bounded readiness capture of a worktree child's verified process identity. */
|
|
175
|
+
captureProcessIdentity?(
|
|
176
|
+
surface: string,
|
|
177
|
+
expected: ProcessIdentityExpectation,
|
|
178
|
+
options?: { timeoutMs?: number },
|
|
179
|
+
): Promise<PiProcessIdentity>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function placementFromPaneConfig(config: PaneConfig) {
|
|
183
|
+
if (config.mode === "split") {
|
|
184
|
+
return { kind: "split" as const, direction: config.direction };
|
|
185
|
+
}
|
|
186
|
+
if (config.mode === "tab") return { kind: "tab" as const };
|
|
187
|
+
return { kind: "grouped" as const };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function worktreeSurfaceForLaunch(
|
|
191
|
+
worktree: WorktreeSurface,
|
|
192
|
+
): WorktreeSurfaceForLaunch {
|
|
193
|
+
return {
|
|
194
|
+
path: worktree.path,
|
|
195
|
+
branch: worktree.branch,
|
|
196
|
+
workspaceId: worktree.workspaceId,
|
|
197
|
+
paneId: worktree.surfaceId,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function isExpectedPiProcess(
|
|
202
|
+
process: {
|
|
203
|
+
name?: string;
|
|
204
|
+
argv0?: string;
|
|
205
|
+
argv?: string[];
|
|
206
|
+
cwd?: string;
|
|
207
|
+
},
|
|
208
|
+
sessionFile: string,
|
|
209
|
+
cwd: string,
|
|
210
|
+
): boolean {
|
|
211
|
+
const sessionIndex = process.argv?.indexOf("--session") ?? -1;
|
|
212
|
+
return (
|
|
213
|
+
(process.name === "pi" || process.argv0?.split("/").pop() === "pi") &&
|
|
214
|
+
sessionIndex >= 0 &&
|
|
215
|
+
process.argv?.[sessionIndex + 1] === sessionFile &&
|
|
216
|
+
process.cwd === cwd
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function waitForSurfacePiReady(
|
|
221
|
+
provider: SurfaceProvider,
|
|
222
|
+
surface: string,
|
|
223
|
+
sessionFile: string,
|
|
224
|
+
cwd: string,
|
|
225
|
+
options: { timeoutMs?: number; intervalMs?: number } = {},
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
228
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
229
|
+
const deadline = Date.now() + timeoutMs;
|
|
230
|
+
let lastError = "expected Pi process not observed";
|
|
231
|
+
|
|
232
|
+
while (Date.now() <= deadline) {
|
|
233
|
+
try {
|
|
234
|
+
const info = await Promise.resolve(provider.getProcessInfo(surface));
|
|
235
|
+
if (
|
|
236
|
+
info.foregroundProcesses.some((process) =>
|
|
237
|
+
isExpectedPiProcess(process, sessionFile, cwd),
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
244
|
+
}
|
|
245
|
+
if (Date.now() >= deadline) break;
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
247
|
+
}
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Timed out waiting for Pi session ${sessionFile} in Herdr pane ${surface}: ${lastError}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Wait for the child to publish its own identity, then accept it only while
|
|
255
|
+
* that process is alive in this host's namespace as the Herdr pane shell or a
|
|
256
|
+
* descendant of it. Command-line text is never consulted: Pi rewrites it.
|
|
257
|
+
*/
|
|
258
|
+
export async function captureSurfacePiProcessIdentity(
|
|
259
|
+
provider: Pick<SurfaceProvider, "getProcessInfo">,
|
|
260
|
+
surface: string,
|
|
261
|
+
expected: ProcessIdentityExpectation,
|
|
262
|
+
options: {
|
|
263
|
+
timeoutMs?: number;
|
|
264
|
+
intervalMs?: number;
|
|
265
|
+
probe?: ProcessIdentityProbe;
|
|
266
|
+
} = {},
|
|
267
|
+
): Promise<PiProcessIdentity> {
|
|
268
|
+
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
269
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
270
|
+
const deadline = Date.now() + timeoutMs;
|
|
271
|
+
let lastError = "the child has not recorded its process identity";
|
|
272
|
+
let answered = false;
|
|
273
|
+
for (;;) {
|
|
274
|
+
try {
|
|
275
|
+
const record = readProcessIdentityRecord(expected.file);
|
|
276
|
+
if (record) {
|
|
277
|
+
// The deadline bounds Herdr too; a late answer is dropped, never accepted.
|
|
278
|
+
const info = await settleBefore(
|
|
279
|
+
Promise.resolve().then(() => provider.getProcessInfo(surface)),
|
|
280
|
+
deadline,
|
|
281
|
+
{ unref: true },
|
|
282
|
+
);
|
|
283
|
+
if (!info) {
|
|
284
|
+
// The deadline passed; an earlier answer's refusal says more.
|
|
285
|
+
if (!answered)
|
|
286
|
+
lastError = `Herdr process info for pane ${surface} timed out`;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
answered = true;
|
|
290
|
+
if (info.shellPid === undefined)
|
|
291
|
+
throw new Error(`Herdr reports no shell for pane ${surface}`);
|
|
292
|
+
return verifyProcessIdentityRecord(
|
|
293
|
+
record,
|
|
294
|
+
{ ...expected, shellPid: info.shellPid },
|
|
295
|
+
options.probe ?? linuxProcessProbe,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
lastError = errorMessage(error);
|
|
300
|
+
}
|
|
301
|
+
const remaining = deadline - Date.now();
|
|
302
|
+
if (remaining <= 0) break;
|
|
303
|
+
// Background capture must never hold the parent process open.
|
|
304
|
+
await new Promise((resolve) =>
|
|
305
|
+
setTimeout(resolve, Math.min(intervalMs, remaining)).unref(),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Process identity not captured within ${timeoutMs}ms: ${lastError}`,
|
|
310
|
+
);
|
|
185
311
|
}
|
|
186
312
|
|
|
187
|
-
|
|
313
|
+
/**
|
|
314
|
+
* The pending value if it settles before the deadline; otherwise undefined.
|
|
315
|
+
* A value that arrives after the deadline is dropped, never reported, even
|
|
316
|
+
* when a delayed event loop runs it before the overdue timer.
|
|
317
|
+
*/
|
|
318
|
+
export async function settleBefore<T>(
|
|
319
|
+
pending: Promise<T> | undefined,
|
|
320
|
+
deadline: number,
|
|
321
|
+
options: { unref?: boolean } = {},
|
|
322
|
+
): Promise<T | undefined> {
|
|
323
|
+
if (!pending) return undefined;
|
|
324
|
+
let expire!: () => void;
|
|
325
|
+
const expired = new Promise<undefined>((resolve) => {
|
|
326
|
+
expire = () => resolve(undefined);
|
|
327
|
+
});
|
|
328
|
+
const timer = setTimeout(expire, Math.max(0, deadline - Date.now()));
|
|
329
|
+
if (options.unref) timer.unref();
|
|
330
|
+
try {
|
|
331
|
+
const onTime = pending.then((value) =>
|
|
332
|
+
Date.now() > deadline ? undefined : value,
|
|
333
|
+
);
|
|
334
|
+
return await Promise.race([onTime, expired]);
|
|
335
|
+
} finally {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
188
339
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
340
|
+
export function launchOperationsFromSurface(
|
|
341
|
+
provider: SurfaceProvider,
|
|
342
|
+
config: PaneConfig,
|
|
343
|
+
worktree?: WorktreeOperations,
|
|
344
|
+
): PiLaunchOperations {
|
|
345
|
+
return {
|
|
346
|
+
worktree,
|
|
347
|
+
createPane(name, cwd) {
|
|
348
|
+
return provider.createSurface({
|
|
349
|
+
name,
|
|
350
|
+
cwd: cwd ?? process.cwd(),
|
|
351
|
+
placement: placementFromPaneConfig(config),
|
|
352
|
+
});
|
|
353
|
+
},
|
|
354
|
+
async createWorktree(name, cwd, branch, base) {
|
|
355
|
+
const worktree = await Promise.resolve(
|
|
356
|
+
provider.createWorktreeSurface({ name, cwd, branch, base }),
|
|
357
|
+
);
|
|
358
|
+
return worktreeSurfaceForLaunch(worktree);
|
|
359
|
+
},
|
|
360
|
+
waitForShellReady(surface) {
|
|
361
|
+
return provider.waitForShellReady(surface);
|
|
362
|
+
},
|
|
363
|
+
runScript(surface, command, options) {
|
|
364
|
+
return provider.runScript(surface, command, options);
|
|
365
|
+
},
|
|
366
|
+
closePane(pane) {
|
|
367
|
+
return provider.closeSurface(pane);
|
|
368
|
+
},
|
|
369
|
+
waitForPiReady(surface, sessionFile, cwd) {
|
|
370
|
+
return waitForSurfacePiReady(provider, surface, sessionFile, cwd);
|
|
371
|
+
},
|
|
372
|
+
focusWorkspace(workspaceId) {
|
|
373
|
+
return provider.focusWorkspace(workspaceId);
|
|
374
|
+
},
|
|
375
|
+
captureProcessIdentity(surface, expected, options) {
|
|
376
|
+
return captureSurfacePiProcessIdentity(
|
|
377
|
+
provider,
|
|
378
|
+
surface,
|
|
379
|
+
expected,
|
|
380
|
+
options,
|
|
381
|
+
);
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
203
385
|
|
|
204
386
|
interface ResolvedLaunch {
|
|
205
387
|
request: FreshPiLaunchRequest;
|
|
@@ -224,6 +406,7 @@ interface PreparedSurface {
|
|
|
224
406
|
interface PreparedSession extends PreparedSurface {
|
|
225
407
|
sessionFile: string;
|
|
226
408
|
activityFile: string;
|
|
409
|
+
processIdentityFile?: string;
|
|
227
410
|
}
|
|
228
411
|
|
|
229
412
|
interface PreparedArtifacts extends PreparedSession {
|
|
@@ -237,7 +420,7 @@ interface PreparedArtifacts extends PreparedSession {
|
|
|
237
420
|
*/
|
|
238
421
|
export async function launchPiSubagent(
|
|
239
422
|
request: PiLaunchRequest,
|
|
240
|
-
operations: PiLaunchOperations
|
|
423
|
+
operations: PiLaunchOperations,
|
|
241
424
|
): Promise<PiRunningChild> {
|
|
242
425
|
return request.kind === "resume"
|
|
243
426
|
? launchResumedPiSubagent(request, operations)
|
|
@@ -246,7 +429,7 @@ export async function launchPiSubagent(
|
|
|
246
429
|
|
|
247
430
|
export async function launchPiWorktreeHandoff(
|
|
248
431
|
request: FreshPiLaunchRequest,
|
|
249
|
-
operations: PiLaunchOperations
|
|
432
|
+
operations: PiLaunchOperations,
|
|
250
433
|
): Promise<{ running: PiRunningChild; focusError?: string }> {
|
|
251
434
|
if (!request.worktree || !request.handoff) {
|
|
252
435
|
throw new Error("A worktree handoff requires a worktree and active leaf");
|
|
@@ -256,13 +439,16 @@ export async function launchPiWorktreeHandoff(
|
|
|
256
439
|
throw new Error("Worktree handoff did not create a managed worktree");
|
|
257
440
|
}
|
|
258
441
|
try {
|
|
259
|
-
operations.focusWorkspace?.(running.worktree.workspaceId);
|
|
442
|
+
await operations.focusWorkspace?.(running.worktree.workspaceId);
|
|
260
443
|
} catch (error) {
|
|
261
444
|
const focusError = errorMessage(error);
|
|
262
|
-
writeWorktreeManifest(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
445
|
+
requireWorktreeOperations(operations).writeWorktreeManifest(
|
|
446
|
+
running.worktree.manifestFile,
|
|
447
|
+
{
|
|
448
|
+
state: "running",
|
|
449
|
+
focusError,
|
|
450
|
+
},
|
|
451
|
+
);
|
|
266
452
|
return {
|
|
267
453
|
running,
|
|
268
454
|
focusError,
|
|
@@ -279,16 +465,16 @@ async function launchFreshPiSubagent(
|
|
|
279
465
|
let surface: PreparedSurface | undefined;
|
|
280
466
|
|
|
281
467
|
try {
|
|
282
|
-
surface = prepareLaunchSurface(resolved, operations);
|
|
283
|
-
const session = prepareChildSession(resolved, surface);
|
|
468
|
+
surface = await prepareLaunchSurface(resolved, operations);
|
|
469
|
+
const session = prepareChildSession(resolved, surface, operations);
|
|
284
470
|
const handoffArtifacts = request.handoff
|
|
285
|
-
? prepareTaskArtifacts(resolved, session)
|
|
471
|
+
? prepareTaskArtifacts(resolved, session, operations)
|
|
286
472
|
: undefined;
|
|
287
473
|
await confirmShellReady(session, operations);
|
|
288
474
|
const artifacts =
|
|
289
|
-
handoffArtifacts ?? prepareTaskArtifacts(resolved, session);
|
|
475
|
+
handoffArtifacts ?? prepareTaskArtifacts(resolved, session, operations);
|
|
290
476
|
const command = buildPiCommand(resolved, artifacts);
|
|
291
|
-
const launchScriptFile = startPiProcess(
|
|
477
|
+
const launchScriptFile = await startPiProcess(
|
|
292
478
|
resolved,
|
|
293
479
|
artifacts,
|
|
294
480
|
command,
|
|
@@ -304,25 +490,31 @@ async function launchFreshPiSubagent(
|
|
|
304
490
|
artifacts.targetCwd,
|
|
305
491
|
);
|
|
306
492
|
if (artifacts.worktree) {
|
|
307
|
-
persistWorktreeResult(
|
|
493
|
+
requireWorktreeOperations(operations).persistWorktreeResult(
|
|
494
|
+
artifacts.worktree,
|
|
495
|
+
"running",
|
|
496
|
+
);
|
|
308
497
|
}
|
|
309
498
|
}
|
|
310
|
-
|
|
499
|
+
const running = createRunningChild(resolved, artifacts, launchScriptFile);
|
|
500
|
+
startProcessIdentityCapture(running, operations);
|
|
501
|
+
return running;
|
|
311
502
|
} catch (error) {
|
|
312
503
|
if (!surface) throw error;
|
|
313
504
|
if (!surface.worktree) {
|
|
314
505
|
if (!request.surface) {
|
|
315
506
|
try {
|
|
316
|
-
operations.closePane(surface.surface);
|
|
507
|
+
await operations.closePane(surface.surface);
|
|
317
508
|
} catch {
|
|
318
509
|
// The launch error remains authoritative when cleanup also fails.
|
|
319
510
|
}
|
|
320
511
|
}
|
|
321
512
|
throw error;
|
|
322
513
|
}
|
|
323
|
-
const
|
|
514
|
+
const worktreeOps = requireWorktreeOperations(operations);
|
|
515
|
+
const handoff = worktreeOps.captureWorktreeHandoff(surface.worktree);
|
|
324
516
|
try {
|
|
325
|
-
persistWorktreeResult(surface.worktree, "failed", handoff);
|
|
517
|
+
worktreeOps.persistWorktreeResult(surface.worktree, "failed", handoff);
|
|
326
518
|
} catch {
|
|
327
519
|
// The launch error remains authoritative when persistence also fails.
|
|
328
520
|
}
|
|
@@ -371,16 +563,16 @@ function resolveLaunchRequest(request: FreshPiLaunchRequest): ResolvedLaunch {
|
|
|
371
563
|
};
|
|
372
564
|
}
|
|
373
565
|
|
|
374
|
-
function prepareLaunchSurface(
|
|
566
|
+
async function prepareLaunchSurface(
|
|
375
567
|
resolved: ResolvedLaunch,
|
|
376
568
|
operations: PiLaunchOperations,
|
|
377
|
-
): PreparedSurface {
|
|
569
|
+
): Promise<PreparedSurface> {
|
|
378
570
|
const { request } = resolved;
|
|
379
571
|
if (!request.worktree) {
|
|
380
572
|
return {
|
|
381
573
|
surface:
|
|
382
574
|
request.surface ??
|
|
383
|
-
operations.createPane(request.name, resolved.sourceCwd),
|
|
575
|
+
(await operations.createPane(request.name, resolved.sourceCwd)),
|
|
384
576
|
targetCwd: resolved.sourceCwd,
|
|
385
577
|
effectiveAgentDir: resolved.localAgentDir ?? resolved.agentDir,
|
|
386
578
|
localAgentDir: resolved.localAgentDir,
|
|
@@ -389,8 +581,12 @@ function prepareLaunchSurface(
|
|
|
389
581
|
if (request.surface)
|
|
390
582
|
throw new Error("A worktree subagent cannot use a pre-created pane");
|
|
391
583
|
|
|
584
|
+
const worktreeOps = requireWorktreeOperations(operations);
|
|
392
585
|
const baseRef = request.worktree.base ?? "HEAD";
|
|
393
|
-
const baseSha = resolveGitCommit(resolved.sourceCwd, baseRef);
|
|
586
|
+
const baseSha = worktreeOps.resolveGitCommit(resolved.sourceCwd, baseRef);
|
|
587
|
+
const provisionCwd = worktreeOps.resolveWorktreeProvisionCwd(
|
|
588
|
+
resolved.sourceCwd,
|
|
589
|
+
);
|
|
394
590
|
const manifestFile = join(
|
|
395
591
|
resolved.artifactDir,
|
|
396
592
|
"worktree-runs",
|
|
@@ -405,16 +601,16 @@ function prepareLaunchSurface(
|
|
|
405
601
|
baseSha,
|
|
406
602
|
createdAt: resolved.startTime,
|
|
407
603
|
};
|
|
408
|
-
writeWorktreeManifest(manifestFile, {
|
|
604
|
+
worktreeOps.writeWorktreeManifest(manifestFile, {
|
|
409
605
|
state: "provisioning",
|
|
410
606
|
...ownership,
|
|
411
607
|
});
|
|
412
608
|
|
|
413
|
-
let created:
|
|
609
|
+
let created: WorktreeSurfaceForLaunch;
|
|
414
610
|
try {
|
|
415
|
-
created = operations.createWorktree(
|
|
611
|
+
created = await operations.createWorktree(
|
|
416
612
|
request.name,
|
|
417
|
-
|
|
613
|
+
provisionCwd,
|
|
418
614
|
request.worktree.branch,
|
|
419
615
|
baseSha,
|
|
420
616
|
);
|
|
@@ -423,11 +619,11 @@ function prepareLaunchSurface(
|
|
|
423
619
|
state: "failed",
|
|
424
620
|
...ownership,
|
|
425
621
|
};
|
|
426
|
-
if (error instanceof
|
|
622
|
+
if (error instanceof WorktreeProvisioningError) {
|
|
427
623
|
Object.assign(failedManifest, error.recoveredWorktree);
|
|
428
624
|
}
|
|
429
625
|
failedManifest.error = errorMessage(error);
|
|
430
|
-
writeWorktreeManifest(manifestFile, failedManifest);
|
|
626
|
+
worktreeOps.writeWorktreeManifest(manifestFile, failedManifest);
|
|
431
627
|
throw error;
|
|
432
628
|
}
|
|
433
629
|
|
|
@@ -440,7 +636,7 @@ function prepareLaunchSurface(
|
|
|
440
636
|
baseSha,
|
|
441
637
|
manifestFile,
|
|
442
638
|
};
|
|
443
|
-
writeWorktreeManifest(manifestFile, {
|
|
639
|
+
worktreeOps.writeWorktreeManifest(manifestFile, {
|
|
444
640
|
state: "provisioned",
|
|
445
641
|
...ownership,
|
|
446
642
|
...worktree,
|
|
@@ -461,6 +657,7 @@ function prepareLaunchSurface(
|
|
|
461
657
|
function prepareChildSession(
|
|
462
658
|
resolved: ResolvedLaunch,
|
|
463
659
|
surface: PreparedSurface,
|
|
660
|
+
operations: PiLaunchOperations,
|
|
464
661
|
): PreparedSession {
|
|
465
662
|
const sessionDir = getDefaultSessionDirFor(
|
|
466
663
|
surface.targetCwd,
|
|
@@ -476,7 +673,10 @@ function prepareChildSession(
|
|
|
476
673
|
const sessionFile = join(sessionDir, `${timestamp}_${uuid}.jsonl`);
|
|
477
674
|
if (surface.worktree) {
|
|
478
675
|
surface.worktree.sessionFile = sessionFile;
|
|
479
|
-
writeWorktreeManifest(
|
|
676
|
+
requireWorktreeOperations(operations).writeWorktreeManifest(
|
|
677
|
+
surface.worktree.manifestFile,
|
|
678
|
+
{ sessionFile },
|
|
679
|
+
);
|
|
480
680
|
}
|
|
481
681
|
writeSubagentSessionPolicy(sessionFile, {
|
|
482
682
|
owner: surface.worktree ? "managed-worktree" : "public",
|
|
@@ -498,7 +698,16 @@ function prepareChildSession(
|
|
|
498
698
|
resolved.artifactDir,
|
|
499
699
|
resolved.id,
|
|
500
700
|
);
|
|
501
|
-
return {
|
|
701
|
+
return {
|
|
702
|
+
...surface,
|
|
703
|
+
sessionFile,
|
|
704
|
+
activityFile,
|
|
705
|
+
// A handoff Pi loads no child protocol extension, so it records nothing.
|
|
706
|
+
processIdentityFile:
|
|
707
|
+
surface.worktree && !resolved.request.handoff
|
|
708
|
+
? getSubagentProcessIdentityFile(sessionFile)
|
|
709
|
+
: undefined,
|
|
710
|
+
};
|
|
502
711
|
}
|
|
503
712
|
|
|
504
713
|
async function confirmShellReady(
|
|
@@ -530,6 +739,7 @@ function buildWorktreeHandoffMessage(
|
|
|
530
739
|
function prepareTaskArtifacts(
|
|
531
740
|
resolved: ResolvedLaunch,
|
|
532
741
|
session: PreparedSession,
|
|
742
|
+
operations: PiLaunchOperations,
|
|
533
743
|
): PreparedArtifacts {
|
|
534
744
|
const { request } = resolved;
|
|
535
745
|
if (request.handoff) {
|
|
@@ -550,10 +760,13 @@ function prepareTaskArtifacts(
|
|
|
550
760
|
});
|
|
551
761
|
session.worktree.sourceSessionFile = request.parent.sessionFile;
|
|
552
762
|
session.worktree.handoffMessage = handoffMessage;
|
|
553
|
-
writeWorktreeManifest(
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
763
|
+
requireWorktreeOperations(operations).writeWorktreeManifest(
|
|
764
|
+
session.worktree.manifestFile,
|
|
765
|
+
{
|
|
766
|
+
sourceSessionFile: request.parent.sessionFile,
|
|
767
|
+
handoffMessage,
|
|
768
|
+
},
|
|
769
|
+
);
|
|
557
770
|
} else if (resolved.sessionMode !== "standalone") {
|
|
558
771
|
seedSubagentSessionFile({
|
|
559
772
|
mode: resolved.sessionMode,
|
|
@@ -615,7 +828,7 @@ function buildPiCommand(
|
|
|
615
828
|
shellQuote(artifacts.sessionFile),
|
|
616
829
|
...(request.handoff
|
|
617
830
|
? []
|
|
618
|
-
: ["-e", shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts"))]),
|
|
831
|
+
: ["-e", shellQuote(join(SUBAGENTS_DIR, "child", "subagent-done.ts"))]),
|
|
619
832
|
"--model",
|
|
620
833
|
shellQuote(request.runtimePlan.model),
|
|
621
834
|
"--thinking",
|
|
@@ -675,6 +888,10 @@ function buildPiCommand(
|
|
|
675
888
|
env.push(`PI_SUBAGENT_ID=${shellQuote(resolved.id)}`);
|
|
676
889
|
env.push(`PI_SUBAGENT_ACTIVITY_FILE=${shellQuote(artifacts.activityFile)}`);
|
|
677
890
|
env.push(`PI_SUBAGENT_SURFACE=${shellQuote(artifacts.surface)}`);
|
|
891
|
+
if (artifacts.processIdentityFile)
|
|
892
|
+
env.push(
|
|
893
|
+
`PI_SUBAGENT_PROCESS_FILE=${shellQuote(artifacts.processIdentityFile)}`,
|
|
894
|
+
);
|
|
678
895
|
}
|
|
679
896
|
|
|
680
897
|
const piCommand =
|
|
@@ -685,21 +902,24 @@ function buildPiCommand(
|
|
|
685
902
|
: `${piCommand}; echo '__SUBAGENT_DONE_'$?'__'`;
|
|
686
903
|
}
|
|
687
904
|
|
|
688
|
-
function startPiProcess(
|
|
905
|
+
async function startPiProcess(
|
|
689
906
|
resolved: ResolvedLaunch,
|
|
690
907
|
artifacts: PreparedArtifacts,
|
|
691
908
|
command: string,
|
|
692
909
|
operations: PiLaunchOperations,
|
|
693
|
-
): string {
|
|
910
|
+
): Promise<string> {
|
|
694
911
|
const launchScriptFile = join(
|
|
695
912
|
resolved.artifactDir,
|
|
696
913
|
"subagent-scripts",
|
|
697
914
|
`${safeName(resolved.request.name) || "subagent"}-${resolved.id}.sh`,
|
|
698
915
|
);
|
|
699
916
|
if (artifacts.worktree && !resolved.request.handoff) {
|
|
700
|
-
persistWorktreeResult(
|
|
917
|
+
requireWorktreeOperations(operations).persistWorktreeResult(
|
|
918
|
+
artifacts.worktree,
|
|
919
|
+
"running",
|
|
920
|
+
);
|
|
701
921
|
}
|
|
702
|
-
return operations.runScript(artifacts.surface, command, {
|
|
922
|
+
return await operations.runScript(artifacts.surface, command, {
|
|
703
923
|
scriptPath: launchScriptFile,
|
|
704
924
|
scriptPreamble: [
|
|
705
925
|
shellComment(`Subagent launch script for ${resolved.request.name}`),
|
|
@@ -729,9 +949,43 @@ function createRunningChild(
|
|
|
729
949
|
runtimePlan: resolved.request.runtimePlan,
|
|
730
950
|
worktree: artifacts.worktree,
|
|
731
951
|
lifecycle: createLifecycle(resolved.startTime),
|
|
952
|
+
processIdentityFile: artifacts.processIdentityFile,
|
|
732
953
|
};
|
|
733
954
|
}
|
|
734
955
|
|
|
956
|
+
// Capture runs beside the launch so readiness never delays the acknowledgement;
|
|
957
|
+
// a cancel awaits it, and without a verified identity nothing is signalled. A
|
|
958
|
+
// cancel after an expired capture starts a fresh one bounded by its own budget.
|
|
959
|
+
export function startProcessIdentityCapture(
|
|
960
|
+
child: PiRunningChild,
|
|
961
|
+
operations: PiLaunchOperations,
|
|
962
|
+
options?: { timeoutMs?: number },
|
|
963
|
+
): void {
|
|
964
|
+
const file = child.processIdentityFile;
|
|
965
|
+
if (!file) return;
|
|
966
|
+
const capture = operations.captureProcessIdentity;
|
|
967
|
+
if (!capture) {
|
|
968
|
+
child.processIdentityError = "process identity capture is unavailable";
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
child.processIdentityError = undefined;
|
|
972
|
+
child.processIdentityCapture = Promise.resolve()
|
|
973
|
+
.then(() =>
|
|
974
|
+
capture(
|
|
975
|
+
child.surface,
|
|
976
|
+
{ file, id: child.id, sessionFile: child.sessionFile },
|
|
977
|
+
options,
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
.then(
|
|
981
|
+
(identity) => (child.processIdentity = identity),
|
|
982
|
+
(error) => {
|
|
983
|
+
child.processIdentityError = errorMessage(error);
|
|
984
|
+
return undefined;
|
|
985
|
+
},
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
735
989
|
async function launchResumedPiSubagent(
|
|
736
990
|
request: ResumePiLaunchRequest,
|
|
737
991
|
operations: PiLaunchOperations,
|
|
@@ -761,7 +1015,7 @@ async function launchResumedPiSubagent(
|
|
|
761
1015
|
(entry) => entry.type === "session",
|
|
762
1016
|
);
|
|
763
1017
|
const cwd = isNonEmptyString(header?.cwd) ? header.cwd : process.cwd();
|
|
764
|
-
const surface = operations.createPane(request.name, cwd);
|
|
1018
|
+
const surface = await operations.createPane(request.name, cwd);
|
|
765
1019
|
try {
|
|
766
1020
|
await operations.waitForShellReady(surface);
|
|
767
1021
|
const activityFile = getSubagentActivityFile(artifactDir, id);
|
|
@@ -802,10 +1056,10 @@ async function launchResumedPiSubagent(
|
|
|
802
1056
|
shellQuote(request.sessionFile),
|
|
803
1057
|
...(toolAllowlist ? ["--tools", shellQuote(toolAllowlist)] : []),
|
|
804
1058
|
"-e",
|
|
805
|
-
shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts")),
|
|
1059
|
+
shellQuote(join(SUBAGENTS_DIR, "child", "subagent-done.ts")),
|
|
806
1060
|
...(messageFile ? [shellQuote(`@${messageFile}`)] : []),
|
|
807
1061
|
].join(" ");
|
|
808
|
-
const launchScriptFile = operations.runScript(
|
|
1062
|
+
const launchScriptFile = await operations.runScript(
|
|
809
1063
|
surface,
|
|
810
1064
|
`${command}; echo '__SUBAGENT_DONE_'$?'__'`,
|
|
811
1065
|
{
|
|
@@ -840,7 +1094,7 @@ async function launchResumedPiSubagent(
|
|
|
840
1094
|
};
|
|
841
1095
|
} catch (error) {
|
|
842
1096
|
try {
|
|
843
|
-
operations.closePane(surface);
|
|
1097
|
+
await operations.closePane(surface);
|
|
844
1098
|
} catch {
|
|
845
1099
|
// The launch error remains authoritative when cleanup also fails.
|
|
846
1100
|
}
|
|
@@ -911,163 +1165,12 @@ function safeName(name: string): string {
|
|
|
911
1165
|
.replace(/^-|-$/g, "");
|
|
912
1166
|
}
|
|
913
1167
|
|
|
914
|
-
function
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
export function readWorktreeManifest(path: string): JsonObject | undefined {
|
|
922
|
-
try {
|
|
923
|
-
const value: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
924
|
-
if (
|
|
925
|
-
isRecord(value) &&
|
|
926
|
-
value.version === 1 &&
|
|
927
|
-
value.kind === "worktree-run" &&
|
|
928
|
-
value.owner === "pi-herdr-subagents"
|
|
929
|
-
)
|
|
930
|
-
return value;
|
|
931
|
-
} catch {
|
|
932
|
-
// Unreachable or malformed manifests do not establish ownership.
|
|
933
|
-
}
|
|
934
|
-
return undefined;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
export function writeWorktreeManifest(path: string, value: JsonObject): void {
|
|
938
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
939
|
-
let existing: JsonObject = {};
|
|
940
|
-
if (existsSync(path)) {
|
|
941
|
-
try {
|
|
942
|
-
existing = JSON.parse(readFileSync(path, "utf8"));
|
|
943
|
-
} catch {
|
|
944
|
-
existing = {};
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
const tempPath = `${path}.tmp`;
|
|
948
|
-
writeFileSync(
|
|
949
|
-
tempPath,
|
|
950
|
-
`${JSON.stringify(
|
|
951
|
-
{
|
|
952
|
-
...existing,
|
|
953
|
-
...value,
|
|
954
|
-
version: 1,
|
|
955
|
-
kind: "worktree-run",
|
|
956
|
-
owner: "pi-herdr-subagents",
|
|
957
|
-
updatedAt: Date.now(),
|
|
958
|
-
},
|
|
959
|
-
null,
|
|
960
|
-
2,
|
|
961
|
-
)}\n`,
|
|
962
|
-
);
|
|
963
|
-
renameSync(tempPath, path);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
function gitPathList(cwd: string, args: string[]): string[] {
|
|
967
|
-
return execFileSync("git", args, { cwd, encoding: "utf8" })
|
|
968
|
-
.split("\0")
|
|
969
|
-
.filter(Boolean);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
export function captureWorktreeHandoff(
|
|
973
|
-
worktree: WorktreeLaunch,
|
|
974
|
-
): WorktreeHandoff {
|
|
975
|
-
try {
|
|
976
|
-
const headSha = resolveGitCommit(worktree.path, "HEAD");
|
|
977
|
-
const status = execFileSync(
|
|
978
|
-
"git",
|
|
979
|
-
["status", "--porcelain=v1", "--untracked-files=all", "-z"],
|
|
980
|
-
{ cwd: worktree.path, encoding: "utf8" },
|
|
981
|
-
);
|
|
982
|
-
const untrackedFiles = gitPathList(worktree.path, [
|
|
983
|
-
"ls-files",
|
|
984
|
-
"--others",
|
|
985
|
-
"--exclude-standard",
|
|
986
|
-
"-z",
|
|
987
|
-
]);
|
|
988
|
-
const conflictedFiles = gitPathList(worktree.path, [
|
|
989
|
-
"diff",
|
|
990
|
-
"--name-only",
|
|
991
|
-
"--diff-filter=U",
|
|
992
|
-
"-z",
|
|
993
|
-
]);
|
|
994
|
-
const changedFiles = new Set([
|
|
995
|
-
...gitPathList(worktree.path, [
|
|
996
|
-
"diff",
|
|
997
|
-
"--name-only",
|
|
998
|
-
"-z",
|
|
999
|
-
`${worktree.baseSha}...HEAD`,
|
|
1000
|
-
]),
|
|
1001
|
-
...gitPathList(worktree.path, ["diff", "--name-only", "-z"]),
|
|
1002
|
-
...gitPathList(worktree.path, ["diff", "--cached", "--name-only", "-z"]),
|
|
1003
|
-
...untrackedFiles,
|
|
1004
|
-
]);
|
|
1005
|
-
const commitsAhead = Number.parseInt(
|
|
1006
|
-
execFileSync(
|
|
1007
|
-
"git",
|
|
1008
|
-
["rev-list", "--count", `${worktree.baseSha}..HEAD`],
|
|
1009
|
-
{ cwd: worktree.path, encoding: "utf8" },
|
|
1010
|
-
).trim(),
|
|
1011
|
-
10,
|
|
1012
|
-
);
|
|
1013
|
-
return {
|
|
1014
|
-
...worktree,
|
|
1015
|
-
headSha,
|
|
1016
|
-
commitsAhead: Number.isFinite(commitsAhead) ? commitsAhead : 0,
|
|
1017
|
-
clean: status.length === 0,
|
|
1018
|
-
conflicted: conflictedFiles.length > 0,
|
|
1019
|
-
changedFiles: [...changedFiles].sort(),
|
|
1020
|
-
untrackedFiles: untrackedFiles.sort(),
|
|
1021
|
-
};
|
|
1022
|
-
} catch (error) {
|
|
1023
|
-
return {
|
|
1024
|
-
...worktree,
|
|
1025
|
-
headSha: null,
|
|
1026
|
-
commitsAhead: null,
|
|
1027
|
-
clean: null,
|
|
1028
|
-
conflicted: null,
|
|
1029
|
-
changedFiles: null,
|
|
1030
|
-
untrackedFiles: null,
|
|
1031
|
-
gitError: errorMessage(error),
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
export function persistWorktreeResult(
|
|
1037
|
-
worktree: WorktreeLaunch,
|
|
1038
|
-
state: "running" | "ready_for_review" | "failed" | "needs_help" | "removed",
|
|
1039
|
-
handoff?: WorktreeHandoff,
|
|
1040
|
-
): void {
|
|
1041
|
-
writeWorktreeManifest(worktree.manifestFile, {
|
|
1042
|
-
state,
|
|
1043
|
-
...worktree,
|
|
1044
|
-
...handoff,
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
export function runSubagentScript(
|
|
1049
|
-
surface: string,
|
|
1050
|
-
command: string,
|
|
1051
|
-
options: Parameters<typeof runScriptInPane>[2],
|
|
1052
|
-
worktree?: WorktreeLaunch,
|
|
1053
|
-
run: typeof runScriptInPane = runScriptInPane,
|
|
1054
|
-
): string {
|
|
1055
|
-
if (worktree) persistWorktreeResult(worktree, "running");
|
|
1056
|
-
try {
|
|
1057
|
-
return run(surface, command, options);
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
if (!worktree) throw error;
|
|
1060
|
-
const handoff = captureWorktreeHandoff(worktree);
|
|
1061
|
-
try {
|
|
1062
|
-
persistWorktreeResult(worktree, "failed", handoff);
|
|
1063
|
-
} catch {
|
|
1064
|
-
// The launch error remains authoritative when persistence also fails.
|
|
1065
|
-
}
|
|
1066
|
-
throw new Error(
|
|
1067
|
-
`Failed to launch subagent; worktree retained at ${worktree.path} ` +
|
|
1068
|
-
`(workspace ${worktree.workspaceId}): ${errorMessage(error)}`,
|
|
1069
|
-
);
|
|
1070
|
-
}
|
|
1168
|
+
function requireWorktreeOperations(
|
|
1169
|
+
operations: PiLaunchOperations,
|
|
1170
|
+
): WorktreeOperations {
|
|
1171
|
+
if (!operations.worktree)
|
|
1172
|
+
throw new Error("Worktree operations are unavailable");
|
|
1173
|
+
return operations.worktree;
|
|
1071
1174
|
}
|
|
1072
1175
|
|
|
1073
1176
|
function errorMessage(error: any): string {
|