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,774 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { appendLineageEvent, hasLineageEvent, lineageFromEnvironment, lineageEnvironment, registerLineage, isLineageNodeDrained, reduceLineage, } from "./lineage.js";
|
|
8
|
+
import { getSubagentActivityFile, getSubagentSettledEventsFile, } from "./activity.js";
|
|
9
|
+
import { createLifecycle } from "./lifecycle.js";
|
|
10
|
+
import { HerdrWorktreeCreateError } from "./herdr.js";
|
|
11
|
+
import { captureSessionBaseline, seedStandaloneSessionFile, seedSubagentSessionFile, seedReplicationSessionFile, } from "./session.js";
|
|
12
|
+
import { createSubagentPane, createSubagentPaneInWorkspace, closePane, getPaneTabId, getPaneProcessInfo, createSubagentWorktree, runScriptInPane, shellQuote, waitForShellReady, } from "./terminal.js";
|
|
13
|
+
import { readSessionBinding, registerSessionLineage, seedSessionBinding, sessionBindingStore, captureLaunchPaneOwnership } from "./session-binding.js";
|
|
14
|
+
import { attachReplicaWork, attachSharedReplicaPane, captureReplicaLaunchPane, cleanupReplicaWork, failReplicaLaunch } from "./replica-work.js";
|
|
15
|
+
import { sourceRepositoryAnchor } from "./source-state.js";
|
|
16
|
+
const SUBAGENTS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const defaultOperations = {
|
|
18
|
+
createPane: (name, workspace) => workspace
|
|
19
|
+
? createSubagentPaneInWorkspace(name, workspace.cwd, workspace.id)
|
|
20
|
+
: createSubagentPane(name),
|
|
21
|
+
closePane,
|
|
22
|
+
getPaneProcessInfo,
|
|
23
|
+
createWorktree: createSubagentWorktree,
|
|
24
|
+
waitForShellReady,
|
|
25
|
+
runScript: runScriptInPane,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Launch one validated Pi-backed request. Lifecycle watching and parent
|
|
29
|
+
* delivery begin only after this transaction returns the running child.
|
|
30
|
+
*/
|
|
31
|
+
export async function launchPiSubagent(request, operations = defaultOperations) {
|
|
32
|
+
return request.kind === "resume"
|
|
33
|
+
? launchResumedPiSubagent(request, operations)
|
|
34
|
+
: launchFreshPiSubagent(request, operations);
|
|
35
|
+
}
|
|
36
|
+
async function launchFreshPiSubagent(request, operations) {
|
|
37
|
+
const resolved = resolveLaunchRequest(request);
|
|
38
|
+
// Registration is the ownership boundary: publish lineage before Herdr creates resources.
|
|
39
|
+
const inherited = readSessionBinding(request.parent.sessionFile)?.child?.lineage ?? lineageFromEnvironment();
|
|
40
|
+
resolved.lineage = request.lineage ?? registerLineage({
|
|
41
|
+
artifactDir: resolved.artifactDir,
|
|
42
|
+
nodeId: resolved.id,
|
|
43
|
+
parentNodeId: inherited?.nodeId,
|
|
44
|
+
parentSessionId: request.parent.sessionId,
|
|
45
|
+
parentSessionFile: request.parent.sessionFile,
|
|
46
|
+
launchKind: request.worktree ? "worktree" : request.fork ? "fork" : "fresh",
|
|
47
|
+
inheritedRootDir: inherited?.rootDir,
|
|
48
|
+
inheritedRootId: inherited?.rootId,
|
|
49
|
+
});
|
|
50
|
+
const storeDir = sessionBindingStore(request.parent.sessionFile, resolved.agentDir);
|
|
51
|
+
registerSessionLineage(storeDir, request.parent.sessionId, resolved.lineage.rootDir);
|
|
52
|
+
let surface;
|
|
53
|
+
const abort = new AbortController();
|
|
54
|
+
const signal = request.signal ?? abort.signal;
|
|
55
|
+
try {
|
|
56
|
+
if (request.replication) {
|
|
57
|
+
const metadataId = `metadata:${resolved.id}:reserved`;
|
|
58
|
+
if (!appendLineageEvent(resolved.lineage.rootDir, metadataId, "launch_metadata", resolved.id, {
|
|
59
|
+
name: request.name,
|
|
60
|
+
task: request.task,
|
|
61
|
+
startTime: resolved.startTime,
|
|
62
|
+
executionOwner: request.executionOwner,
|
|
63
|
+
replication: true,
|
|
64
|
+
replicaWorkFile: request.replication.recordFile,
|
|
65
|
+
}) && !hasLineageEvent(resolved.lineage.rootDir, metadataId, "launch_metadata")) {
|
|
66
|
+
throw new Error("Unable to publish replica launch recovery metadata.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
surface = prepareLaunchSurface(resolved, operations);
|
|
70
|
+
if (request.replication?.recordFile) {
|
|
71
|
+
if (surface.worktree) attachReplicaWork(request.replication.recordFile, surface.worktree);
|
|
72
|
+
else attachSharedReplicaPane(request.replication.recordFile, surface.surface);
|
|
73
|
+
captureLaunchSurface(resolved, surface, operations);
|
|
74
|
+
}
|
|
75
|
+
const session = prepareChildSession(resolved, surface);
|
|
76
|
+
const artifacts = prepareTaskArtifacts(resolved, session);
|
|
77
|
+
seedSessionBinding(artifacts.sessionFile, {
|
|
78
|
+
version: 1, storeDir,
|
|
79
|
+
child: {
|
|
80
|
+
lineage: resolved.lineage,
|
|
81
|
+
environment: {
|
|
82
|
+
...lineageEnvironment(resolved.lineage),
|
|
83
|
+
PI_SUBAGENT_ID: resolved.id,
|
|
84
|
+
PI_SUBAGENT_NAME: request.name,
|
|
85
|
+
PI_SUBAGENT_AGENT: request.agent ?? "",
|
|
86
|
+
PI_SUBAGENT_AUTO_EXIT: request.behavior.autoExit ? "1" : "0",
|
|
87
|
+
PI_SUBAGENT_SKILLS: normalizeSkillNames(request.behavior.skills ?? "").join(","),
|
|
88
|
+
PI_DENY_TOOLS: request.behavior.deniedTools.join(","),
|
|
89
|
+
PI_SUBAGENT_TOOLS: buildSubagentToolAllowlist(request.behavior.tools, request.behavior.autoExit, request.behavior.includeControlTools ?? true) ?? "",
|
|
90
|
+
PI_SUBAGENT_ACTIVITY_FILE: artifacts.activityFile,
|
|
91
|
+
PI_SUBAGENT_SETTLED_EVENTS_FILE: artifacts.settledEventsFile,
|
|
92
|
+
...(request.replication ? {
|
|
93
|
+
PI_SUBAGENT_REPLICATION: "1",
|
|
94
|
+
...(request.replication.recordFile ? { PI_SUBAGENT_REPLICA_WORK_FILE: request.replication.recordFile } : {}),
|
|
95
|
+
PI_SUBAGENT_REPLICATION_PROMPT_FILE: artifacts.replicationPromptFile,
|
|
96
|
+
PI_SUBAGENT_REPLICATION_EXTENSIONS: JSON.stringify(request.replication.extensionArgs),
|
|
97
|
+
PI_SUBAGENT_REPLICATION_ASSISTANTS: JSON.stringify(request.replication.entries.filter((entry) => entry.type === "message" && entry.message?.role === "assistant").map((entry) => entry.id)),
|
|
98
|
+
} : {}),
|
|
99
|
+
...Object.fromEntries(Object.entries(request.environment ?? {}).filter(([key]) => key.startsWith("PI_SUBAGENT_TREE_"))),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const sessionBaseline = captureSessionBaseline(artifacts.sessionFile);
|
|
104
|
+
if (resolved.lineage) {
|
|
105
|
+
appendLineageEvent(resolved.lineage.rootDir, `metadata:${resolved.id}:${resolved.startTime}`, "launch_metadata", resolved.id, {
|
|
106
|
+
name: request.name,
|
|
107
|
+
executionOwner: request.executionOwner,
|
|
108
|
+
task: request.task,
|
|
109
|
+
agent: request.agent,
|
|
110
|
+
surface: artifacts.surface,
|
|
111
|
+
tabId: artifacts.tabId,
|
|
112
|
+
sessionFile: artifacts.sessionFile,
|
|
113
|
+
activityFile: artifacts.activityFile,
|
|
114
|
+
settledEventsFile: artifacts.settledEventsFile,
|
|
115
|
+
startTime: resolved.startTime,
|
|
116
|
+
interactive: request.behavior.interactive,
|
|
117
|
+
autoExit: request.behavior.autoExit,
|
|
118
|
+
...(request.replication ? { replication: true, replicaWorkFile: request.replication.recordFile } : {}),
|
|
119
|
+
cwd: artifacts.targetCwd,
|
|
120
|
+
...(artifacts.worktree ? { worktree: artifacts.worktree } : {}),
|
|
121
|
+
...(artifacts.worktree?.workspaceId ? { workspaceId: artifacts.worktree.workspaceId } : {}),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Publish recovery evidence before waiting on external shell startup.
|
|
125
|
+
await confirmShellReady(session, operations, signal);
|
|
126
|
+
captureLaunchSurface(resolved, surface, operations);
|
|
127
|
+
if (request.replication?.recordFile && !surface.worktree && !resolved.launchPane) throw new Error("Cannot verify shared replica pane ownership.");
|
|
128
|
+
if (signal.aborted)
|
|
129
|
+
throw new Error("subagent launch aborted");
|
|
130
|
+
const command = buildPiCommand(resolved, artifacts);
|
|
131
|
+
const launchScriptFile = startPiProcess(resolved, artifacts, command, operations);
|
|
132
|
+
return createRunningChild(resolved, artifacts, launchScriptFile, sessionBaseline);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
abort.abort();
|
|
136
|
+
if (request.replication?.recordFile) {
|
|
137
|
+
try {
|
|
138
|
+
failReplicaLaunch(request.replication.recordFile, error);
|
|
139
|
+
}
|
|
140
|
+
catch { /* Preserve the original launch failure. */ }
|
|
141
|
+
}
|
|
142
|
+
if (surface && !surface.worktree) {
|
|
143
|
+
if (request.replication?.recordFile) {
|
|
144
|
+
try { captureLaunchSurface(resolved, surface, operations); cleanupReplicaWork(request.replication.recordFile, resolved.sourceCwd); } catch { /* Unknown ownership stays recoverable. */ }
|
|
145
|
+
} else {
|
|
146
|
+
try { operations.closePane?.(surface.surface); } catch { /* best effort */ }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!surface?.worktree)
|
|
150
|
+
throw error;
|
|
151
|
+
if (request.replication?.recordFile) {
|
|
152
|
+
try {
|
|
153
|
+
captureLaunchSurface(resolved, surface, operations);
|
|
154
|
+
cleanupReplicaWork(request.replication.recordFile, resolved.sourceCwd);
|
|
155
|
+
}
|
|
156
|
+
catch { /* Unknown ownership keeps the failed work recoverable. */ }
|
|
157
|
+
try {
|
|
158
|
+
persistWorktreeResult(surface.worktree, "failed");
|
|
159
|
+
}
|
|
160
|
+
catch { /* The work record also retains the failure. */ }
|
|
161
|
+
throw new Error(`Failed to launch replica; worktree retained at ${surface.worktree.path}: ${errorMessage(error)}`);
|
|
162
|
+
}
|
|
163
|
+
const handoff = captureWorktreeHandoff(surface.worktree);
|
|
164
|
+
try {
|
|
165
|
+
persistWorktreeResult(surface.worktree, "failed", handoff);
|
|
166
|
+
}
|
|
167
|
+
catch { /* launch error remains authoritative */ }
|
|
168
|
+
throw new Error(`Failed to launch subagent; worktree retained at ${surface.worktree.path} (workspace ${surface.worktree.workspaceId}): ${errorMessage(error)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Record the shell before dispatch; child startup may never reach session_start. */
|
|
172
|
+
function captureLaunchSurface(resolved, surface, operations) {
|
|
173
|
+
const file = resolved.request.replication?.recordFile;
|
|
174
|
+
if (!file || !operations.getPaneProcessInfo)
|
|
175
|
+
return;
|
|
176
|
+
let shellPid;
|
|
177
|
+
try {
|
|
178
|
+
shellPid = operations.getPaneProcessInfo(surface.surface).shellPid;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return;
|
|
182
|
+
} // A shell may still be starting; retry after readiness.
|
|
183
|
+
const owner = captureLaunchPaneOwnership(surface.surface, shellPid);
|
|
184
|
+
if (owner) {
|
|
185
|
+
captureReplicaLaunchPane(file, owner);
|
|
186
|
+
resolved.launchPane = owner;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function resolveLaunchRequest(request) {
|
|
190
|
+
const id = request.id ?? randomUUID();
|
|
191
|
+
const agentDir = request.parent.agentDir ??
|
|
192
|
+
process.env.PI_CODING_AGENT_DIR ??
|
|
193
|
+
join(homedir(), ".pi", "agent");
|
|
194
|
+
const rawCwd = request.cwd ?? request.behavior.cwd;
|
|
195
|
+
const cwdBase = request.cwd == null && request.behavior.cwd != null
|
|
196
|
+
? agentDir
|
|
197
|
+
: (request.parent.invocationCwd ?? request.parent.cwd);
|
|
198
|
+
const sourceCwd = rawCwd
|
|
199
|
+
? rawCwd.startsWith("/")
|
|
200
|
+
? rawCwd
|
|
201
|
+
: join(cwdBase, rawCwd)
|
|
202
|
+
: request.parent.cwd;
|
|
203
|
+
const localAgentDir = rawCwd ? join(sourceCwd, ".pi", "agent") : null;
|
|
204
|
+
const sessionMode = request.fork ? "fork" : request.behavior.sessionMode;
|
|
205
|
+
return {
|
|
206
|
+
request,
|
|
207
|
+
id,
|
|
208
|
+
startTime: Date.now(),
|
|
209
|
+
agentDir,
|
|
210
|
+
localAgentDir: localAgentDir && existsSync(localAgentDir) ? localAgentDir : null,
|
|
211
|
+
sourceCwd,
|
|
212
|
+
artifactDir: request.replication?.recordFile ? join(request.replication.recordFile + ".artifacts", "launch") : join(request.parent.sessionDir, "artifacts", request.parent.sessionId),
|
|
213
|
+
sessionMode,
|
|
214
|
+
taskDelivery: sessionMode === "fork" ? "direct" : "artifact",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function prepareLaunchSurface(resolved, operations) {
|
|
218
|
+
const { request } = resolved;
|
|
219
|
+
if (!request.worktree) {
|
|
220
|
+
const surface = request.surface ?? operations.createPane(request.name);
|
|
221
|
+
const tabId = getPaneTabId(surface);
|
|
222
|
+
return {
|
|
223
|
+
surface,
|
|
224
|
+
...(tabId ? { tabId } : {}),
|
|
225
|
+
targetCwd: resolved.sourceCwd,
|
|
226
|
+
effectiveAgentDir: resolved.localAgentDir ?? resolved.agentDir,
|
|
227
|
+
localAgentDir: resolved.localAgentDir,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (request.surface)
|
|
231
|
+
throw new Error("A worktree subagent cannot use a pre-created pane");
|
|
232
|
+
const baseRef = request.worktree.base ?? "HEAD";
|
|
233
|
+
const baseSha = resolveGitCommit(resolved.sourceCwd, baseRef);
|
|
234
|
+
const manifestFile = join(resolved.artifactDir, "worktree-runs", `${resolved.id}.json`);
|
|
235
|
+
const ownership = {
|
|
236
|
+
id: resolved.id,
|
|
237
|
+
name: request.name,
|
|
238
|
+
sourceCwd: resolved.sourceCwd,
|
|
239
|
+
branch: request.worktree.branch,
|
|
240
|
+
baseRef,
|
|
241
|
+
baseSha,
|
|
242
|
+
createdAt: resolved.startTime,
|
|
243
|
+
};
|
|
244
|
+
writeWorktreeManifest(manifestFile, {
|
|
245
|
+
state: "provisioning",
|
|
246
|
+
...ownership,
|
|
247
|
+
});
|
|
248
|
+
let created;
|
|
249
|
+
try {
|
|
250
|
+
created = operations.createWorktree(request.name, request.replication ? sourceRepositoryAnchor(resolved.sourceCwd) : resolved.sourceCwd, request.worktree.branch, baseSha);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
writeWorktreeManifest(manifestFile, {
|
|
254
|
+
state: "failed",
|
|
255
|
+
...ownership,
|
|
256
|
+
...(error instanceof HerdrWorktreeCreateError
|
|
257
|
+
? error.recoveredWorktree
|
|
258
|
+
: {}),
|
|
259
|
+
error: errorMessage(error),
|
|
260
|
+
});
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
const worktree = {
|
|
264
|
+
path: created.path,
|
|
265
|
+
workspaceId: created.workspaceId,
|
|
266
|
+
paneId: created.paneId,
|
|
267
|
+
branch: created.branch,
|
|
268
|
+
baseRef,
|
|
269
|
+
baseSha,
|
|
270
|
+
manifestFile,
|
|
271
|
+
};
|
|
272
|
+
writeWorktreeManifest(manifestFile, {
|
|
273
|
+
state: "provisioned",
|
|
274
|
+
...ownership,
|
|
275
|
+
...worktree,
|
|
276
|
+
});
|
|
277
|
+
const isolatedAgentDir = join(created.path, ".pi", "agent");
|
|
278
|
+
const hasIsolatedAgentDir = existsSync(isolatedAgentDir);
|
|
279
|
+
return {
|
|
280
|
+
surface: created.paneId,
|
|
281
|
+
targetCwd: created.path,
|
|
282
|
+
effectiveAgentDir: hasIsolatedAgentDir
|
|
283
|
+
? isolatedAgentDir
|
|
284
|
+
: resolved.agentDir,
|
|
285
|
+
localAgentDir: hasIsolatedAgentDir ? isolatedAgentDir : null,
|
|
286
|
+
worktree,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function prepareChildSession(resolved, surface) {
|
|
290
|
+
const sessionDir = resolved.request.replication?.recordFile
|
|
291
|
+
? join(resolved.request.replication.recordFile + ".artifacts", "sessions")
|
|
292
|
+
: getDefaultSessionDirFor(surface.targetCwd, surface.effectiveAgentDir);
|
|
293
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
294
|
+
const timestamp = timestampForFile();
|
|
295
|
+
const uuid = [resolved.id, randomUUID(), randomUUID(), randomUUID()].join("-");
|
|
296
|
+
const sessionFile = join(sessionDir, `${timestamp}_${uuid}.jsonl`);
|
|
297
|
+
if (resolved.sessionMode === "standalone") {
|
|
298
|
+
seedStandaloneSessionFile({ childSessionFile: sessionFile, childCwd: surface.targetCwd });
|
|
299
|
+
}
|
|
300
|
+
if (surface.worktree) {
|
|
301
|
+
surface.worktree.sessionFile = sessionFile;
|
|
302
|
+
writeWorktreeManifest(surface.worktree.manifestFile, { sessionFile });
|
|
303
|
+
if (resolved.request.replication?.recordFile)
|
|
304
|
+
attachReplicaWork(resolved.request.replication.recordFile, surface.worktree, sessionFile);
|
|
305
|
+
} else if (resolved.request.replication?.recordFile) {
|
|
306
|
+
attachSharedReplicaPane(resolved.request.replication.recordFile, surface.surface, sessionFile);
|
|
307
|
+
}
|
|
308
|
+
const activityFile = getSubagentActivityFile(resolved.artifactDir, resolved.id);
|
|
309
|
+
const settledEventsFile = getSubagentSettledEventsFile(resolved.artifactDir, resolved.id);
|
|
310
|
+
if (resolved.lineage) {
|
|
311
|
+
writeFileSync(`${sessionFile}.lineage.json`, JSON.stringify(resolved.lineage), "utf8");
|
|
312
|
+
}
|
|
313
|
+
return { ...surface, sessionFile, activityFile, settledEventsFile };
|
|
314
|
+
}
|
|
315
|
+
async function confirmShellReady(session, operations, signal) {
|
|
316
|
+
await operations.waitForShellReady(session.surface, { signal });
|
|
317
|
+
}
|
|
318
|
+
function prepareTaskArtifacts(resolved, session) {
|
|
319
|
+
const { request } = resolved;
|
|
320
|
+
if (request.replication) {
|
|
321
|
+
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 });
|
|
322
|
+
}
|
|
323
|
+
else if (resolved.sessionMode !== "standalone") {
|
|
324
|
+
seedSubagentSessionFile({
|
|
325
|
+
mode: resolved.sessionMode,
|
|
326
|
+
parentSessionFile: request.parent.sessionFile,
|
|
327
|
+
childSessionFile: session.sessionFile,
|
|
328
|
+
childCwd: session.targetCwd,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
mkdirSync(dirname(session.activityFile), { recursive: true });
|
|
332
|
+
const identityInSystemPrompt = request.behavior.systemPromptMode && request.behavior.identity;
|
|
333
|
+
const roleBlock = request.behavior.identity && !identityInSystemPrompt
|
|
334
|
+
? `\n\n${request.behavior.identity}`
|
|
335
|
+
: "";
|
|
336
|
+
const modeHint = request.behavior.autoExit
|
|
337
|
+
? "Complete your task autonomously."
|
|
338
|
+
: "Complete your task. The user can interact with you at any time.";
|
|
339
|
+
const summaryInstruction = "Your FINAL assistant message should summarize what you accomplished.";
|
|
340
|
+
const fullTask = resolved.sessionMode === "fork"
|
|
341
|
+
? request.task
|
|
342
|
+
: `${roleBlock}\n\n${modeHint}\n\n${request.task}\n\n${summaryInstruction}`;
|
|
343
|
+
let taskArg = fullTask;
|
|
344
|
+
if (resolved.taskDelivery === "artifact") {
|
|
345
|
+
const artifactPath = join(resolved.artifactDir, `context/${safeName(request.name) || "subagent"}-${timestampForFile(false)}.md`);
|
|
346
|
+
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
347
|
+
writeFileSync(artifactPath, fullTask, "utf8");
|
|
348
|
+
taskArg = `@${artifactPath}`;
|
|
349
|
+
}
|
|
350
|
+
let systemPromptFile;
|
|
351
|
+
if (identityInSystemPrompt) {
|
|
352
|
+
systemPromptFile = join(resolved.artifactDir, `context/${safeName(request.name) || "subagent"}-sysprompt-${timestampForFile(false)}.md`);
|
|
353
|
+
mkdirSync(dirname(systemPromptFile), { recursive: true });
|
|
354
|
+
writeFileSync(systemPromptFile, identityInSystemPrompt, "utf8");
|
|
355
|
+
}
|
|
356
|
+
let replicationPromptFile;
|
|
357
|
+
if (request.replication) {
|
|
358
|
+
replicationPromptFile = join(resolved.artifactDir, "replication", `${resolved.id}.system.txt`);
|
|
359
|
+
mkdirSync(dirname(replicationPromptFile), { recursive: true });
|
|
360
|
+
writeFileSync(replicationPromptFile, request.replication.systemPrompt, { mode: 0o600 });
|
|
361
|
+
}
|
|
362
|
+
return { ...session, taskArg, systemPromptFile, replicationPromptFile };
|
|
363
|
+
}
|
|
364
|
+
function buildPiCommand(resolved, artifacts) {
|
|
365
|
+
const { request } = resolved;
|
|
366
|
+
const parts = [
|
|
367
|
+
"pi",
|
|
368
|
+
"--session",
|
|
369
|
+
shellQuote(artifacts.sessionFile),
|
|
370
|
+
...(!request.replication ? ["-e", shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts"))] : []),
|
|
371
|
+
...(process.env.HERDR_ENV === "1" && existsSync(join(process.env.HOME ?? "", ".pi/agent/extensions/herdr-agent-state.ts"))
|
|
372
|
+
? ["-e", shellQuote(join(process.env.HOME ?? "", ".pi/agent/extensions/herdr-agent-state.ts"))]
|
|
373
|
+
: []),
|
|
374
|
+
"--model",
|
|
375
|
+
shellQuote(request.runtimePlan.model),
|
|
376
|
+
"--thinking",
|
|
377
|
+
shellQuote(request.runtimePlan.thinking),
|
|
378
|
+
];
|
|
379
|
+
if (request.replication)
|
|
380
|
+
parts.push(...request.replication.extensionArgs.map(shellQuote), "-e", shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts")));
|
|
381
|
+
if (artifacts.systemPromptFile) {
|
|
382
|
+
parts.push(request.behavior.systemPromptMode === "replace"
|
|
383
|
+
? "--system-prompt"
|
|
384
|
+
: "--append-system-prompt", shellQuote(artifacts.systemPromptFile));
|
|
385
|
+
}
|
|
386
|
+
const toolAllowlist = buildSubagentToolAllowlist(request.behavior.tools, request.behavior.autoExit, request.behavior.includeControlTools ?? true);
|
|
387
|
+
if (toolAllowlist)
|
|
388
|
+
parts.push("--tools", shellQuote(toolAllowlist));
|
|
389
|
+
parts.push(shellQuote(artifacts.taskArg));
|
|
390
|
+
const env = [];
|
|
391
|
+
if (!request.replication)
|
|
392
|
+
env.push("PI_SUBAGENT_REPLICATION=''", "PI_SUBAGENT_REPLICA_WORK_FILE=''", "PI_SUBAGENT_REPLICATION_PROMPT_FILE=''");
|
|
393
|
+
if (request.replication) {
|
|
394
|
+
env.push("PI_SUBAGENT_REPLICATION=1", "PI_DENY_TOOLS=''", "PI_SUBAGENT_AGENT=''", `PI_SUBAGENT_REPLICATION_PROMPT_FILE=${shellQuote(artifacts.replicationPromptFile)}`);
|
|
395
|
+
if (request.replication.recordFile)
|
|
396
|
+
env.push(`PI_SUBAGENT_REPLICA_WORK_FILE=${shellQuote(request.replication.recordFile)}`);
|
|
397
|
+
}
|
|
398
|
+
for (const [key, value] of Object.entries(request.environment ?? {})) {
|
|
399
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
400
|
+
env.push(`${key}=${shellQuote(value)}`);
|
|
401
|
+
}
|
|
402
|
+
if (resolved.lineage) {
|
|
403
|
+
env.push("PI_SUBAGENT_LAUNCHER_PID=$$");
|
|
404
|
+
for (const [key, value] of Object.entries(lineageEnvironment(resolved.lineage))) {
|
|
405
|
+
env.push(`${key}=${shellQuote(value)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (artifacts.localAgentDir) {
|
|
409
|
+
env.push(`PI_CODING_AGENT_DIR=${shellQuote(artifacts.localAgentDir)}`);
|
|
410
|
+
}
|
|
411
|
+
else if (process.env.PI_CODING_AGENT_DIR) {
|
|
412
|
+
env.push(`PI_CODING_AGENT_DIR=${shellQuote(process.env.PI_CODING_AGENT_DIR)}`);
|
|
413
|
+
}
|
|
414
|
+
if (request.behavior.deniedTools.length > 0) {
|
|
415
|
+
env.push(`PI_DENY_TOOLS=${shellQuote(request.behavior.deniedTools.join(","))}`);
|
|
416
|
+
}
|
|
417
|
+
env.push(`PI_SUBAGENT_NAME=${shellQuote(request.name)}`);
|
|
418
|
+
if (request.agent)
|
|
419
|
+
env.push(`PI_SUBAGENT_AGENT=${shellQuote(request.agent)}`);
|
|
420
|
+
env.push(`PI_SUBAGENT_SKILLS=${shellQuote(normalizeSkillNames(request.behavior.skills ?? "").join(","))}`);
|
|
421
|
+
env.push(`PI_SUBAGENT_AUTO_EXIT=${request.behavior.autoExit ? "1" : "0"}`);
|
|
422
|
+
env.push(`PI_SUBAGENT_SESSION=${shellQuote(artifacts.sessionFile)}`);
|
|
423
|
+
env.push(`PI_SUBAGENT_ID=${shellQuote(resolved.id)}`);
|
|
424
|
+
env.push(`PI_SUBAGENT_ACTIVITY_FILE=${shellQuote(artifacts.activityFile)}`);
|
|
425
|
+
env.push(`PI_SUBAGENT_SETTLED_EVENTS_FILE=${shellQuote(artifacts.settledEventsFile)}`);
|
|
426
|
+
env.push(`PI_SUBAGENT_SURFACE=${shellQuote(artifacts.surface)}`);
|
|
427
|
+
const piCommand = `cd ${shellQuote(artifacts.targetCwd)} && ` +
|
|
428
|
+
`${env.join(" ")} ${parts.join(" ")}`;
|
|
429
|
+
return `${piCommand}; echo '__SUBAGENT_DONE_'$?'__'`;
|
|
430
|
+
}
|
|
431
|
+
function startPiProcess(resolved, artifacts, command, operations) {
|
|
432
|
+
const launchScriptFile = join(resolved.artifactDir, "subagent-scripts", `${safeName(resolved.request.name) || "subagent"}-${resolved.id}.sh`);
|
|
433
|
+
if (artifacts.worktree) {
|
|
434
|
+
persistWorktreeResult(artifacts.worktree, "running");
|
|
435
|
+
}
|
|
436
|
+
return operations.runScript(artifacts.surface, command, {
|
|
437
|
+
scriptPath: launchScriptFile,
|
|
438
|
+
scriptPreamble: [
|
|
439
|
+
`# Subagent launch script for ${resolved.request.name}`,
|
|
440
|
+
`# Generated: ${new Date().toISOString()}`,
|
|
441
|
+
`# Session: ${artifacts.sessionFile}`,
|
|
442
|
+
`# Surface: ${artifacts.surface}`,
|
|
443
|
+
].join("\n"),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
function createRunningChild(resolved, artifacts, launchScriptFile, sessionBaseline) {
|
|
447
|
+
return {
|
|
448
|
+
lineage: resolved.lineage,
|
|
449
|
+
executionOwner: resolved.request.executionOwner,
|
|
450
|
+
...(resolved.launchPane ? { launchPane: resolved.launchPane } : {}),
|
|
451
|
+
...(resolved.request.replication ? { replication: true, replicaWorkFile: resolved.request.replication.recordFile } : {}),
|
|
452
|
+
id: resolved.id,
|
|
453
|
+
name: resolved.request.name,
|
|
454
|
+
task: resolved.request.task,
|
|
455
|
+
agent: resolved.request.agent,
|
|
456
|
+
surface: artifacts.surface,
|
|
457
|
+
startTime: resolved.startTime,
|
|
458
|
+
...(artifacts.tabId ? { tabId: artifacts.tabId } : {}),
|
|
459
|
+
sessionFile: artifacts.sessionFile,
|
|
460
|
+
launchScriptFile,
|
|
461
|
+
activityFile: artifacts.activityFile,
|
|
462
|
+
settledEventsFile: artifacts.settledEventsFile,
|
|
463
|
+
interactive: resolved.request.behavior.interactive,
|
|
464
|
+
sessionBaseline,
|
|
465
|
+
runtimePlan: resolved.request.runtimePlan,
|
|
466
|
+
worktree: artifacts.worktree,
|
|
467
|
+
...(artifacts.worktree?.workspaceId
|
|
468
|
+
? { recoveryWorkspace: { id: artifacts.worktree.workspaceId, cwd: artifacts.worktree.path } }
|
|
469
|
+
: {}),
|
|
470
|
+
lifecycle: createLifecycle(resolved.startTime),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async function launchResumedPiSubagent(request, operations) {
|
|
474
|
+
let id = request.id ?? randomUUID();
|
|
475
|
+
const autoExit = request.behavior?.autoExit ?? true;
|
|
476
|
+
const artifactDir = join(request.parent.sessionDir, "artifacts", request.parent.sessionId);
|
|
477
|
+
const inherited = readSessionBinding(request.parent.sessionFile)?.child?.lineage ?? lineageFromEnvironment();
|
|
478
|
+
const priorBinding = readSessionBinding(request.sessionFile);
|
|
479
|
+
let prior = priorBinding?.child?.lineage;
|
|
480
|
+
try {
|
|
481
|
+
prior ??= JSON.parse(readFileSync(`${request.sessionFile}.lineage.json`, "utf8"));
|
|
482
|
+
if (!prior.rootDir || !prior.rootId || !prior.nodeId)
|
|
483
|
+
prior = undefined;
|
|
484
|
+
}
|
|
485
|
+
catch { }
|
|
486
|
+
const reusable = prior && !isLineageNodeDrained(reduceLineage(prior.rootDir), prior.nodeId);
|
|
487
|
+
if (reusable && prior)
|
|
488
|
+
id = prior.nodeId;
|
|
489
|
+
const lineage = (reusable ? prior : registerLineage({
|
|
490
|
+
artifactDir,
|
|
491
|
+
nodeId: id,
|
|
492
|
+
parentNodeId: inherited?.nodeId,
|
|
493
|
+
parentSessionId: request.parent.sessionId,
|
|
494
|
+
parentSessionFile: request.parent.sessionFile,
|
|
495
|
+
launchKind: "resume",
|
|
496
|
+
inheritedRootDir: inherited?.rootDir,
|
|
497
|
+
inheritedRootId: inherited?.rootId,
|
|
498
|
+
}));
|
|
499
|
+
const storeDir = sessionBindingStore(request.parent.sessionFile);
|
|
500
|
+
registerSessionLineage(storeDir, request.parent.sessionId, lineage.rootDir);
|
|
501
|
+
const interactive = request.behavior?.interactive ?? !autoExit;
|
|
502
|
+
const executionOwner = request.executionOwner;
|
|
503
|
+
const startTime = Date.now();
|
|
504
|
+
const surface = request.surface ?? operations.createPane(request.name, request.workspace);
|
|
505
|
+
const allocatedSurface = request.surface === undefined;
|
|
506
|
+
try {
|
|
507
|
+
const tabId = getPaneTabId(surface);
|
|
508
|
+
await operations.waitForShellReady(surface);
|
|
509
|
+
const activityFile = getSubagentActivityFile(artifactDir, id);
|
|
510
|
+
writeFileSync(`${request.sessionFile}.lineage.json`, JSON.stringify(lineage), "utf8");
|
|
511
|
+
const settledEventsFile = getSubagentSettledEventsFile(artifactDir, id);
|
|
512
|
+
mkdirSync(dirname(activityFile), { recursive: true });
|
|
513
|
+
if (lineage) {
|
|
514
|
+
appendLineageEvent(lineage.rootDir, `metadata:${lineage.nodeId}:${startTime}`, "launch_metadata", lineage.nodeId, {
|
|
515
|
+
name: request.name,
|
|
516
|
+
executionOwner,
|
|
517
|
+
task: request.message ?? "resumed session",
|
|
518
|
+
surface,
|
|
519
|
+
sessionFile: request.sessionFile,
|
|
520
|
+
tabId,
|
|
521
|
+
activityFile,
|
|
522
|
+
settledEventsFile,
|
|
523
|
+
startTime,
|
|
524
|
+
interactive,
|
|
525
|
+
autoExit,
|
|
526
|
+
...(request.workspace ? { workspaceId: request.workspace.id, cwd: request.workspace.cwd } : {}),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
let messageFile;
|
|
530
|
+
if (request.message) {
|
|
531
|
+
messageFile = join(artifactDir, "subagent-resume", `${safeName(request.name) || "resume"}-${timestampForFile(false)}.md`);
|
|
532
|
+
mkdirSync(dirname(messageFile), { recursive: true });
|
|
533
|
+
writeFileSync(messageFile, request.message, "utf8");
|
|
534
|
+
}
|
|
535
|
+
// Resume must use the existing session position from before the new Pi
|
|
536
|
+
// process starts, not a line count observed after launch.
|
|
537
|
+
const sessionBaseline = captureSessionBaseline(request.sessionFile);
|
|
538
|
+
seedSessionBinding(request.sessionFile, {
|
|
539
|
+
version: 1, storeDir,
|
|
540
|
+
child: {
|
|
541
|
+
lineage,
|
|
542
|
+
environment: {
|
|
543
|
+
...priorBinding?.child?.environment,
|
|
544
|
+
...lineageEnvironment(lineage),
|
|
545
|
+
PI_SUBAGENT_NAME: request.name,
|
|
546
|
+
PI_SUBAGENT_ID: id,
|
|
547
|
+
PI_SUBAGENT_ACTIVITY_FILE: activityFile,
|
|
548
|
+
PI_SUBAGENT_SETTLED_EVENTS_FILE: settledEventsFile,
|
|
549
|
+
PI_SUBAGENT_AUTO_EXIT: autoExit ? "1" : "0",
|
|
550
|
+
PI_SUBAGENT_RESUME_BASELINE_ASSISTANTS: JSON.stringify(sessionBaseline.assistantEntryIds),
|
|
551
|
+
PI_SUBAGENT_RESUME: "1",
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
const env = [
|
|
556
|
+
...(process.env.PI_CODING_AGENT_DIR
|
|
557
|
+
? [`PI_CODING_AGENT_DIR=${shellQuote(process.env.PI_CODING_AGENT_DIR)}`]
|
|
558
|
+
: []),
|
|
559
|
+
"PI_SUBAGENT_LAUNCHER_PID=$$",
|
|
560
|
+
...Object.entries(lineageEnvironment(lineage)).map(([key, value]) => `${key}=${shellQuote(value)}`),
|
|
561
|
+
`PI_SUBAGENT_NAME=${shellQuote(request.name)}`,
|
|
562
|
+
`PI_SUBAGENT_SESSION=${shellQuote(request.sessionFile)}`,
|
|
563
|
+
`PI_SUBAGENT_ID=${shellQuote(id)}`,
|
|
564
|
+
`PI_SUBAGENT_ACTIVITY_FILE=${shellQuote(activityFile)}`,
|
|
565
|
+
`PI_SUBAGENT_SETTLED_EVENTS_FILE=${shellQuote(settledEventsFile)}`,
|
|
566
|
+
`PI_SUBAGENT_AUTO_EXIT=${autoExit ? "1" : "0"}`,
|
|
567
|
+
`PI_SUBAGENT_RESUME_BASELINE_ASSISTANTS=${shellQuote(JSON.stringify(sessionBaseline.assistantEntryIds))}`,
|
|
568
|
+
"PI_SUBAGENT_RESUME=1",
|
|
569
|
+
];
|
|
570
|
+
const command = [
|
|
571
|
+
...env,
|
|
572
|
+
"pi",
|
|
573
|
+
"--session",
|
|
574
|
+
shellQuote(request.sessionFile),
|
|
575
|
+
...JSON.parse(priorBinding?.child?.environment.PI_SUBAGENT_REPLICATION_EXTENSIONS ?? "[]").map(shellQuote),
|
|
576
|
+
"-e",
|
|
577
|
+
shellQuote(join(SUBAGENTS_DIR, "subagent-done.ts")),
|
|
578
|
+
...(messageFile ? [shellQuote(`@${messageFile}`)] : []),
|
|
579
|
+
].join(" ");
|
|
580
|
+
const launchScriptFile = operations.runScript(surface, `${command}; echo '__SUBAGENT_DONE_'$?'__'`, {
|
|
581
|
+
scriptPath: join(artifactDir, "subagent-scripts", `${safeName(request.name) || "resume"}-resume-${Date.now()}.sh`),
|
|
582
|
+
scriptPreamble: [
|
|
583
|
+
`# Subagent resume script for ${request.name}`,
|
|
584
|
+
`# Generated: ${new Date().toISOString()}`,
|
|
585
|
+
`# Session: ${request.sessionFile}`,
|
|
586
|
+
`# Surface: ${surface}`,
|
|
587
|
+
...(messageFile ? [`# Resume message file: ${messageFile}`] : []),
|
|
588
|
+
].join("\n"),
|
|
589
|
+
});
|
|
590
|
+
return {
|
|
591
|
+
lineage,
|
|
592
|
+
executionOwner,
|
|
593
|
+
id,
|
|
594
|
+
name: request.name,
|
|
595
|
+
task: request.message ?? "resumed session",
|
|
596
|
+
surface,
|
|
597
|
+
startTime,
|
|
598
|
+
sessionFile: request.sessionFile,
|
|
599
|
+
...(tabId ? { tabId } : {}),
|
|
600
|
+
launchScriptFile,
|
|
601
|
+
activityFile,
|
|
602
|
+
settledEventsFile,
|
|
603
|
+
...(priorBinding?.child?.environment.PI_SUBAGENT_REPLICATION === "1" ? { replication: true } : {}),
|
|
604
|
+
interactive,
|
|
605
|
+
runtimePlan: undefined,
|
|
606
|
+
sessionBaseline,
|
|
607
|
+
...(request.workspace ? { recoveryWorkspace: request.workspace } : {}),
|
|
608
|
+
lifecycle: createLifecycle(startTime),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
if (allocatedSurface) {
|
|
613
|
+
try {
|
|
614
|
+
operations.closePane?.(surface);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// The original launch failure remains authoritative.
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
export function buildSubagentToolAllowlist(tools, _autoExit = false, _includeControlTools = true) {
|
|
624
|
+
const requested = (tools ?? "")
|
|
625
|
+
.split(",")
|
|
626
|
+
.map((tool) => tool.trim())
|
|
627
|
+
.filter(Boolean);
|
|
628
|
+
if (requested.length === 0)
|
|
629
|
+
return null;
|
|
630
|
+
const removed = new Set(["subagent", "subagent_resume", "subagent_interrupt", "subagent_cancel", "subagents_list", "caller_ping", "subagent_done"]);
|
|
631
|
+
return [...new Set(requested.filter((tool) => !removed.has(tool)))].join(",");
|
|
632
|
+
}
|
|
633
|
+
/** Normalize requested skill names for safe, deterministic child transport. */
|
|
634
|
+
export function normalizeSkillNames(skills) {
|
|
635
|
+
return [...new Set(skills
|
|
636
|
+
.split(",")
|
|
637
|
+
.map((skill) => skill.trim())
|
|
638
|
+
.filter(Boolean))];
|
|
639
|
+
}
|
|
640
|
+
function getDefaultSessionDirFor(cwd, agentDir) {
|
|
641
|
+
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
642
|
+
const sessionDir = join(agentDir, "sessions", safePath);
|
|
643
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
644
|
+
return sessionDir;
|
|
645
|
+
}
|
|
646
|
+
function timestampForFile(includeMilliseconds = true) {
|
|
647
|
+
return (new Date()
|
|
648
|
+
.toISOString()
|
|
649
|
+
.replace(/[:.]/g, "-")
|
|
650
|
+
.slice(0, includeMilliseconds ? 23 : 19) +
|
|
651
|
+
(includeMilliseconds ? "Z" : ""));
|
|
652
|
+
}
|
|
653
|
+
function safeName(name) {
|
|
654
|
+
return name
|
|
655
|
+
.toLowerCase()
|
|
656
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
657
|
+
.replace(/\s+/g, "-")
|
|
658
|
+
.replace(/-+/g, "-")
|
|
659
|
+
.replace(/^-|-$/g, "");
|
|
660
|
+
}
|
|
661
|
+
function resolveGitCommit(cwd, ref) {
|
|
662
|
+
return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
|
|
663
|
+
cwd,
|
|
664
|
+
encoding: "utf8",
|
|
665
|
+
}).trim();
|
|
666
|
+
}
|
|
667
|
+
export function writeWorktreeManifest(path, value) {
|
|
668
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
669
|
+
let existing = {};
|
|
670
|
+
if (existsSync(path)) {
|
|
671
|
+
try {
|
|
672
|
+
existing = JSON.parse(readFileSync(path, "utf8"));
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
existing = {};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const tempPath = `${path}.tmp`;
|
|
679
|
+
writeFileSync(tempPath, `${JSON.stringify({
|
|
680
|
+
...existing,
|
|
681
|
+
...value,
|
|
682
|
+
version: 1,
|
|
683
|
+
kind: "worktree-run",
|
|
684
|
+
owner: "pi-herdr-subagents",
|
|
685
|
+
updatedAt: Date.now(),
|
|
686
|
+
}, null, 2)}\n`);
|
|
687
|
+
renameSync(tempPath, path);
|
|
688
|
+
}
|
|
689
|
+
function gitPathList(cwd, args) {
|
|
690
|
+
return execFileSync("git", args, { cwd, encoding: "utf8" })
|
|
691
|
+
.split("\0")
|
|
692
|
+
.filter(Boolean);
|
|
693
|
+
}
|
|
694
|
+
export function captureWorktreeHandoff(worktree) {
|
|
695
|
+
try {
|
|
696
|
+
const headSha = resolveGitCommit(worktree.path, "HEAD");
|
|
697
|
+
const status = execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all", "-z"], { cwd: worktree.path, encoding: "utf8" });
|
|
698
|
+
const untrackedFiles = gitPathList(worktree.path, [
|
|
699
|
+
"ls-files",
|
|
700
|
+
"--others",
|
|
701
|
+
"--exclude-standard",
|
|
702
|
+
"-z",
|
|
703
|
+
]);
|
|
704
|
+
const conflictedFiles = gitPathList(worktree.path, [
|
|
705
|
+
"diff",
|
|
706
|
+
"--name-only",
|
|
707
|
+
"--diff-filter=U",
|
|
708
|
+
"-z",
|
|
709
|
+
]);
|
|
710
|
+
const changedFiles = new Set([
|
|
711
|
+
...gitPathList(worktree.path, [
|
|
712
|
+
"diff",
|
|
713
|
+
"--name-only",
|
|
714
|
+
"-z",
|
|
715
|
+
`${worktree.baseSha}...HEAD`,
|
|
716
|
+
]),
|
|
717
|
+
...gitPathList(worktree.path, ["diff", "--name-only", "-z"]),
|
|
718
|
+
...gitPathList(worktree.path, ["diff", "--cached", "--name-only", "-z"]),
|
|
719
|
+
...untrackedFiles,
|
|
720
|
+
]);
|
|
721
|
+
const commitsAhead = Number.parseInt(execFileSync("git", ["rev-list", "--count", `${worktree.baseSha}..HEAD`], { cwd: worktree.path, encoding: "utf8" }).trim(), 10);
|
|
722
|
+
return {
|
|
723
|
+
...worktree,
|
|
724
|
+
headSha,
|
|
725
|
+
commitsAhead: Number.isFinite(commitsAhead) ? commitsAhead : 0,
|
|
726
|
+
clean: status.length === 0,
|
|
727
|
+
conflicted: conflictedFiles.length > 0,
|
|
728
|
+
changedFiles: [...changedFiles].sort(),
|
|
729
|
+
untrackedFiles: untrackedFiles.sort(),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
return {
|
|
734
|
+
...worktree,
|
|
735
|
+
headSha: null,
|
|
736
|
+
commitsAhead: null,
|
|
737
|
+
clean: null,
|
|
738
|
+
conflicted: null,
|
|
739
|
+
changedFiles: null,
|
|
740
|
+
untrackedFiles: null,
|
|
741
|
+
gitError: errorMessage(error),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
export function persistWorktreeResult(worktree, state, handoff) {
|
|
746
|
+
writeWorktreeManifest(worktree.manifestFile, {
|
|
747
|
+
state,
|
|
748
|
+
...worktree,
|
|
749
|
+
...handoff,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
export function runSubagentScript(surface, command, options, worktree, run = runScriptInPane) {
|
|
753
|
+
if (worktree)
|
|
754
|
+
persistWorktreeResult(worktree, "running");
|
|
755
|
+
try {
|
|
756
|
+
return run(surface, command, options);
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
if (!worktree)
|
|
760
|
+
throw error;
|
|
761
|
+
const handoff = captureWorktreeHandoff(worktree);
|
|
762
|
+
try {
|
|
763
|
+
persistWorktreeResult(worktree, "failed", handoff);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
// The launch error remains authoritative when persistence also fails.
|
|
767
|
+
}
|
|
768
|
+
throw new Error(`Failed to launch subagent; worktree retained at ${worktree.path} ` +
|
|
769
|
+
`(workspace ${worktree.workspaceId}): ${errorMessage(error)}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function errorMessage(error) {
|
|
773
|
+
return error instanceof Error ? error.message : String(error);
|
|
774
|
+
}
|