@rafaeltab/sandcastle 0.13.0-fork.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1410 -0
- package/dist/MountConfig-CmXclHA5.d.ts +26 -0
- package/dist/SandboxProvider-EkSMuBp8.d.ts +243 -0
- package/dist/chunk-62WN33RK.js +104 -0
- package/dist/chunk-62WN33RK.js.map +1 -0
- package/dist/chunk-BBMMM5CR.js +23 -0
- package/dist/chunk-BBMMM5CR.js.map +1 -0
- package/dist/chunk-BIWNFKGV.js +22 -0
- package/dist/chunk-BIWNFKGV.js.map +1 -0
- package/dist/chunk-CP3TYXZA.js +362 -0
- package/dist/chunk-CP3TYXZA.js.map +1 -0
- package/dist/chunk-DJRHWPEH.js +25569 -0
- package/dist/chunk-DJRHWPEH.js.map +1 -0
- package/dist/chunk-NGBM7T3E.js +76 -0
- package/dist/chunk-NGBM7T3E.js.map +1 -0
- package/dist/chunk-VOG34SRF.js +27012 -0
- package/dist/chunk-VOG34SRF.js.map +1 -0
- package/dist/index.d.ts +1051 -0
- package/dist/index.js +3485 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +19367 -0
- package/dist/main.js.map +1 -0
- package/dist/mountUtils-CCA-bbpK.d.ts +25 -0
- package/dist/sandboxes/daytona.d.ts +60 -0
- package/dist/sandboxes/daytona.js +136 -0
- package/dist/sandboxes/daytona.js.map +1 -0
- package/dist/sandboxes/docker.d.ts +110 -0
- package/dist/sandboxes/docker.js +9 -0
- package/dist/sandboxes/docker.js.map +1 -0
- package/dist/sandboxes/no-sandbox.d.ts +38 -0
- package/dist/sandboxes/no-sandbox.js +7 -0
- package/dist/sandboxes/no-sandbox.js.map +1 -0
- package/dist/sandboxes/podman.d.ts +124 -0
- package/dist/sandboxes/podman.js +299 -0
- package/dist/sandboxes/podman.js.map +1 -0
- package/dist/sandboxes/vercel.d.ts +104 -0
- package/dist/sandboxes/vercel.js +189 -0
- package/dist/sandboxes/vercel.js.map +1 -0
- package/dist/templates/blank/main.mts +12 -0
- package/dist/templates/blank/prompt.md +12 -0
- package/dist/templates/blank/template.json +4 -0
- package/dist/templates/parallel-planner/implement-prompt.md +62 -0
- package/dist/templates/parallel-planner/main.mts +204 -0
- package/dist/templates/parallel-planner/merge-prompt.md +26 -0
- package/dist/templates/parallel-planner/plan-prompt.md +37 -0
- package/dist/templates/parallel-planner/template.json +4 -0
- package/dist/templates/parallel-planner-with-review/CODING_STANDARDS.md +27 -0
- package/dist/templates/parallel-planner-with-review/implement-prompt.md +62 -0
- package/dist/templates/parallel-planner-with-review/main.mts +226 -0
- package/dist/templates/parallel-planner-with-review/merge-prompt.md +26 -0
- package/dist/templates/parallel-planner-with-review/plan-prompt.md +37 -0
- package/dist/templates/parallel-planner-with-review/review-prompt.md +55 -0
- package/dist/templates/parallel-planner-with-review/template.json +4 -0
- package/dist/templates/sequential-reviewer/CODING_STANDARDS.md +27 -0
- package/dist/templates/sequential-reviewer/implement-prompt.md +53 -0
- package/dist/templates/sequential-reviewer/main.mts +119 -0
- package/dist/templates/sequential-reviewer/review-prompt.md +55 -0
- package/dist/templates/sequential-reviewer/template.json +4 -0
- package/dist/templates/simple-loop/main.mts +50 -0
- package/dist/templates/simple-loop/prompt.md +53 -0
- package/dist/templates/simple-loop/template.json +4 -0
- package/package.json +107 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3485 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { NodeContext_exports, NodeFileSystem_exports, formatErrorMessage } from './chunk-DJRHWPEH.js';
|
|
3
|
+
import { Context_exports, CwdError, Effect_exports, resolveCwd, getCurrentBranch, generateTempBranchName, Layer_exports, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, pruneStale, create, copyToWorktree, runHostHooks, startSandbox, resolveGitMounts, patchGitMountsForWindows, SANDBOX_REPO_DIR, remove, makeSandboxFromHandle, syncOut, withSandboxLifecycle, hasUncommittedChanges, registerShutdown, SandboxFactory, PromptError, FileSystem_exports, SessionCaptureError, Clock_exports, Duration_exports, Option_exports, PromptExpansionTimeoutError, Deferred_exports, AgentError, Ref_exports, SilentDisplay, AgentIdleTimeoutError, Fiber_exports } from './chunk-VOG34SRF.js';
|
|
4
|
+
export { createBindMountSandboxProvider, createIsolatedSandboxProvider } from './chunk-BIWNFKGV.js';
|
|
5
|
+
import { noSandbox } from './chunk-62WN33RK.js';
|
|
6
|
+
import './chunk-NGBM7T3E.js';
|
|
7
|
+
import { mkdirSync, appendFileSync } from 'fs';
|
|
8
|
+
import path, { join, posix, dirname, relative } from 'path';
|
|
9
|
+
import { styleText } from 'util';
|
|
10
|
+
import * as clack from '@clack/prompts';
|
|
11
|
+
import { readdir, access, readFile, mkdir, writeFile, rm } from 'fs/promises';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
13
|
+
|
|
14
|
+
createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
// src/resumePrecheck.ts
|
|
17
|
+
var assertResumeSessionExists = async (params) => {
|
|
18
|
+
const { provider, sandboxTag, hostRepoDir, resumeSession } = params;
|
|
19
|
+
if (!provider.sessionStorage) {
|
|
20
|
+
throw new Error(`${provider.name} does not support resumeSession`);
|
|
21
|
+
}
|
|
22
|
+
if (sandboxTag === "none") {
|
|
23
|
+
const found = await provider.sessionStorage.findByIdOnHost(resumeSession);
|
|
24
|
+
if (!found.path) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`resumeSession "${resumeSession}" not found under ${found.searchedRoot}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const exists = await provider.sessionStorage.existsOnHost(
|
|
32
|
+
hostRepoDir,
|
|
33
|
+
resumeSession
|
|
34
|
+
);
|
|
35
|
+
if (!exists) {
|
|
36
|
+
const sessionPath = provider.sessionStorage.hostSessionFilePath(
|
|
37
|
+
hostRepoDir,
|
|
38
|
+
resumeSession
|
|
39
|
+
);
|
|
40
|
+
throw new Error(
|
|
41
|
+
sessionPath ? `resumeSession "${resumeSession}" not found: expected session file at ${sessionPath}` : `resumeSession "${resumeSession}" not found`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/AgentStreamEmitter.ts
|
|
47
|
+
var AgentStreamEmitter = class extends Context_exports.Tag("AgentStreamEmitter")() {
|
|
48
|
+
};
|
|
49
|
+
var agentStreamEmitterLayer = (onEvent) => Layer_exports.succeed(AgentStreamEmitter, {
|
|
50
|
+
emit: onEvent ? (event) => Effect_exports.sync(() => {
|
|
51
|
+
try {
|
|
52
|
+
onEvent(event);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}) : () => Effect_exports.void
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// src/PromptPreprocessor.ts
|
|
59
|
+
var PROMPT_EXPANSION_TIMEOUT_MS = 3e4;
|
|
60
|
+
var SHELL_BLOCK_MARKER = "";
|
|
61
|
+
var MARKED_SHELL_BLOCK_PATTERN = new RegExp(
|
|
62
|
+
`!${SHELL_BLOCK_MARKER}\`([^\`]+)\``,
|
|
63
|
+
"g"
|
|
64
|
+
);
|
|
65
|
+
var preprocessPrompt = (prompt, sandbox, cwd) => {
|
|
66
|
+
const matches = [...prompt.matchAll(MARKED_SHELL_BLOCK_PATTERN)];
|
|
67
|
+
if (matches.length === 0) {
|
|
68
|
+
return Effect_exports.succeed(prompt.replaceAll(SHELL_BLOCK_MARKER, ""));
|
|
69
|
+
}
|
|
70
|
+
return Effect_exports.gen(function* () {
|
|
71
|
+
const display = yield* Display;
|
|
72
|
+
return yield* display.taskLog(
|
|
73
|
+
"Expanding shell expressions",
|
|
74
|
+
(message) => Effect_exports.gen(function* () {
|
|
75
|
+
const results = yield* Effect_exports.all(
|
|
76
|
+
matches.map((match) => {
|
|
77
|
+
const command = match[1];
|
|
78
|
+
return Effect_exports.gen(function* () {
|
|
79
|
+
const start = yield* Clock_exports.currentTimeMillis;
|
|
80
|
+
const maybeResult = yield* sandbox.exec(command, { cwd }).pipe(
|
|
81
|
+
Effect_exports.timeoutOption(
|
|
82
|
+
Duration_exports.millis(PROMPT_EXPANSION_TIMEOUT_MS)
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
if (Option_exports.isNone(maybeResult)) {
|
|
86
|
+
const elapsedMs = (yield* Clock_exports.currentTimeMillis) - start;
|
|
87
|
+
return yield* Effect_exports.fail(
|
|
88
|
+
new PromptExpansionTimeoutError({
|
|
89
|
+
message: `Shell expression \`${command}\` timed out after ${elapsedMs}ms`,
|
|
90
|
+
timeoutMs: PROMPT_EXPANSION_TIMEOUT_MS,
|
|
91
|
+
expression: command,
|
|
92
|
+
elapsedMs
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const execResult = maybeResult.value;
|
|
97
|
+
if (execResult.exitCode !== 0) {
|
|
98
|
+
return yield* Effect_exports.fail(
|
|
99
|
+
new PromptError({
|
|
100
|
+
message: `Command \`${command}\` exited with code ${execResult.exitCode}: ${execResult.stderr}`,
|
|
101
|
+
exitCode: execResult.exitCode
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return execResult.stdout.trimEnd();
|
|
106
|
+
});
|
|
107
|
+
}),
|
|
108
|
+
{ concurrency: "unbounded" }
|
|
109
|
+
);
|
|
110
|
+
for (let i = 0; i < matches.length; i++) {
|
|
111
|
+
const command = matches[i][1];
|
|
112
|
+
const tokens = Math.ceil(results[i].length / 4);
|
|
113
|
+
message(`${command} \u2192 ~${tokens} tokens`);
|
|
114
|
+
}
|
|
115
|
+
let result = prompt;
|
|
116
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
117
|
+
const match = matches[i];
|
|
118
|
+
const index = match.index;
|
|
119
|
+
result = result.slice(0, index) + results[i] + result.slice(index + match[0].length);
|
|
120
|
+
}
|
|
121
|
+
return result.replaceAll(SHELL_BLOCK_MARKER, "");
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/TextDeltaBuffer.ts
|
|
128
|
+
var LENGTH_THRESHOLD = 80;
|
|
129
|
+
var DEBOUNCE_MS = 50;
|
|
130
|
+
var SENTENCE_BOUNDARY_RE = /[.!?] $/;
|
|
131
|
+
var TextDeltaBuffer = class {
|
|
132
|
+
buffer = "";
|
|
133
|
+
timer = null;
|
|
134
|
+
onFlush;
|
|
135
|
+
constructor(onFlush) {
|
|
136
|
+
this.onFlush = onFlush;
|
|
137
|
+
}
|
|
138
|
+
write(text2) {
|
|
139
|
+
if (text2.length === 0) return;
|
|
140
|
+
this.buffer += text2;
|
|
141
|
+
this.clearTimer();
|
|
142
|
+
if (this.shouldFlush()) {
|
|
143
|
+
this.doFlush();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.timer = setTimeout(() => {
|
|
147
|
+
this.doFlush();
|
|
148
|
+
}, DEBOUNCE_MS);
|
|
149
|
+
}
|
|
150
|
+
/** Force-flush any buffered text. */
|
|
151
|
+
flush() {
|
|
152
|
+
this.clearTimer();
|
|
153
|
+
this.doFlush();
|
|
154
|
+
}
|
|
155
|
+
/** Flush remaining buffer and clean up. */
|
|
156
|
+
dispose() {
|
|
157
|
+
this.flush();
|
|
158
|
+
}
|
|
159
|
+
shouldFlush() {
|
|
160
|
+
if (this.buffer.includes("\n")) return true;
|
|
161
|
+
if (SENTENCE_BOUNDARY_RE.test(this.buffer)) return true;
|
|
162
|
+
if (this.buffer.length >= LENGTH_THRESHOLD) return true;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
doFlush() {
|
|
166
|
+
if (this.buffer.length === 0) return;
|
|
167
|
+
const text2 = this.buffer;
|
|
168
|
+
this.buffer = "";
|
|
169
|
+
this.onFlush(text2);
|
|
170
|
+
}
|
|
171
|
+
clearTimer() {
|
|
172
|
+
if (this.timer !== null) {
|
|
173
|
+
clearTimeout(this.timer);
|
|
174
|
+
this.timer = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/Orchestrator.ts
|
|
180
|
+
var IDLE_WARNING_INTERVAL_MS = 6e4;
|
|
181
|
+
var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, completionTimeoutMs, completionSignals, onText, onToolCall, onRawLine, onIdleWarning, onCompletionTimeout, idleWarningIntervalMs = IDLE_WARNING_INTERVAL_MS, resumeSession, forkSession, signal) => Effect_exports.gen(function* () {
|
|
182
|
+
let resultText = "";
|
|
183
|
+
let sessionId;
|
|
184
|
+
let usage;
|
|
185
|
+
let accumulatedOutput = "";
|
|
186
|
+
const timeoutSignal = yield* Deferred_exports.make();
|
|
187
|
+
const completionTimeoutDeferred = yield* Deferred_exports.make();
|
|
188
|
+
let timeoutFiber = null;
|
|
189
|
+
let completionDetected = false;
|
|
190
|
+
let warningFiber = null;
|
|
191
|
+
let idleMinuteCounter = 0;
|
|
192
|
+
const interruptFiber = (fiber) => {
|
|
193
|
+
if (fiber !== null) Effect_exports.runFork(Fiber_exports.interrupt(fiber));
|
|
194
|
+
};
|
|
195
|
+
const startWarningInterval = () => {
|
|
196
|
+
interruptFiber(warningFiber);
|
|
197
|
+
idleMinuteCounter = 0;
|
|
198
|
+
warningFiber = Effect_exports.runFork(
|
|
199
|
+
Effect_exports.gen(function* () {
|
|
200
|
+
while (true) {
|
|
201
|
+
yield* Effect_exports.sleep(Duration_exports.millis(idleWarningIntervalMs));
|
|
202
|
+
idleMinuteCounter++;
|
|
203
|
+
onIdleWarning(idleMinuteCounter);
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
const resetTimer = () => {
|
|
209
|
+
interruptFiber(timeoutFiber);
|
|
210
|
+
if (completionDetected) {
|
|
211
|
+
timeoutFiber = Effect_exports.runFork(
|
|
212
|
+
Effect_exports.gen(function* () {
|
|
213
|
+
yield* Effect_exports.sleep(Duration_exports.millis(completionTimeoutMs));
|
|
214
|
+
onCompletionTimeout(completionTimeoutMs);
|
|
215
|
+
yield* Deferred_exports.succeed(completionTimeoutDeferred, {
|
|
216
|
+
result: resultText || accumulatedOutput,
|
|
217
|
+
sessionId,
|
|
218
|
+
usage
|
|
219
|
+
});
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
timeoutFiber = Effect_exports.runFork(
|
|
224
|
+
Effect_exports.gen(function* () {
|
|
225
|
+
yield* Effect_exports.sleep(Duration_exports.millis(idleTimeoutMs));
|
|
226
|
+
yield* Deferred_exports.fail(
|
|
227
|
+
timeoutSignal,
|
|
228
|
+
new AgentIdleTimeoutError({
|
|
229
|
+
message: `Agent idle for ${idleTimeoutMs / 1e3} seconds \u2014 no output received. Consider increasing the idle timeout with --idle-timeout.`,
|
|
230
|
+
timeoutMs: idleTimeoutMs
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
startWarningInterval();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const abortDeferred = yield* Deferred_exports.make();
|
|
239
|
+
let abortCleanup = null;
|
|
240
|
+
if (signal) {
|
|
241
|
+
if (signal.aborted) {
|
|
242
|
+
return yield* Effect_exports.die(signal.reason);
|
|
243
|
+
}
|
|
244
|
+
const onAbort = () => {
|
|
245
|
+
Effect_exports.runFork(Deferred_exports.die(abortDeferred, signal.reason));
|
|
246
|
+
};
|
|
247
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
248
|
+
abortCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
249
|
+
}
|
|
250
|
+
resetTimer();
|
|
251
|
+
const execEffect = Effect_exports.gen(function* () {
|
|
252
|
+
const printCmd = provider.buildPrintCommand({
|
|
253
|
+
prompt,
|
|
254
|
+
dangerouslySkipPermissions: true,
|
|
255
|
+
resumeSession,
|
|
256
|
+
forkSession
|
|
257
|
+
});
|
|
258
|
+
const execResult = yield* sandbox.exec(printCmd.command, {
|
|
259
|
+
onLine: (line) => {
|
|
260
|
+
try {
|
|
261
|
+
onRawLine(line);
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
for (const parsed of provider.parseStreamLine(line)) {
|
|
265
|
+
if (parsed.type === "text") {
|
|
266
|
+
onText(parsed.text);
|
|
267
|
+
accumulatedOutput += parsed.text;
|
|
268
|
+
} else if (parsed.type === "result") {
|
|
269
|
+
resultText = parsed.result;
|
|
270
|
+
accumulatedOutput += parsed.result;
|
|
271
|
+
} else if (parsed.type === "tool_call") {
|
|
272
|
+
onToolCall(parsed.name, parsed.args);
|
|
273
|
+
} else if (parsed.type === "session_id") {
|
|
274
|
+
sessionId = parsed.sessionId;
|
|
275
|
+
} else if (parsed.type === "usage") {
|
|
276
|
+
usage = parsed.usage;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!completionDetected && completionSignals.some((sig) => accumulatedOutput.includes(sig))) {
|
|
280
|
+
completionDetected = true;
|
|
281
|
+
interruptFiber(warningFiber);
|
|
282
|
+
warningFiber = null;
|
|
283
|
+
}
|
|
284
|
+
resetTimer();
|
|
285
|
+
},
|
|
286
|
+
cwd: sandboxRepoDir,
|
|
287
|
+
stdin: printCmd.stdin
|
|
288
|
+
});
|
|
289
|
+
if (execResult.exitCode !== 0) {
|
|
290
|
+
let errorDetail = execResult.stderr;
|
|
291
|
+
if (!errorDetail.trim()) {
|
|
292
|
+
errorDetail = resultText;
|
|
293
|
+
}
|
|
294
|
+
if (!errorDetail.trim()) {
|
|
295
|
+
const lines = execResult.stdout.split("\n").filter((l) => l.trim());
|
|
296
|
+
errorDetail = lines.slice(-20).join("\n");
|
|
297
|
+
}
|
|
298
|
+
return yield* Effect_exports.fail(
|
|
299
|
+
new AgentError({
|
|
300
|
+
message: `${provider.name} exited with code ${execResult.exitCode}:
|
|
301
|
+
${errorDetail}`
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return { result: resultText || execResult.stdout, sessionId, usage };
|
|
306
|
+
}).pipe(
|
|
307
|
+
Effect_exports.ensuring(
|
|
308
|
+
Effect_exports.sync(() => {
|
|
309
|
+
interruptFiber(timeoutFiber);
|
|
310
|
+
timeoutFiber = null;
|
|
311
|
+
interruptFiber(warningFiber);
|
|
312
|
+
warningFiber = null;
|
|
313
|
+
})
|
|
314
|
+
)
|
|
315
|
+
);
|
|
316
|
+
let raced = Effect_exports.raceFirst(execEffect, Deferred_exports.await(timeoutSignal));
|
|
317
|
+
raced = Effect_exports.raceFirst(raced, Deferred_exports.await(completionTimeoutDeferred));
|
|
318
|
+
if (signal) {
|
|
319
|
+
raced = Effect_exports.raceFirst(
|
|
320
|
+
raced,
|
|
321
|
+
Deferred_exports.await(abortDeferred)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return yield* raced.pipe(
|
|
325
|
+
Effect_exports.ensuring(
|
|
326
|
+
Effect_exports.sync(() => {
|
|
327
|
+
abortCleanup?.();
|
|
328
|
+
interruptFiber(timeoutFiber);
|
|
329
|
+
timeoutFiber = null;
|
|
330
|
+
interruptFiber(warningFiber);
|
|
331
|
+
warningFiber = null;
|
|
332
|
+
})
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
var DEFAULT_COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
|
|
337
|
+
var DEFAULT_IDLE_TIMEOUT_SECONDS = 10 * 60;
|
|
338
|
+
var DEFAULT_COMPLETION_TIMEOUT_SECONDS = 60;
|
|
339
|
+
var orchestrate = (options) => {
|
|
340
|
+
const idleTimeoutMs = (options.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS) * 1e3;
|
|
341
|
+
const completionTimeoutMs = (options.completionTimeoutSeconds ?? DEFAULT_COMPLETION_TIMEOUT_SECONDS) * 1e3;
|
|
342
|
+
return Effect_exports.gen(function* () {
|
|
343
|
+
const factory = yield* SandboxFactory;
|
|
344
|
+
const display = yield* Display;
|
|
345
|
+
const streamEmitter = yield* AgentStreamEmitter;
|
|
346
|
+
const { hostRepoDir, iterations, hooks, prompt, branch, provider } = options;
|
|
347
|
+
let completionSignals;
|
|
348
|
+
if (options.completionSignal === void 0) {
|
|
349
|
+
completionSignals = [DEFAULT_COMPLETION_SIGNAL];
|
|
350
|
+
} else if (Array.isArray(options.completionSignal)) {
|
|
351
|
+
completionSignals = options.completionSignal;
|
|
352
|
+
} else {
|
|
353
|
+
completionSignals = [options.completionSignal];
|
|
354
|
+
}
|
|
355
|
+
const label = (msg) => options.name ? `[${options.name}] ${msg}` : msg;
|
|
356
|
+
const allCommits = [];
|
|
357
|
+
const allIterations = [];
|
|
358
|
+
let allStdout = "";
|
|
359
|
+
let resolvedBranch = "";
|
|
360
|
+
let iterationPreservedPath;
|
|
361
|
+
const checkAbort = () => options.signal?.aborted ? Effect_exports.die(options.signal.reason) : Effect_exports.void;
|
|
362
|
+
for (let i = 1; i <= iterations; i++) {
|
|
363
|
+
yield* checkAbort();
|
|
364
|
+
yield* display.status(label(`Iteration ${i}/${iterations}`), "info");
|
|
365
|
+
const sandboxResult = yield* factory.withSandbox(
|
|
366
|
+
({ hostWorktreePath, sandboxRepoPath, applyToHost, bindMountHandle }, sandbox) => withSandboxLifecycle(
|
|
367
|
+
{
|
|
368
|
+
hostRepoDir,
|
|
369
|
+
sandboxRepoDir: sandboxRepoPath,
|
|
370
|
+
hooks,
|
|
371
|
+
branch,
|
|
372
|
+
hostWorktreePath,
|
|
373
|
+
applyToHost,
|
|
374
|
+
signal: options.signal,
|
|
375
|
+
timeouts: options.timeouts,
|
|
376
|
+
keepSourceBranch: options.keepSourceBranch
|
|
377
|
+
},
|
|
378
|
+
sandbox,
|
|
379
|
+
(ctx) => Effect_exports.gen(function* () {
|
|
380
|
+
const iterationResumeSession = i === 1 ? options.resumeSession : void 0;
|
|
381
|
+
const iterationForkSession = i === 1 ? options.forkSession : void 0;
|
|
382
|
+
if (iterationResumeSession && bindMountHandle && provider.sessionStorage) {
|
|
383
|
+
yield* display.status(label("Resuming session"), "info");
|
|
384
|
+
yield* Effect_exports.tryPromise({
|
|
385
|
+
try: () => provider.sessionStorage.resumeIntoSandbox({
|
|
386
|
+
hostCwd: hostRepoDir,
|
|
387
|
+
sandboxCwd: ctx.sandboxRepoDir,
|
|
388
|
+
sessionId: iterationResumeSession,
|
|
389
|
+
handle: bindMountHandle
|
|
390
|
+
}),
|
|
391
|
+
catch: (e) => new SessionCaptureError({
|
|
392
|
+
message: `Session resume failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
393
|
+
sessionId: iterationResumeSession
|
|
394
|
+
})
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const fullPrompt = options.skipPromptExpansion ? prompt : yield* preprocessPrompt(
|
|
398
|
+
prompt,
|
|
399
|
+
ctx.sandbox,
|
|
400
|
+
ctx.sandboxRepoDir
|
|
401
|
+
);
|
|
402
|
+
yield* display.status(label("Agent started"), "success");
|
|
403
|
+
const textBuffer = new TextDeltaBuffer((chunk) => {
|
|
404
|
+
Effect_exports.runPromise(display.textChunk(chunk));
|
|
405
|
+
Effect_exports.runPromise(
|
|
406
|
+
streamEmitter.emit({
|
|
407
|
+
type: "text",
|
|
408
|
+
message: chunk,
|
|
409
|
+
iteration: i,
|
|
410
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
const onText = (text2) => {
|
|
415
|
+
textBuffer.write(text2);
|
|
416
|
+
};
|
|
417
|
+
const onToolCall = (name, formattedArgs) => {
|
|
418
|
+
textBuffer.flush();
|
|
419
|
+
Effect_exports.runPromise(display.toolCall(name, formattedArgs));
|
|
420
|
+
Effect_exports.runPromise(
|
|
421
|
+
streamEmitter.emit({
|
|
422
|
+
type: "toolCall",
|
|
423
|
+
name,
|
|
424
|
+
formattedArgs,
|
|
425
|
+
iteration: i,
|
|
426
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
427
|
+
})
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
const onRawLine = (line) => {
|
|
431
|
+
Effect_exports.runPromise(
|
|
432
|
+
streamEmitter.emit({
|
|
433
|
+
type: "raw",
|
|
434
|
+
line,
|
|
435
|
+
iteration: i,
|
|
436
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
437
|
+
})
|
|
438
|
+
);
|
|
439
|
+
};
|
|
440
|
+
const onIdleWarning = (minutes) => {
|
|
441
|
+
const msg = minutes === 1 ? "Agent idle for 1 minute" : `Agent idle for ${minutes} minutes`;
|
|
442
|
+
Effect_exports.runPromise(display.status(label(msg), "warn"));
|
|
443
|
+
};
|
|
444
|
+
const onCompletionTimeout = (timeoutMs) => {
|
|
445
|
+
Effect_exports.runPromise(
|
|
446
|
+
display.status(
|
|
447
|
+
label(
|
|
448
|
+
`Completion signal seen but agent process is hanging \u2014 force-completing after ${timeoutMs / 1e3}s grace window.`
|
|
449
|
+
),
|
|
450
|
+
"warn"
|
|
451
|
+
)
|
|
452
|
+
);
|
|
453
|
+
};
|
|
454
|
+
const {
|
|
455
|
+
result: agentOutput,
|
|
456
|
+
sessionId,
|
|
457
|
+
usage: streamUsage
|
|
458
|
+
} = yield* invokeAgent(
|
|
459
|
+
ctx.sandbox,
|
|
460
|
+
ctx.sandboxRepoDir,
|
|
461
|
+
fullPrompt,
|
|
462
|
+
provider,
|
|
463
|
+
idleTimeoutMs,
|
|
464
|
+
completionTimeoutMs,
|
|
465
|
+
completionSignals,
|
|
466
|
+
onText,
|
|
467
|
+
onToolCall,
|
|
468
|
+
onRawLine,
|
|
469
|
+
onIdleWarning,
|
|
470
|
+
onCompletionTimeout,
|
|
471
|
+
options._idleWarningIntervalMs,
|
|
472
|
+
iterationResumeSession,
|
|
473
|
+
iterationForkSession,
|
|
474
|
+
options.signal
|
|
475
|
+
);
|
|
476
|
+
textBuffer.dispose();
|
|
477
|
+
yield* display.status(label("Agent stopped"), "info");
|
|
478
|
+
let sessionFilePath;
|
|
479
|
+
let usage = streamUsage;
|
|
480
|
+
if (provider.captureSessions && provider.sessionStorage && sessionId && bindMountHandle) {
|
|
481
|
+
yield* display.status(label("Capturing session"), "info");
|
|
482
|
+
yield* Effect_exports.tryPromise({
|
|
483
|
+
try: () => provider.sessionStorage.captureToHost({
|
|
484
|
+
hostCwd: hostRepoDir,
|
|
485
|
+
sandboxCwd: ctx.sandboxRepoDir,
|
|
486
|
+
sessionId,
|
|
487
|
+
handle: bindMountHandle
|
|
488
|
+
}),
|
|
489
|
+
catch: (e) => new SessionCaptureError({
|
|
490
|
+
message: `Session capture failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
491
|
+
sessionId
|
|
492
|
+
})
|
|
493
|
+
});
|
|
494
|
+
sessionFilePath = provider.sessionStorage.hostSessionFilePath(
|
|
495
|
+
hostRepoDir,
|
|
496
|
+
sessionId
|
|
497
|
+
);
|
|
498
|
+
if (provider.parseSessionUsage) {
|
|
499
|
+
const content = yield* Effect_exports.promise(
|
|
500
|
+
() => provider.sessionStorage.readHostSession(hostRepoDir, sessionId).catch(() => void 0)
|
|
501
|
+
);
|
|
502
|
+
if (content) {
|
|
503
|
+
const parsedUsage = provider.parseSessionUsage(content);
|
|
504
|
+
if (parsedUsage) usage = parsedUsage;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const matchedSignal = completionSignals.find(
|
|
509
|
+
(sig) => agentOutput.includes(sig)
|
|
510
|
+
);
|
|
511
|
+
return {
|
|
512
|
+
completionSignal: matchedSignal,
|
|
513
|
+
stdout: agentOutput,
|
|
514
|
+
sessionId,
|
|
515
|
+
sessionFilePath,
|
|
516
|
+
usage
|
|
517
|
+
};
|
|
518
|
+
})
|
|
519
|
+
)
|
|
520
|
+
);
|
|
521
|
+
const lifecycleResult = sandboxResult.value;
|
|
522
|
+
iterationPreservedPath = sandboxResult.preservedWorktreePath;
|
|
523
|
+
allCommits.push(...lifecycleResult.commits);
|
|
524
|
+
allStdout += lifecycleResult.result.stdout;
|
|
525
|
+
resolvedBranch = lifecycleResult.branch;
|
|
526
|
+
allIterations.push({
|
|
527
|
+
sessionId: lifecycleResult.result.sessionId,
|
|
528
|
+
sessionFilePath: lifecycleResult.result.sessionFilePath,
|
|
529
|
+
usage: lifecycleResult.result.usage
|
|
530
|
+
});
|
|
531
|
+
if (lifecycleResult.result.completionSignal !== void 0) {
|
|
532
|
+
yield* display.status(
|
|
533
|
+
label(`Agent signaled completion after ${i} iteration(s).`),
|
|
534
|
+
"success"
|
|
535
|
+
);
|
|
536
|
+
return {
|
|
537
|
+
iterations: allIterations,
|
|
538
|
+
completionSignal: lifecycleResult.result.completionSignal,
|
|
539
|
+
stdout: allStdout,
|
|
540
|
+
commits: allCommits,
|
|
541
|
+
branch: resolvedBranch,
|
|
542
|
+
preservedWorktreePath: iterationPreservedPath
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
yield* display.status(
|
|
547
|
+
label(`Reached max iterations (${iterations}).`),
|
|
548
|
+
"info"
|
|
549
|
+
);
|
|
550
|
+
return {
|
|
551
|
+
iterations: allIterations,
|
|
552
|
+
completionSignal: void 0,
|
|
553
|
+
stdout: allStdout,
|
|
554
|
+
commits: allCommits,
|
|
555
|
+
branch: resolvedBranch,
|
|
556
|
+
preservedWorktreePath: iterationPreservedPath
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// src/PromptResolver.ts
|
|
562
|
+
var resolvePrompt = (options) => {
|
|
563
|
+
const { prompt, promptFile } = options;
|
|
564
|
+
if (prompt !== void 0 && promptFile !== void 0) {
|
|
565
|
+
return Effect_exports.fail(
|
|
566
|
+
new PromptError({
|
|
567
|
+
message: "Cannot provide both --prompt and --prompt-file"
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
if (prompt !== void 0) {
|
|
572
|
+
return Effect_exports.succeed({ text: prompt, source: "inline" });
|
|
573
|
+
}
|
|
574
|
+
if (promptFile === void 0) {
|
|
575
|
+
return Effect_exports.fail(
|
|
576
|
+
new PromptError({
|
|
577
|
+
message: "Must provide either prompt or promptFile. Pass prompt: '...' or promptFile: './.sandcastle/prompt.md' to run()."
|
|
578
|
+
})
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
return Effect_exports.gen(function* () {
|
|
582
|
+
const fs = yield* FileSystem_exports.FileSystem;
|
|
583
|
+
const text2 = yield* fs.readFileString(promptFile).pipe(
|
|
584
|
+
Effect_exports.catchAll(
|
|
585
|
+
(e) => Effect_exports.fail(
|
|
586
|
+
new PromptError({
|
|
587
|
+
message: `Failed to read prompt from ${promptFile}: ${e}`
|
|
588
|
+
})
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
return { text: text2, source: "template" };
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
var parseEnvFile = (filePath) => Effect_exports.gen(function* () {
|
|
596
|
+
const fs = yield* FileSystem_exports.FileSystem;
|
|
597
|
+
const content = yield* fs.readFileString(filePath).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(null)));
|
|
598
|
+
if (content === null) return {};
|
|
599
|
+
const vars = {};
|
|
600
|
+
for (const line of content.split("\n")) {
|
|
601
|
+
const trimmed = line.trim();
|
|
602
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
603
|
+
const eqIndex = trimmed.indexOf("=");
|
|
604
|
+
if (eqIndex === -1) continue;
|
|
605
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
606
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
607
|
+
const isDoubleQuoted = value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"';
|
|
608
|
+
const isSingleQuoted = value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'";
|
|
609
|
+
if (isDoubleQuoted || isSingleQuoted) {
|
|
610
|
+
value = value.slice(1, -1);
|
|
611
|
+
}
|
|
612
|
+
if (isDoubleQuoted) {
|
|
613
|
+
value = value.replace(/\\([nrt\\])/g, (_, ch) => {
|
|
614
|
+
const escapes = {
|
|
615
|
+
n: "\n",
|
|
616
|
+
r: "\r",
|
|
617
|
+
t: " ",
|
|
618
|
+
"\\": "\\"
|
|
619
|
+
};
|
|
620
|
+
return escapes[ch] ?? ch;
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
vars[key] = value;
|
|
624
|
+
}
|
|
625
|
+
return vars;
|
|
626
|
+
});
|
|
627
|
+
var resolveEnv = (repoDir) => Effect_exports.gen(function* () {
|
|
628
|
+
const sandcastleEnv = yield* parseEnvFile(
|
|
629
|
+
join(repoDir, ".sandcastle", ".env")
|
|
630
|
+
);
|
|
631
|
+
const result = {};
|
|
632
|
+
for (const key of Object.keys(sandcastleEnv)) {
|
|
633
|
+
const value = sandcastleEnv[key] || process.env[key];
|
|
634
|
+
if (value) {
|
|
635
|
+
result[key] = value;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return result;
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// src/mergeProviderEnv.ts
|
|
642
|
+
var mergeProviderEnv = (options) => {
|
|
643
|
+
const { resolvedEnv, agentProviderEnv, sandboxProviderEnv } = options;
|
|
644
|
+
const agentKeys = Object.keys(agentProviderEnv);
|
|
645
|
+
const sandboxKeys = new Set(Object.keys(sandboxProviderEnv));
|
|
646
|
+
const overlapping = agentKeys.filter((k) => sandboxKeys.has(k));
|
|
647
|
+
if (overlapping.length > 0) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`Overlapping env keys between agent provider and sandbox provider: ${overlapping.join(", ")}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
...resolvedEnv,
|
|
654
|
+
...sandboxProviderEnv,
|
|
655
|
+
...agentProviderEnv
|
|
656
|
+
};
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// src/PromptArgumentSubstitution.ts
|
|
660
|
+
var SHELL_BLOCK_PATTERN = /!`([^`]+)`/g;
|
|
661
|
+
var BUILT_IN_PROMPT_ARG_KEYS = [
|
|
662
|
+
"SOURCE_BRANCH",
|
|
663
|
+
"TARGET_BRANCH"
|
|
664
|
+
];
|
|
665
|
+
var PLACEHOLDER_PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
666
|
+
var validateNoArgsWithInlinePrompt = (args) => {
|
|
667
|
+
if (Object.keys(args).length === 0) return Effect_exports.void;
|
|
668
|
+
return Effect_exports.fail(
|
|
669
|
+
new PromptError({
|
|
670
|
+
message: 'promptArgs is only supported with promptFile. Inline prompts (prompt: "...") are passed to the agent as-is \u2014 interpolate values directly in JavaScript, or switch to promptFile to use {{KEY}} substitution.'
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
};
|
|
674
|
+
var validateNoBuiltInArgOverride = (args) => {
|
|
675
|
+
for (const key of BUILT_IN_PROMPT_ARG_KEYS) {
|
|
676
|
+
if (key in args) {
|
|
677
|
+
return Effect_exports.fail(
|
|
678
|
+
new PromptError({
|
|
679
|
+
message: `"${key}" is a built-in prompt argument and cannot be overridden via promptArgs`
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return Effect_exports.void;
|
|
685
|
+
};
|
|
686
|
+
var findMissingPromptArgKeys = (prompt, providedArgs) => {
|
|
687
|
+
const matches = [...prompt.matchAll(PLACEHOLDER_PATTERN)];
|
|
688
|
+
const builtInSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
|
|
689
|
+
const seen = /* @__PURE__ */ new Set();
|
|
690
|
+
const missing = [];
|
|
691
|
+
for (const m of matches) {
|
|
692
|
+
const key = m[1];
|
|
693
|
+
if (seen.has(key)) continue;
|
|
694
|
+
seen.add(key);
|
|
695
|
+
if (builtInSet.has(key)) continue;
|
|
696
|
+
if (key in providedArgs && providedArgs[key] != null) continue;
|
|
697
|
+
missing.push(key);
|
|
698
|
+
}
|
|
699
|
+
return missing;
|
|
700
|
+
};
|
|
701
|
+
var substitutePromptArgs = (prompt, args, silentKeys) => {
|
|
702
|
+
const markedPrompt = prompt.replaceAll(SHELL_BLOCK_MARKER, "").replace(SHELL_BLOCK_PATTERN, `!${SHELL_BLOCK_MARKER}\`$1\``);
|
|
703
|
+
const sanitizedArgs = Object.fromEntries(
|
|
704
|
+
Object.entries(args).map(([key, value]) => [
|
|
705
|
+
key,
|
|
706
|
+
typeof value === "string" ? value.replaceAll(SHELL_BLOCK_MARKER, "") : value
|
|
707
|
+
])
|
|
708
|
+
);
|
|
709
|
+
const matches = [...markedPrompt.matchAll(PLACEHOLDER_PATTERN)];
|
|
710
|
+
if (matches.length === 0 && Object.keys(sanitizedArgs).length === 0) {
|
|
711
|
+
return Effect_exports.succeed(markedPrompt);
|
|
712
|
+
}
|
|
713
|
+
return Effect_exports.gen(function* () {
|
|
714
|
+
const display = yield* Display;
|
|
715
|
+
const referencedKeys = new Set(matches.map((m) => m[1]));
|
|
716
|
+
for (const key of referencedKeys) {
|
|
717
|
+
if (!(key in sanitizedArgs)) {
|
|
718
|
+
return yield* Effect_exports.fail(
|
|
719
|
+
new PromptError({
|
|
720
|
+
message: `Prompt argument "{{${key}}}" has no matching value in promptArgs`
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const value = sanitizedArgs[key];
|
|
725
|
+
if (value == null) {
|
|
726
|
+
return yield* Effect_exports.fail(
|
|
727
|
+
new PromptError({
|
|
728
|
+
message: `Prompt argument "{{${key}}}" has value ${value === null ? "null" : "undefined"} in promptArgs`
|
|
729
|
+
})
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const key of Object.keys(sanitizedArgs)) {
|
|
734
|
+
if (!referencedKeys.has(key) && !silentKeys?.has(key)) {
|
|
735
|
+
yield* display.status(
|
|
736
|
+
`Prompt argument "${key}" was provided but not referenced in the prompt`,
|
|
737
|
+
"warn"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const result = markedPrompt.replace(
|
|
742
|
+
PLACEHOLDER_PATTERN,
|
|
743
|
+
(_match, key) => sanitizedArgs[key].toString()
|
|
744
|
+
);
|
|
745
|
+
return result;
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// src/Output.ts
|
|
750
|
+
var Output = {
|
|
751
|
+
/**
|
|
752
|
+
* Declare an object-typed structured output extracted from an XML tag in
|
|
753
|
+
* the agent's stdout. The tag contents are JSON-parsed (with fence-aware
|
|
754
|
+
* unwrapping) and validated against the provided Standard Schema validator.
|
|
755
|
+
*
|
|
756
|
+
* Set `maxRetries` to have `run()` automatically resume the failed session
|
|
757
|
+
* and ask the agent to re-emit corrected output when extraction or
|
|
758
|
+
* validation fails. Default: `0` (no retries).
|
|
759
|
+
*/
|
|
760
|
+
object: (opts) => ({
|
|
761
|
+
_tag: "object",
|
|
762
|
+
tag: opts.tag,
|
|
763
|
+
schema: opts.schema,
|
|
764
|
+
maxRetries: opts.maxRetries
|
|
765
|
+
}),
|
|
766
|
+
/**
|
|
767
|
+
* Declare a string-typed structured output extracted from an XML tag in
|
|
768
|
+
* the agent's stdout. The tag contents are whitespace-trimmed and returned
|
|
769
|
+
* as a plain string — no JSON parsing, no schema validation.
|
|
770
|
+
*
|
|
771
|
+
* Set `maxRetries` to have `run()` automatically resume the failed session
|
|
772
|
+
* and ask the agent to re-emit corrected output when extraction fails.
|
|
773
|
+
* Default: `0` (no retries).
|
|
774
|
+
*/
|
|
775
|
+
string: (opts) => ({
|
|
776
|
+
_tag: "string",
|
|
777
|
+
tag: opts.tag,
|
|
778
|
+
maxRetries: opts.maxRetries
|
|
779
|
+
})
|
|
780
|
+
};
|
|
781
|
+
var StructuredOutputError = class extends Error {
|
|
782
|
+
tag;
|
|
783
|
+
rawMatched;
|
|
784
|
+
cause;
|
|
785
|
+
commits;
|
|
786
|
+
branch;
|
|
787
|
+
preservedWorktreePath;
|
|
788
|
+
/** Session ID of the iteration that produced the bad output, when available. */
|
|
789
|
+
sessionId;
|
|
790
|
+
/** Host path to the captured session JSONL, when the session was captured. */
|
|
791
|
+
sessionFilePath;
|
|
792
|
+
constructor(message, options) {
|
|
793
|
+
super(message);
|
|
794
|
+
this.name = "StructuredOutputError";
|
|
795
|
+
this.tag = options.tag;
|
|
796
|
+
this.rawMatched = options.rawMatched;
|
|
797
|
+
this.cause = options.cause;
|
|
798
|
+
this.commits = options.commits;
|
|
799
|
+
this.branch = options.branch;
|
|
800
|
+
this.preservedWorktreePath = options.preservedWorktreePath;
|
|
801
|
+
this.sessionId = options.sessionId;
|
|
802
|
+
this.sessionFilePath = options.sessionFilePath;
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// src/extractStructuredOutput.ts
|
|
807
|
+
var extractStructuredOutput = async (stdout, definition, context) => {
|
|
808
|
+
if (definition._tag === "object") {
|
|
809
|
+
return extractObject(
|
|
810
|
+
stdout,
|
|
811
|
+
definition,
|
|
812
|
+
context
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return extractString(stdout, definition, context);
|
|
816
|
+
};
|
|
817
|
+
var extractObject = async (stdout, definition, context) => {
|
|
818
|
+
const raw = findLastTagContent(stdout, definition.tag);
|
|
819
|
+
if (raw === void 0) {
|
|
820
|
+
throw new StructuredOutputError(
|
|
821
|
+
`Structured output tag <${definition.tag}> not found in agent output`,
|
|
822
|
+
{ tag: definition.tag, rawMatched: void 0, ...context }
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
const unwrapped = unwrapFences(raw.trim());
|
|
826
|
+
let parsed;
|
|
827
|
+
try {
|
|
828
|
+
parsed = JSON.parse(unwrapped);
|
|
829
|
+
} catch (cause) {
|
|
830
|
+
throw new StructuredOutputError(
|
|
831
|
+
`Structured output tag <${definition.tag}> contains invalid JSON`,
|
|
832
|
+
{ tag: definition.tag, rawMatched: raw, cause, ...context }
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const result = await definition.schema["~standard"].validate(parsed);
|
|
836
|
+
if (result.issues) {
|
|
837
|
+
throw new StructuredOutputError(
|
|
838
|
+
`Structured output tag <${definition.tag}> failed schema validation`,
|
|
839
|
+
{
|
|
840
|
+
tag: definition.tag,
|
|
841
|
+
rawMatched: raw,
|
|
842
|
+
cause: result.issues,
|
|
843
|
+
...context
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return result.value;
|
|
848
|
+
};
|
|
849
|
+
var extractString = (stdout, definition, context) => {
|
|
850
|
+
const raw = findLastTagContent(stdout, definition.tag);
|
|
851
|
+
if (raw === void 0) {
|
|
852
|
+
throw new StructuredOutputError(
|
|
853
|
+
`Structured output tag <${definition.tag}> not found in agent output`,
|
|
854
|
+
{ tag: definition.tag, rawMatched: void 0, ...context }
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
return raw.trim();
|
|
858
|
+
};
|
|
859
|
+
var findLastTagContent = (text2, tag) => {
|
|
860
|
+
const openTag = `<${tag}>`;
|
|
861
|
+
const closeTag = `</${tag}>`;
|
|
862
|
+
let lastContent;
|
|
863
|
+
let searchFrom = 0;
|
|
864
|
+
while (true) {
|
|
865
|
+
const openIdx = text2.indexOf(openTag, searchFrom);
|
|
866
|
+
if (openIdx === -1) break;
|
|
867
|
+
const contentStart = openIdx + openTag.length;
|
|
868
|
+
const closeIdx = text2.indexOf(closeTag, contentStart);
|
|
869
|
+
if (closeIdx === -1) break;
|
|
870
|
+
lastContent = text2.slice(contentStart, closeIdx);
|
|
871
|
+
searchFrom = closeIdx + closeTag.length;
|
|
872
|
+
}
|
|
873
|
+
return lastContent;
|
|
874
|
+
};
|
|
875
|
+
var unwrapFences = (text2) => {
|
|
876
|
+
const fenceMatch = text2.match(/^```(?:json)?\s*\n([\s\S]*?)\n\s*```\s*$/);
|
|
877
|
+
if (fenceMatch) {
|
|
878
|
+
return fenceMatch[1].trim();
|
|
879
|
+
}
|
|
880
|
+
return text2;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// src/run.ts
|
|
884
|
+
var buildStructuredOutputRetryFeedback = (error, retriesRemaining) => {
|
|
885
|
+
const raw = error.rawMatched === void 0 ? "(no matching tag was emitted)" : error.rawMatched;
|
|
886
|
+
const cause = error.cause === void 0 ? "(no parser detail)" : typeof error.cause === "string" ? error.cause : JSON.stringify(error.cause, null, 2);
|
|
887
|
+
return `Your previous response did not produce valid structured output.
|
|
888
|
+
|
|
889
|
+
Retries remaining after this attempt: ${retriesRemaining}.
|
|
890
|
+
|
|
891
|
+
Problem:
|
|
892
|
+
${error.message}
|
|
893
|
+
|
|
894
|
+
Parser detail:
|
|
895
|
+
${cause}
|
|
896
|
+
|
|
897
|
+
Previous matched output:
|
|
898
|
+
${raw}
|
|
899
|
+
|
|
900
|
+
Emit only a corrected <${error.tag}> block. Do not change files or run commands.`;
|
|
901
|
+
};
|
|
902
|
+
var DEFAULT_MAX_ITERATIONS = 1;
|
|
903
|
+
var sanitizeBranchForFilename = (branch) => branch.replace(/[/\\:*?"<>|]/g, "-");
|
|
904
|
+
var printFileDisplayStartup = (options) => {
|
|
905
|
+
const name = options.agentName ?? "Agent";
|
|
906
|
+
const label = styleText("bold", `[${name}]`);
|
|
907
|
+
const branchPart = options.branch ? ` on branch ${options.branch}` : "";
|
|
908
|
+
const hostRepoDir = options.hostRepoDir ?? process.cwd();
|
|
909
|
+
const displayLogPath = hostRepoDir === process.cwd() ? path.relative(process.cwd(), options.logPath) : options.logPath;
|
|
910
|
+
console.log(`${label} Started${branchPart}`);
|
|
911
|
+
console.log(styleText("dim", ` tail -f ${displayLogPath}`));
|
|
912
|
+
};
|
|
913
|
+
var buildLogFilename = (resolvedBranch, targetBranch, name) => {
|
|
914
|
+
const sanitized = sanitizeBranchForFilename(resolvedBranch);
|
|
915
|
+
const nameSuffix = name ? `-${name.toLowerCase().replace(/[^a-z0-9_.-]/g, "-")}` : "";
|
|
916
|
+
if (targetBranch) {
|
|
917
|
+
return `${sanitizeBranchForFilename(targetBranch)}-${sanitized}${nameSuffix}.log`;
|
|
918
|
+
}
|
|
919
|
+
return `${sanitized}${nameSuffix}.log`;
|
|
920
|
+
};
|
|
921
|
+
var buildRunSummaryRows = (options) => ({
|
|
922
|
+
Agent: options.name ?? options.agentName,
|
|
923
|
+
Sandbox: options.sandboxName,
|
|
924
|
+
"Max iterations": String(options.maxIterations),
|
|
925
|
+
Branch: options.branch
|
|
926
|
+
});
|
|
927
|
+
var buildCompletionMessage = (completionSignal, iterationsRun) => {
|
|
928
|
+
if (completionSignal !== void 0) {
|
|
929
|
+
return {
|
|
930
|
+
message: `Run complete: agent finished after ${iterationsRun} iteration(s).`,
|
|
931
|
+
severity: "success"
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
message: `Run complete: reached ${iterationsRun} iteration(s) without completion signal.`,
|
|
936
|
+
severity: "warn"
|
|
937
|
+
};
|
|
938
|
+
};
|
|
939
|
+
var formatContextWindowSize = (usage) => {
|
|
940
|
+
const total = usage.inputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
|
|
941
|
+
return `${Math.ceil(total / 1e3)}k`;
|
|
942
|
+
};
|
|
943
|
+
var buildContextWindowLines = (iterations) => iterations.filter((it) => it.usage !== void 0).map((it) => `Context window: ${formatContextWindowSize(it.usage)}`);
|
|
944
|
+
var buildAgentStreamHandler = (logging) => {
|
|
945
|
+
const userHandler = logging.type === "file" ? logging.onAgentStreamEvent : void 0;
|
|
946
|
+
const verboseSink = logging.verbose ? buildVerboseRawLineSink(logging) : void 0;
|
|
947
|
+
if (!userHandler && !verboseSink) return void 0;
|
|
948
|
+
return (event) => {
|
|
949
|
+
if (userHandler) {
|
|
950
|
+
try {
|
|
951
|
+
userHandler(event);
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (verboseSink && event.type === "raw") {
|
|
956
|
+
verboseSink(event.line);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
var buildVerboseRawLineSink = (logging) => {
|
|
961
|
+
if (logging.type === "file") {
|
|
962
|
+
const logPath = logging.path;
|
|
963
|
+
try {
|
|
964
|
+
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
965
|
+
} catch {
|
|
966
|
+
}
|
|
967
|
+
return (line) => {
|
|
968
|
+
try {
|
|
969
|
+
appendFileSync(logPath, line + "\n");
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
return (line) => {
|
|
975
|
+
process.stdout.write(line + "\n");
|
|
976
|
+
};
|
|
977
|
+
};
|
|
978
|
+
async function run(options) {
|
|
979
|
+
options.signal?.throwIfAborted();
|
|
980
|
+
const {
|
|
981
|
+
prompt,
|
|
982
|
+
promptFile,
|
|
983
|
+
maxIterations = DEFAULT_MAX_ITERATIONS,
|
|
984
|
+
hooks,
|
|
985
|
+
agent: provider
|
|
986
|
+
} = options;
|
|
987
|
+
const branchStrategy = options.branchStrategy ?? (options.sandbox.tag === "isolated" ? { type: "merge-to-head" } : { type: "head" });
|
|
988
|
+
const effectiveBranchType = branchStrategy.type;
|
|
989
|
+
if (effectiveBranchType === "head" && options.sandbox.tag === "isolated") {
|
|
990
|
+
throw new Error(
|
|
991
|
+
"head branch strategy is not supported with isolated providers"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (effectiveBranchType === "head" && options.copyToWorktree && options.copyToWorktree.length > 0) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
"copyToWorktree is not supported with head branch strategy. In head mode the host working directory is bind-mounted directly."
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (options.resumeSession && maxIterations > 1) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
"resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (options.forkSession && !options.resumeSession) {
|
|
1005
|
+
throw new Error(
|
|
1006
|
+
"forkSession requires resumeSession. Use RunResult.fork(prompt) to fork the last captured session."
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (options.output && maxIterations !== 1) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
"output requires maxIterations to be 1. Structured output is only supported for single-iteration runs."
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
const outputMaxRetries = options.output?.maxRetries ?? 0;
|
|
1015
|
+
if (outputMaxRetries < 0 || !Number.isInteger(outputMaxRetries)) {
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`output.maxRetries must be a non-negative integer. Received: ${outputMaxRetries}`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
if (outputMaxRetries > 0 && !provider.sessionStorage) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
`output.maxRetries requires an agent provider that supports session resumption. The "${provider.name}" provider does not. Use claudeCode, codex, or pi, or set maxRetries to 0.`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
|
|
1026
|
+
const hostRepoDir = await Effect_exports.runPromise(
|
|
1027
|
+
resolveCwd(options.cwd).pipe(Effect_exports.provide(NodeContext_exports.layer))
|
|
1028
|
+
);
|
|
1029
|
+
if (options.resumeSession) {
|
|
1030
|
+
await assertResumeSessionExists({
|
|
1031
|
+
provider,
|
|
1032
|
+
sandboxTag: options.sandbox.tag,
|
|
1033
|
+
hostRepoDir,
|
|
1034
|
+
resumeSession: options.resumeSession
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
const resolved = await Effect_exports.runPromise(
|
|
1038
|
+
resolvePrompt({ prompt, promptFile }).pipe(
|
|
1039
|
+
Effect_exports.provide(NodeContext_exports.layer)
|
|
1040
|
+
)
|
|
1041
|
+
);
|
|
1042
|
+
const rawPrompt = resolved.text;
|
|
1043
|
+
const isInlinePrompt = resolved.source === "inline";
|
|
1044
|
+
if (options.output) {
|
|
1045
|
+
const openTag = `<${options.output.tag}>`;
|
|
1046
|
+
if (!rawPrompt.includes(openTag)) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`output tag <${options.output.tag}> not found in the resolved prompt. The caller must instruct the agent to emit the configured tag.`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const agentName = provider.name;
|
|
1053
|
+
const resolvedEnv = await Effect_exports.runPromise(
|
|
1054
|
+
resolveEnv(hostRepoDir).pipe(Effect_exports.provide(NodeContext_exports.layer))
|
|
1055
|
+
);
|
|
1056
|
+
const env = mergeProviderEnv({
|
|
1057
|
+
resolvedEnv,
|
|
1058
|
+
agentProviderEnv: provider.env,
|
|
1059
|
+
sandboxProviderEnv: options.sandbox.env
|
|
1060
|
+
});
|
|
1061
|
+
const currentHostBranch = await Effect_exports.runPromise(
|
|
1062
|
+
getCurrentBranch(hostRepoDir)
|
|
1063
|
+
);
|
|
1064
|
+
const resolvedBranch = effectiveBranchType === "head" ? currentHostBranch : branch ?? generateTempBranchName(options.name);
|
|
1065
|
+
const targetBranch = effectiveBranchType === "merge-to-head" ? currentHostBranch : void 0;
|
|
1066
|
+
const resolvedLogging = options.logging ?? {
|
|
1067
|
+
type: "file",
|
|
1068
|
+
path: join(
|
|
1069
|
+
hostRepoDir,
|
|
1070
|
+
".sandcastle",
|
|
1071
|
+
"logs",
|
|
1072
|
+
buildLogFilename(resolvedBranch, targetBranch, options.name)
|
|
1073
|
+
)
|
|
1074
|
+
};
|
|
1075
|
+
const displayLayer = resolvedLogging.type === "file" ? (() => {
|
|
1076
|
+
printFileDisplayStartup({
|
|
1077
|
+
logPath: resolvedLogging.path,
|
|
1078
|
+
agentName: options.name,
|
|
1079
|
+
branch: resolvedBranch,
|
|
1080
|
+
hostRepoDir
|
|
1081
|
+
});
|
|
1082
|
+
return Layer_exports.provide(
|
|
1083
|
+
FileDisplay.layer(resolvedLogging.path),
|
|
1084
|
+
NodeFileSystem_exports.layer
|
|
1085
|
+
);
|
|
1086
|
+
})() : ClackDisplay.layer;
|
|
1087
|
+
const factoryLayer = Layer_exports.provide(
|
|
1088
|
+
WorktreeDockerSandboxFactory.layer,
|
|
1089
|
+
Layer_exports.mergeAll(
|
|
1090
|
+
Layer_exports.succeed(SandboxConfig, {
|
|
1091
|
+
env,
|
|
1092
|
+
hostRepoDir,
|
|
1093
|
+
copyToWorktree: options.copyToWorktree,
|
|
1094
|
+
name: options.name,
|
|
1095
|
+
sandboxProvider: options.sandbox,
|
|
1096
|
+
branchStrategy,
|
|
1097
|
+
hooks,
|
|
1098
|
+
signal: options.signal,
|
|
1099
|
+
timeouts: options.timeouts
|
|
1100
|
+
}),
|
|
1101
|
+
NodeFileSystem_exports.layer,
|
|
1102
|
+
displayLayer
|
|
1103
|
+
)
|
|
1104
|
+
);
|
|
1105
|
+
const streamEmitterLayer = agentStreamEmitterLayer(
|
|
1106
|
+
buildAgentStreamHandler(resolvedLogging)
|
|
1107
|
+
);
|
|
1108
|
+
const runLayer = Layer_exports.mergeAll(
|
|
1109
|
+
factoryLayer,
|
|
1110
|
+
displayLayer,
|
|
1111
|
+
streamEmitterLayer
|
|
1112
|
+
);
|
|
1113
|
+
const baseEffect = Effect_exports.gen(function* () {
|
|
1114
|
+
const d = yield* Display;
|
|
1115
|
+
yield* d.intro(options.name ?? "sandcastle");
|
|
1116
|
+
const rows = buildRunSummaryRows({
|
|
1117
|
+
name: options.name,
|
|
1118
|
+
agentName,
|
|
1119
|
+
sandboxName: options.sandbox.name,
|
|
1120
|
+
maxIterations,
|
|
1121
|
+
branch: resolvedBranch
|
|
1122
|
+
});
|
|
1123
|
+
yield* d.summary("Sandcastle Run", rows);
|
|
1124
|
+
const userArgs = options.promptArgs ?? {};
|
|
1125
|
+
let resolvedPrompt;
|
|
1126
|
+
if (isInlinePrompt) {
|
|
1127
|
+
yield* validateNoArgsWithInlinePrompt(userArgs);
|
|
1128
|
+
resolvedPrompt = rawPrompt;
|
|
1129
|
+
} else {
|
|
1130
|
+
yield* validateNoBuiltInArgOverride(userArgs);
|
|
1131
|
+
const effectiveArgs = {
|
|
1132
|
+
SOURCE_BRANCH: resolvedBranch,
|
|
1133
|
+
TARGET_BRANCH: currentHostBranch,
|
|
1134
|
+
...userArgs
|
|
1135
|
+
};
|
|
1136
|
+
const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
|
|
1137
|
+
resolvedPrompt = yield* substitutePromptArgs(
|
|
1138
|
+
rawPrompt,
|
|
1139
|
+
effectiveArgs,
|
|
1140
|
+
builtInArgKeysSet
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
const orchestrateBranch = effectiveBranchType === "head" ? currentHostBranch : branch;
|
|
1144
|
+
const orchestrateResult = yield* orchestrate({
|
|
1145
|
+
hostRepoDir,
|
|
1146
|
+
iterations: maxIterations,
|
|
1147
|
+
hooks,
|
|
1148
|
+
prompt: resolvedPrompt,
|
|
1149
|
+
branch: orchestrateBranch,
|
|
1150
|
+
provider,
|
|
1151
|
+
completionSignal: options.completionSignal,
|
|
1152
|
+
idleTimeoutSeconds: options.idleTimeoutSeconds,
|
|
1153
|
+
completionTimeoutSeconds: options.completionTimeoutSeconds,
|
|
1154
|
+
name: options.name,
|
|
1155
|
+
resumeSession: options.resumeSession,
|
|
1156
|
+
forkSession: options.forkSession,
|
|
1157
|
+
signal: options.signal,
|
|
1158
|
+
skipPromptExpansion: isInlinePrompt,
|
|
1159
|
+
timeouts: options.timeouts
|
|
1160
|
+
});
|
|
1161
|
+
const completion = buildCompletionMessage(
|
|
1162
|
+
orchestrateResult.completionSignal,
|
|
1163
|
+
orchestrateResult.iterations.length
|
|
1164
|
+
);
|
|
1165
|
+
yield* d.status(completion.message, completion.severity);
|
|
1166
|
+
for (const line of buildContextWindowLines(orchestrateResult.iterations)) {
|
|
1167
|
+
yield* d.text(line);
|
|
1168
|
+
}
|
|
1169
|
+
return orchestrateResult;
|
|
1170
|
+
});
|
|
1171
|
+
const withErrorLog = resolvedLogging.type === "file" ? baseEffect.pipe(
|
|
1172
|
+
Effect_exports.tapError(
|
|
1173
|
+
(error) => Effect_exports.gen(function* () {
|
|
1174
|
+
const d = yield* Display;
|
|
1175
|
+
yield* d.status(
|
|
1176
|
+
formatErrorMessage(error),
|
|
1177
|
+
"error"
|
|
1178
|
+
);
|
|
1179
|
+
})
|
|
1180
|
+
)
|
|
1181
|
+
) : baseEffect;
|
|
1182
|
+
let result;
|
|
1183
|
+
try {
|
|
1184
|
+
result = await Effect_exports.runPromise(
|
|
1185
|
+
withErrorLog.pipe(Effect_exports.provide(runLayer))
|
|
1186
|
+
);
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
options.signal?.throwIfAborted();
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1191
|
+
const baseResult = {
|
|
1192
|
+
...result,
|
|
1193
|
+
logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : void 0,
|
|
1194
|
+
resume: async (prompt2, resumeOptions) => {
|
|
1195
|
+
const lastIteration = result.iterations.at(-1);
|
|
1196
|
+
if (!lastIteration?.sessionId) {
|
|
1197
|
+
throw new Error("Cannot resume: no sessionId was captured");
|
|
1198
|
+
}
|
|
1199
|
+
return run({
|
|
1200
|
+
...options,
|
|
1201
|
+
...resumeOptions,
|
|
1202
|
+
prompt: prompt2,
|
|
1203
|
+
promptFile: void 0,
|
|
1204
|
+
maxIterations: 1,
|
|
1205
|
+
resumeSession: lastIteration.sessionId
|
|
1206
|
+
});
|
|
1207
|
+
},
|
|
1208
|
+
fork: async (prompt2, forkOptions) => {
|
|
1209
|
+
const lastIteration = result.iterations.at(-1);
|
|
1210
|
+
if (!lastIteration?.sessionId) {
|
|
1211
|
+
throw new Error("Cannot fork: no sessionId was captured");
|
|
1212
|
+
}
|
|
1213
|
+
return run({
|
|
1214
|
+
...options,
|
|
1215
|
+
...forkOptions,
|
|
1216
|
+
prompt: prompt2,
|
|
1217
|
+
promptFile: void 0,
|
|
1218
|
+
maxIterations: 1,
|
|
1219
|
+
resumeSession: lastIteration.sessionId,
|
|
1220
|
+
forkSession: true
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
if (options.output) {
|
|
1225
|
+
const lastIteration = baseResult.iterations.at(-1);
|
|
1226
|
+
try {
|
|
1227
|
+
const output = await extractStructuredOutput(
|
|
1228
|
+
baseResult.stdout,
|
|
1229
|
+
options.output,
|
|
1230
|
+
{
|
|
1231
|
+
commits: baseResult.commits,
|
|
1232
|
+
branch: baseResult.branch,
|
|
1233
|
+
preservedWorktreePath: baseResult.preservedWorktreePath,
|
|
1234
|
+
sessionId: lastIteration?.sessionId,
|
|
1235
|
+
sessionFilePath: lastIteration?.sessionFilePath
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
return { ...baseResult, output };
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
if (error instanceof StructuredOutputError && outputMaxRetries > 0 && error.sessionId !== void 0) {
|
|
1241
|
+
const retriesRemainingAfter = outputMaxRetries - 1;
|
|
1242
|
+
const retryOutput = {
|
|
1243
|
+
...options.output,
|
|
1244
|
+
maxRetries: retriesRemainingAfter
|
|
1245
|
+
};
|
|
1246
|
+
return run({
|
|
1247
|
+
...options,
|
|
1248
|
+
prompt: buildStructuredOutputRetryFeedback(
|
|
1249
|
+
error,
|
|
1250
|
+
retriesRemainingAfter
|
|
1251
|
+
),
|
|
1252
|
+
promptFile: void 0,
|
|
1253
|
+
promptArgs: void 0,
|
|
1254
|
+
resumeSession: error.sessionId,
|
|
1255
|
+
forkSession: false,
|
|
1256
|
+
output: retryOutput
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return baseResult;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/raceAbortSignal.ts
|
|
1266
|
+
var raceAbortSignal = (effect, signal) => {
|
|
1267
|
+
if (!signal) return effect;
|
|
1268
|
+
return Effect_exports.gen(function* () {
|
|
1269
|
+
if (signal.aborted) {
|
|
1270
|
+
return yield* Effect_exports.die(signal.reason);
|
|
1271
|
+
}
|
|
1272
|
+
const abortDeferred = yield* Deferred_exports.make();
|
|
1273
|
+
const onAbort = () => {
|
|
1274
|
+
Effect_exports.runPromise(Deferred_exports.die(abortDeferred, signal.reason)).catch(
|
|
1275
|
+
() => {
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1278
|
+
};
|
|
1279
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1280
|
+
return yield* Effect_exports.raceFirst(
|
|
1281
|
+
effect,
|
|
1282
|
+
Deferred_exports.await(abortDeferred)
|
|
1283
|
+
).pipe(
|
|
1284
|
+
Effect_exports.ensuring(
|
|
1285
|
+
Effect_exports.sync(() => signal.removeEventListener("abort", onAbort))
|
|
1286
|
+
)
|
|
1287
|
+
);
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// src/interactive.ts
|
|
1292
|
+
var interactive = async (options) => {
|
|
1293
|
+
options.signal?.throwIfAborted();
|
|
1294
|
+
const { prompt, promptFile, hooks, agent: provider } = options;
|
|
1295
|
+
const resolvedSandbox = options.sandbox ?? noSandbox();
|
|
1296
|
+
const branchStrategy = options.branchStrategy ?? (resolvedSandbox.tag === "isolated" ? { type: "merge-to-head" } : { type: "head" });
|
|
1297
|
+
if (branchStrategy.type === "head" && resolvedSandbox.tag === "isolated") {
|
|
1298
|
+
throw new Error(
|
|
1299
|
+
"head branch strategy is not supported with isolated providers"
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
if (branchStrategy.type === "head" && options.copyToWorktree && options.copyToWorktree.length > 0) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
"copyToWorktree is not supported with head branch strategy. In head mode the host working directory is bind-mounted directly."
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
if (!provider.buildInteractiveArgs) {
|
|
1308
|
+
throw new Error(
|
|
1309
|
+
`Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
|
|
1313
|
+
const isHeadMode = branchStrategy.type === "head";
|
|
1314
|
+
const sandboxProvider = resolvedSandbox;
|
|
1315
|
+
const inner = Effect_exports.gen(function* () {
|
|
1316
|
+
const hostRepoDir = yield* resolveCwd(options.cwd);
|
|
1317
|
+
const d = yield* Display;
|
|
1318
|
+
const hasPromptSource = prompt !== void 0 || promptFile !== void 0;
|
|
1319
|
+
const resolved = hasPromptSource ? yield* resolvePrompt({ prompt, promptFile }) : void 0;
|
|
1320
|
+
const rawPrompt = resolved?.text ?? "";
|
|
1321
|
+
const isInlinePrompt = resolved?.source === "inline";
|
|
1322
|
+
const resolvedEnv = yield* resolveEnv(hostRepoDir);
|
|
1323
|
+
const env = mergeProviderEnv({
|
|
1324
|
+
resolvedEnv,
|
|
1325
|
+
agentProviderEnv: provider.env,
|
|
1326
|
+
sandboxProviderEnv: sandboxProvider.env
|
|
1327
|
+
});
|
|
1328
|
+
const effectiveEnv = { ...env, ...options.env ?? {} };
|
|
1329
|
+
const currentHostBranch = yield* getCurrentBranch(hostRepoDir);
|
|
1330
|
+
const resolvedBranch = branchStrategy.type === "head" ? currentHostBranch : branch ?? generateTempBranchName(options.name);
|
|
1331
|
+
let substitutedPrompt = rawPrompt;
|
|
1332
|
+
if (hasPromptSource && !isInlinePrompt) {
|
|
1333
|
+
const userArgs = options.promptArgs ?? {};
|
|
1334
|
+
yield* validateNoBuiltInArgOverride(userArgs);
|
|
1335
|
+
const missingKeys = findMissingPromptArgKeys(rawPrompt, userArgs);
|
|
1336
|
+
const collectedArgs = {};
|
|
1337
|
+
for (const key of missingKeys) {
|
|
1338
|
+
const value = yield* Effect_exports.promise(
|
|
1339
|
+
() => clack.text({
|
|
1340
|
+
message: `Enter value for {{${key}}}`,
|
|
1341
|
+
validate: (v) => {
|
|
1342
|
+
if (!v) return `A value is required for {{${key}}}`;
|
|
1343
|
+
}
|
|
1344
|
+
})
|
|
1345
|
+
);
|
|
1346
|
+
if (clack.isCancel(value)) {
|
|
1347
|
+
clack.cancel("Prompt arg collection cancelled.");
|
|
1348
|
+
return yield* Effect_exports.fail(
|
|
1349
|
+
new Error("User cancelled prompt arg collection")
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
collectedArgs[key] = value;
|
|
1353
|
+
}
|
|
1354
|
+
const mergedUserArgs = { ...userArgs, ...collectedArgs };
|
|
1355
|
+
const effectiveArgs = {
|
|
1356
|
+
SOURCE_BRANCH: resolvedBranch,
|
|
1357
|
+
TARGET_BRANCH: currentHostBranch,
|
|
1358
|
+
...mergedUserArgs
|
|
1359
|
+
};
|
|
1360
|
+
const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
|
|
1361
|
+
substitutedPrompt = yield* substitutePromptArgs(
|
|
1362
|
+
rawPrompt,
|
|
1363
|
+
effectiveArgs,
|
|
1364
|
+
builtInArgKeysSet
|
|
1365
|
+
);
|
|
1366
|
+
} else if (isInlinePrompt) {
|
|
1367
|
+
const userArgs = options.promptArgs ?? {};
|
|
1368
|
+
yield* validateNoArgsWithInlinePrompt(userArgs);
|
|
1369
|
+
}
|
|
1370
|
+
const lifecycleBranch = isHeadMode ? currentHostBranch : branch;
|
|
1371
|
+
yield* d.intro(options.name ?? "sandcastle interactive");
|
|
1372
|
+
yield* d.summary("Interactive Session", {
|
|
1373
|
+
Agent: options.name ?? provider.name,
|
|
1374
|
+
Sandbox: sandboxProvider.name,
|
|
1375
|
+
Branch: resolvedBranch
|
|
1376
|
+
});
|
|
1377
|
+
let worktreeInfo;
|
|
1378
|
+
if (!isHeadMode) {
|
|
1379
|
+
worktreeInfo = yield* d.taskLog(
|
|
1380
|
+
"Creating worktree",
|
|
1381
|
+
() => pruneStale(hostRepoDir).pipe(
|
|
1382
|
+
Effect_exports.catchAll(() => Effect_exports.void),
|
|
1383
|
+
Effect_exports.andThen(
|
|
1384
|
+
branch ? create(hostRepoDir, { branch }) : create(hostRepoDir, { name: options.name })
|
|
1385
|
+
)
|
|
1386
|
+
)
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const handle = yield* Effect_exports.gen(function* () {
|
|
1390
|
+
if (!isHeadMode) {
|
|
1391
|
+
if ((sandboxProvider.tag === "bind-mount" || sandboxProvider.tag === "none") && options.copyToWorktree && options.copyToWorktree.length > 0) {
|
|
1392
|
+
yield* d.taskLog(
|
|
1393
|
+
"Copying files to worktree",
|
|
1394
|
+
() => copyToWorktree(
|
|
1395
|
+
options.copyToWorktree,
|
|
1396
|
+
hostRepoDir,
|
|
1397
|
+
worktreeInfo.path,
|
|
1398
|
+
options.timeouts?.copyToWorktreeMs
|
|
1399
|
+
)
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
if (hooks?.host?.onWorktreeReady?.length) {
|
|
1403
|
+
yield* runHostHooks(hooks.host.onWorktreeReady, worktreeInfo.path);
|
|
1404
|
+
}
|
|
1405
|
+
} else if (hooks?.host?.onWorktreeReady?.length) {
|
|
1406
|
+
yield* runHostHooks(hooks.host.onWorktreeReady, hostRepoDir);
|
|
1407
|
+
}
|
|
1408
|
+
if (sandboxProvider.tag === "none") {
|
|
1409
|
+
const worktreePath = isHeadMode ? hostRepoDir : worktreeInfo.path;
|
|
1410
|
+
return yield* Effect_exports.promise(
|
|
1411
|
+
() => sandboxProvider.create({
|
|
1412
|
+
worktreePath,
|
|
1413
|
+
env: effectiveEnv
|
|
1414
|
+
})
|
|
1415
|
+
);
|
|
1416
|
+
} else if (sandboxProvider.tag === "isolated") {
|
|
1417
|
+
const startResult = yield* d.taskLog(
|
|
1418
|
+
"Starting sandbox",
|
|
1419
|
+
() => startSandbox({
|
|
1420
|
+
provider: sandboxProvider,
|
|
1421
|
+
hostRepoDir: worktreeInfo.path,
|
|
1422
|
+
env: effectiveEnv,
|
|
1423
|
+
copyPaths: options.copyToWorktree
|
|
1424
|
+
})
|
|
1425
|
+
);
|
|
1426
|
+
return startResult.handle;
|
|
1427
|
+
} else {
|
|
1428
|
+
const gitPath = join(hostRepoDir, ".git");
|
|
1429
|
+
const rawGitMounts = yield* resolveGitMounts(gitPath);
|
|
1430
|
+
const worktreeOrRepoPath = isHeadMode ? hostRepoDir : worktreeInfo.path;
|
|
1431
|
+
const gitMounts = yield* patchGitMountsForWindows(
|
|
1432
|
+
rawGitMounts,
|
|
1433
|
+
worktreeOrRepoPath,
|
|
1434
|
+
SANDBOX_REPO_DIR
|
|
1435
|
+
);
|
|
1436
|
+
const startResult = yield* d.taskLog(
|
|
1437
|
+
"Starting sandbox",
|
|
1438
|
+
() => startSandbox({
|
|
1439
|
+
provider: sandboxProvider,
|
|
1440
|
+
hostRepoDir,
|
|
1441
|
+
env: effectiveEnv,
|
|
1442
|
+
worktreeOrRepoPath,
|
|
1443
|
+
gitMounts,
|
|
1444
|
+
repoDir: SANDBOX_REPO_DIR
|
|
1445
|
+
})
|
|
1446
|
+
);
|
|
1447
|
+
return startResult.handle;
|
|
1448
|
+
}
|
|
1449
|
+
}).pipe(
|
|
1450
|
+
Effect_exports.tapError(
|
|
1451
|
+
() => worktreeInfo ? remove(worktreeInfo.path).pipe(
|
|
1452
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
1453
|
+
) : Effect_exports.void
|
|
1454
|
+
)
|
|
1455
|
+
);
|
|
1456
|
+
return yield* Effect_exports.gen(function* () {
|
|
1457
|
+
if (!handle.interactiveExec) {
|
|
1458
|
+
throw new Error(
|
|
1459
|
+
`Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
const interactiveExecFn = handle.interactiveExec.bind(handle);
|
|
1463
|
+
const sandbox = makeSandboxFromHandle(handle);
|
|
1464
|
+
const worktreePath = handle.worktreePath;
|
|
1465
|
+
const applyToHost = sandboxProvider.tag === "isolated" && worktreeInfo ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
|
|
1466
|
+
const lifecycleEffect = withSandboxLifecycle(
|
|
1467
|
+
{
|
|
1468
|
+
hostRepoDir,
|
|
1469
|
+
sandboxRepoDir: worktreePath,
|
|
1470
|
+
hooks,
|
|
1471
|
+
branch: lifecycleBranch,
|
|
1472
|
+
hostWorktreePath: isHeadMode ? hostRepoDir : worktreeInfo?.path,
|
|
1473
|
+
applyToHost,
|
|
1474
|
+
timeouts: options.timeouts
|
|
1475
|
+
},
|
|
1476
|
+
sandbox,
|
|
1477
|
+
(ctx) => Effect_exports.gen(function* () {
|
|
1478
|
+
const fullPrompt = !hasPromptSource || isInlinePrompt ? substitutedPrompt : yield* preprocessPrompt(
|
|
1479
|
+
substitutedPrompt,
|
|
1480
|
+
ctx.sandbox,
|
|
1481
|
+
ctx.sandboxRepoDir
|
|
1482
|
+
);
|
|
1483
|
+
const interactiveArgs = provider.buildInteractiveArgs({
|
|
1484
|
+
prompt: fullPrompt,
|
|
1485
|
+
dangerouslySkipPermissions: sandboxProvider.tag !== "none"
|
|
1486
|
+
});
|
|
1487
|
+
const result2 = yield* raceAbortSignal(
|
|
1488
|
+
Effect_exports.promise(
|
|
1489
|
+
() => interactiveExecFn(interactiveArgs, {
|
|
1490
|
+
stdin: process.stdin,
|
|
1491
|
+
stdout: process.stdout,
|
|
1492
|
+
stderr: process.stderr,
|
|
1493
|
+
cwd: worktreePath
|
|
1494
|
+
})
|
|
1495
|
+
),
|
|
1496
|
+
options.signal
|
|
1497
|
+
);
|
|
1498
|
+
return result2.exitCode;
|
|
1499
|
+
})
|
|
1500
|
+
);
|
|
1501
|
+
const lifecycleResult = yield* lifecycleEffect;
|
|
1502
|
+
const exitCode = lifecycleResult.result;
|
|
1503
|
+
let preservedWorktreePath;
|
|
1504
|
+
if (worktreeInfo) {
|
|
1505
|
+
const hasUncommitted = yield* hasUncommittedChanges(
|
|
1506
|
+
worktreeInfo.path
|
|
1507
|
+
).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
|
|
1508
|
+
if (hasUncommitted) {
|
|
1509
|
+
preservedWorktreePath = worktreeInfo.path;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (worktreeInfo && !preservedWorktreePath) {
|
|
1513
|
+
yield* remove(worktreeInfo.path).pipe(
|
|
1514
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
yield* d.summary("Session Complete", {
|
|
1518
|
+
Commits: String(lifecycleResult.commits.length),
|
|
1519
|
+
Branch: lifecycleResult.branch,
|
|
1520
|
+
"Exit code": String(exitCode),
|
|
1521
|
+
...preservedWorktreePath ? { "Preserved worktree": preservedWorktreePath } : {}
|
|
1522
|
+
});
|
|
1523
|
+
return {
|
|
1524
|
+
commits: lifecycleResult.commits,
|
|
1525
|
+
branch: lifecycleResult.branch,
|
|
1526
|
+
preservedWorktreePath,
|
|
1527
|
+
exitCode
|
|
1528
|
+
};
|
|
1529
|
+
}).pipe(
|
|
1530
|
+
// On error, always clean up worktree (on success, handled above with preserve check)
|
|
1531
|
+
Effect_exports.tapError(
|
|
1532
|
+
() => worktreeInfo ? remove(worktreeInfo.path).pipe(
|
|
1533
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
1534
|
+
) : Effect_exports.void
|
|
1535
|
+
),
|
|
1536
|
+
// Always close sandbox handle
|
|
1537
|
+
Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
|
|
1538
|
+
})))
|
|
1539
|
+
);
|
|
1540
|
+
});
|
|
1541
|
+
let result;
|
|
1542
|
+
try {
|
|
1543
|
+
result = await Effect_exports.runPromise(
|
|
1544
|
+
inner.pipe(
|
|
1545
|
+
Effect_exports.provide(ClackDisplay.layer),
|
|
1546
|
+
Effect_exports.provide(NodeContext_exports.layer),
|
|
1547
|
+
Effect_exports.provide(NodeFileSystem_exports.layer)
|
|
1548
|
+
)
|
|
1549
|
+
);
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
options.signal?.throwIfAborted();
|
|
1552
|
+
throw error;
|
|
1553
|
+
}
|
|
1554
|
+
return result;
|
|
1555
|
+
};
|
|
1556
|
+
var buildSandboxHandle = (ctx, close) => {
|
|
1557
|
+
const {
|
|
1558
|
+
branch,
|
|
1559
|
+
worktreePath,
|
|
1560
|
+
hostRepoDir,
|
|
1561
|
+
sandboxRepoDir,
|
|
1562
|
+
sandbox,
|
|
1563
|
+
providerHandle,
|
|
1564
|
+
bindMountHandle,
|
|
1565
|
+
applyToHost,
|
|
1566
|
+
timeouts,
|
|
1567
|
+
branchStrategy
|
|
1568
|
+
} = ctx;
|
|
1569
|
+
const mergeToHead = branchStrategy?.type === "merge-to-head";
|
|
1570
|
+
const sandboxHandle = {
|
|
1571
|
+
branch,
|
|
1572
|
+
worktreePath,
|
|
1573
|
+
run: async (runOptions) => {
|
|
1574
|
+
runOptions.signal?.throwIfAborted();
|
|
1575
|
+
const {
|
|
1576
|
+
agent: provider,
|
|
1577
|
+
prompt,
|
|
1578
|
+
promptFile,
|
|
1579
|
+
maxIterations = 1
|
|
1580
|
+
} = runOptions;
|
|
1581
|
+
if (runOptions.resumeSession && maxIterations > 1) {
|
|
1582
|
+
throw new Error(
|
|
1583
|
+
"resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
if (runOptions.forkSession && !runOptions.resumeSession) {
|
|
1587
|
+
throw new Error(
|
|
1588
|
+
"forkSession requires resumeSession. Use sandboxRunResult.fork(prompt) to fork the most recent captured session."
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
if (runOptions.resumeSession) {
|
|
1592
|
+
await assertResumeSessionExists({
|
|
1593
|
+
provider,
|
|
1594
|
+
sandboxTag: ctx.providerTag,
|
|
1595
|
+
hostRepoDir,
|
|
1596
|
+
resumeSession: runOptions.resumeSession
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
const resolved = await Effect_exports.runPromise(
|
|
1600
|
+
resolvePrompt({ prompt, promptFile }).pipe(
|
|
1601
|
+
Effect_exports.provide(NodeContext_exports.layer)
|
|
1602
|
+
)
|
|
1603
|
+
);
|
|
1604
|
+
const rawPrompt = resolved.text;
|
|
1605
|
+
const isInlinePrompt = resolved.source === "inline";
|
|
1606
|
+
const userArgs = runOptions.promptArgs ?? {};
|
|
1607
|
+
const currentHostBranch = await Effect_exports.runPromise(
|
|
1608
|
+
getCurrentBranch(hostRepoDir)
|
|
1609
|
+
);
|
|
1610
|
+
const displayRef = Ref_exports.unsafeMake([]);
|
|
1611
|
+
const silentDisplayLayer = SilentDisplay.layer(displayRef);
|
|
1612
|
+
const resolvedPrompt = await Effect_exports.runPromise(
|
|
1613
|
+
Effect_exports.gen(function* () {
|
|
1614
|
+
if (isInlinePrompt) {
|
|
1615
|
+
yield* validateNoArgsWithInlinePrompt(userArgs);
|
|
1616
|
+
return rawPrompt;
|
|
1617
|
+
}
|
|
1618
|
+
yield* validateNoBuiltInArgOverride(userArgs);
|
|
1619
|
+
const effectiveArgs = {
|
|
1620
|
+
SOURCE_BRANCH: branch,
|
|
1621
|
+
TARGET_BRANCH: currentHostBranch,
|
|
1622
|
+
...userArgs
|
|
1623
|
+
};
|
|
1624
|
+
const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
|
|
1625
|
+
return yield* substitutePromptArgs(
|
|
1626
|
+
rawPrompt,
|
|
1627
|
+
effectiveArgs,
|
|
1628
|
+
builtInArgKeysSet
|
|
1629
|
+
);
|
|
1630
|
+
}).pipe(Effect_exports.provide(silentDisplayLayer))
|
|
1631
|
+
);
|
|
1632
|
+
const resolvedLogging = runOptions.logging ?? {
|
|
1633
|
+
type: "file",
|
|
1634
|
+
path: join(
|
|
1635
|
+
hostRepoDir,
|
|
1636
|
+
".sandcastle",
|
|
1637
|
+
"logs",
|
|
1638
|
+
buildLogFilename(branch, void 0, runOptions.name)
|
|
1639
|
+
)
|
|
1640
|
+
};
|
|
1641
|
+
const runDisplayLayer = resolvedLogging.type === "file" ? (() => {
|
|
1642
|
+
printFileDisplayStartup({
|
|
1643
|
+
logPath: resolvedLogging.path,
|
|
1644
|
+
agentName: runOptions.name,
|
|
1645
|
+
branch
|
|
1646
|
+
});
|
|
1647
|
+
return Layer_exports.provide(
|
|
1648
|
+
FileDisplay.layer(resolvedLogging.path),
|
|
1649
|
+
NodeFileSystem_exports.layer
|
|
1650
|
+
);
|
|
1651
|
+
})() : silentDisplayLayer;
|
|
1652
|
+
const reuseFactoryLayer = Layer_exports.succeed(SandboxFactory, {
|
|
1653
|
+
withSandbox: (makeEffect) => makeEffect(
|
|
1654
|
+
{
|
|
1655
|
+
hostWorktreePath: worktreePath,
|
|
1656
|
+
sandboxRepoPath: sandboxRepoDir,
|
|
1657
|
+
applyToHost,
|
|
1658
|
+
bindMountHandle
|
|
1659
|
+
},
|
|
1660
|
+
sandbox
|
|
1661
|
+
).pipe(
|
|
1662
|
+
Effect_exports.map((value) => ({
|
|
1663
|
+
value,
|
|
1664
|
+
preservedWorktreePath: void 0
|
|
1665
|
+
}))
|
|
1666
|
+
)
|
|
1667
|
+
});
|
|
1668
|
+
const streamEmitterLayer = agentStreamEmitterLayer(
|
|
1669
|
+
buildAgentStreamHandler(resolvedLogging)
|
|
1670
|
+
);
|
|
1671
|
+
const runLayer = Layer_exports.mergeAll(
|
|
1672
|
+
reuseFactoryLayer,
|
|
1673
|
+
runDisplayLayer,
|
|
1674
|
+
streamEmitterLayer
|
|
1675
|
+
);
|
|
1676
|
+
let result;
|
|
1677
|
+
try {
|
|
1678
|
+
result = await Effect_exports.runPromise(
|
|
1679
|
+
Effect_exports.gen(function* () {
|
|
1680
|
+
const display = yield* Display;
|
|
1681
|
+
yield* display.intro(runOptions.name ?? "sandcastle");
|
|
1682
|
+
const orchestrateResult = yield* orchestrate({
|
|
1683
|
+
hostRepoDir,
|
|
1684
|
+
iterations: maxIterations,
|
|
1685
|
+
prompt: resolvedPrompt,
|
|
1686
|
+
branch: mergeToHead ? void 0 : branch,
|
|
1687
|
+
provider,
|
|
1688
|
+
completionSignal: runOptions.completionSignal,
|
|
1689
|
+
idleTimeoutSeconds: runOptions.idleTimeoutSeconds,
|
|
1690
|
+
completionTimeoutSeconds: runOptions.completionTimeoutSeconds,
|
|
1691
|
+
name: runOptions.name,
|
|
1692
|
+
resumeSession: runOptions.resumeSession,
|
|
1693
|
+
forkSession: runOptions.forkSession,
|
|
1694
|
+
signal: runOptions.signal,
|
|
1695
|
+
skipPromptExpansion: isInlinePrompt,
|
|
1696
|
+
timeouts,
|
|
1697
|
+
keepSourceBranch: mergeToHead
|
|
1698
|
+
});
|
|
1699
|
+
const completion = buildCompletionMessage(
|
|
1700
|
+
orchestrateResult.completionSignal,
|
|
1701
|
+
orchestrateResult.iterations.length
|
|
1702
|
+
);
|
|
1703
|
+
yield* display.status(completion.message, completion.severity);
|
|
1704
|
+
for (const line of buildContextWindowLines(
|
|
1705
|
+
orchestrateResult.iterations
|
|
1706
|
+
)) {
|
|
1707
|
+
yield* display.text(line);
|
|
1708
|
+
}
|
|
1709
|
+
return orchestrateResult;
|
|
1710
|
+
}).pipe(Effect_exports.provide(runLayer))
|
|
1711
|
+
);
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
runOptions.signal?.throwIfAborted();
|
|
1714
|
+
throw error;
|
|
1715
|
+
}
|
|
1716
|
+
const baseResult = {
|
|
1717
|
+
iterations: result.iterations,
|
|
1718
|
+
completionSignal: result.completionSignal,
|
|
1719
|
+
stdout: result.stdout,
|
|
1720
|
+
commits: result.commits,
|
|
1721
|
+
logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : void 0
|
|
1722
|
+
};
|
|
1723
|
+
const lastIteration = result.iterations.at(-1);
|
|
1724
|
+
if (provider.sessionStorage && lastIteration?.sessionId) {
|
|
1725
|
+
const capturedSessionId = lastIteration.sessionId;
|
|
1726
|
+
return {
|
|
1727
|
+
...baseResult,
|
|
1728
|
+
resume: (nextPrompt, resumeOptions) => sandboxHandle.run({
|
|
1729
|
+
...runOptions,
|
|
1730
|
+
...resumeOptions,
|
|
1731
|
+
prompt: nextPrompt,
|
|
1732
|
+
promptFile: void 0,
|
|
1733
|
+
maxIterations: 1,
|
|
1734
|
+
resumeSession: capturedSessionId
|
|
1735
|
+
}),
|
|
1736
|
+
fork: (nextPrompt, forkOptions) => sandboxHandle.run({
|
|
1737
|
+
...runOptions,
|
|
1738
|
+
...forkOptions,
|
|
1739
|
+
prompt: nextPrompt,
|
|
1740
|
+
promptFile: void 0,
|
|
1741
|
+
maxIterations: 1,
|
|
1742
|
+
resumeSession: capturedSessionId,
|
|
1743
|
+
forkSession: true
|
|
1744
|
+
})
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
return baseResult;
|
|
1748
|
+
},
|
|
1749
|
+
interactive: async (interactiveOptions) => {
|
|
1750
|
+
interactiveOptions.signal?.throwIfAborted();
|
|
1751
|
+
const { agent: provider, prompt, promptFile } = interactiveOptions;
|
|
1752
|
+
if (!provider.buildInteractiveArgs) {
|
|
1753
|
+
throw new Error(
|
|
1754
|
+
`Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
if (!providerHandle?.interactiveExec) {
|
|
1758
|
+
throw new Error(
|
|
1759
|
+
`Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
const interactiveExecFn = providerHandle.interactiveExec.bind(providerHandle);
|
|
1763
|
+
let lifecycleResult;
|
|
1764
|
+
try {
|
|
1765
|
+
lifecycleResult = await Effect_exports.runPromise(
|
|
1766
|
+
Effect_exports.gen(function* () {
|
|
1767
|
+
const resolved = yield* resolvePrompt({ prompt, promptFile });
|
|
1768
|
+
const rawPrompt = resolved.text;
|
|
1769
|
+
const isInlinePrompt = resolved.source === "inline";
|
|
1770
|
+
const userArgs = interactiveOptions.promptArgs ?? {};
|
|
1771
|
+
const currentHostBranch = yield* getCurrentBranch(hostRepoDir);
|
|
1772
|
+
let resolvedPrompt;
|
|
1773
|
+
if (isInlinePrompt) {
|
|
1774
|
+
yield* validateNoArgsWithInlinePrompt(userArgs);
|
|
1775
|
+
resolvedPrompt = rawPrompt;
|
|
1776
|
+
} else {
|
|
1777
|
+
yield* validateNoBuiltInArgOverride(userArgs);
|
|
1778
|
+
const effectiveArgs = {
|
|
1779
|
+
SOURCE_BRANCH: branch,
|
|
1780
|
+
TARGET_BRANCH: currentHostBranch,
|
|
1781
|
+
...userArgs
|
|
1782
|
+
};
|
|
1783
|
+
const builtInArgKeysSet = new Set(
|
|
1784
|
+
BUILT_IN_PROMPT_ARG_KEYS
|
|
1785
|
+
);
|
|
1786
|
+
resolvedPrompt = yield* substitutePromptArgs(
|
|
1787
|
+
rawPrompt,
|
|
1788
|
+
effectiveArgs,
|
|
1789
|
+
builtInArgKeysSet
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
return yield* withSandboxLifecycle(
|
|
1793
|
+
{
|
|
1794
|
+
hostRepoDir,
|
|
1795
|
+
sandboxRepoDir,
|
|
1796
|
+
branch: mergeToHead ? void 0 : branch,
|
|
1797
|
+
hostWorktreePath: worktreePath,
|
|
1798
|
+
applyToHost,
|
|
1799
|
+
timeouts,
|
|
1800
|
+
keepSourceBranch: mergeToHead
|
|
1801
|
+
},
|
|
1802
|
+
sandbox,
|
|
1803
|
+
(ctx2) => Effect_exports.gen(function* () {
|
|
1804
|
+
const fullPrompt = isInlinePrompt ? resolvedPrompt : yield* preprocessPrompt(
|
|
1805
|
+
resolvedPrompt,
|
|
1806
|
+
ctx2.sandbox,
|
|
1807
|
+
ctx2.sandboxRepoDir
|
|
1808
|
+
);
|
|
1809
|
+
const interactiveArgs = provider.buildInteractiveArgs({
|
|
1810
|
+
prompt: fullPrompt,
|
|
1811
|
+
dangerouslySkipPermissions: true
|
|
1812
|
+
});
|
|
1813
|
+
const execPromise = interactiveExecFn(interactiveArgs, {
|
|
1814
|
+
stdin: process.stdin,
|
|
1815
|
+
stdout: process.stdout,
|
|
1816
|
+
stderr: process.stderr,
|
|
1817
|
+
cwd: sandboxRepoDir
|
|
1818
|
+
});
|
|
1819
|
+
const signal = interactiveOptions.signal;
|
|
1820
|
+
const result = yield* Effect_exports.promise(() => {
|
|
1821
|
+
if (!signal) return execPromise;
|
|
1822
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
1823
|
+
return new Promise(
|
|
1824
|
+
(resolve, reject) => {
|
|
1825
|
+
const onAbort = () => reject(signal.reason);
|
|
1826
|
+
signal.addEventListener("abort", onAbort, {
|
|
1827
|
+
once: true
|
|
1828
|
+
});
|
|
1829
|
+
execPromise.then(
|
|
1830
|
+
(r) => {
|
|
1831
|
+
signal.removeEventListener("abort", onAbort);
|
|
1832
|
+
resolve(r);
|
|
1833
|
+
},
|
|
1834
|
+
(e) => {
|
|
1835
|
+
signal.removeEventListener("abort", onAbort);
|
|
1836
|
+
reject(e);
|
|
1837
|
+
}
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
);
|
|
1841
|
+
});
|
|
1842
|
+
return result.exitCode;
|
|
1843
|
+
})
|
|
1844
|
+
);
|
|
1845
|
+
}).pipe(
|
|
1846
|
+
Effect_exports.provide(ClackDisplay.layer),
|
|
1847
|
+
Effect_exports.provide(NodeContext_exports.layer)
|
|
1848
|
+
)
|
|
1849
|
+
);
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
interactiveOptions.signal?.throwIfAborted();
|
|
1852
|
+
throw error;
|
|
1853
|
+
}
|
|
1854
|
+
return {
|
|
1855
|
+
commits: lifecycleResult.commits,
|
|
1856
|
+
exitCode: lifecycleResult.result
|
|
1857
|
+
};
|
|
1858
|
+
},
|
|
1859
|
+
exec: async (command, options) => {
|
|
1860
|
+
const mergedOptions = { cwd: sandboxRepoDir, ...options };
|
|
1861
|
+
if (providerHandle) {
|
|
1862
|
+
return providerHandle.exec(command, mergedOptions);
|
|
1863
|
+
}
|
|
1864
|
+
return Effect_exports.runPromise(sandbox.exec(command, mergedOptions));
|
|
1865
|
+
},
|
|
1866
|
+
close: async () => close(),
|
|
1867
|
+
[Symbol.asyncDispose]: async () => {
|
|
1868
|
+
await sandboxHandle.close();
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
return sandboxHandle;
|
|
1872
|
+
};
|
|
1873
|
+
var createSandboxFromWorktree = async (options) => {
|
|
1874
|
+
const { branch, worktreePath, hostRepoDir } = options;
|
|
1875
|
+
const isTestMode = !!options._test?.buildSandbox;
|
|
1876
|
+
if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
|
|
1877
|
+
await Effect_exports.runPromise(
|
|
1878
|
+
copyToWorktree(
|
|
1879
|
+
options.copyToWorktree,
|
|
1880
|
+
hostRepoDir,
|
|
1881
|
+
worktreePath,
|
|
1882
|
+
options.timeouts?.copyToWorktreeMs
|
|
1883
|
+
)
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
let providerHandle;
|
|
1887
|
+
let sandbox;
|
|
1888
|
+
let sandboxRepoDir;
|
|
1889
|
+
const isIsolated = options.sandbox.tag === "isolated";
|
|
1890
|
+
if (isTestMode) {
|
|
1891
|
+
sandbox = options._test.buildSandbox(worktreePath);
|
|
1892
|
+
sandboxRepoDir = worktreePath;
|
|
1893
|
+
providerHandle = options._test.bindMountHandle;
|
|
1894
|
+
} else {
|
|
1895
|
+
const resolvedEnv = await Effect_exports.runPromise(
|
|
1896
|
+
resolveEnv(hostRepoDir).pipe(Effect_exports.provide(NodeContext_exports.layer))
|
|
1897
|
+
);
|
|
1898
|
+
const env = mergeProviderEnv({
|
|
1899
|
+
resolvedEnv,
|
|
1900
|
+
agentProviderEnv: {},
|
|
1901
|
+
sandboxProviderEnv: options.sandbox.env
|
|
1902
|
+
});
|
|
1903
|
+
const provider = options.sandbox;
|
|
1904
|
+
let startEffect;
|
|
1905
|
+
if (provider.tag === "isolated") {
|
|
1906
|
+
startEffect = startSandbox({
|
|
1907
|
+
provider,
|
|
1908
|
+
hostRepoDir: worktreePath,
|
|
1909
|
+
env,
|
|
1910
|
+
copyPaths: options.copyToWorktree
|
|
1911
|
+
});
|
|
1912
|
+
} else if (provider.tag === "none") {
|
|
1913
|
+
startEffect = startSandbox({
|
|
1914
|
+
provider,
|
|
1915
|
+
hostRepoDir,
|
|
1916
|
+
env,
|
|
1917
|
+
worktreeOrRepoPath: worktreePath
|
|
1918
|
+
});
|
|
1919
|
+
} else {
|
|
1920
|
+
startEffect = resolveGitMounts(join(hostRepoDir, ".git")).pipe(
|
|
1921
|
+
Effect_exports.provide(NodeFileSystem_exports.layer),
|
|
1922
|
+
Effect_exports.catchAll(() => Effect_exports.succeed([])),
|
|
1923
|
+
// Patch git mounts for Windows worktree compatibility (ADR-0006)
|
|
1924
|
+
Effect_exports.flatMap(
|
|
1925
|
+
(gitMounts) => patchGitMountsForWindows(gitMounts, worktreePath, SANDBOX_REPO_DIR)
|
|
1926
|
+
),
|
|
1927
|
+
Effect_exports.flatMap(
|
|
1928
|
+
(gitMounts) => startSandbox({
|
|
1929
|
+
provider,
|
|
1930
|
+
hostRepoDir,
|
|
1931
|
+
env,
|
|
1932
|
+
worktreeOrRepoPath: worktreePath,
|
|
1933
|
+
gitMounts,
|
|
1934
|
+
repoDir: SANDBOX_REPO_DIR
|
|
1935
|
+
})
|
|
1936
|
+
)
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
const startResult = await Effect_exports.runPromise(startEffect);
|
|
1940
|
+
providerHandle = startResult.handle;
|
|
1941
|
+
sandbox = startResult.sandbox;
|
|
1942
|
+
sandboxRepoDir = startResult.worktreePath;
|
|
1943
|
+
}
|
|
1944
|
+
const sandboxOnReady = options.hooks?.sandbox?.onSandboxReady;
|
|
1945
|
+
const hostOnReady = options.hooks?.host?.onSandboxReady;
|
|
1946
|
+
if (sandboxOnReady?.length || hostOnReady?.length) {
|
|
1947
|
+
await Effect_exports.runPromise(
|
|
1948
|
+
Effect_exports.gen(function* () {
|
|
1949
|
+
yield* sandbox.exec(
|
|
1950
|
+
`git config --global --add safe.directory "${sandboxRepoDir}"`
|
|
1951
|
+
);
|
|
1952
|
+
const sandboxEffects = (sandboxOnReady ?? []).map(
|
|
1953
|
+
(hook) => sandbox.exec(hook.command, {
|
|
1954
|
+
cwd: sandboxRepoDir,
|
|
1955
|
+
sudo: hook.sudo
|
|
1956
|
+
})
|
|
1957
|
+
);
|
|
1958
|
+
const allEffects = [...sandboxEffects];
|
|
1959
|
+
if (hostOnReady?.length) {
|
|
1960
|
+
allEffects.push(runHostHooks(hostOnReady, worktreePath));
|
|
1961
|
+
}
|
|
1962
|
+
yield* Effect_exports.all(allEffects, {
|
|
1963
|
+
concurrency: "unbounded"
|
|
1964
|
+
});
|
|
1965
|
+
})
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect_exports.void;
|
|
1969
|
+
let closed = false;
|
|
1970
|
+
const bindMountHandle = options.sandbox.tag === "bind-mount" ? providerHandle : void 0;
|
|
1971
|
+
return buildSandboxHandle(
|
|
1972
|
+
{
|
|
1973
|
+
branch,
|
|
1974
|
+
worktreePath,
|
|
1975
|
+
hostRepoDir,
|
|
1976
|
+
sandboxRepoDir,
|
|
1977
|
+
sandbox,
|
|
1978
|
+
providerHandle,
|
|
1979
|
+
bindMountHandle,
|
|
1980
|
+
providerTag: options.sandbox.tag,
|
|
1981
|
+
applyToHost,
|
|
1982
|
+
timeouts: options.timeouts,
|
|
1983
|
+
branchStrategy: options.branchStrategy
|
|
1984
|
+
},
|
|
1985
|
+
async () => {
|
|
1986
|
+
if (closed) return { preservedWorktreePath: void 0 };
|
|
1987
|
+
closed = true;
|
|
1988
|
+
if (providerHandle) await providerHandle.close();
|
|
1989
|
+
return { preservedWorktreePath: void 0 };
|
|
1990
|
+
}
|
|
1991
|
+
);
|
|
1992
|
+
};
|
|
1993
|
+
var createSandbox = async (options) => {
|
|
1994
|
+
const { branch } = options;
|
|
1995
|
+
const isTestMode = !!options._test?.buildSandbox;
|
|
1996
|
+
const isIsolated = options.sandbox.tag === "isolated";
|
|
1997
|
+
const { hostRepoDir, worktreePath, providerHandle, sandbox, sandboxRepoDir } = await Effect_exports.runPromise(
|
|
1998
|
+
Effect_exports.gen(function* () {
|
|
1999
|
+
const hostRepoDir2 = yield* resolveCwd(options.cwd);
|
|
2000
|
+
yield* pruneStale(hostRepoDir2).pipe(
|
|
2001
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
2002
|
+
);
|
|
2003
|
+
const { path: worktreePath2 } = yield* create(
|
|
2004
|
+
hostRepoDir2,
|
|
2005
|
+
{ branch, baseBranch: options.baseBranch }
|
|
2006
|
+
);
|
|
2007
|
+
const prepared = yield* Effect_exports.gen(function* () {
|
|
2008
|
+
if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
|
|
2009
|
+
yield* copyToWorktree(
|
|
2010
|
+
options.copyToWorktree,
|
|
2011
|
+
hostRepoDir2,
|
|
2012
|
+
worktreePath2,
|
|
2013
|
+
options.timeouts?.copyToWorktreeMs
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
if (options.hooks?.host?.onWorktreeReady?.length) {
|
|
2017
|
+
yield* runHostHooks(
|
|
2018
|
+
options.hooks.host.onWorktreeReady,
|
|
2019
|
+
worktreePath2
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
let providerHandle2;
|
|
2023
|
+
let sandbox2;
|
|
2024
|
+
let sandboxRepoDir2;
|
|
2025
|
+
if (isTestMode) {
|
|
2026
|
+
sandbox2 = options._test.buildSandbox(worktreePath2);
|
|
2027
|
+
sandboxRepoDir2 = worktreePath2;
|
|
2028
|
+
providerHandle2 = options._test.bindMountHandle;
|
|
2029
|
+
} else {
|
|
2030
|
+
const resolvedEnv = yield* resolveEnv(hostRepoDir2);
|
|
2031
|
+
const env = mergeProviderEnv({
|
|
2032
|
+
resolvedEnv,
|
|
2033
|
+
agentProviderEnv: {},
|
|
2034
|
+
sandboxProviderEnv: options.sandbox.env
|
|
2035
|
+
});
|
|
2036
|
+
const provider = options.sandbox;
|
|
2037
|
+
const startResult = yield* provider.tag === "isolated" ? startSandbox({
|
|
2038
|
+
provider,
|
|
2039
|
+
hostRepoDir: worktreePath2,
|
|
2040
|
+
env,
|
|
2041
|
+
copyPaths: options.copyToWorktree
|
|
2042
|
+
}) : provider.tag === "none" ? startSandbox({
|
|
2043
|
+
provider,
|
|
2044
|
+
hostRepoDir: hostRepoDir2,
|
|
2045
|
+
env,
|
|
2046
|
+
worktreeOrRepoPath: worktreePath2
|
|
2047
|
+
}) : resolveGitMounts(join(hostRepoDir2, ".git")).pipe(
|
|
2048
|
+
Effect_exports.provide(NodeFileSystem_exports.layer),
|
|
2049
|
+
Effect_exports.catchAll(() => Effect_exports.succeed([])),
|
|
2050
|
+
// Patch git mounts for Windows worktree compatibility (ADR-0006)
|
|
2051
|
+
Effect_exports.flatMap(
|
|
2052
|
+
(gitMounts) => patchGitMountsForWindows(
|
|
2053
|
+
gitMounts,
|
|
2054
|
+
worktreePath2,
|
|
2055
|
+
SANDBOX_REPO_DIR
|
|
2056
|
+
)
|
|
2057
|
+
),
|
|
2058
|
+
Effect_exports.flatMap(
|
|
2059
|
+
(gitMounts) => startSandbox({
|
|
2060
|
+
provider,
|
|
2061
|
+
hostRepoDir: hostRepoDir2,
|
|
2062
|
+
env,
|
|
2063
|
+
worktreeOrRepoPath: worktreePath2,
|
|
2064
|
+
gitMounts,
|
|
2065
|
+
repoDir: SANDBOX_REPO_DIR
|
|
2066
|
+
})
|
|
2067
|
+
)
|
|
2068
|
+
);
|
|
2069
|
+
providerHandle2 = startResult.handle;
|
|
2070
|
+
sandbox2 = startResult.sandbox;
|
|
2071
|
+
sandboxRepoDir2 = startResult.worktreePath;
|
|
2072
|
+
}
|
|
2073
|
+
const sandboxOnReady = options.hooks?.sandbox?.onSandboxReady;
|
|
2074
|
+
const hostOnReady = options.hooks?.host?.onSandboxReady;
|
|
2075
|
+
if (sandboxOnReady?.length || hostOnReady?.length) {
|
|
2076
|
+
yield* Effect_exports.gen(function* () {
|
|
2077
|
+
yield* sandbox2.exec(
|
|
2078
|
+
`git config --global --add safe.directory "${sandboxRepoDir2}"`
|
|
2079
|
+
);
|
|
2080
|
+
const sandboxEffects = (sandboxOnReady ?? []).map(
|
|
2081
|
+
(hook) => sandbox2.exec(hook.command, {
|
|
2082
|
+
cwd: sandboxRepoDir2,
|
|
2083
|
+
sudo: hook.sudo
|
|
2084
|
+
})
|
|
2085
|
+
);
|
|
2086
|
+
const allEffects = [...sandboxEffects];
|
|
2087
|
+
if (hostOnReady?.length) {
|
|
2088
|
+
allEffects.push(runHostHooks(hostOnReady, worktreePath2));
|
|
2089
|
+
}
|
|
2090
|
+
yield* Effect_exports.all(allEffects, { concurrency: "unbounded" });
|
|
2091
|
+
}).pipe(
|
|
2092
|
+
Effect_exports.onError(
|
|
2093
|
+
() => providerHandle2 ? Effect_exports.promise(
|
|
2094
|
+
() => providerHandle2.close().catch(() => {
|
|
2095
|
+
})
|
|
2096
|
+
) : Effect_exports.void
|
|
2097
|
+
)
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
return { providerHandle: providerHandle2, sandbox: sandbox2, sandboxRepoDir: sandboxRepoDir2 };
|
|
2101
|
+
}).pipe(
|
|
2102
|
+
Effect_exports.onError(
|
|
2103
|
+
() => remove(worktreePath2).pipe(
|
|
2104
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
2105
|
+
)
|
|
2106
|
+
)
|
|
2107
|
+
);
|
|
2108
|
+
return { hostRepoDir: hostRepoDir2, worktreePath: worktreePath2, ...prepared };
|
|
2109
|
+
}).pipe(Effect_exports.provide(NodeContext_exports.layer))
|
|
2110
|
+
);
|
|
2111
|
+
const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect_exports.void;
|
|
2112
|
+
let closed = false;
|
|
2113
|
+
const forceCleanup = () => {
|
|
2114
|
+
console.error(`
|
|
2115
|
+
Worktree preserved at ${worktreePath}`);
|
|
2116
|
+
console.error(` To review: cd ${worktreePath}`);
|
|
2117
|
+
console.error(` To clean up: git worktree remove --force ${worktreePath}`);
|
|
2118
|
+
};
|
|
2119
|
+
const unregisterShutdown = registerShutdown(forceCleanup);
|
|
2120
|
+
const doClose = async () => {
|
|
2121
|
+
if (closed) return { preservedWorktreePath: void 0 };
|
|
2122
|
+
closed = true;
|
|
2123
|
+
return Effect_exports.runPromise(
|
|
2124
|
+
Effect_exports.gen(function* () {
|
|
2125
|
+
if (providerHandle) {
|
|
2126
|
+
yield* Effect_exports.promise(() => providerHandle.close());
|
|
2127
|
+
}
|
|
2128
|
+
const isDirty = yield* hasUncommittedChanges(
|
|
2129
|
+
worktreePath
|
|
2130
|
+
).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
|
|
2131
|
+
if (isDirty) {
|
|
2132
|
+
return { preservedWorktreePath: worktreePath };
|
|
2133
|
+
}
|
|
2134
|
+
yield* remove(worktreePath).pipe(
|
|
2135
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
2136
|
+
);
|
|
2137
|
+
return { preservedWorktreePath: void 0 };
|
|
2138
|
+
})
|
|
2139
|
+
);
|
|
2140
|
+
};
|
|
2141
|
+
const bindMountHandle = options.sandbox.tag === "bind-mount" ? providerHandle : void 0;
|
|
2142
|
+
return buildSandboxHandle(
|
|
2143
|
+
{
|
|
2144
|
+
branch,
|
|
2145
|
+
worktreePath,
|
|
2146
|
+
hostRepoDir,
|
|
2147
|
+
sandboxRepoDir,
|
|
2148
|
+
sandbox,
|
|
2149
|
+
providerHandle,
|
|
2150
|
+
bindMountHandle,
|
|
2151
|
+
providerTag: options.sandbox.tag,
|
|
2152
|
+
applyToHost,
|
|
2153
|
+
timeouts: options.timeouts
|
|
2154
|
+
},
|
|
2155
|
+
async () => {
|
|
2156
|
+
unregisterShutdown();
|
|
2157
|
+
return doClose();
|
|
2158
|
+
}
|
|
2159
|
+
);
|
|
2160
|
+
};
|
|
2161
|
+
var createWorktree = async (options) => {
|
|
2162
|
+
const branch = options.branchStrategy.type === "branch" ? options.branchStrategy.branch : void 0;
|
|
2163
|
+
const baseBranch = options.branchStrategy.type === "branch" ? options.branchStrategy.baseBranch : void 0;
|
|
2164
|
+
const isMergeToHead = options.branchStrategy.type === "merge-to-head";
|
|
2165
|
+
const { hostRepoDir, worktreeInfo } = await Effect_exports.gen(function* () {
|
|
2166
|
+
const hostRepoDir2 = yield* resolveCwd(options.cwd);
|
|
2167
|
+
yield* pruneStale(hostRepoDir2).pipe(
|
|
2168
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
2169
|
+
);
|
|
2170
|
+
const info = yield* create(hostRepoDir2, {
|
|
2171
|
+
branch,
|
|
2172
|
+
baseBranch
|
|
2173
|
+
});
|
|
2174
|
+
if (options.copyToWorktree && options.copyToWorktree.length > 0) {
|
|
2175
|
+
yield* copyToWorktree(
|
|
2176
|
+
options.copyToWorktree,
|
|
2177
|
+
hostRepoDir2,
|
|
2178
|
+
info.path,
|
|
2179
|
+
options.timeouts?.copyToWorktreeMs
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
if (options.hooks?.host?.onWorktreeReady?.length) {
|
|
2183
|
+
yield* runHostHooks(options.hooks.host.onWorktreeReady, info.path);
|
|
2184
|
+
}
|
|
2185
|
+
return { hostRepoDir: hostRepoDir2, worktreeInfo: info };
|
|
2186
|
+
}).pipe(Effect_exports.provide(NodeContext_exports.layer), Effect_exports.runPromise);
|
|
2187
|
+
let closed = false;
|
|
2188
|
+
const close = async () => {
|
|
2189
|
+
if (closed) return { preservedWorktreePath: void 0 };
|
|
2190
|
+
closed = true;
|
|
2191
|
+
return Effect_exports.gen(function* () {
|
|
2192
|
+
const isDirty = yield* hasUncommittedChanges(
|
|
2193
|
+
worktreeInfo.path
|
|
2194
|
+
).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
|
|
2195
|
+
if (isDirty) {
|
|
2196
|
+
return { preservedWorktreePath: worktreeInfo.path };
|
|
2197
|
+
}
|
|
2198
|
+
yield* remove(worktreeInfo.path).pipe(
|
|
2199
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
2200
|
+
);
|
|
2201
|
+
return { preservedWorktreePath: void 0 };
|
|
2202
|
+
}).pipe(Effect_exports.runPromise);
|
|
2203
|
+
};
|
|
2204
|
+
const worktreeInteractive = async (opts) => {
|
|
2205
|
+
opts.signal?.throwIfAborted();
|
|
2206
|
+
const { prompt, promptFile, hooks, agent: provider } = opts;
|
|
2207
|
+
const resolvedSandbox = opts.sandbox ?? noSandbox();
|
|
2208
|
+
if (!provider.buildInteractiveArgs) {
|
|
2209
|
+
throw new Error(
|
|
2210
|
+
`Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
const inner = Effect_exports.gen(function* () {
|
|
2214
|
+
const d = yield* Display;
|
|
2215
|
+
const hasPromptSource = prompt !== void 0 || promptFile !== void 0;
|
|
2216
|
+
const resolved = hasPromptSource ? yield* resolvePrompt({ prompt, promptFile }) : void 0;
|
|
2217
|
+
const rawPrompt = resolved?.text ?? "";
|
|
2218
|
+
const isInlinePrompt = resolved?.source === "inline";
|
|
2219
|
+
const resolvedEnv = yield* resolveEnv(hostRepoDir);
|
|
2220
|
+
const env = mergeProviderEnv({
|
|
2221
|
+
resolvedEnv,
|
|
2222
|
+
agentProviderEnv: provider.env,
|
|
2223
|
+
sandboxProviderEnv: resolvedSandbox.env
|
|
2224
|
+
});
|
|
2225
|
+
const effectiveEnv = { ...env, ...opts.env ?? {} };
|
|
2226
|
+
let substitutedPrompt = rawPrompt;
|
|
2227
|
+
if (hasPromptSource && !isInlinePrompt) {
|
|
2228
|
+
const userArgs = opts.promptArgs ?? {};
|
|
2229
|
+
yield* validateNoBuiltInArgOverride(userArgs);
|
|
2230
|
+
const effectiveArgs = {
|
|
2231
|
+
SOURCE_BRANCH: worktreeInfo.branch,
|
|
2232
|
+
TARGET_BRANCH: worktreeInfo.branch,
|
|
2233
|
+
...userArgs
|
|
2234
|
+
};
|
|
2235
|
+
const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
|
|
2236
|
+
substitutedPrompt = yield* substitutePromptArgs(
|
|
2237
|
+
rawPrompt,
|
|
2238
|
+
effectiveArgs,
|
|
2239
|
+
builtInArgKeysSet
|
|
2240
|
+
);
|
|
2241
|
+
} else if (isInlinePrompt) {
|
|
2242
|
+
yield* validateNoArgsWithInlinePrompt(opts.promptArgs ?? {});
|
|
2243
|
+
}
|
|
2244
|
+
yield* d.intro(opts.name ?? "sandcastle interactive");
|
|
2245
|
+
yield* d.summary("Interactive Session", {
|
|
2246
|
+
Agent: opts.name ?? provider.name,
|
|
2247
|
+
Sandbox: resolvedSandbox.name,
|
|
2248
|
+
Branch: worktreeInfo.branch
|
|
2249
|
+
});
|
|
2250
|
+
let handle;
|
|
2251
|
+
if (resolvedSandbox.tag === "none") {
|
|
2252
|
+
handle = yield* Effect_exports.promise(
|
|
2253
|
+
() => resolvedSandbox.create({
|
|
2254
|
+
worktreePath: worktreeInfo.path,
|
|
2255
|
+
env: effectiveEnv
|
|
2256
|
+
})
|
|
2257
|
+
);
|
|
2258
|
+
} else if (resolvedSandbox.tag === "isolated") {
|
|
2259
|
+
const startResult = yield* d.taskLog(
|
|
2260
|
+
"Starting sandbox",
|
|
2261
|
+
() => startSandbox({
|
|
2262
|
+
provider: resolvedSandbox,
|
|
2263
|
+
hostRepoDir: worktreeInfo.path,
|
|
2264
|
+
env: effectiveEnv
|
|
2265
|
+
})
|
|
2266
|
+
);
|
|
2267
|
+
handle = startResult.handle;
|
|
2268
|
+
} else {
|
|
2269
|
+
const gitPath = join(hostRepoDir, ".git");
|
|
2270
|
+
const rawGitMounts = yield* resolveGitMounts(gitPath);
|
|
2271
|
+
const gitMounts = yield* patchGitMountsForWindows(
|
|
2272
|
+
rawGitMounts,
|
|
2273
|
+
worktreeInfo.path,
|
|
2274
|
+
SANDBOX_REPO_DIR
|
|
2275
|
+
);
|
|
2276
|
+
const startResult = yield* d.taskLog(
|
|
2277
|
+
"Starting sandbox",
|
|
2278
|
+
() => startSandbox({
|
|
2279
|
+
provider: resolvedSandbox,
|
|
2280
|
+
hostRepoDir,
|
|
2281
|
+
env: effectiveEnv,
|
|
2282
|
+
worktreeOrRepoPath: worktreeInfo.path,
|
|
2283
|
+
gitMounts,
|
|
2284
|
+
repoDir: SANDBOX_REPO_DIR
|
|
2285
|
+
})
|
|
2286
|
+
);
|
|
2287
|
+
handle = startResult.handle;
|
|
2288
|
+
}
|
|
2289
|
+
return yield* Effect_exports.gen(function* () {
|
|
2290
|
+
if (!handle.interactiveExec) {
|
|
2291
|
+
throw new Error(
|
|
2292
|
+
`Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
const interactiveExecFn = handle.interactiveExec.bind(handle);
|
|
2296
|
+
const sandbox = makeSandboxFromHandle(handle);
|
|
2297
|
+
const worktreePath = handle.worktreePath;
|
|
2298
|
+
const applyToHost = resolvedSandbox.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
|
|
2299
|
+
const lifecycleEffect = withSandboxLifecycle(
|
|
2300
|
+
{
|
|
2301
|
+
hostRepoDir,
|
|
2302
|
+
sandboxRepoDir: worktreePath,
|
|
2303
|
+
hooks,
|
|
2304
|
+
// merge-to-head: pass `undefined` so the lifecycle records the
|
|
2305
|
+
// host's current branch and merges the worktree's commits back into
|
|
2306
|
+
// it. branch strategy: pin to the worktree's branch.
|
|
2307
|
+
branch: isMergeToHead ? void 0 : worktreeInfo.branch,
|
|
2308
|
+
hostWorktreePath: worktreeInfo.path,
|
|
2309
|
+
applyToHost,
|
|
2310
|
+
timeouts: options.timeouts,
|
|
2311
|
+
keepSourceBranch: isMergeToHead
|
|
2312
|
+
},
|
|
2313
|
+
sandbox,
|
|
2314
|
+
(ctx) => Effect_exports.gen(function* () {
|
|
2315
|
+
const fullPrompt = !hasPromptSource || isInlinePrompt ? substitutedPrompt : yield* preprocessPrompt(
|
|
2316
|
+
substitutedPrompt,
|
|
2317
|
+
ctx.sandbox,
|
|
2318
|
+
ctx.sandboxRepoDir
|
|
2319
|
+
);
|
|
2320
|
+
const interactiveArgs = provider.buildInteractiveArgs({
|
|
2321
|
+
prompt: fullPrompt,
|
|
2322
|
+
dangerouslySkipPermissions: resolvedSandbox.tag !== "none"
|
|
2323
|
+
});
|
|
2324
|
+
const result = yield* raceAbortSignal(
|
|
2325
|
+
Effect_exports.promise(
|
|
2326
|
+
() => interactiveExecFn(interactiveArgs, {
|
|
2327
|
+
stdin: process.stdin,
|
|
2328
|
+
stdout: process.stdout,
|
|
2329
|
+
stderr: process.stderr,
|
|
2330
|
+
cwd: worktreePath
|
|
2331
|
+
})
|
|
2332
|
+
),
|
|
2333
|
+
opts.signal
|
|
2334
|
+
);
|
|
2335
|
+
return result.exitCode;
|
|
2336
|
+
})
|
|
2337
|
+
);
|
|
2338
|
+
const lifecycleResult = yield* lifecycleEffect;
|
|
2339
|
+
const exitCode = lifecycleResult.result;
|
|
2340
|
+
yield* d.summary("Session Complete", {
|
|
2341
|
+
Commits: String(lifecycleResult.commits.length),
|
|
2342
|
+
Branch: lifecycleResult.branch,
|
|
2343
|
+
"Exit code": String(exitCode)
|
|
2344
|
+
});
|
|
2345
|
+
return {
|
|
2346
|
+
commits: lifecycleResult.commits,
|
|
2347
|
+
branch: lifecycleResult.branch,
|
|
2348
|
+
preservedWorktreePath: void 0,
|
|
2349
|
+
exitCode
|
|
2350
|
+
};
|
|
2351
|
+
}).pipe(
|
|
2352
|
+
// Always close sandbox handle
|
|
2353
|
+
Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
|
|
2354
|
+
})))
|
|
2355
|
+
);
|
|
2356
|
+
});
|
|
2357
|
+
try {
|
|
2358
|
+
return await Effect_exports.runPromise(
|
|
2359
|
+
inner.pipe(
|
|
2360
|
+
Effect_exports.provide(ClackDisplay.layer),
|
|
2361
|
+
Effect_exports.provide(NodeContext_exports.layer),
|
|
2362
|
+
Effect_exports.provide(NodeFileSystem_exports.layer)
|
|
2363
|
+
)
|
|
2364
|
+
);
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
opts.signal?.throwIfAborted();
|
|
2367
|
+
throw error;
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
const worktreeRun = async (opts) => {
|
|
2371
|
+
opts.signal?.throwIfAborted();
|
|
2372
|
+
const { prompt, promptFile, hooks, agent: provider } = opts;
|
|
2373
|
+
const sandboxProvider = opts.sandbox;
|
|
2374
|
+
const maxIterations = opts.maxIterations ?? 1;
|
|
2375
|
+
if (opts.resumeSession && maxIterations > 1) {
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
"resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
if (opts.resumeSession) {
|
|
2381
|
+
await assertResumeSessionExists({
|
|
2382
|
+
provider,
|
|
2383
|
+
sandboxTag: sandboxProvider.tag,
|
|
2384
|
+
hostRepoDir,
|
|
2385
|
+
resumeSession: opts.resumeSession
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
const inner = Effect_exports.gen(function* () {
|
|
2389
|
+
const resolved = yield* resolvePrompt({ prompt, promptFile });
|
|
2390
|
+
const rawPrompt = resolved.text;
|
|
2391
|
+
const isInlinePrompt = resolved.source === "inline";
|
|
2392
|
+
const resolvedEnv = yield* resolveEnv(hostRepoDir);
|
|
2393
|
+
const env = mergeProviderEnv({
|
|
2394
|
+
resolvedEnv,
|
|
2395
|
+
agentProviderEnv: provider.env,
|
|
2396
|
+
sandboxProviderEnv: sandboxProvider.env
|
|
2397
|
+
});
|
|
2398
|
+
const effectiveEnv = { ...env, ...opts.env ?? {} };
|
|
2399
|
+
const userArgs = opts.promptArgs ?? {};
|
|
2400
|
+
let resolvedPrompt;
|
|
2401
|
+
if (isInlinePrompt) {
|
|
2402
|
+
yield* validateNoArgsWithInlinePrompt(userArgs);
|
|
2403
|
+
resolvedPrompt = rawPrompt;
|
|
2404
|
+
} else {
|
|
2405
|
+
yield* validateNoBuiltInArgOverride(userArgs);
|
|
2406
|
+
const effectiveArgs = {
|
|
2407
|
+
SOURCE_BRANCH: worktreeInfo.branch,
|
|
2408
|
+
TARGET_BRANCH: worktreeInfo.branch,
|
|
2409
|
+
...userArgs
|
|
2410
|
+
};
|
|
2411
|
+
const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
|
|
2412
|
+
resolvedPrompt = yield* substitutePromptArgs(
|
|
2413
|
+
rawPrompt,
|
|
2414
|
+
effectiveArgs,
|
|
2415
|
+
builtInArgKeysSet
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
let handle;
|
|
2419
|
+
let sandboxRepoDir;
|
|
2420
|
+
if (sandboxProvider.tag === "isolated") {
|
|
2421
|
+
const startResult = yield* startSandbox({
|
|
2422
|
+
provider: sandboxProvider,
|
|
2423
|
+
hostRepoDir: worktreeInfo.path,
|
|
2424
|
+
env: effectiveEnv
|
|
2425
|
+
});
|
|
2426
|
+
handle = startResult.handle;
|
|
2427
|
+
sandboxRepoDir = startResult.worktreePath;
|
|
2428
|
+
} else if (sandboxProvider.tag === "none") {
|
|
2429
|
+
const startResult = yield* startSandbox({
|
|
2430
|
+
provider: sandboxProvider,
|
|
2431
|
+
hostRepoDir,
|
|
2432
|
+
env: effectiveEnv,
|
|
2433
|
+
worktreeOrRepoPath: worktreeInfo.path
|
|
2434
|
+
});
|
|
2435
|
+
handle = startResult.handle;
|
|
2436
|
+
sandboxRepoDir = startResult.worktreePath;
|
|
2437
|
+
} else {
|
|
2438
|
+
const gitPath = join(hostRepoDir, ".git");
|
|
2439
|
+
const rawGitMounts = yield* resolveGitMounts(gitPath);
|
|
2440
|
+
const gitMounts = yield* patchGitMountsForWindows(
|
|
2441
|
+
rawGitMounts,
|
|
2442
|
+
worktreeInfo.path,
|
|
2443
|
+
SANDBOX_REPO_DIR
|
|
2444
|
+
);
|
|
2445
|
+
const startResult = yield* startSandbox({
|
|
2446
|
+
provider: sandboxProvider,
|
|
2447
|
+
hostRepoDir,
|
|
2448
|
+
env: effectiveEnv,
|
|
2449
|
+
worktreeOrRepoPath: worktreeInfo.path,
|
|
2450
|
+
gitMounts,
|
|
2451
|
+
repoDir: SANDBOX_REPO_DIR
|
|
2452
|
+
});
|
|
2453
|
+
handle = startResult.handle;
|
|
2454
|
+
sandboxRepoDir = startResult.worktreePath;
|
|
2455
|
+
}
|
|
2456
|
+
const sandbox = makeSandboxFromHandle(handle);
|
|
2457
|
+
const applyToHost = sandboxProvider.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
|
|
2458
|
+
const resolvedLogging = opts.logging ?? {
|
|
2459
|
+
type: "file",
|
|
2460
|
+
path: join(
|
|
2461
|
+
hostRepoDir,
|
|
2462
|
+
".sandcastle",
|
|
2463
|
+
"logs",
|
|
2464
|
+
buildLogFilename(worktreeInfo.branch, void 0, opts.name)
|
|
2465
|
+
)
|
|
2466
|
+
};
|
|
2467
|
+
const runDisplayLayer = resolvedLogging.type === "file" ? (() => {
|
|
2468
|
+
printFileDisplayStartup({
|
|
2469
|
+
logPath: resolvedLogging.path,
|
|
2470
|
+
agentName: opts.name,
|
|
2471
|
+
branch: worktreeInfo.branch
|
|
2472
|
+
});
|
|
2473
|
+
return Layer_exports.provide(
|
|
2474
|
+
FileDisplay.layer(resolvedLogging.path),
|
|
2475
|
+
NodeFileSystem_exports.layer
|
|
2476
|
+
);
|
|
2477
|
+
})() : ClackDisplay.layer;
|
|
2478
|
+
const bindMountHandle = sandboxProvider.tag === "bind-mount" ? handle : void 0;
|
|
2479
|
+
const reuseFactoryLayer = Layer_exports.succeed(SandboxFactory, {
|
|
2480
|
+
withSandbox: (makeEffect) => makeEffect(
|
|
2481
|
+
{
|
|
2482
|
+
hostWorktreePath: worktreeInfo.path,
|
|
2483
|
+
sandboxRepoPath: sandboxRepoDir,
|
|
2484
|
+
applyToHost,
|
|
2485
|
+
bindMountHandle
|
|
2486
|
+
},
|
|
2487
|
+
sandbox
|
|
2488
|
+
).pipe(
|
|
2489
|
+
Effect_exports.map((value) => ({
|
|
2490
|
+
value,
|
|
2491
|
+
preservedWorktreePath: void 0
|
|
2492
|
+
}))
|
|
2493
|
+
)
|
|
2494
|
+
});
|
|
2495
|
+
const streamEmitterLayer = agentStreamEmitterLayer(
|
|
2496
|
+
buildAgentStreamHandler(resolvedLogging)
|
|
2497
|
+
);
|
|
2498
|
+
const runLayer = Layer_exports.mergeAll(
|
|
2499
|
+
reuseFactoryLayer,
|
|
2500
|
+
runDisplayLayer,
|
|
2501
|
+
streamEmitterLayer
|
|
2502
|
+
);
|
|
2503
|
+
const result = yield* Effect_exports.gen(function* () {
|
|
2504
|
+
const display = yield* Display;
|
|
2505
|
+
yield* display.intro(opts.name ?? "sandcastle");
|
|
2506
|
+
const orchestrateResult = yield* orchestrate({
|
|
2507
|
+
hostRepoDir,
|
|
2508
|
+
iterations: maxIterations,
|
|
2509
|
+
hooks,
|
|
2510
|
+
prompt: resolvedPrompt,
|
|
2511
|
+
// merge-to-head: pass `undefined` so the lifecycle records the host's
|
|
2512
|
+
// current branch and routes through the merge step. branch strategy:
|
|
2513
|
+
// pin to the worktree's branch so commits stay there.
|
|
2514
|
+
branch: isMergeToHead ? void 0 : worktreeInfo.branch,
|
|
2515
|
+
provider,
|
|
2516
|
+
completionSignal: opts.completionSignal,
|
|
2517
|
+
idleTimeoutSeconds: opts.idleTimeoutSeconds,
|
|
2518
|
+
completionTimeoutSeconds: opts.completionTimeoutSeconds,
|
|
2519
|
+
name: opts.name,
|
|
2520
|
+
resumeSession: opts.resumeSession,
|
|
2521
|
+
signal: opts.signal,
|
|
2522
|
+
skipPromptExpansion: isInlinePrompt,
|
|
2523
|
+
timeouts: options.timeouts,
|
|
2524
|
+
keepSourceBranch: isMergeToHead
|
|
2525
|
+
});
|
|
2526
|
+
const completion = buildCompletionMessage(
|
|
2527
|
+
orchestrateResult.completionSignal,
|
|
2528
|
+
orchestrateResult.iterations.length
|
|
2529
|
+
);
|
|
2530
|
+
yield* display.status(completion.message, completion.severity);
|
|
2531
|
+
for (const line of buildContextWindowLines(
|
|
2532
|
+
orchestrateResult.iterations
|
|
2533
|
+
)) {
|
|
2534
|
+
yield* display.text(line);
|
|
2535
|
+
}
|
|
2536
|
+
return orchestrateResult;
|
|
2537
|
+
}).pipe(
|
|
2538
|
+
Effect_exports.provide(runLayer),
|
|
2539
|
+
// Always close sandbox handle
|
|
2540
|
+
Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
|
|
2541
|
+
})))
|
|
2542
|
+
);
|
|
2543
|
+
return {
|
|
2544
|
+
iterations: result.iterations,
|
|
2545
|
+
completionSignal: result.completionSignal,
|
|
2546
|
+
stdout: result.stdout,
|
|
2547
|
+
commits: result.commits,
|
|
2548
|
+
branch: result.branch,
|
|
2549
|
+
logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : void 0
|
|
2550
|
+
};
|
|
2551
|
+
});
|
|
2552
|
+
try {
|
|
2553
|
+
return await Effect_exports.runPromise(
|
|
2554
|
+
inner.pipe(
|
|
2555
|
+
Effect_exports.provide(ClackDisplay.layer),
|
|
2556
|
+
Effect_exports.provide(NodeContext_exports.layer),
|
|
2557
|
+
Effect_exports.provide(NodeFileSystem_exports.layer)
|
|
2558
|
+
)
|
|
2559
|
+
);
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
opts.signal?.throwIfAborted();
|
|
2562
|
+
throw error;
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
const worktreeCreateSandbox = async (opts) => {
|
|
2566
|
+
return createSandboxFromWorktree({
|
|
2567
|
+
branch: worktreeInfo.branch,
|
|
2568
|
+
worktreePath: worktreeInfo.path,
|
|
2569
|
+
hostRepoDir,
|
|
2570
|
+
sandbox: opts.sandbox,
|
|
2571
|
+
hooks: opts.hooks,
|
|
2572
|
+
copyToWorktree: opts.copyToWorktree,
|
|
2573
|
+
timeouts: opts.timeouts,
|
|
2574
|
+
branchStrategy: options.branchStrategy,
|
|
2575
|
+
_test: opts._test
|
|
2576
|
+
});
|
|
2577
|
+
};
|
|
2578
|
+
return {
|
|
2579
|
+
branch: worktreeInfo.branch,
|
|
2580
|
+
worktreePath: worktreeInfo.path,
|
|
2581
|
+
run: worktreeRun,
|
|
2582
|
+
interactive: worktreeInteractive,
|
|
2583
|
+
createSandbox: worktreeCreateSandbox,
|
|
2584
|
+
close,
|
|
2585
|
+
async [Symbol.asyncDispose]() {
|
|
2586
|
+
await close();
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
};
|
|
2590
|
+
var pathExists = async (path2) => {
|
|
2591
|
+
try {
|
|
2592
|
+
await access(path2);
|
|
2593
|
+
return true;
|
|
2594
|
+
} catch {
|
|
2595
|
+
return false;
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
var encodeProjectPath = (cwd) => {
|
|
2599
|
+
const isRoot = cwd === "/" || /^[A-Za-z]:[\\/]?$/.test(cwd);
|
|
2600
|
+
const normalized = isRoot ? cwd : cwd.replace(/[\\/]+$/, "");
|
|
2601
|
+
return normalized.replace(/^([A-Za-z]):/, "$1").replace(/[\\/]/g, "-");
|
|
2602
|
+
};
|
|
2603
|
+
var claudeHostSessionPath = (cwd, id, projectsDir) => {
|
|
2604
|
+
const base = projectsDir ?? join(process.env.HOME ?? "~", ".claude", "projects");
|
|
2605
|
+
return join(base, encodeProjectPath(cwd), `${id}.jsonl`);
|
|
2606
|
+
};
|
|
2607
|
+
var claudeSandboxSessionPath = (cwd, id, projectsDir) => posix.join(projectsDir, encodeProjectPath(cwd), `${id}.jsonl`);
|
|
2608
|
+
var claudeSubagentsDirInSandbox = (cwd, id, projectsDir) => posix.join(projectsDir, encodeProjectPath(cwd), id, "subagents");
|
|
2609
|
+
var claudeSubagentsDirOnHost = (cwd, id, projectsDir) => {
|
|
2610
|
+
const base = projectsDir ?? join(process.env.HOME ?? "~", ".claude", "projects");
|
|
2611
|
+
return join(base, encodeProjectPath(cwd), id, "subagents");
|
|
2612
|
+
};
|
|
2613
|
+
var listClaudeSubagentSessionsInSandbox = async (cwd, id, handle, sandboxProjectsDir) => {
|
|
2614
|
+
const dir = claudeSubagentsDirInSandbox(cwd, id, sandboxProjectsDir);
|
|
2615
|
+
const result = await handle.exec(
|
|
2616
|
+
`find ${JSON.stringify(dir)} -type f -name ${JSON.stringify("agent-*.jsonl")} 2>/dev/null`
|
|
2617
|
+
);
|
|
2618
|
+
if (result.exitCode !== 0) return [];
|
|
2619
|
+
const stdout = result.stdout.trim();
|
|
2620
|
+
if (stdout === "") return [];
|
|
2621
|
+
return stdout.split("\n").filter((line) => line !== "");
|
|
2622
|
+
};
|
|
2623
|
+
var findClaudeSessionOnHost = async (id, projectsDir) => {
|
|
2624
|
+
const root = projectsDir ?? join(process.env.HOME ?? "~", ".claude", "projects");
|
|
2625
|
+
let entries;
|
|
2626
|
+
try {
|
|
2627
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
2628
|
+
} catch {
|
|
2629
|
+
return { path: void 0, searchedRoot: root };
|
|
2630
|
+
}
|
|
2631
|
+
for (const entry of entries) {
|
|
2632
|
+
if (!entry.isDirectory()) continue;
|
|
2633
|
+
const candidate = join(root, entry.name, `${id}.jsonl`);
|
|
2634
|
+
if (await pathExists(candidate)) {
|
|
2635
|
+
return { path: candidate, searchedRoot: root };
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return { path: void 0, searchedRoot: root };
|
|
2639
|
+
};
|
|
2640
|
+
var rewriteSessionCwd = (content, fromCwd, toCwd) => {
|
|
2641
|
+
if (content === "") return "";
|
|
2642
|
+
return content.split("\n").map((line) => {
|
|
2643
|
+
if (line === "") return line;
|
|
2644
|
+
try {
|
|
2645
|
+
const entry = JSON.parse(line);
|
|
2646
|
+
if (typeof entry.cwd === "string" && entry.cwd === fromCwd) {
|
|
2647
|
+
entry.cwd = toCwd;
|
|
2648
|
+
}
|
|
2649
|
+
if (entry.type === "session_meta" && typeof entry.payload === "object" && entry.payload !== null && typeof entry.payload.cwd === "string" && entry.payload.cwd === fromCwd) {
|
|
2650
|
+
entry.payload.cwd = toCwd;
|
|
2651
|
+
}
|
|
2652
|
+
return JSON.stringify(entry);
|
|
2653
|
+
} catch {
|
|
2654
|
+
return line;
|
|
2655
|
+
}
|
|
2656
|
+
}).join("\n");
|
|
2657
|
+
};
|
|
2658
|
+
var transferClaudeSession = (jsonl, fromCwd, toCwd) => rewriteSessionCwd(jsonl, fromCwd, toCwd);
|
|
2659
|
+
var isCodexSessionFilename = (filename, id) => filename.startsWith("rollout-") && filename.endsWith(`-${id}.jsonl`);
|
|
2660
|
+
var findCodexSessionPath = async (rootDir, id) => {
|
|
2661
|
+
const visit = async (dir) => {
|
|
2662
|
+
let entries;
|
|
2663
|
+
try {
|
|
2664
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
2665
|
+
} catch {
|
|
2666
|
+
return void 0;
|
|
2667
|
+
}
|
|
2668
|
+
for (const entry of entries) {
|
|
2669
|
+
const child = join(dir, entry.name);
|
|
2670
|
+
if (entry.isFile() && isCodexSessionFilename(entry.name, id)) {
|
|
2671
|
+
return child;
|
|
2672
|
+
}
|
|
2673
|
+
if (entry.isDirectory()) {
|
|
2674
|
+
const found = await visit(child);
|
|
2675
|
+
if (found) return found;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
return void 0;
|
|
2679
|
+
};
|
|
2680
|
+
return visit(rootDir);
|
|
2681
|
+
};
|
|
2682
|
+
var findCodexSessionOnHost = async (id, sessionsDir) => {
|
|
2683
|
+
const root = sessionsDir ?? join(process.env.HOME ?? "~", ".codex", "sessions");
|
|
2684
|
+
const path2 = await findCodexSessionPath(root, id);
|
|
2685
|
+
return { path: path2, searchedRoot: root };
|
|
2686
|
+
};
|
|
2687
|
+
var locateCodexHostSession = async (id, sessionsDir) => {
|
|
2688
|
+
const root = sessionsDir ?? join(process.env.HOME ?? "~", ".codex", "sessions");
|
|
2689
|
+
const path2 = await findCodexSessionPath(root, id);
|
|
2690
|
+
if (!path2) throw new Error(`session ${id} not found in ${root}`);
|
|
2691
|
+
return { path: path2, relativePath: relative(root, path2) };
|
|
2692
|
+
};
|
|
2693
|
+
var locateCodexSandboxSession = async (id, handle, sessionsDir) => {
|
|
2694
|
+
const result = await handle.exec(
|
|
2695
|
+
`find ${JSON.stringify(sessionsDir)} -type f -name ${JSON.stringify(`rollout-*-${id}.jsonl`)} -print -quit`
|
|
2696
|
+
);
|
|
2697
|
+
const path2 = result.stdout.trim().split("\n")[0];
|
|
2698
|
+
if (result.exitCode !== 0 || !path2) {
|
|
2699
|
+
throw new Error(`session ${id} not found in ${sessionsDir}`);
|
|
2700
|
+
}
|
|
2701
|
+
return { path: path2, relativePath: posix.relative(sessionsDir, path2) };
|
|
2702
|
+
};
|
|
2703
|
+
var transferCodexSession = (jsonl, fromCwd, toCwd) => rewriteSessionCwd(jsonl, fromCwd, toCwd);
|
|
2704
|
+
var encodePiSessionDir = (cwd) => {
|
|
2705
|
+
const stripped = cwd.replace(/^[/\\]/, "");
|
|
2706
|
+
const replaced = stripped.replace(/[/\\:]/g, "-");
|
|
2707
|
+
return `--${replaced}--`;
|
|
2708
|
+
};
|
|
2709
|
+
var piSessionDirPath = (cwd, sessionsDir) => {
|
|
2710
|
+
const base = sessionsDir ?? join(process.env.HOME ?? "~", ".pi", "agent", "sessions");
|
|
2711
|
+
return join(base, encodePiSessionDir(cwd));
|
|
2712
|
+
};
|
|
2713
|
+
var isPiSessionFilename = (filename, id) => filename.endsWith(`_${id}.jsonl`);
|
|
2714
|
+
var findPiSessionPath = async (rootDir, id) => {
|
|
2715
|
+
let entries;
|
|
2716
|
+
try {
|
|
2717
|
+
entries = await readdir(rootDir, { withFileTypes: true });
|
|
2718
|
+
} catch {
|
|
2719
|
+
return void 0;
|
|
2720
|
+
}
|
|
2721
|
+
for (const entry of entries) {
|
|
2722
|
+
if (!entry.isDirectory()) continue;
|
|
2723
|
+
const dirAbs = join(rootDir, entry.name);
|
|
2724
|
+
let files;
|
|
2725
|
+
try {
|
|
2726
|
+
files = await readdir(dirAbs);
|
|
2727
|
+
} catch {
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
const match = files.find((name) => isPiSessionFilename(name, id));
|
|
2731
|
+
if (match) {
|
|
2732
|
+
return {
|
|
2733
|
+
path: join(dirAbs, match),
|
|
2734
|
+
relativePath: join(entry.name, match)
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
return void 0;
|
|
2739
|
+
};
|
|
2740
|
+
var findPiSessionOnHost = async (id, sessionsDir) => {
|
|
2741
|
+
const root = sessionsDir ?? join(process.env.HOME ?? "~", ".pi", "agent", "sessions");
|
|
2742
|
+
const found = await findPiSessionPath(root, id);
|
|
2743
|
+
return { path: found?.path, searchedRoot: root };
|
|
2744
|
+
};
|
|
2745
|
+
var locatePiHostSession = async (id, sessionsDir) => {
|
|
2746
|
+
const root = sessionsDir ?? join(process.env.HOME ?? "~", ".pi", "agent", "sessions");
|
|
2747
|
+
const found = await findPiSessionPath(root, id);
|
|
2748
|
+
if (!found) throw new Error(`session ${id} not found in ${root}`);
|
|
2749
|
+
return found;
|
|
2750
|
+
};
|
|
2751
|
+
var locatePiSandboxSession = async (id, handle, sessionsDir) => {
|
|
2752
|
+
const result = await handle.exec(
|
|
2753
|
+
`find ${JSON.stringify(sessionsDir)} -type f -name ${JSON.stringify(`*_${id}.jsonl`)} -print -quit`
|
|
2754
|
+
);
|
|
2755
|
+
const path2 = result.stdout.trim().split("\n")[0];
|
|
2756
|
+
if (result.exitCode !== 0 || !path2) {
|
|
2757
|
+
throw new Error(`session ${id} not found in ${sessionsDir}`);
|
|
2758
|
+
}
|
|
2759
|
+
return { path: path2, relativePath: posix.relative(sessionsDir, path2) };
|
|
2760
|
+
};
|
|
2761
|
+
var transferPiSession = (jsonl, fromCwd, toCwd) => {
|
|
2762
|
+
if (jsonl === "") return "";
|
|
2763
|
+
return jsonl.split("\n").map((line) => {
|
|
2764
|
+
if (line === "") return line;
|
|
2765
|
+
try {
|
|
2766
|
+
const entry = JSON.parse(line);
|
|
2767
|
+
if (entry.type === "session" && typeof entry.cwd === "string" && entry.cwd === fromCwd) {
|
|
2768
|
+
entry.cwd = toCwd;
|
|
2769
|
+
return JSON.stringify(entry);
|
|
2770
|
+
}
|
|
2771
|
+
return line;
|
|
2772
|
+
} catch {
|
|
2773
|
+
return line;
|
|
2774
|
+
}
|
|
2775
|
+
}).join("\n");
|
|
2776
|
+
};
|
|
2777
|
+
|
|
2778
|
+
// src/CwdError.ts
|
|
2779
|
+
var CwdError2 = CwdError;
|
|
2780
|
+
var fileExists = async (path2) => {
|
|
2781
|
+
try {
|
|
2782
|
+
await access(path2);
|
|
2783
|
+
return true;
|
|
2784
|
+
} catch {
|
|
2785
|
+
return false;
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
var shellEscape = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
2789
|
+
var TOOL_ARG_FIELDS = {
|
|
2790
|
+
Bash: "command",
|
|
2791
|
+
WebSearch: "query",
|
|
2792
|
+
WebFetch: "url",
|
|
2793
|
+
Agent: "description"
|
|
2794
|
+
};
|
|
2795
|
+
var extractErrorMessage = (obj) => {
|
|
2796
|
+
const err = obj.error;
|
|
2797
|
+
if (typeof err === "string") return err;
|
|
2798
|
+
if (typeof err === "object" && err !== null) {
|
|
2799
|
+
if (typeof err.message === "string") return err.message;
|
|
2800
|
+
if (typeof err.data?.message === "string") return err.data.message;
|
|
2801
|
+
}
|
|
2802
|
+
if (typeof obj.message === "string") return obj.message;
|
|
2803
|
+
return void 0;
|
|
2804
|
+
};
|
|
2805
|
+
var parseStreamJsonLine = (line) => {
|
|
2806
|
+
if (!line.startsWith("{")) return [];
|
|
2807
|
+
try {
|
|
2808
|
+
const obj = JSON.parse(line);
|
|
2809
|
+
if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
|
|
2810
|
+
const events = [];
|
|
2811
|
+
const texts = [];
|
|
2812
|
+
for (const block of obj.message.content) {
|
|
2813
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
2814
|
+
texts.push(block.text);
|
|
2815
|
+
} else if (block.type === "tool_use" && typeof block.name === "string" && block.input !== void 0) {
|
|
2816
|
+
const argField = TOOL_ARG_FIELDS[block.name];
|
|
2817
|
+
if (argField === void 0) continue;
|
|
2818
|
+
const argValue = block.input[argField];
|
|
2819
|
+
if (typeof argValue !== "string") continue;
|
|
2820
|
+
if (texts.length > 0) {
|
|
2821
|
+
events.push({ type: "text", text: texts.join("") });
|
|
2822
|
+
texts.length = 0;
|
|
2823
|
+
}
|
|
2824
|
+
events.push({
|
|
2825
|
+
type: "tool_call",
|
|
2826
|
+
name: block.name,
|
|
2827
|
+
args: argValue
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
if (texts.length > 0) {
|
|
2832
|
+
events.push({ type: "text", text: texts.join("") });
|
|
2833
|
+
}
|
|
2834
|
+
return events;
|
|
2835
|
+
}
|
|
2836
|
+
if (obj.type === "result" && typeof obj.result === "string") {
|
|
2837
|
+
return [{ type: "result", result: obj.result }];
|
|
2838
|
+
}
|
|
2839
|
+
if (obj.type === "system" && obj.subtype === "init" && typeof obj.session_id === "string") {
|
|
2840
|
+
return [{ type: "session_id", sessionId: obj.session_id }];
|
|
2841
|
+
}
|
|
2842
|
+
} catch {
|
|
2843
|
+
}
|
|
2844
|
+
return [];
|
|
2845
|
+
};
|
|
2846
|
+
var PROMPT_ARGV_SAFE_MAX_BYTES = 120 * 1024;
|
|
2847
|
+
var assertPromptFitsArgv = (prompt, description, remediation) => {
|
|
2848
|
+
const bytes = Buffer.byteLength(prompt, "utf8");
|
|
2849
|
+
if (bytes > PROMPT_ARGV_SAFE_MAX_BYTES) {
|
|
2850
|
+
throw new Error(
|
|
2851
|
+
`${description} is ${bytes} bytes (max ${PROMPT_ARGV_SAFE_MAX_BYTES} bytes). ${remediation}`
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
};
|
|
2855
|
+
var parseCursorToolCallStarted = (obj) => {
|
|
2856
|
+
if (obj.type !== "tool_call" || obj.subtype !== "started") return [];
|
|
2857
|
+
const toolCall = obj.tool_call;
|
|
2858
|
+
if (!toolCall || typeof toolCall !== "object") return [];
|
|
2859
|
+
const tc = toolCall;
|
|
2860
|
+
const readToolCall = tc.readToolCall;
|
|
2861
|
+
if (readToolCall?.args && typeof readToolCall.args.path === "string") {
|
|
2862
|
+
return [{ type: "tool_call", name: "Read", args: readToolCall.args.path }];
|
|
2863
|
+
}
|
|
2864
|
+
const writeToolCall = tc.writeToolCall;
|
|
2865
|
+
if (writeToolCall?.args && typeof writeToolCall.args.path === "string") {
|
|
2866
|
+
return [
|
|
2867
|
+
{ type: "tool_call", name: "Write", args: writeToolCall.args.path }
|
|
2868
|
+
];
|
|
2869
|
+
}
|
|
2870
|
+
const fn = tc.function;
|
|
2871
|
+
if (fn && typeof fn.name === "string") {
|
|
2872
|
+
const rawArgs = typeof fn.arguments === "string" ? fn.arguments : "";
|
|
2873
|
+
if (rawArgs) {
|
|
2874
|
+
try {
|
|
2875
|
+
const parsedArgs = JSON.parse(rawArgs);
|
|
2876
|
+
if (typeof parsedArgs.command === "string") {
|
|
2877
|
+
return [
|
|
2878
|
+
{ type: "tool_call", name: "Bash", args: parsedArgs.command }
|
|
2879
|
+
];
|
|
2880
|
+
}
|
|
2881
|
+
} catch {
|
|
2882
|
+
}
|
|
2883
|
+
return [{ type: "tool_call", name: fn.name, args: rawArgs }];
|
|
2884
|
+
}
|
|
2885
|
+
return [{ type: "tool_call", name: fn.name, args: "" }];
|
|
2886
|
+
}
|
|
2887
|
+
return [];
|
|
2888
|
+
};
|
|
2889
|
+
var parseCursorStreamLine = (line) => {
|
|
2890
|
+
if (!line.startsWith("{")) return [];
|
|
2891
|
+
let obj;
|
|
2892
|
+
try {
|
|
2893
|
+
obj = JSON.parse(line);
|
|
2894
|
+
} catch {
|
|
2895
|
+
return [];
|
|
2896
|
+
}
|
|
2897
|
+
if (obj.type === "tool_call") {
|
|
2898
|
+
return parseCursorToolCallStarted(obj);
|
|
2899
|
+
}
|
|
2900
|
+
return parseStreamJsonLine(line);
|
|
2901
|
+
};
|
|
2902
|
+
var readSandboxFile = async (handle, sandboxPath, tag) => {
|
|
2903
|
+
const tmpPath = join(
|
|
2904
|
+
tmpdir(),
|
|
2905
|
+
`sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`
|
|
2906
|
+
);
|
|
2907
|
+
await handle.copyFileOut(sandboxPath, tmpPath);
|
|
2908
|
+
try {
|
|
2909
|
+
return await readFile(tmpPath, "utf-8");
|
|
2910
|
+
} finally {
|
|
2911
|
+
await rm(tmpPath, { force: true }).catch(() => {
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
var writeSandboxFile = async (handle, sandboxPath, content, tag) => {
|
|
2916
|
+
const tmpPath = join(
|
|
2917
|
+
tmpdir(),
|
|
2918
|
+
`sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`
|
|
2919
|
+
);
|
|
2920
|
+
await writeFile(tmpPath, content);
|
|
2921
|
+
try {
|
|
2922
|
+
await handle.exec(`mkdir -p ${JSON.stringify(posix.dirname(sandboxPath))}`);
|
|
2923
|
+
await handle.copyFileIn(tmpPath, sandboxPath);
|
|
2924
|
+
} finally {
|
|
2925
|
+
await rm(tmpPath, { force: true }).catch(() => {
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
var copyClaudeSessionFile = async ({
|
|
2930
|
+
handle,
|
|
2931
|
+
sourcePath,
|
|
2932
|
+
fromCwd,
|
|
2933
|
+
toCwd,
|
|
2934
|
+
destPath,
|
|
2935
|
+
tag
|
|
2936
|
+
}) => {
|
|
2937
|
+
const jsonl = await readSandboxFile(handle, sourcePath, tag);
|
|
2938
|
+
const rewritten = transferClaudeSession(jsonl, fromCwd, toCwd);
|
|
2939
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
2940
|
+
await writeFile(destPath, rewritten);
|
|
2941
|
+
};
|
|
2942
|
+
var makeClaudeSessionStorage = (options) => {
|
|
2943
|
+
const hostProjectsDir = options?.sessionStorage?.hostProjectsDir;
|
|
2944
|
+
const sandboxProjectsDir = options?.sessionStorage?.sandboxProjectsDir ?? "/home/agent/.claude/projects";
|
|
2945
|
+
return {
|
|
2946
|
+
hostSessionFilePath: (cwd, id) => claudeHostSessionPath(cwd, id, hostProjectsDir),
|
|
2947
|
+
existsOnHost: (cwd, id) => fileExists(claudeHostSessionPath(cwd, id, hostProjectsDir)),
|
|
2948
|
+
readHostSession: async (cwd, id) => {
|
|
2949
|
+
const path2 = claudeHostSessionPath(cwd, id, hostProjectsDir);
|
|
2950
|
+
if (!await fileExists(path2)) return void 0;
|
|
2951
|
+
return readFile(path2, "utf-8");
|
|
2952
|
+
},
|
|
2953
|
+
captureToHost: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
2954
|
+
await copyClaudeSessionFile({
|
|
2955
|
+
handle,
|
|
2956
|
+
sourcePath: claudeSandboxSessionPath(
|
|
2957
|
+
sandboxCwd,
|
|
2958
|
+
sessionId,
|
|
2959
|
+
sandboxProjectsDir
|
|
2960
|
+
),
|
|
2961
|
+
fromCwd: sandboxCwd,
|
|
2962
|
+
toCwd: hostCwd,
|
|
2963
|
+
destPath: claudeHostSessionPath(hostCwd, sessionId, hostProjectsDir),
|
|
2964
|
+
tag: "claude-cap"
|
|
2965
|
+
});
|
|
2966
|
+
const subagentSandboxPaths = await listClaudeSubagentSessionsInSandbox(
|
|
2967
|
+
sandboxCwd,
|
|
2968
|
+
sessionId,
|
|
2969
|
+
handle,
|
|
2970
|
+
sandboxProjectsDir
|
|
2971
|
+
);
|
|
2972
|
+
const hostSubagentsDir = claudeSubagentsDirOnHost(
|
|
2973
|
+
hostCwd,
|
|
2974
|
+
sessionId,
|
|
2975
|
+
hostProjectsDir
|
|
2976
|
+
);
|
|
2977
|
+
for (const sandboxSubagentPath of subagentSandboxPaths) {
|
|
2978
|
+
try {
|
|
2979
|
+
await copyClaudeSessionFile({
|
|
2980
|
+
handle,
|
|
2981
|
+
sourcePath: sandboxSubagentPath,
|
|
2982
|
+
fromCwd: sandboxCwd,
|
|
2983
|
+
toCwd: hostCwd,
|
|
2984
|
+
destPath: join(
|
|
2985
|
+
hostSubagentsDir,
|
|
2986
|
+
posix.basename(sandboxSubagentPath)
|
|
2987
|
+
),
|
|
2988
|
+
tag: "claude-sub"
|
|
2989
|
+
});
|
|
2990
|
+
} catch (err) {
|
|
2991
|
+
console.error(
|
|
2992
|
+
`sandcastle: failed to capture Claude subagent transcript ${sandboxSubagentPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
},
|
|
2997
|
+
resumeIntoSandbox: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
2998
|
+
const hostPath = claudeHostSessionPath(
|
|
2999
|
+
hostCwd,
|
|
3000
|
+
sessionId,
|
|
3001
|
+
hostProjectsDir
|
|
3002
|
+
);
|
|
3003
|
+
const jsonl = await readFile(hostPath, "utf-8");
|
|
3004
|
+
const rewritten = transferClaudeSession(jsonl, hostCwd, sandboxCwd);
|
|
3005
|
+
const sandboxPath = claudeSandboxSessionPath(
|
|
3006
|
+
sandboxCwd,
|
|
3007
|
+
sessionId,
|
|
3008
|
+
sandboxProjectsDir
|
|
3009
|
+
);
|
|
3010
|
+
await writeSandboxFile(handle, sandboxPath, rewritten, "claude-res");
|
|
3011
|
+
},
|
|
3012
|
+
findByIdOnHost: (id) => findClaudeSessionOnHost(id, hostProjectsDir)
|
|
3013
|
+
};
|
|
3014
|
+
};
|
|
3015
|
+
var makeCodexSessionStorage = (options) => {
|
|
3016
|
+
const hostSessionsDir = options?.sessionStorage?.hostSessionsDir;
|
|
3017
|
+
const sandboxSessionsDir = options?.sessionStorage?.sandboxSessionsDir ?? posix.join("/home/agent", ".codex", "sessions");
|
|
3018
|
+
const capturedPaths = /* @__PURE__ */ new Map();
|
|
3019
|
+
return {
|
|
3020
|
+
hostSessionFilePath: (_cwd, id) => capturedPaths.get(id),
|
|
3021
|
+
existsOnHost: async (_cwd, id) => {
|
|
3022
|
+
const found = await findCodexSessionOnHost(id, hostSessionsDir);
|
|
3023
|
+
return found.path !== void 0;
|
|
3024
|
+
},
|
|
3025
|
+
readHostSession: async (_cwd, id) => {
|
|
3026
|
+
const found = await findCodexSessionOnHost(id, hostSessionsDir);
|
|
3027
|
+
if (!found.path) return void 0;
|
|
3028
|
+
return readFile(found.path, "utf-8");
|
|
3029
|
+
},
|
|
3030
|
+
captureToHost: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
3031
|
+
const located = await locateCodexSandboxSession(
|
|
3032
|
+
sessionId,
|
|
3033
|
+
handle,
|
|
3034
|
+
sandboxSessionsDir
|
|
3035
|
+
);
|
|
3036
|
+
const jsonl = await readSandboxFile(handle, located.path, "codex-cap");
|
|
3037
|
+
const rewritten = transferCodexSession(jsonl, sandboxCwd, hostCwd);
|
|
3038
|
+
const root = hostSessionsDir ?? join(process.env.HOME ?? "~", ".codex", "sessions");
|
|
3039
|
+
const target = join(root, located.relativePath);
|
|
3040
|
+
await mkdir(dirname(target), { recursive: true });
|
|
3041
|
+
await writeFile(target, rewritten);
|
|
3042
|
+
capturedPaths.set(sessionId, target);
|
|
3043
|
+
},
|
|
3044
|
+
resumeIntoSandbox: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
3045
|
+
const located = await locateCodexHostSession(sessionId, hostSessionsDir);
|
|
3046
|
+
const jsonl = await readFile(located.path, "utf-8");
|
|
3047
|
+
const rewritten = transferCodexSession(jsonl, hostCwd, sandboxCwd);
|
|
3048
|
+
const target = posix.join(sandboxSessionsDir, located.relativePath);
|
|
3049
|
+
await writeSandboxFile(handle, target, rewritten, "codex-res");
|
|
3050
|
+
},
|
|
3051
|
+
findByIdOnHost: (id) => findCodexSessionOnHost(id, hostSessionsDir)
|
|
3052
|
+
};
|
|
3053
|
+
};
|
|
3054
|
+
var makePiSessionStorage = (options) => {
|
|
3055
|
+
const hostSessionsDir = options?.sessionStorage?.hostSessionsDir;
|
|
3056
|
+
const sandboxSessionsDir = options?.sessionStorage?.sandboxSessionsDir ?? posix.join("/home/agent", ".pi", "agent", "sessions");
|
|
3057
|
+
return {
|
|
3058
|
+
hostSessionFilePath: (cwd, _id) => piSessionDirPath(cwd, hostSessionsDir),
|
|
3059
|
+
existsOnHost: async (_cwd, id) => {
|
|
3060
|
+
const found = await findPiSessionOnHost(id, hostSessionsDir);
|
|
3061
|
+
return found.path !== void 0;
|
|
3062
|
+
},
|
|
3063
|
+
readHostSession: async (_cwd, id) => {
|
|
3064
|
+
const found = await findPiSessionOnHost(id, hostSessionsDir);
|
|
3065
|
+
if (!found.path) return void 0;
|
|
3066
|
+
return readFile(found.path, "utf-8");
|
|
3067
|
+
},
|
|
3068
|
+
captureToHost: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
3069
|
+
const located = await locatePiSandboxSession(
|
|
3070
|
+
sessionId,
|
|
3071
|
+
handle,
|
|
3072
|
+
sandboxSessionsDir
|
|
3073
|
+
);
|
|
3074
|
+
const jsonl = await readSandboxFile(handle, located.path, "pi-cap");
|
|
3075
|
+
const rewritten = transferPiSession(jsonl, sandboxCwd, hostCwd);
|
|
3076
|
+
const filename = posix.basename(located.path);
|
|
3077
|
+
const target = join(piSessionDirPath(hostCwd, hostSessionsDir), filename);
|
|
3078
|
+
await mkdir(dirname(target), { recursive: true });
|
|
3079
|
+
await writeFile(target, rewritten);
|
|
3080
|
+
},
|
|
3081
|
+
resumeIntoSandbox: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
3082
|
+
const located = await locatePiHostSession(sessionId, hostSessionsDir);
|
|
3083
|
+
const jsonl = await readFile(located.path, "utf-8");
|
|
3084
|
+
const rewritten = transferPiSession(jsonl, hostCwd, sandboxCwd);
|
|
3085
|
+
const filename = located.relativePath.split(/[\\/]/).pop();
|
|
3086
|
+
const target = posix.join(
|
|
3087
|
+
sandboxSessionsDir,
|
|
3088
|
+
encodePiSessionDir(sandboxCwd),
|
|
3089
|
+
filename
|
|
3090
|
+
);
|
|
3091
|
+
await writeSandboxFile(handle, target, rewritten, "pi-res");
|
|
3092
|
+
},
|
|
3093
|
+
findByIdOnHost: (id) => findPiSessionOnHost(id, hostSessionsDir)
|
|
3094
|
+
};
|
|
3095
|
+
};
|
|
3096
|
+
var parsePiStreamLine = (line) => {
|
|
3097
|
+
if (!line.startsWith("{")) return [];
|
|
3098
|
+
try {
|
|
3099
|
+
const obj = JSON.parse(line);
|
|
3100
|
+
if (obj.type === "session" && typeof obj.id === "string") {
|
|
3101
|
+
return [{ type: "session_id", sessionId: obj.id }];
|
|
3102
|
+
}
|
|
3103
|
+
if (obj.type === "message_update" && obj.assistantMessageEvent) {
|
|
3104
|
+
const evt = obj.assistantMessageEvent;
|
|
3105
|
+
if (evt.type === "text_delta" && typeof evt.delta === "string") {
|
|
3106
|
+
return [{ type: "text", text: evt.delta }];
|
|
3107
|
+
}
|
|
3108
|
+
return [];
|
|
3109
|
+
}
|
|
3110
|
+
if (obj.type === "tool_execution_start") {
|
|
3111
|
+
const toolName = obj.toolName;
|
|
3112
|
+
if (typeof toolName !== "string") return [];
|
|
3113
|
+
const argField = TOOL_ARG_FIELDS[toolName];
|
|
3114
|
+
if (argField === void 0) return [];
|
|
3115
|
+
const args = obj.args;
|
|
3116
|
+
if (!args) return [];
|
|
3117
|
+
const argValue = args[argField];
|
|
3118
|
+
if (typeof argValue !== "string") return [];
|
|
3119
|
+
return [{ type: "tool_call", name: toolName, args: argValue }];
|
|
3120
|
+
}
|
|
3121
|
+
if (obj.type === "agent_error" || obj.type === "error") {
|
|
3122
|
+
const msg = extractErrorMessage(obj);
|
|
3123
|
+
return msg ? [{ type: "result", result: msg }] : [];
|
|
3124
|
+
}
|
|
3125
|
+
if (obj.type === "agent_end" && Array.isArray(obj.messages)) {
|
|
3126
|
+
const messages = obj.messages;
|
|
3127
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3128
|
+
const msg = messages[i];
|
|
3129
|
+
if (msg?.role === "assistant") {
|
|
3130
|
+
const texts = [];
|
|
3131
|
+
for (const block of msg.content) {
|
|
3132
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
3133
|
+
texts.push(block.text);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if (texts.length > 0) {
|
|
3137
|
+
return [{ type: "result", result: texts.join("") }];
|
|
3138
|
+
}
|
|
3139
|
+
break;
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
return [];
|
|
3143
|
+
}
|
|
3144
|
+
} catch {
|
|
3145
|
+
}
|
|
3146
|
+
return [];
|
|
3147
|
+
};
|
|
3148
|
+
var pi = (model, options) => ({
|
|
3149
|
+
name: "pi",
|
|
3150
|
+
env: options?.env ?? {},
|
|
3151
|
+
captureSessions: options?.captureSessions ?? true,
|
|
3152
|
+
sessionStorage: makePiSessionStorage(options),
|
|
3153
|
+
buildPrintCommand({
|
|
3154
|
+
prompt,
|
|
3155
|
+
resumeSession
|
|
3156
|
+
}) {
|
|
3157
|
+
const thinkingFlag = options?.thinking ? ` --thinking ${options.thinking}` : "";
|
|
3158
|
+
const sessionFlag = resumeSession ? ` --session ${shellEscape(resumeSession)}` : "";
|
|
3159
|
+
return {
|
|
3160
|
+
command: `pi -p --mode json --model ${shellEscape(model)}${thinkingFlag}${sessionFlag}`,
|
|
3161
|
+
stdin: prompt
|
|
3162
|
+
};
|
|
3163
|
+
},
|
|
3164
|
+
buildInteractiveArgs({ prompt }) {
|
|
3165
|
+
const args = ["pi", "--model", model];
|
|
3166
|
+
if (prompt) args.push(prompt);
|
|
3167
|
+
return args;
|
|
3168
|
+
},
|
|
3169
|
+
parseStreamLine(line) {
|
|
3170
|
+
return parsePiStreamLine(line);
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
var parseCodexUsage = (usage) => {
|
|
3174
|
+
if (typeof usage !== "object" || usage === null) return void 0;
|
|
3175
|
+
const u = usage;
|
|
3176
|
+
if (typeof u.input_tokens !== "number" || typeof u.cached_input_tokens !== "number" || typeof u.output_tokens !== "number") {
|
|
3177
|
+
return void 0;
|
|
3178
|
+
}
|
|
3179
|
+
return {
|
|
3180
|
+
inputTokens: u.input_tokens - u.cached_input_tokens,
|
|
3181
|
+
cacheCreationInputTokens: 0,
|
|
3182
|
+
cacheReadInputTokens: u.cached_input_tokens,
|
|
3183
|
+
outputTokens: u.output_tokens
|
|
3184
|
+
};
|
|
3185
|
+
};
|
|
3186
|
+
var parseCodexStreamLine = (line) => {
|
|
3187
|
+
if (!line.startsWith("{")) return [];
|
|
3188
|
+
try {
|
|
3189
|
+
const obj = JSON.parse(line);
|
|
3190
|
+
if (obj.type === "thread.started" && typeof obj.thread_id === "string") {
|
|
3191
|
+
return [{ type: "session_id", sessionId: obj.thread_id }];
|
|
3192
|
+
}
|
|
3193
|
+
if (obj.type === "item.completed" && obj.item?.type === "agent_message" && typeof obj.item.text === "string") {
|
|
3194
|
+
const text2 = obj.item.text;
|
|
3195
|
+
return [
|
|
3196
|
+
{ type: "text", text: text2 },
|
|
3197
|
+
{ type: "result", result: text2 }
|
|
3198
|
+
];
|
|
3199
|
+
}
|
|
3200
|
+
if (obj.type === "item.started" && obj.item?.type === "command_execution" && typeof obj.item.command === "string") {
|
|
3201
|
+
return [{ type: "tool_call", name: "Bash", args: obj.item.command }];
|
|
3202
|
+
}
|
|
3203
|
+
if (obj.type === "error") {
|
|
3204
|
+
const msg = extractErrorMessage(obj);
|
|
3205
|
+
return msg ? [{ type: "result", result: msg }] : [];
|
|
3206
|
+
}
|
|
3207
|
+
if (obj.type === "turn.completed") {
|
|
3208
|
+
const usage = parseCodexUsage(obj.usage);
|
|
3209
|
+
return usage ? [{ type: "usage", usage }] : [];
|
|
3210
|
+
}
|
|
3211
|
+
} catch {
|
|
3212
|
+
}
|
|
3213
|
+
return [];
|
|
3214
|
+
};
|
|
3215
|
+
var codex = (model, options) => ({
|
|
3216
|
+
name: "codex",
|
|
3217
|
+
env: options?.env ?? {},
|
|
3218
|
+
captureSessions: options?.captureSessions ?? true,
|
|
3219
|
+
sessionStorage: makeCodexSessionStorage(options),
|
|
3220
|
+
buildPrintCommand({
|
|
3221
|
+
prompt,
|
|
3222
|
+
resumeSession,
|
|
3223
|
+
forkSession
|
|
3224
|
+
}) {
|
|
3225
|
+
const effortFlag = options?.effort ? ` -c ${shellEscape(`model_reasoning_effort="${options.effort}"`)}` : "";
|
|
3226
|
+
const approvalsFlags = options?.approvalsReviewer === "auto_review" ? ` -a on-request -s danger-full-access -c ${shellEscape(`approvals_reviewer="auto_review"`)}` : " --dangerously-bypass-approvals-and-sandbox";
|
|
3227
|
+
let base;
|
|
3228
|
+
if (resumeSession && forkSession) {
|
|
3229
|
+
base = `codex exec fork ${shellEscape(resumeSession)}`;
|
|
3230
|
+
} else if (resumeSession) {
|
|
3231
|
+
base = `codex exec resume ${shellEscape(resumeSession)}`;
|
|
3232
|
+
} else {
|
|
3233
|
+
base = "codex exec";
|
|
3234
|
+
}
|
|
3235
|
+
const stdinArg = resumeSession ? " -" : "";
|
|
3236
|
+
return {
|
|
3237
|
+
command: `${base} --json${approvalsFlags} -m ${shellEscape(model)}${effortFlag}${stdinArg}`,
|
|
3238
|
+
stdin: prompt
|
|
3239
|
+
};
|
|
3240
|
+
},
|
|
3241
|
+
buildInteractiveArgs({ prompt }) {
|
|
3242
|
+
const args = ["codex", "--model", model];
|
|
3243
|
+
if (prompt) args.push(prompt);
|
|
3244
|
+
return args;
|
|
3245
|
+
},
|
|
3246
|
+
parseStreamLine(line) {
|
|
3247
|
+
return parseCodexStreamLine(line);
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
var cursor = (model, options) => ({
|
|
3251
|
+
name: "cursor",
|
|
3252
|
+
env: options?.env ?? {},
|
|
3253
|
+
captureSessions: false,
|
|
3254
|
+
// Cursor has no filesystem-backed session storage (captureSessions: false, no
|
|
3255
|
+
// sessionStorage), so it is non-resumable per ADR 0012/0016. resumeSession is
|
|
3256
|
+
// ignored here — like pi and opencode — rather than wired to --resume.
|
|
3257
|
+
buildPrintCommand({
|
|
3258
|
+
prompt,
|
|
3259
|
+
dangerouslySkipPermissions
|
|
3260
|
+
}) {
|
|
3261
|
+
assertPromptFitsArgv(
|
|
3262
|
+
prompt,
|
|
3263
|
+
"Cursor print-mode prompt",
|
|
3264
|
+
"The Cursor CLI accepts the prompt only as a command-line argument; shorten the prompt or split the work. Other Sandcastle providers use stdin for large prompts."
|
|
3265
|
+
);
|
|
3266
|
+
const forceFlag = dangerouslySkipPermissions ? " --force" : "";
|
|
3267
|
+
return {
|
|
3268
|
+
command: `agent --print --output-format stream-json --model ${shellEscape(model)} ${forceFlag} ${shellEscape(prompt)}`
|
|
3269
|
+
};
|
|
3270
|
+
},
|
|
3271
|
+
buildInteractiveArgs({
|
|
3272
|
+
prompt,
|
|
3273
|
+
dangerouslySkipPermissions
|
|
3274
|
+
}) {
|
|
3275
|
+
const args = ["agent", "--model", model];
|
|
3276
|
+
if (dangerouslySkipPermissions) args.push("--force");
|
|
3277
|
+
if (prompt) args.push(prompt);
|
|
3278
|
+
return args;
|
|
3279
|
+
},
|
|
3280
|
+
parseStreamLine(line) {
|
|
3281
|
+
return parseCursorStreamLine(line);
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
var OPENCODE_TOOL_ARG_FIELDS = {
|
|
3285
|
+
bash: "command",
|
|
3286
|
+
webfetch: "url",
|
|
3287
|
+
task: "description"
|
|
3288
|
+
};
|
|
3289
|
+
var parseOpenCodeStreamLine = (line) => {
|
|
3290
|
+
if (!line.startsWith("{")) return [];
|
|
3291
|
+
try {
|
|
3292
|
+
const obj = JSON.parse(line);
|
|
3293
|
+
const part = obj.part;
|
|
3294
|
+
if (obj.type === "step_start" && typeof obj.sessionID === "string") {
|
|
3295
|
+
return [{ type: "session_id", sessionId: obj.sessionID }];
|
|
3296
|
+
}
|
|
3297
|
+
if (obj.type === "text" && part?.type === "text" && typeof part.text === "string") {
|
|
3298
|
+
return [
|
|
3299
|
+
{ type: "text", text: part.text },
|
|
3300
|
+
{ type: "result", result: part.text }
|
|
3301
|
+
];
|
|
3302
|
+
}
|
|
3303
|
+
if (obj.type === "tool_use" && part?.type === "tool") {
|
|
3304
|
+
if (typeof part.tool !== "string") return [];
|
|
3305
|
+
const state = part.state;
|
|
3306
|
+
if (state?.status !== "completed") return [];
|
|
3307
|
+
const input = state.input;
|
|
3308
|
+
if (!input) return [];
|
|
3309
|
+
const argField = OPENCODE_TOOL_ARG_FIELDS[part.tool];
|
|
3310
|
+
const argValue = argField !== void 0 ? input[argField] : void 0;
|
|
3311
|
+
const args = typeof argValue === "string" ? argValue : JSON.stringify(input);
|
|
3312
|
+
return [{ type: "tool_call", name: part.tool, args }];
|
|
3313
|
+
}
|
|
3314
|
+
if (obj.type === "error") {
|
|
3315
|
+
const msg = extractErrorMessage(obj);
|
|
3316
|
+
return msg ? [{ type: "result", result: msg }] : [];
|
|
3317
|
+
}
|
|
3318
|
+
} catch {
|
|
3319
|
+
}
|
|
3320
|
+
return [];
|
|
3321
|
+
};
|
|
3322
|
+
var opencode = (model, options) => ({
|
|
3323
|
+
name: "opencode",
|
|
3324
|
+
env: options?.env ?? {},
|
|
3325
|
+
captureSessions: false,
|
|
3326
|
+
buildPrintCommand({
|
|
3327
|
+
prompt,
|
|
3328
|
+
dangerouslySkipPermissions
|
|
3329
|
+
}) {
|
|
3330
|
+
const variantFlag = options?.variant ? ` --variant ${shellEscape(options.variant)}` : "";
|
|
3331
|
+
const agentFlag = options?.agent ? ` --agent ${shellEscape(options.agent)}` : "";
|
|
3332
|
+
const permissionsFlag = dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
|
|
3333
|
+
return {
|
|
3334
|
+
command: `opencode run --format json --model ${shellEscape(model)}${variantFlag}${agentFlag}${permissionsFlag}`,
|
|
3335
|
+
stdin: prompt
|
|
3336
|
+
};
|
|
3337
|
+
},
|
|
3338
|
+
buildInteractiveArgs({ prompt }) {
|
|
3339
|
+
assertPromptFitsArgv(
|
|
3340
|
+
prompt,
|
|
3341
|
+
"OpenCode interactive prompt",
|
|
3342
|
+
"Shorten the prompt or use non-interactive run()."
|
|
3343
|
+
);
|
|
3344
|
+
const args = ["opencode", "--model", model];
|
|
3345
|
+
if (options?.agent) args.push("--agent", options.agent);
|
|
3346
|
+
if (prompt) args.push("--prompt", prompt);
|
|
3347
|
+
return args;
|
|
3348
|
+
},
|
|
3349
|
+
parseStreamLine(line) {
|
|
3350
|
+
return parseOpenCodeStreamLine(line);
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
var parseCopilotStreamLine = (line) => {
|
|
3354
|
+
if (!line.startsWith("{")) return [];
|
|
3355
|
+
try {
|
|
3356
|
+
const obj = JSON.parse(line);
|
|
3357
|
+
if (obj.type === "assistant.message_delta" && typeof obj.data?.deltaContent === "string") {
|
|
3358
|
+
return [{ type: "text", text: obj.data.deltaContent }];
|
|
3359
|
+
}
|
|
3360
|
+
if (obj.type === "tool.execution_start") {
|
|
3361
|
+
const rawName = obj.data?.toolName;
|
|
3362
|
+
if (typeof rawName !== "string") return [];
|
|
3363
|
+
const toolName = rawName === "bash" ? "Bash" : rawName;
|
|
3364
|
+
const argField = TOOL_ARG_FIELDS[toolName];
|
|
3365
|
+
if (argField === void 0) return [];
|
|
3366
|
+
const args = obj.data?.arguments;
|
|
3367
|
+
if (!args) return [];
|
|
3368
|
+
const argValue = args[argField];
|
|
3369
|
+
if (typeof argValue !== "string") return [];
|
|
3370
|
+
return [{ type: "tool_call", name: toolName, args: argValue }];
|
|
3371
|
+
}
|
|
3372
|
+
if (obj.type === "assistant.message" && typeof obj.data?.content === "string" && obj.data.content.length > 0) {
|
|
3373
|
+
return [{ type: "result", result: obj.data.content }];
|
|
3374
|
+
}
|
|
3375
|
+
if (obj.type === "result" && typeof obj.sessionId === "string") {
|
|
3376
|
+
return [{ type: "session_id", sessionId: obj.sessionId }];
|
|
3377
|
+
}
|
|
3378
|
+
if (obj.type === "error" || obj.type === "agent_error") {
|
|
3379
|
+
const msg = extractErrorMessage(obj);
|
|
3380
|
+
return msg ? [{ type: "result", result: msg }] : [];
|
|
3381
|
+
}
|
|
3382
|
+
} catch {
|
|
3383
|
+
}
|
|
3384
|
+
return [];
|
|
3385
|
+
};
|
|
3386
|
+
var copilot = (model, options) => ({
|
|
3387
|
+
name: "copilot",
|
|
3388
|
+
env: options?.env ?? {},
|
|
3389
|
+
captureSessions: false,
|
|
3390
|
+
// Copilot CLI does expose `--resume <id>`, but its session state is indexed by
|
|
3391
|
+
// a SQLite database alongside the JSONL files in ~/.copilot/session-state/, so
|
|
3392
|
+
// transferring a single session file between host and sandbox is not enough to
|
|
3393
|
+
// make resume work (see ADR 0016). Until the round-trip is verified end-to-end,
|
|
3394
|
+
// copilot is non-resumable: captureSessions is false, there is no sessionStorage,
|
|
3395
|
+
// and resumeSession is ignored here — like cursor, pi, and opencode.
|
|
3396
|
+
buildPrintCommand({
|
|
3397
|
+
prompt,
|
|
3398
|
+
dangerouslySkipPermissions
|
|
3399
|
+
}) {
|
|
3400
|
+
assertPromptFitsArgv(
|
|
3401
|
+
prompt,
|
|
3402
|
+
"Copilot print-mode prompt",
|
|
3403
|
+
"This provider passes the prompt as a command-line argument; shorten the prompt or split the work. Other Sandcastle providers use stdin for large prompts."
|
|
3404
|
+
);
|
|
3405
|
+
const allowAll = dangerouslySkipPermissions ? " --allow-all-tools" : "";
|
|
3406
|
+
const effortFlag = options?.effort ? ` --effort ${options.effort}` : "";
|
|
3407
|
+
return {
|
|
3408
|
+
command: `copilot -p ${shellEscape(prompt)} --output-format json --model ${shellEscape(model)}${allowAll}${effortFlag}`
|
|
3409
|
+
};
|
|
3410
|
+
},
|
|
3411
|
+
buildInteractiveArgs({ prompt }) {
|
|
3412
|
+
const args = ["copilot", "--model", model];
|
|
3413
|
+
if (prompt) args.push("-i", prompt);
|
|
3414
|
+
return args;
|
|
3415
|
+
},
|
|
3416
|
+
parseStreamLine(line) {
|
|
3417
|
+
return parseCopilotStreamLine(line);
|
|
3418
|
+
}
|
|
3419
|
+
});
|
|
3420
|
+
var claudeCode = (model, options) => ({
|
|
3421
|
+
name: "claude-code",
|
|
3422
|
+
env: options?.env ?? {},
|
|
3423
|
+
captureSessions: options?.captureSessions ?? true,
|
|
3424
|
+
sessionStorage: makeClaudeSessionStorage(options),
|
|
3425
|
+
buildPrintCommand({
|
|
3426
|
+
prompt,
|
|
3427
|
+
dangerouslySkipPermissions,
|
|
3428
|
+
resumeSession,
|
|
3429
|
+
forkSession
|
|
3430
|
+
}) {
|
|
3431
|
+
const permissionFlag = options?.permissionMode ? ` --permission-mode ${options.permissionMode}` : dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
|
|
3432
|
+
const effortFlag = options?.effort ? ` --effort ${options.effort}` : "";
|
|
3433
|
+
const resumeFlag = resumeSession ? ` --resume ${shellEscape(resumeSession)}` : "";
|
|
3434
|
+
const forkFlag = resumeSession && forkSession ? " --fork-session" : "";
|
|
3435
|
+
return {
|
|
3436
|
+
command: `claude --print --verbose${permissionFlag} --output-format stream-json --model ${shellEscape(model)}${effortFlag}${resumeFlag}${forkFlag} -p -`,
|
|
3437
|
+
stdin: prompt
|
|
3438
|
+
};
|
|
3439
|
+
},
|
|
3440
|
+
buildInteractiveArgs({
|
|
3441
|
+
prompt,
|
|
3442
|
+
dangerouslySkipPermissions
|
|
3443
|
+
}) {
|
|
3444
|
+
const args = ["claude"];
|
|
3445
|
+
if (options?.permissionMode) {
|
|
3446
|
+
args.push("--permission-mode", options.permissionMode);
|
|
3447
|
+
} else if (dangerouslySkipPermissions) {
|
|
3448
|
+
args.push("--dangerously-skip-permissions");
|
|
3449
|
+
}
|
|
3450
|
+
args.push("--model", model);
|
|
3451
|
+
if (options?.effort) args.push("--effort", options.effort);
|
|
3452
|
+
if (prompt) args.push(prompt);
|
|
3453
|
+
return args;
|
|
3454
|
+
},
|
|
3455
|
+
parseStreamLine(line) {
|
|
3456
|
+
return parseStreamJsonLine(line);
|
|
3457
|
+
},
|
|
3458
|
+
parseSessionUsage(content) {
|
|
3459
|
+
const lines = content.split("\n");
|
|
3460
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3461
|
+
const line = lines[i];
|
|
3462
|
+
if (!line.startsWith("{")) continue;
|
|
3463
|
+
try {
|
|
3464
|
+
const obj = JSON.parse(line);
|
|
3465
|
+
if (obj.type === "assistant" && obj.message?.usage) {
|
|
3466
|
+
const u = obj.message.usage;
|
|
3467
|
+
if (typeof u.input_tokens === "number" && typeof u.cache_creation_input_tokens === "number" && typeof u.cache_read_input_tokens === "number" && typeof u.output_tokens === "number") {
|
|
3468
|
+
return {
|
|
3469
|
+
inputTokens: u.input_tokens,
|
|
3470
|
+
cacheCreationInputTokens: u.cache_creation_input_tokens,
|
|
3471
|
+
cacheReadInputTokens: u.cache_read_input_tokens,
|
|
3472
|
+
outputTokens: u.output_tokens
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
} catch {
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return void 0;
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
|
|
3483
|
+
export { CwdError2 as CwdError, Output, StructuredOutputError, claudeCode, claudeHostSessionPath, claudeSandboxSessionPath, codex, copilot, createSandbox, createWorktree, cursor, encodeProjectPath, findClaudeSessionOnHost, findCodexSessionOnHost, interactive, opencode, pi, run, transferClaudeSession, transferCodexSession };
|
|
3484
|
+
//# sourceMappingURL=index.js.map
|
|
3485
|
+
//# sourceMappingURL=index.js.map
|