pi-agent-replicate 0.0.0-stage → 1.4.1
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 +27 -0
- package/CHANGELOG.md +152 -0
- package/CONTEXT.md +63 -0
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/RELEASING.md +132 -0
- package/assets/banner.png +0 -0
- package/config.json.example +6 -0
- package/docs/README.md +23 -0
- package/docs/adr/0003-installable-role-packs.md +29 -0
- package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
- package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
- package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
- package/docs/configuration.md +14 -0
- package/docs/development.md +27 -0
- package/docs/public-subagent-tree-api.md +58 -0
- package/docs/retained-roles.md +71 -0
- package/docs/settled-lifecycle-contract.md +71 -0
- package/docs/worktree-subagents.md +58 -0
- package/examples/role-pack/extension.ts +18 -0
- package/examples/role-pack/package.json +16 -0
- package/examples/role-pack/roles/example-reviewer.md +12 -0
- package/index.d.ts +116 -0
- package/index.js +1064 -0
- package/index.ts +155 -0
- package/package.json +70 -4
- package/pi-extension/subagents/activity.js +575 -0
- package/pi-extension/subagents/activity.ts +980 -0
- package/pi-extension/subagents/completion.js +184 -0
- package/pi-extension/subagents/completion.ts +229 -0
- package/pi-extension/subagents/delivery-drain.js +15 -0
- package/pi-extension/subagents/delivery-drain.ts +20 -0
- package/pi-extension/subagents/delivery-files.js +86 -0
- package/pi-extension/subagents/delivery-files.ts +105 -0
- package/pi-extension/subagents/delivery-log.js +51 -0
- package/pi-extension/subagents/delivery-log.ts +51 -0
- package/pi-extension/subagents/delivery-payload.ts +55 -0
- package/pi-extension/subagents/delivery.ts +219 -0
- package/pi-extension/subagents/execution-owner.js +30 -0
- package/pi-extension/subagents/execution-owner.ts +38 -0
- package/pi-extension/subagents/herdr.js +645 -0
- package/pi-extension/subagents/herdr.ts +927 -0
- package/pi-extension/subagents/index.ts +3400 -0
- package/pi-extension/subagents/launch.js +774 -0
- package/pi-extension/subagents/launch.ts +1124 -0
- package/pi-extension/subagents/lifecycle.js +477 -0
- package/pi-extension/subagents/lifecycle.ts +582 -0
- package/pi-extension/subagents/lineage.js +806 -0
- package/pi-extension/subagents/lineage.ts +841 -0
- package/pi-extension/subagents/replica-background.js +32 -0
- package/pi-extension/subagents/replica-background.ts +38 -0
- package/pi-extension/subagents/replica-branch.ts +45 -0
- package/pi-extension/subagents/replica-directory-settings.ts +51 -0
- package/pi-extension/subagents/replica-history.js +22 -0
- package/pi-extension/subagents/replica-history.ts +21 -0
- package/pi-extension/subagents/replica-work.js +326 -0
- package/pi-extension/subagents/replica-work.ts +336 -0
- package/pi-extension/subagents/replica-worker.js +74 -0
- package/pi-extension/subagents/replica-worker.ts +71 -0
- package/pi-extension/subagents/replication.ts +99 -0
- package/pi-extension/subagents/result-delivery-queue.ts +33 -0
- package/pi-extension/subagents/role-catalog.js +123 -0
- package/pi-extension/subagents/role-catalog.ts +24 -0
- package/pi-extension/subagents/runtime-routing.js +154 -0
- package/pi-extension/subagents/runtime-routing.ts +239 -0
- package/pi-extension/subagents/session-binding.js +346 -0
- package/pi-extension/subagents/session-binding.ts +339 -0
- package/pi-extension/subagents/session-work.js +219 -0
- package/pi-extension/subagents/session-work.ts +167 -0
- package/pi-extension/subagents/session.js +318 -0
- package/pi-extension/subagents/session.ts +448 -0
- package/pi-extension/subagents/settled-contract.js +47 -0
- package/pi-extension/subagents/settled-contract.ts +175 -0
- package/pi-extension/subagents/source-state.js +180 -0
- package/pi-extension/subagents/source-state.ts +184 -0
- package/pi-extension/subagents/status.ts +493 -0
- package/pi-extension/subagents/subagent-done.js +805 -0
- package/pi-extension/subagents/subagent-done.ts +878 -0
- package/pi-extension/subagents/terminal.js +111 -0
- package/pi-extension/subagents/terminal.ts +208 -0
- package/pi-extension/subagents/watchdog.js +122 -0
- package/pi-extension/subagents/watchdog.ts +171 -0
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
appendLineageEvent,
|
|
15
|
+
hasLineageEvent,
|
|
16
|
+
lineageFromEnvironment,
|
|
17
|
+
lineageEnvironment,
|
|
18
|
+
registerLineage,
|
|
19
|
+
isLineageNodeDrained,
|
|
20
|
+
reduceLineage,
|
|
21
|
+
type LineageRegistration,
|
|
22
|
+
} from "./lineage.ts";
|
|
23
|
+
import {
|
|
24
|
+
getSubagentActivityFile,
|
|
25
|
+
getSubagentSettledEventsFile,
|
|
26
|
+
} from "./activity.ts";
|
|
27
|
+
import { createLifecycle, type SubagentLifecycle } from "./lifecycle.ts";
|
|
28
|
+
import type { ResolvedRuntimePlan } from "./runtime-routing.ts";
|
|
29
|
+
import { HerdrWorktreeCreateError } from "./herdr.ts";
|
|
30
|
+
import {
|
|
31
|
+
captureSessionBaseline,
|
|
32
|
+
seedStandaloneSessionFile,
|
|
33
|
+
seedSubagentSessionFile,
|
|
34
|
+
seedReplicationSessionFile,
|
|
35
|
+
} from "./session.ts";
|
|
36
|
+
import type { SessionBaselineCursor } from "./settled-contract.ts";
|
|
37
|
+
import {
|
|
38
|
+
createSubagentPane,
|
|
39
|
+
|
|
40
|
+
createSubagentPaneInWorkspace,
|
|
41
|
+
closePane,
|
|
42
|
+
getPaneTabId,
|
|
43
|
+
getPaneProcessInfo,
|
|
44
|
+
createSubagentWorktree,
|
|
45
|
+
runScriptInPane,
|
|
46
|
+
shellQuote,
|
|
47
|
+
waitForShellReady,
|
|
48
|
+
type HerdrWorktreeSurface,
|
|
49
|
+
} from "./terminal.ts";
|
|
50
|
+
|
|
51
|
+
import { readSessionBinding, registerSessionLineage, seedSessionBinding, sessionBindingStore, captureLaunchPaneOwnership, type LaunchPaneOwnership } from "./session-binding.ts";
|
|
52
|
+
import type { ReplicationSnapshot } from "./replication.ts";
|
|
53
|
+
import type { ExecutionOwner } from "./execution-owner.ts";
|
|
54
|
+
import { attachReplicaWork, attachSharedReplicaPane, captureReplicaLaunchPane, cleanupReplicaWork, failReplicaLaunch } from "./replica-work.ts";
|
|
55
|
+
import { sourceRepositoryAnchor } from "./source-state.ts";
|
|
56
|
+
|
|
57
|
+
const SUBAGENTS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
|
|
59
|
+
type SubagentSessionMode = "standalone" | "lineage-only" | "fork";
|
|
60
|
+
|
|
61
|
+
export interface WorktreeLaunch {
|
|
62
|
+
path: string;
|
|
63
|
+
workspaceId: string;
|
|
64
|
+
paneId: string;
|
|
65
|
+
branch: string;
|
|
66
|
+
baseRef: string;
|
|
67
|
+
baseSha: string;
|
|
68
|
+
manifestFile: string;
|
|
69
|
+
sessionFile?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WorktreeHandoff extends WorktreeLaunch {
|
|
73
|
+
headSha: string | null;
|
|
74
|
+
commitsAhead: number | null;
|
|
75
|
+
clean: boolean | null;
|
|
76
|
+
conflicted: boolean | null;
|
|
77
|
+
changedFiles: string[] | null;
|
|
78
|
+
untrackedFiles: string[] | null;
|
|
79
|
+
gitError?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FreshPiLaunchRequest {
|
|
83
|
+
kind: "fresh";
|
|
84
|
+
/** Real initiating tool ownership; absent for command/API launches. */
|
|
85
|
+
executionOwner?: ExecutionOwner;
|
|
86
|
+
id?: string;
|
|
87
|
+
name: string;
|
|
88
|
+
task: string;
|
|
89
|
+
agent?: string;
|
|
90
|
+
cwd?: string;
|
|
91
|
+
worktree?: { branch: string; base?: string };
|
|
92
|
+
fork?: boolean;
|
|
93
|
+
/** Captured current-branch state for a role-free replication. */
|
|
94
|
+
replication?: ReplicationSnapshot;
|
|
95
|
+
surface?: string;
|
|
96
|
+
parent: {
|
|
97
|
+
cwd: string;
|
|
98
|
+
invocationCwd?: string;
|
|
99
|
+
sessionFile: string;
|
|
100
|
+
sessionId: string;
|
|
101
|
+
sessionDir: string;
|
|
102
|
+
agentDir?: string;
|
|
103
|
+
};
|
|
104
|
+
runtimePlan: ResolvedRuntimePlan;
|
|
105
|
+
lineage?: LineageRegistration;
|
|
106
|
+
|
|
107
|
+
behavior: {
|
|
108
|
+
tools?: string;
|
|
109
|
+
skills?: string;
|
|
110
|
+
deniedTools: readonly string[];
|
|
111
|
+
autoExit: boolean;
|
|
112
|
+
interactive: boolean;
|
|
113
|
+
/** Keep the public child tool list exact instead of adding control tools. */
|
|
114
|
+
includeControlTools?: boolean;
|
|
115
|
+
identity?: string;
|
|
116
|
+
systemPromptMode?: "append" | "replace";
|
|
117
|
+
sessionMode: SubagentSessionMode;
|
|
118
|
+
cwd?: string;
|
|
119
|
+
};
|
|
120
|
+
environment?: Readonly<Record<string, string>>;
|
|
121
|
+
signal?: AbortSignal;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ResumePiLaunchRequest {
|
|
125
|
+
kind: "resume";
|
|
126
|
+
/** Explicit continuations get a new owner; watchdog recovery preserves the old one. */
|
|
127
|
+
executionOwner?: ExecutionOwner;
|
|
128
|
+
id?: string;
|
|
129
|
+
name: string;
|
|
130
|
+
sessionFile: string;
|
|
131
|
+
message?: string;
|
|
132
|
+
|
|
133
|
+
/** Existing pane to reuse after a verified child-process crash. */
|
|
134
|
+
surface?: string;
|
|
135
|
+
/** Exact workspace used when recovery needs a replacement pane. */
|
|
136
|
+
workspace?: { id: string; cwd: string };
|
|
137
|
+
parent: {
|
|
138
|
+
sessionId: string;
|
|
139
|
+
sessionFile: string;
|
|
140
|
+
sessionDir: string;
|
|
141
|
+
};
|
|
142
|
+
behavior?: {
|
|
143
|
+
autoExit?: boolean;
|
|
144
|
+
interactive?: boolean;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type PiLaunchRequest = FreshPiLaunchRequest | ResumePiLaunchRequest;
|
|
149
|
+
|
|
150
|
+
export interface PiRunningChild {
|
|
151
|
+
executionOwner?: ExecutionOwner;
|
|
152
|
+
lineage?: LineageRegistration;
|
|
153
|
+
/** Distinguish replication settlement from retained role-based launches. */
|
|
154
|
+
replication?: boolean;
|
|
155
|
+
/** Durable work pointer, independent of any moved session file. */
|
|
156
|
+
replicaWorkFile?: string;
|
|
157
|
+
launchPane?: LaunchPaneOwnership;
|
|
158
|
+
id: string;
|
|
159
|
+
name: string;
|
|
160
|
+
task: string;
|
|
161
|
+
agent?: string;
|
|
162
|
+
surface: string;
|
|
163
|
+
startTime: number;
|
|
164
|
+
tabId?: string;
|
|
165
|
+
sessionFile: string;
|
|
166
|
+
launchScriptFile: string;
|
|
167
|
+
activityFile: string;
|
|
168
|
+
settledEventsFile: string;
|
|
169
|
+
interactive: boolean;
|
|
170
|
+
sessionBaseline: SessionBaselineCursor;
|
|
171
|
+
runtimePlan: ResolvedRuntimePlan | undefined;
|
|
172
|
+
worktree?: WorktreeLaunch;
|
|
173
|
+
/** Exact Herdr workspace used for replacement-pane recovery. */
|
|
174
|
+
recoveryWorkspace?: { id: string; cwd: string };
|
|
175
|
+
lifecycle: SubagentLifecycle;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface PiLaunchOperations {
|
|
179
|
+
createPane(name: string, workspace?: { id: string; cwd: string }): string;
|
|
180
|
+
createWorktree(
|
|
181
|
+
name: string,
|
|
182
|
+
cwd: string,
|
|
183
|
+
branch: string,
|
|
184
|
+
base: string,
|
|
185
|
+
): HerdrWorktreeSurface;
|
|
186
|
+
waitForShellReady(surface: string, options?: { signal?: AbortSignal }): Promise<void>;
|
|
187
|
+
closePane?(surface: string): void;
|
|
188
|
+
getPaneProcessInfo?: (surface: string) => ReturnType<typeof getPaneProcessInfo>;
|
|
189
|
+
runScript(
|
|
190
|
+
surface: string,
|
|
191
|
+
command: string,
|
|
192
|
+
options: { scriptPath: string; scriptPreamble: string },
|
|
193
|
+
): string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const defaultOperations: PiLaunchOperations = {
|
|
197
|
+
createPane: (name, workspace) => workspace
|
|
198
|
+
? createSubagentPaneInWorkspace(name, workspace.cwd, workspace.id)
|
|
199
|
+
: createSubagentPane(name),
|
|
200
|
+
closePane,
|
|
201
|
+
getPaneProcessInfo,
|
|
202
|
+
createWorktree: createSubagentWorktree,
|
|
203
|
+
waitForShellReady,
|
|
204
|
+
runScript: runScriptInPane,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
interface ResolvedLaunch {
|
|
208
|
+
launchPane?: LaunchPaneOwnership;
|
|
209
|
+
request: FreshPiLaunchRequest;
|
|
210
|
+
id: string;
|
|
211
|
+
startTime: number;
|
|
212
|
+
agentDir: string;
|
|
213
|
+
localAgentDir: string | null;
|
|
214
|
+
sourceCwd: string;
|
|
215
|
+
artifactDir: string;
|
|
216
|
+
lineage?: LineageRegistration;
|
|
217
|
+
sessionMode: SubagentSessionMode;
|
|
218
|
+
taskDelivery: "direct" | "artifact";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface PreparedSurface {
|
|
222
|
+
surface: string;
|
|
223
|
+
tabId?: string;
|
|
224
|
+
targetCwd: string;
|
|
225
|
+
effectiveAgentDir: string;
|
|
226
|
+
localAgentDir: string | null;
|
|
227
|
+
worktree?: WorktreeLaunch;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface PreparedSession extends PreparedSurface {
|
|
231
|
+
sessionFile: string;
|
|
232
|
+
activityFile: string;
|
|
233
|
+
settledEventsFile: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
interface PreparedArtifacts extends PreparedSession {
|
|
237
|
+
taskArg: string;
|
|
238
|
+
systemPromptFile?: string;
|
|
239
|
+
replicationPromptFile?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Launch one validated Pi-backed request. Lifecycle watching and parent
|
|
244
|
+
* delivery begin only after this transaction returns the running child.
|
|
245
|
+
*/
|
|
246
|
+
export async function launchPiSubagent(
|
|
247
|
+
request: PiLaunchRequest,
|
|
248
|
+
operations: PiLaunchOperations = defaultOperations,
|
|
249
|
+
): Promise<PiRunningChild> {
|
|
250
|
+
return request.kind === "resume"
|
|
251
|
+
? launchResumedPiSubagent(request, operations)
|
|
252
|
+
: launchFreshPiSubagent(request, operations);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function launchFreshPiSubagent(
|
|
256
|
+
request: FreshPiLaunchRequest,
|
|
257
|
+
operations: PiLaunchOperations,
|
|
258
|
+
): Promise<PiRunningChild> {
|
|
259
|
+
const resolved = resolveLaunchRequest(request);
|
|
260
|
+
// Registration is the ownership boundary: publish lineage before Herdr creates resources.
|
|
261
|
+
const inherited = readSessionBinding(request.parent.sessionFile)?.child?.lineage ?? lineageFromEnvironment();
|
|
262
|
+
resolved.lineage = request.lineage ?? registerLineage({
|
|
263
|
+
artifactDir: resolved.artifactDir,
|
|
264
|
+
nodeId: resolved.id,
|
|
265
|
+
parentNodeId: inherited?.nodeId,
|
|
266
|
+
parentSessionId: request.parent.sessionId,
|
|
267
|
+
parentSessionFile: request.parent.sessionFile,
|
|
268
|
+
launchKind: request.worktree ? "worktree" : request.fork ? "fork" : "fresh",
|
|
269
|
+
inheritedRootDir: inherited?.rootDir,
|
|
270
|
+
inheritedRootId: inherited?.rootId,
|
|
271
|
+
});
|
|
272
|
+
const storeDir = sessionBindingStore(request.parent.sessionFile, resolved.agentDir);
|
|
273
|
+
registerSessionLineage(storeDir, request.parent.sessionId, resolved.lineage.rootDir);
|
|
274
|
+
let surface: PreparedSurface | undefined;
|
|
275
|
+
const abort = new AbortController();
|
|
276
|
+
const signal = request.signal ?? abort.signal;
|
|
277
|
+
try {
|
|
278
|
+
if (request.replication) {
|
|
279
|
+
const metadataId = `metadata:${resolved.id}:reserved`;
|
|
280
|
+
if (!appendLineageEvent(resolved.lineage.rootDir, metadataId, "launch_metadata", resolved.id, {
|
|
281
|
+
name: request.name,
|
|
282
|
+
task: request.task,
|
|
283
|
+
startTime: resolved.startTime,
|
|
284
|
+
executionOwner: request.executionOwner,
|
|
285
|
+
replication: true,
|
|
286
|
+
replicaWorkFile: request.replication.recordFile,
|
|
287
|
+
}) && !hasLineageEvent(resolved.lineage.rootDir, metadataId, "launch_metadata")) {
|
|
288
|
+
throw new Error("Unable to publish replica launch recovery metadata.");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
surface = prepareLaunchSurface(resolved, operations);
|
|
292
|
+
if (request.replication?.recordFile) {
|
|
293
|
+
if (surface.worktree) attachReplicaWork(request.replication.recordFile, surface.worktree);
|
|
294
|
+
else attachSharedReplicaPane(request.replication.recordFile, surface.surface);
|
|
295
|
+
captureLaunchSurface(resolved, surface, operations);
|
|
296
|
+
}
|
|
297
|
+
const session = prepareChildSession(resolved, surface);
|
|
298
|
+
const artifacts = prepareTaskArtifacts(resolved, session);
|
|
299
|
+
seedSessionBinding(artifacts.sessionFile, {
|
|
300
|
+
version: 1, storeDir,
|
|
301
|
+
child: {
|
|
302
|
+
lineage: resolved.lineage,
|
|
303
|
+
environment: {
|
|
304
|
+
...lineageEnvironment(resolved.lineage),
|
|
305
|
+
PI_SUBAGENT_ID: resolved.id,
|
|
306
|
+
PI_SUBAGENT_NAME: request.name,
|
|
307
|
+
PI_SUBAGENT_AGENT: request.agent ?? "",
|
|
308
|
+
PI_SUBAGENT_AUTO_EXIT: request.behavior.autoExit ? "1" : "0",
|
|
309
|
+
PI_SUBAGENT_SKILLS: normalizeSkillNames(request.behavior.skills ?? "").join(","),
|
|
310
|
+
PI_DENY_TOOLS: request.behavior.deniedTools.join(","),
|
|
311
|
+
PI_SUBAGENT_TOOLS: buildSubagentToolAllowlist(request.behavior.tools, request.behavior.autoExit, request.behavior.includeControlTools ?? true) ?? "",
|
|
312
|
+
PI_SUBAGENT_ACTIVITY_FILE: artifacts.activityFile,
|
|
313
|
+
PI_SUBAGENT_SETTLED_EVENTS_FILE: artifacts.settledEventsFile,
|
|
314
|
+
...(request.replication ? {
|
|
315
|
+
PI_SUBAGENT_REPLICATION: "1",
|
|
316
|
+
...(request.replication.recordFile ? { PI_SUBAGENT_REPLICA_WORK_FILE: request.replication.recordFile } : {}),
|
|
317
|
+
PI_SUBAGENT_REPLICATION_PROMPT_FILE: artifacts.replicationPromptFile!,
|
|
318
|
+
PI_SUBAGENT_REPLICATION_EXTENSIONS: JSON.stringify(request.replication.extensionArgs),
|
|
319
|
+
PI_SUBAGENT_REPLICATION_ASSISTANTS: JSON.stringify(request.replication.entries.filter((entry) => entry.type === "message" && (entry.message as any)?.role === "assistant").map((entry) => entry.id)),
|
|
320
|
+
} : {}),
|
|
321
|
+
...Object.fromEntries(Object.entries(request.environment ?? {}).filter(([key]) => key.startsWith("PI_SUBAGENT_TREE_"))),
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
const sessionBaseline = captureSessionBaseline(artifacts.sessionFile);
|
|
326
|
+
if (resolved.lineage) {
|
|
327
|
+
appendLineageEvent(resolved.lineage.rootDir, `metadata:${resolved.id}:${resolved.startTime}`, "launch_metadata", resolved.id, {
|
|
328
|
+
name: request.name,
|
|
329
|
+
executionOwner: request.executionOwner,
|
|
330
|
+
task: request.task,
|
|
331
|
+
agent: request.agent,
|
|
332
|
+
surface: artifacts.surface,
|
|
333
|
+
tabId: artifacts.tabId,
|
|
334
|
+
sessionFile: artifacts.sessionFile,
|
|
335
|
+
activityFile: artifacts.activityFile,
|
|
336
|
+
settledEventsFile: artifacts.settledEventsFile,
|
|
337
|
+
startTime: resolved.startTime,
|
|
338
|
+
|
|
339
|
+
interactive: request.behavior.interactive,
|
|
340
|
+
autoExit: request.behavior.autoExit,
|
|
341
|
+
...(request.replication ? { replication: true, replicaWorkFile: request.replication.recordFile } : {}),
|
|
342
|
+
cwd: artifacts.targetCwd,
|
|
343
|
+
...(artifacts.worktree ? { worktree: artifacts.worktree } : {}),
|
|
344
|
+
...(artifacts.worktree?.workspaceId ? { workspaceId: artifacts.worktree.workspaceId } : {}),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
// Publish recovery evidence before waiting on external shell startup.
|
|
348
|
+
await confirmShellReady(session, operations, signal);
|
|
349
|
+
captureLaunchSurface(resolved, surface, operations);
|
|
350
|
+
if (request.replication?.recordFile && !surface.worktree && !resolved.launchPane) throw new Error("Cannot verify shared replica pane ownership.");
|
|
351
|
+
if (signal.aborted) throw new Error("subagent launch aborted");
|
|
352
|
+
const command = buildPiCommand(resolved, artifacts);
|
|
353
|
+
const launchScriptFile = startPiProcess(resolved, artifacts, command, operations);
|
|
354
|
+
return createRunningChild(resolved, artifacts, launchScriptFile, sessionBaseline);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
abort.abort();
|
|
357
|
+
if (request.replication?.recordFile) {
|
|
358
|
+
try { failReplicaLaunch(request.replication.recordFile, error); } catch { /* Preserve the original launch failure. */ }
|
|
359
|
+
}
|
|
360
|
+
if (surface && !surface.worktree) {
|
|
361
|
+
if (request.replication?.recordFile) {
|
|
362
|
+
try { captureLaunchSurface(resolved, surface, operations); cleanupReplicaWork(request.replication.recordFile, resolved.sourceCwd); } catch { /* Unknown ownership stays recoverable. */ }
|
|
363
|
+
} else {
|
|
364
|
+
try { operations.closePane?.(surface.surface); } catch { /* best effort */ }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (!surface?.worktree) throw error;
|
|
368
|
+
if (request.replication?.recordFile) {
|
|
369
|
+
try {
|
|
370
|
+
captureLaunchSurface(resolved, surface, operations);
|
|
371
|
+
cleanupReplicaWork(request.replication.recordFile, resolved.sourceCwd);
|
|
372
|
+
} catch { /* Unknown ownership keeps the failed work recoverable. */ }
|
|
373
|
+
try { persistWorktreeResult(surface.worktree, "failed"); } catch { /* The work record also retains the failure. */ }
|
|
374
|
+
throw new Error(`Failed to launch replica; worktree retained at ${surface.worktree.path}: ${errorMessage(error)}`);
|
|
375
|
+
}
|
|
376
|
+
const handoff = captureWorktreeHandoff(surface.worktree);
|
|
377
|
+
try { persistWorktreeResult(surface.worktree, "failed", handoff); } catch { /* launch error remains authoritative */ }
|
|
378
|
+
throw new Error(`Failed to launch subagent; worktree retained at ${surface.worktree.path} (workspace ${surface.worktree.workspaceId}): ${errorMessage(error)}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Record the shell before dispatch; child startup may never reach session_start. */
|
|
383
|
+
function captureLaunchSurface(resolved: ResolvedLaunch, surface: PreparedSurface, operations: PiLaunchOperations): void {
|
|
384
|
+
const file = resolved.request.replication?.recordFile;
|
|
385
|
+
if (!file || !operations.getPaneProcessInfo) return;
|
|
386
|
+
let shellPid: number | undefined;
|
|
387
|
+
try { shellPid = operations.getPaneProcessInfo(surface.surface).shellPid; }
|
|
388
|
+
catch { return; } // A shell may still be starting; retry after readiness.
|
|
389
|
+
const owner = captureLaunchPaneOwnership(surface.surface, shellPid);
|
|
390
|
+
if (owner) {
|
|
391
|
+
captureReplicaLaunchPane(file, owner);
|
|
392
|
+
resolved.launchPane = owner;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function resolveLaunchRequest(request: FreshPiLaunchRequest): ResolvedLaunch {
|
|
396
|
+
const id = request.id ?? randomUUID();
|
|
397
|
+
const agentDir =
|
|
398
|
+
request.parent.agentDir ??
|
|
399
|
+
process.env.PI_CODING_AGENT_DIR ??
|
|
400
|
+
join(homedir(), ".pi", "agent");
|
|
401
|
+
const rawCwd = request.cwd ?? request.behavior.cwd;
|
|
402
|
+
const cwdBase =
|
|
403
|
+
request.cwd == null && request.behavior.cwd != null
|
|
404
|
+
? agentDir
|
|
405
|
+
: (request.parent.invocationCwd ?? request.parent.cwd);
|
|
406
|
+
const sourceCwd = rawCwd
|
|
407
|
+
? rawCwd.startsWith("/")
|
|
408
|
+
? rawCwd
|
|
409
|
+
: join(cwdBase, rawCwd)
|
|
410
|
+
: request.parent.cwd;
|
|
411
|
+
const localAgentDir = rawCwd ? join(sourceCwd, ".pi", "agent") : null;
|
|
412
|
+
const sessionMode = request.fork ? "fork" : request.behavior.sessionMode;
|
|
413
|
+
return {
|
|
414
|
+
request,
|
|
415
|
+
id,
|
|
416
|
+
startTime: Date.now(),
|
|
417
|
+
agentDir,
|
|
418
|
+
localAgentDir:
|
|
419
|
+
localAgentDir && existsSync(localAgentDir) ? localAgentDir : null,
|
|
420
|
+
sourceCwd,
|
|
421
|
+
artifactDir: request.replication?.recordFile ? join(request.replication.recordFile + ".artifacts", "launch") : join(
|
|
422
|
+
request.parent.sessionDir,
|
|
423
|
+
"artifacts",
|
|
424
|
+
request.parent.sessionId,
|
|
425
|
+
),
|
|
426
|
+
sessionMode,
|
|
427
|
+
taskDelivery: sessionMode === "fork" ? "direct" : "artifact",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function prepareLaunchSurface(
|
|
432
|
+
resolved: ResolvedLaunch,
|
|
433
|
+
operations: PiLaunchOperations,
|
|
434
|
+
): PreparedSurface {
|
|
435
|
+
const { request } = resolved;
|
|
436
|
+
if (!request.worktree) {
|
|
437
|
+
const surface = request.surface ?? operations.createPane(request.name);
|
|
438
|
+
const tabId = getPaneTabId(surface);
|
|
439
|
+
return {
|
|
440
|
+
surface,
|
|
441
|
+
...(tabId ? { tabId } : {}),
|
|
442
|
+
targetCwd: resolved.sourceCwd,
|
|
443
|
+
effectiveAgentDir: resolved.localAgentDir ?? resolved.agentDir,
|
|
444
|
+
localAgentDir: resolved.localAgentDir,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (request.surface)
|
|
448
|
+
throw new Error("A worktree subagent cannot use a pre-created pane");
|
|
449
|
+
|
|
450
|
+
const baseRef = request.worktree.base ?? "HEAD";
|
|
451
|
+
const baseSha = resolveGitCommit(resolved.sourceCwd, baseRef);
|
|
452
|
+
const manifestFile = join(
|
|
453
|
+
resolved.artifactDir,
|
|
454
|
+
"worktree-runs",
|
|
455
|
+
`${resolved.id}.json`,
|
|
456
|
+
);
|
|
457
|
+
const ownership = {
|
|
458
|
+
id: resolved.id,
|
|
459
|
+
name: request.name,
|
|
460
|
+
sourceCwd: resolved.sourceCwd,
|
|
461
|
+
branch: request.worktree.branch,
|
|
462
|
+
baseRef,
|
|
463
|
+
baseSha,
|
|
464
|
+
createdAt: resolved.startTime,
|
|
465
|
+
};
|
|
466
|
+
writeWorktreeManifest(manifestFile, {
|
|
467
|
+
state: "provisioning",
|
|
468
|
+
...ownership,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
let created: HerdrWorktreeSurface;
|
|
472
|
+
try {
|
|
473
|
+
created = operations.createWorktree(
|
|
474
|
+
request.name,
|
|
475
|
+
request.replication ? sourceRepositoryAnchor(resolved.sourceCwd) : resolved.sourceCwd,
|
|
476
|
+
request.worktree.branch,
|
|
477
|
+
baseSha,
|
|
478
|
+
);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
writeWorktreeManifest(manifestFile, {
|
|
481
|
+
state: "failed",
|
|
482
|
+
...ownership,
|
|
483
|
+
...(error instanceof HerdrWorktreeCreateError
|
|
484
|
+
? error.recoveredWorktree
|
|
485
|
+
: {}),
|
|
486
|
+
error: errorMessage(error),
|
|
487
|
+
});
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const worktree: WorktreeLaunch = {
|
|
492
|
+
path: created.path,
|
|
493
|
+
workspaceId: created.workspaceId,
|
|
494
|
+
paneId: created.paneId,
|
|
495
|
+
branch: created.branch,
|
|
496
|
+
baseRef,
|
|
497
|
+
baseSha,
|
|
498
|
+
manifestFile,
|
|
499
|
+
};
|
|
500
|
+
writeWorktreeManifest(manifestFile, {
|
|
501
|
+
state: "provisioned",
|
|
502
|
+
...ownership,
|
|
503
|
+
...worktree,
|
|
504
|
+
});
|
|
505
|
+
const isolatedAgentDir = join(created.path, ".pi", "agent");
|
|
506
|
+
const hasIsolatedAgentDir = existsSync(isolatedAgentDir);
|
|
507
|
+
return {
|
|
508
|
+
surface: created.paneId,
|
|
509
|
+
targetCwd: created.path,
|
|
510
|
+
effectiveAgentDir: hasIsolatedAgentDir
|
|
511
|
+
? isolatedAgentDir
|
|
512
|
+
: resolved.agentDir,
|
|
513
|
+
localAgentDir: hasIsolatedAgentDir ? isolatedAgentDir : null,
|
|
514
|
+
worktree,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function prepareChildSession(
|
|
519
|
+
resolved: ResolvedLaunch,
|
|
520
|
+
surface: PreparedSurface,
|
|
521
|
+
): PreparedSession {
|
|
522
|
+
const sessionDir = resolved.request.replication?.recordFile
|
|
523
|
+
? join(resolved.request.replication.recordFile + ".artifacts", "sessions")
|
|
524
|
+
: getDefaultSessionDirFor(surface.targetCwd, surface.effectiveAgentDir);
|
|
525
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
526
|
+
const timestamp = timestampForFile();
|
|
527
|
+
const uuid = [resolved.id, randomUUID(), randomUUID(), randomUUID()].join("-");
|
|
528
|
+
const sessionFile = join(sessionDir, `${timestamp}_${uuid}.jsonl`);
|
|
529
|
+
if (resolved.sessionMode === "standalone") {
|
|
530
|
+
seedStandaloneSessionFile({ childSessionFile: sessionFile, childCwd: surface.targetCwd });
|
|
531
|
+
}
|
|
532
|
+
if (surface.worktree) {
|
|
533
|
+
surface.worktree.sessionFile = sessionFile;
|
|
534
|
+
writeWorktreeManifest(surface.worktree.manifestFile, { sessionFile });
|
|
535
|
+
if (resolved.request.replication?.recordFile) attachReplicaWork(resolved.request.replication.recordFile, surface.worktree, sessionFile);
|
|
536
|
+
} else if (resolved.request.replication?.recordFile) {
|
|
537
|
+
attachSharedReplicaPane(resolved.request.replication.recordFile, surface.surface, sessionFile);
|
|
538
|
+
}
|
|
539
|
+
const activityFile = getSubagentActivityFile(
|
|
540
|
+
resolved.artifactDir,
|
|
541
|
+
resolved.id,
|
|
542
|
+
);
|
|
543
|
+
const settledEventsFile = getSubagentSettledEventsFile(
|
|
544
|
+
resolved.artifactDir,
|
|
545
|
+
resolved.id,
|
|
546
|
+
);
|
|
547
|
+
if (resolved.lineage) {
|
|
548
|
+
writeFileSync(`${sessionFile}.lineage.json`, JSON.stringify(resolved.lineage), "utf8");
|
|
549
|
+
}
|
|
550
|
+
return { ...surface, sessionFile, activityFile, settledEventsFile };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function confirmShellReady(
|
|
554
|
+
session: PreparedSession,
|
|
555
|
+
operations: PiLaunchOperations,
|
|
556
|
+
signal?: AbortSignal,
|
|
557
|
+
): Promise<void> {
|
|
558
|
+
await operations.waitForShellReady(session.surface, { signal });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function prepareTaskArtifacts(
|
|
562
|
+
resolved: ResolvedLaunch,
|
|
563
|
+
session: PreparedSession,
|
|
564
|
+
): PreparedArtifacts {
|
|
565
|
+
const { request } = resolved;
|
|
566
|
+
if (request.replication) {
|
|
567
|
+
seedReplicationSessionFile({ parentSessionFile: request.parent.sessionFile, childSessionFile: session.sessionFile, childCwd: session.targetCwd, entries: request.replication.entries, contextMessages: request.replication.contextMessages, resumeSessionId: request.replication.resumeSessionId, name: request.name });
|
|
568
|
+
} else if (resolved.sessionMode !== "standalone") {
|
|
569
|
+
seedSubagentSessionFile({
|
|
570
|
+
mode: resolved.sessionMode,
|
|
571
|
+
parentSessionFile: request.parent.sessionFile,
|
|
572
|
+
childSessionFile: session.sessionFile,
|
|
573
|
+
childCwd: session.targetCwd,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
mkdirSync(dirname(session.activityFile), { recursive: true });
|
|
577
|
+
|
|
578
|
+
const identityInSystemPrompt =
|
|
579
|
+
request.behavior.systemPromptMode && request.behavior.identity;
|
|
580
|
+
const roleBlock =
|
|
581
|
+
request.behavior.identity && !identityInSystemPrompt
|
|
582
|
+
? `\n\n${request.behavior.identity}`
|
|
583
|
+
: "";
|
|
584
|
+
const modeHint = request.behavior.autoExit
|
|
585
|
+
? "Complete your task autonomously."
|
|
586
|
+
: "Complete your task. The user can interact with you at any time.";
|
|
587
|
+
const summaryInstruction = "Your FINAL assistant message should summarize what you accomplished.";
|
|
588
|
+
const fullTask = resolved.sessionMode === "fork"
|
|
589
|
+
? request.task
|
|
590
|
+
: `${roleBlock}\n\n${modeHint}\n\n${request.task}\n\n${summaryInstruction}`;
|
|
591
|
+
let taskArg = fullTask;
|
|
592
|
+
if (resolved.taskDelivery === "artifact") {
|
|
593
|
+
const artifactPath = join(
|
|
594
|
+
resolved.artifactDir,
|
|
595
|
+
`context/${safeName(request.name) || "subagent"}-${timestampForFile(false)}.md`,
|
|
596
|
+
);
|
|
597
|
+
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
598
|
+
writeFileSync(artifactPath, fullTask, "utf8");
|
|
599
|
+
taskArg = `@${artifactPath}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let systemPromptFile: string | undefined;
|
|
603
|
+
if (identityInSystemPrompt) {
|
|
604
|
+
systemPromptFile = join(
|
|
605
|
+
resolved.artifactDir,
|
|
606
|
+
`context/${safeName(request.name) || "subagent"}-sysprompt-${timestampForFile(false)}.md`,
|
|
607
|
+
);
|
|
608
|
+
mkdirSync(dirname(systemPromptFile), { recursive: true });
|
|
609
|
+
writeFileSync(systemPromptFile, identityInSystemPrompt, "utf8");
|
|
610
|
+
}
|
|
611
|
+
let replicationPromptFile: string | undefined;
|
|
612
|
+
if (request.replication) {
|
|
613
|
+
replicationPromptFile = join(resolved.artifactDir, "replication", `${resolved.id}.system.txt`);
|
|
614
|
+
mkdirSync(dirname(replicationPromptFile), { recursive: true });
|
|
615
|
+
writeFileSync(replicationPromptFile, request.replication.systemPrompt, { mode: 0o600 });
|
|
616
|
+
}
|
|
617
|
+
return { ...session, taskArg, systemPromptFile, replicationPromptFile };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function buildPiCommand(
|
|
621
|
+
resolved: ResolvedLaunch,
|
|
622
|
+
artifacts: PreparedArtifacts,
|
|
623
|
+
): string {
|
|
624
|
+
const { request } = resolved;
|
|
625
|
+
const parts = [
|
|
626
|
+
"pi",
|
|
627
|
+
"--session",
|
|
628
|
+
shellQuote(artifacts.sessionFile),
|
|
629
|
+
...(!request.replication ? ["-e", shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts"))] : []),
|
|
630
|
+
...(process.env.HERDR_ENV === "1" && existsSync(join(process.env.HOME ?? "", ".pi/agent/extensions/herdr-agent-state.ts"))
|
|
631
|
+
? ["-e", shellQuote(join(process.env.HOME ?? "", ".pi/agent/extensions/herdr-agent-state.ts"))]
|
|
632
|
+
: []),
|
|
633
|
+
"--model",
|
|
634
|
+
shellQuote(request.runtimePlan.model),
|
|
635
|
+
"--thinking",
|
|
636
|
+
shellQuote(request.runtimePlan.thinking),
|
|
637
|
+
];
|
|
638
|
+
if (request.replication) parts.push(...request.replication.extensionArgs.map(shellQuote), "-e", shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts")));
|
|
639
|
+
if (artifacts.systemPromptFile) {
|
|
640
|
+
parts.push(
|
|
641
|
+
request.behavior.systemPromptMode === "replace"
|
|
642
|
+
? "--system-prompt"
|
|
643
|
+
: "--append-system-prompt",
|
|
644
|
+
shellQuote(artifacts.systemPromptFile),
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const toolAllowlist = buildSubagentToolAllowlist(
|
|
648
|
+
request.behavior.tools,
|
|
649
|
+
request.behavior.autoExit,
|
|
650
|
+
request.behavior.includeControlTools ?? true,
|
|
651
|
+
);
|
|
652
|
+
if (toolAllowlist) parts.push("--tools", shellQuote(toolAllowlist));
|
|
653
|
+
parts.push(shellQuote(artifacts.taskArg));
|
|
654
|
+
|
|
655
|
+
const env: string[] = [];
|
|
656
|
+
if (!request.replication) env.push("PI_SUBAGENT_REPLICATION=''", "PI_SUBAGENT_REPLICA_WORK_FILE=''", "PI_SUBAGENT_REPLICATION_PROMPT_FILE=''");
|
|
657
|
+
if (request.replication) {
|
|
658
|
+
env.push("PI_SUBAGENT_REPLICATION=1", "PI_DENY_TOOLS=''", "PI_SUBAGENT_AGENT=''", `PI_SUBAGENT_REPLICATION_PROMPT_FILE=${shellQuote(artifacts.replicationPromptFile!)}`);
|
|
659
|
+
if (request.replication.recordFile) env.push(`PI_SUBAGENT_REPLICA_WORK_FILE=${shellQuote(request.replication.recordFile)}`);
|
|
660
|
+
}
|
|
661
|
+
for (const [key, value] of Object.entries(request.environment ?? {})) {
|
|
662
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) env.push(`${key}=${shellQuote(value)}`);
|
|
663
|
+
}
|
|
664
|
+
if (resolved.lineage) {
|
|
665
|
+
env.push("PI_SUBAGENT_LAUNCHER_PID=$$");
|
|
666
|
+
for (const [key, value] of Object.entries(lineageEnvironment(resolved.lineage))) {
|
|
667
|
+
env.push(`${key}=${shellQuote(value)}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (artifacts.localAgentDir) {
|
|
671
|
+
env.push(`PI_CODING_AGENT_DIR=${shellQuote(artifacts.localAgentDir)}`);
|
|
672
|
+
} else if (process.env.PI_CODING_AGENT_DIR) {
|
|
673
|
+
env.push(
|
|
674
|
+
`PI_CODING_AGENT_DIR=${shellQuote(process.env.PI_CODING_AGENT_DIR)}`,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
if (request.behavior.deniedTools.length > 0) {
|
|
678
|
+
env.push(
|
|
679
|
+
`PI_DENY_TOOLS=${shellQuote(request.behavior.deniedTools.join(","))}`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
env.push(`PI_SUBAGENT_NAME=${shellQuote(request.name)}`);
|
|
683
|
+
if (request.agent)
|
|
684
|
+
env.push(`PI_SUBAGENT_AGENT=${shellQuote(request.agent)}`);
|
|
685
|
+
env.push(
|
|
686
|
+
`PI_SUBAGENT_SKILLS=${shellQuote(normalizeSkillNames(request.behavior.skills ?? "").join(","))}`,
|
|
687
|
+
);
|
|
688
|
+
env.push(`PI_SUBAGENT_AUTO_EXIT=${request.behavior.autoExit ? "1" : "0"}`);
|
|
689
|
+
env.push(`PI_SUBAGENT_SESSION=${shellQuote(artifacts.sessionFile)}`);
|
|
690
|
+
env.push(`PI_SUBAGENT_ID=${shellQuote(resolved.id)}`);
|
|
691
|
+
env.push(`PI_SUBAGENT_ACTIVITY_FILE=${shellQuote(artifacts.activityFile)}`);
|
|
692
|
+
env.push(
|
|
693
|
+
`PI_SUBAGENT_SETTLED_EVENTS_FILE=${shellQuote(artifacts.settledEventsFile)}`,
|
|
694
|
+
);
|
|
695
|
+
env.push(`PI_SUBAGENT_SURFACE=${shellQuote(artifacts.surface)}`);
|
|
696
|
+
|
|
697
|
+
const piCommand =
|
|
698
|
+
`cd ${shellQuote(artifacts.targetCwd)} && ` +
|
|
699
|
+
`${env.join(" ")} ${parts.join(" ")}`;
|
|
700
|
+
return `${piCommand}; echo '__SUBAGENT_DONE_'$?'__'`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function startPiProcess(
|
|
704
|
+
resolved: ResolvedLaunch,
|
|
705
|
+
artifacts: PreparedArtifacts,
|
|
706
|
+
command: string,
|
|
707
|
+
operations: PiLaunchOperations,
|
|
708
|
+
): string {
|
|
709
|
+
const launchScriptFile = join(
|
|
710
|
+
resolved.artifactDir,
|
|
711
|
+
"subagent-scripts",
|
|
712
|
+
`${safeName(resolved.request.name) || "subagent"}-${resolved.id}.sh`,
|
|
713
|
+
);
|
|
714
|
+
if (artifacts.worktree) {
|
|
715
|
+
persistWorktreeResult(artifacts.worktree, "running");
|
|
716
|
+
}
|
|
717
|
+
return operations.runScript(artifacts.surface, command, {
|
|
718
|
+
scriptPath: launchScriptFile,
|
|
719
|
+
scriptPreamble: [
|
|
720
|
+
`# Subagent launch script for ${resolved.request.name}`,
|
|
721
|
+
`# Generated: ${new Date().toISOString()}`,
|
|
722
|
+
`# Session: ${artifacts.sessionFile}`,
|
|
723
|
+
`# Surface: ${artifacts.surface}`,
|
|
724
|
+
].join("\n"),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function createRunningChild(
|
|
729
|
+
resolved: ResolvedLaunch,
|
|
730
|
+
artifacts: PreparedArtifacts,
|
|
731
|
+
launchScriptFile: string,
|
|
732
|
+
sessionBaseline: SessionBaselineCursor,
|
|
733
|
+
): PiRunningChild {
|
|
734
|
+
return {
|
|
735
|
+
lineage: resolved.lineage,
|
|
736
|
+
executionOwner: resolved.request.executionOwner,
|
|
737
|
+
...(resolved.launchPane ? { launchPane: resolved.launchPane } : {}),
|
|
738
|
+
...(resolved.request.replication ? { replication: true, replicaWorkFile: resolved.request.replication.recordFile } : {}),
|
|
739
|
+
id: resolved.id,
|
|
740
|
+
name: resolved.request.name,
|
|
741
|
+
task: resolved.request.task,
|
|
742
|
+
agent: resolved.request.agent,
|
|
743
|
+
surface: artifacts.surface,
|
|
744
|
+
startTime: resolved.startTime,
|
|
745
|
+
...(artifacts.tabId ? { tabId: artifacts.tabId } : {}),
|
|
746
|
+
sessionFile: artifacts.sessionFile,
|
|
747
|
+
launchScriptFile,
|
|
748
|
+
activityFile: artifacts.activityFile,
|
|
749
|
+
settledEventsFile: artifacts.settledEventsFile,
|
|
750
|
+
interactive: resolved.request.behavior.interactive,
|
|
751
|
+
sessionBaseline,
|
|
752
|
+
runtimePlan: resolved.request.runtimePlan,
|
|
753
|
+
worktree: artifacts.worktree,
|
|
754
|
+
|
|
755
|
+
...(artifacts.worktree?.workspaceId
|
|
756
|
+
? { recoveryWorkspace: { id: artifacts.worktree.workspaceId, cwd: artifacts.worktree.path } }
|
|
757
|
+
: {}),
|
|
758
|
+
lifecycle: createLifecycle(resolved.startTime),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function launchResumedPiSubagent(
|
|
763
|
+
request: ResumePiLaunchRequest,
|
|
764
|
+
operations: PiLaunchOperations,
|
|
765
|
+
): Promise<PiRunningChild> {
|
|
766
|
+
let id = request.id ?? randomUUID();
|
|
767
|
+
const autoExit = request.behavior?.autoExit ?? true;
|
|
768
|
+
const artifactDir = join(request.parent.sessionDir, "artifacts", request.parent.sessionId);
|
|
769
|
+
const inherited = readSessionBinding(request.parent.sessionFile)?.child?.lineage ?? lineageFromEnvironment();
|
|
770
|
+
const priorBinding = readSessionBinding(request.sessionFile);
|
|
771
|
+
let prior: LineageRegistration | undefined = priorBinding?.child?.lineage;
|
|
772
|
+
try {
|
|
773
|
+
prior ??= JSON.parse(readFileSync(`${request.sessionFile}.lineage.json`, "utf8")) as LineageRegistration;
|
|
774
|
+
if (!prior.rootDir || !prior.rootId || !prior.nodeId) prior = undefined;
|
|
775
|
+
} catch {}
|
|
776
|
+
const reusable = prior && !isLineageNodeDrained(reduceLineage(prior.rootDir), prior.nodeId);
|
|
777
|
+
|
|
778
|
+
if (reusable && prior) id = prior.nodeId;
|
|
779
|
+
const lineage: LineageRegistration = (reusable ? prior : registerLineage({
|
|
780
|
+
artifactDir,
|
|
781
|
+
nodeId: id,
|
|
782
|
+
parentNodeId: inherited?.nodeId,
|
|
783
|
+
parentSessionId: request.parent.sessionId,
|
|
784
|
+
parentSessionFile: request.parent.sessionFile,
|
|
785
|
+
launchKind: "resume",
|
|
786
|
+
inheritedRootDir: inherited?.rootDir,
|
|
787
|
+
inheritedRootId: inherited?.rootId,
|
|
788
|
+
}))!;
|
|
789
|
+
const storeDir = sessionBindingStore(request.parent.sessionFile);
|
|
790
|
+
registerSessionLineage(storeDir, request.parent.sessionId, lineage.rootDir);
|
|
791
|
+
const interactive = request.behavior?.interactive ?? !autoExit;
|
|
792
|
+
const executionOwner = request.executionOwner;
|
|
793
|
+
const startTime = Date.now();
|
|
794
|
+
const surface = request.surface ?? operations.createPane(request.name, request.workspace);
|
|
795
|
+
const allocatedSurface = request.surface === undefined;
|
|
796
|
+
try {
|
|
797
|
+
const tabId = getPaneTabId(surface);
|
|
798
|
+
await operations.waitForShellReady(surface);
|
|
799
|
+
const activityFile = getSubagentActivityFile(artifactDir, id);
|
|
800
|
+
writeFileSync(`${request.sessionFile}.lineage.json`, JSON.stringify(lineage), "utf8");
|
|
801
|
+
const settledEventsFile = getSubagentSettledEventsFile(artifactDir, id);
|
|
802
|
+
mkdirSync(dirname(activityFile), { recursive: true });
|
|
803
|
+
|
|
804
|
+
if (lineage) {
|
|
805
|
+
appendLineageEvent(lineage.rootDir, `metadata:${lineage.nodeId}:${startTime}`, "launch_metadata", lineage.nodeId, {
|
|
806
|
+
name: request.name,
|
|
807
|
+
executionOwner,
|
|
808
|
+
task: request.message ?? "resumed session",
|
|
809
|
+
surface,
|
|
810
|
+
sessionFile: request.sessionFile,
|
|
811
|
+
tabId,
|
|
812
|
+
activityFile,
|
|
813
|
+
settledEventsFile,
|
|
814
|
+
startTime,
|
|
815
|
+
interactive,
|
|
816
|
+
autoExit,
|
|
817
|
+
...(request.workspace ? { workspaceId: request.workspace.id, cwd: request.workspace.cwd } : {}),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
let messageFile: string | undefined;
|
|
821
|
+
if (request.message) {
|
|
822
|
+
messageFile = join(
|
|
823
|
+
artifactDir,
|
|
824
|
+
"subagent-resume",
|
|
825
|
+
`${safeName(request.name) || "resume"}-${timestampForFile(false)}.md`,
|
|
826
|
+
);
|
|
827
|
+
mkdirSync(dirname(messageFile), { recursive: true });
|
|
828
|
+
writeFileSync(messageFile, request.message, "utf8");
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Resume must use the existing session position from before the new Pi
|
|
832
|
+
// process starts, not a line count observed after launch.
|
|
833
|
+
const sessionBaseline = captureSessionBaseline(request.sessionFile);
|
|
834
|
+
seedSessionBinding(request.sessionFile, {
|
|
835
|
+
version: 1, storeDir,
|
|
836
|
+
child: {
|
|
837
|
+
lineage,
|
|
838
|
+
environment: {
|
|
839
|
+
...priorBinding?.child?.environment,
|
|
840
|
+
...lineageEnvironment(lineage),
|
|
841
|
+
PI_SUBAGENT_NAME: request.name,
|
|
842
|
+
PI_SUBAGENT_ID: id,
|
|
843
|
+
PI_SUBAGENT_ACTIVITY_FILE: activityFile,
|
|
844
|
+
PI_SUBAGENT_SETTLED_EVENTS_FILE: settledEventsFile,
|
|
845
|
+
PI_SUBAGENT_AUTO_EXIT: autoExit ? "1" : "0",
|
|
846
|
+
PI_SUBAGENT_RESUME_BASELINE_ASSISTANTS: JSON.stringify(sessionBaseline.assistantEntryIds),
|
|
847
|
+
PI_SUBAGENT_RESUME: "1",
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
const env = [
|
|
852
|
+
...(process.env.PI_CODING_AGENT_DIR
|
|
853
|
+
? [`PI_CODING_AGENT_DIR=${shellQuote(process.env.PI_CODING_AGENT_DIR)}`]
|
|
854
|
+
: []),
|
|
855
|
+
"PI_SUBAGENT_LAUNCHER_PID=$$",
|
|
856
|
+
...Object.entries(lineageEnvironment(lineage)).map(([key, value]) => `${key}=${shellQuote(value)}`),
|
|
857
|
+
`PI_SUBAGENT_NAME=${shellQuote(request.name)}`,
|
|
858
|
+
`PI_SUBAGENT_SESSION=${shellQuote(request.sessionFile)}`,
|
|
859
|
+
`PI_SUBAGENT_ID=${shellQuote(id)}`,
|
|
860
|
+
`PI_SUBAGENT_ACTIVITY_FILE=${shellQuote(activityFile)}`,
|
|
861
|
+
`PI_SUBAGENT_SETTLED_EVENTS_FILE=${shellQuote(settledEventsFile)}`,
|
|
862
|
+
`PI_SUBAGENT_AUTO_EXIT=${autoExit ? "1" : "0"}`,
|
|
863
|
+
`PI_SUBAGENT_RESUME_BASELINE_ASSISTANTS=${shellQuote(JSON.stringify(sessionBaseline.assistantEntryIds))}`,
|
|
864
|
+
"PI_SUBAGENT_RESUME=1",
|
|
865
|
+
];
|
|
866
|
+
const command = [
|
|
867
|
+
...env,
|
|
868
|
+
"pi",
|
|
869
|
+
"--session",
|
|
870
|
+
shellQuote(request.sessionFile),
|
|
871
|
+
...(JSON.parse(priorBinding?.child?.environment.PI_SUBAGENT_REPLICATION_EXTENSIONS ?? "[]") as string[]).map(shellQuote),
|
|
872
|
+
"-e",
|
|
873
|
+
shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts")),
|
|
874
|
+
...(messageFile ? [shellQuote(`@${messageFile}`)] : []),
|
|
875
|
+
].join(" ");
|
|
876
|
+
const launchScriptFile = operations.runScript(
|
|
877
|
+
surface,
|
|
878
|
+
`${command}; echo '__SUBAGENT_DONE_'$?'__'`,
|
|
879
|
+
{
|
|
880
|
+
scriptPath: join(
|
|
881
|
+
artifactDir,
|
|
882
|
+
"subagent-scripts",
|
|
883
|
+
`${safeName(request.name) || "resume"}-resume-${Date.now()}.sh`,
|
|
884
|
+
),
|
|
885
|
+
scriptPreamble: [
|
|
886
|
+
`# Subagent resume script for ${request.name}`,
|
|
887
|
+
`# Generated: ${new Date().toISOString()}`,
|
|
888
|
+
`# Session: ${request.sessionFile}`,
|
|
889
|
+
`# Surface: ${surface}`,
|
|
890
|
+
...(messageFile ? [`# Resume message file: ${messageFile}`] : []),
|
|
891
|
+
].join("\n"),
|
|
892
|
+
},
|
|
893
|
+
);
|
|
894
|
+
return {
|
|
895
|
+
lineage,
|
|
896
|
+
executionOwner,
|
|
897
|
+
id,
|
|
898
|
+
name: request.name,
|
|
899
|
+
task: request.message ?? "resumed session",
|
|
900
|
+
surface,
|
|
901
|
+
startTime,
|
|
902
|
+
sessionFile: request.sessionFile,
|
|
903
|
+
...(tabId ? { tabId } : {}),
|
|
904
|
+
launchScriptFile,
|
|
905
|
+
activityFile,
|
|
906
|
+
settledEventsFile,
|
|
907
|
+
...(priorBinding?.child?.environment.PI_SUBAGENT_REPLICATION === "1" ? { replication: true } : {}),
|
|
908
|
+
interactive,
|
|
909
|
+
runtimePlan: undefined,
|
|
910
|
+
sessionBaseline,
|
|
911
|
+
...(request.workspace ? { recoveryWorkspace: request.workspace } : {}),
|
|
912
|
+
lifecycle: createLifecycle(startTime),
|
|
913
|
+
};
|
|
914
|
+
} catch (error) {
|
|
915
|
+
if (allocatedSurface) {
|
|
916
|
+
try {
|
|
917
|
+
operations.closePane?.(surface);
|
|
918
|
+
} catch {
|
|
919
|
+
// The original launch failure remains authoritative.
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
throw error;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function buildSubagentToolAllowlist(
|
|
927
|
+
tools?: string,
|
|
928
|
+
_autoExit = false,
|
|
929
|
+
_includeControlTools = true,
|
|
930
|
+
): string | null {
|
|
931
|
+
const requested = (tools ?? "")
|
|
932
|
+
.split(",")
|
|
933
|
+
.map((tool) => tool.trim())
|
|
934
|
+
.filter(Boolean);
|
|
935
|
+
if (requested.length === 0) return null;
|
|
936
|
+
const removed = new Set(["subagent", "subagent_resume", "subagent_interrupt", "subagent_cancel", "subagents_list", "caller_ping", "subagent_done"]);
|
|
937
|
+
return [...new Set(requested.filter((tool) => !removed.has(tool)))].join(",");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Normalize requested skill names for safe, deterministic child transport. */
|
|
941
|
+
export function normalizeSkillNames(skills: string): string[] {
|
|
942
|
+
return [...new Set(
|
|
943
|
+
skills
|
|
944
|
+
.split(",")
|
|
945
|
+
.map((skill) => skill.trim())
|
|
946
|
+
.filter(Boolean),
|
|
947
|
+
)];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function getDefaultSessionDirFor(cwd: string, agentDir: string): string {
|
|
951
|
+
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
952
|
+
const sessionDir = join(agentDir, "sessions", safePath);
|
|
953
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
954
|
+
return sessionDir;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function timestampForFile(includeMilliseconds = true): string {
|
|
958
|
+
return (
|
|
959
|
+
new Date()
|
|
960
|
+
.toISOString()
|
|
961
|
+
.replace(/[:.]/g, "-")
|
|
962
|
+
.slice(0, includeMilliseconds ? 23 : 19) +
|
|
963
|
+
(includeMilliseconds ? "Z" : "")
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function safeName(name: string): string {
|
|
968
|
+
return name
|
|
969
|
+
.toLowerCase()
|
|
970
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
971
|
+
.replace(/\s+/g, "-")
|
|
972
|
+
.replace(/-+/g, "-")
|
|
973
|
+
.replace(/^-|-$/g, "");
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function resolveGitCommit(cwd: string, ref: string): string {
|
|
977
|
+
return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
|
|
978
|
+
cwd,
|
|
979
|
+
encoding: "utf8",
|
|
980
|
+
}).trim();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export function writeWorktreeManifest(
|
|
984
|
+
path: string,
|
|
985
|
+
value: Record<string, unknown>,
|
|
986
|
+
): void {
|
|
987
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
988
|
+
let existing: Record<string, unknown> = {};
|
|
989
|
+
if (existsSync(path)) {
|
|
990
|
+
try {
|
|
991
|
+
existing = JSON.parse(readFileSync(path, "utf8"));
|
|
992
|
+
} catch {
|
|
993
|
+
existing = {};
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const tempPath = `${path}.tmp`;
|
|
997
|
+
writeFileSync(
|
|
998
|
+
tempPath,
|
|
999
|
+
`${JSON.stringify(
|
|
1000
|
+
{
|
|
1001
|
+
...existing,
|
|
1002
|
+
...value,
|
|
1003
|
+
version: 1,
|
|
1004
|
+
kind: "worktree-run",
|
|
1005
|
+
owner: "pi-herdr-subagents",
|
|
1006
|
+
updatedAt: Date.now(),
|
|
1007
|
+
},
|
|
1008
|
+
null,
|
|
1009
|
+
2,
|
|
1010
|
+
)}\n`,
|
|
1011
|
+
);
|
|
1012
|
+
renameSync(tempPath, path);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function gitPathList(cwd: string, args: string[]): string[] {
|
|
1016
|
+
return execFileSync("git", args, { cwd, encoding: "utf8" })
|
|
1017
|
+
.split("\0")
|
|
1018
|
+
.filter(Boolean);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
export function captureWorktreeHandoff(
|
|
1022
|
+
worktree: WorktreeLaunch,
|
|
1023
|
+
): WorktreeHandoff {
|
|
1024
|
+
try {
|
|
1025
|
+
const headSha = resolveGitCommit(worktree.path, "HEAD");
|
|
1026
|
+
const status = execFileSync(
|
|
1027
|
+
"git",
|
|
1028
|
+
["status", "--porcelain=v1", "--untracked-files=all", "-z"],
|
|
1029
|
+
{ cwd: worktree.path, encoding: "utf8" },
|
|
1030
|
+
);
|
|
1031
|
+
const untrackedFiles = gitPathList(worktree.path, [
|
|
1032
|
+
"ls-files",
|
|
1033
|
+
"--others",
|
|
1034
|
+
"--exclude-standard",
|
|
1035
|
+
"-z",
|
|
1036
|
+
]);
|
|
1037
|
+
const conflictedFiles = gitPathList(worktree.path, [
|
|
1038
|
+
"diff",
|
|
1039
|
+
"--name-only",
|
|
1040
|
+
"--diff-filter=U",
|
|
1041
|
+
"-z",
|
|
1042
|
+
]);
|
|
1043
|
+
const changedFiles = new Set([
|
|
1044
|
+
...gitPathList(worktree.path, [
|
|
1045
|
+
"diff",
|
|
1046
|
+
"--name-only",
|
|
1047
|
+
"-z",
|
|
1048
|
+
`${worktree.baseSha}...HEAD`,
|
|
1049
|
+
]),
|
|
1050
|
+
...gitPathList(worktree.path, ["diff", "--name-only", "-z"]),
|
|
1051
|
+
...gitPathList(worktree.path, ["diff", "--cached", "--name-only", "-z"]),
|
|
1052
|
+
...untrackedFiles,
|
|
1053
|
+
]);
|
|
1054
|
+
const commitsAhead = Number.parseInt(
|
|
1055
|
+
execFileSync(
|
|
1056
|
+
"git",
|
|
1057
|
+
["rev-list", "--count", `${worktree.baseSha}..HEAD`],
|
|
1058
|
+
{ cwd: worktree.path, encoding: "utf8" },
|
|
1059
|
+
).trim(),
|
|
1060
|
+
10,
|
|
1061
|
+
);
|
|
1062
|
+
return {
|
|
1063
|
+
...worktree,
|
|
1064
|
+
headSha,
|
|
1065
|
+
commitsAhead: Number.isFinite(commitsAhead) ? commitsAhead : 0,
|
|
1066
|
+
clean: status.length === 0,
|
|
1067
|
+
conflicted: conflictedFiles.length > 0,
|
|
1068
|
+
changedFiles: [...changedFiles].sort(),
|
|
1069
|
+
untrackedFiles: untrackedFiles.sort(),
|
|
1070
|
+
};
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
return {
|
|
1073
|
+
...worktree,
|
|
1074
|
+
headSha: null,
|
|
1075
|
+
commitsAhead: null,
|
|
1076
|
+
clean: null,
|
|
1077
|
+
conflicted: null,
|
|
1078
|
+
changedFiles: null,
|
|
1079
|
+
untrackedFiles: null,
|
|
1080
|
+
gitError: errorMessage(error),
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export function persistWorktreeResult(
|
|
1086
|
+
worktree: WorktreeLaunch,
|
|
1087
|
+
state: "running" | "ready_for_review" | "failed" | "needs_help",
|
|
1088
|
+
handoff?: WorktreeHandoff,
|
|
1089
|
+
): void {
|
|
1090
|
+
writeWorktreeManifest(worktree.manifestFile, {
|
|
1091
|
+
state,
|
|
1092
|
+
...worktree,
|
|
1093
|
+
...handoff,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export function runSubagentScript(
|
|
1098
|
+
surface: string,
|
|
1099
|
+
command: string,
|
|
1100
|
+
options: Parameters<typeof runScriptInPane>[2],
|
|
1101
|
+
worktree?: WorktreeLaunch,
|
|
1102
|
+
run: typeof runScriptInPane = runScriptInPane,
|
|
1103
|
+
): string {
|
|
1104
|
+
if (worktree) persistWorktreeResult(worktree, "running");
|
|
1105
|
+
try {
|
|
1106
|
+
return run(surface, command, options);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (!worktree) throw error;
|
|
1109
|
+
const handoff = captureWorktreeHandoff(worktree);
|
|
1110
|
+
try {
|
|
1111
|
+
persistWorktreeResult(worktree, "failed", handoff);
|
|
1112
|
+
} catch {
|
|
1113
|
+
// The launch error remains authoritative when persistence also fails.
|
|
1114
|
+
}
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`Failed to launch subagent; worktree retained at ${worktree.path} ` +
|
|
1117
|
+
`(workspace ${worktree.workspaceId}): ${errorMessage(error)}`,
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function errorMessage(error: unknown): string {
|
|
1123
|
+
return error instanceof Error ? error.message : String(error);
|
|
1124
|
+
}
|