@rockclaver/sandcastle 0.7.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1355 -0
  3. package/dist/MountConfig-CmXclHA5.d.ts +26 -0
  4. package/dist/SandboxProvider-EkSMuBp8.d.ts +243 -0
  5. package/dist/chunk-72UVAC7B.js +99 -0
  6. package/dist/chunk-72UVAC7B.js.map +1 -0
  7. package/dist/chunk-BIWNFKGV.js +22 -0
  8. package/dist/chunk-BIWNFKGV.js.map +1 -0
  9. package/dist/chunk-FKX3DRTL.js +362 -0
  10. package/dist/chunk-FKX3DRTL.js.map +1 -0
  11. package/dist/chunk-NGBM7T3E.js +76 -0
  12. package/dist/chunk-NGBM7T3E.js.map +1 -0
  13. package/dist/chunk-QCLZLPJ7.js +26431 -0
  14. package/dist/chunk-QCLZLPJ7.js.map +1 -0
  15. package/dist/chunk-VAKEM3U2.js +26997 -0
  16. package/dist/chunk-VAKEM3U2.js.map +1 -0
  17. package/dist/index.d.ts +943 -0
  18. package/dist/index.js +2393 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/main.d.ts +1 -0
  21. package/dist/main.js +19268 -0
  22. package/dist/main.js.map +1 -0
  23. package/dist/mountUtils-CCA-bbpK.d.ts +25 -0
  24. package/dist/sandboxes/daytona.d.ts +60 -0
  25. package/dist/sandboxes/daytona.js +122 -0
  26. package/dist/sandboxes/daytona.js.map +1 -0
  27. package/dist/sandboxes/docker.d.ts +110 -0
  28. package/dist/sandboxes/docker.js +9 -0
  29. package/dist/sandboxes/docker.js.map +1 -0
  30. package/dist/sandboxes/no-sandbox.d.ts +38 -0
  31. package/dist/sandboxes/no-sandbox.js +7 -0
  32. package/dist/sandboxes/no-sandbox.js.map +1 -0
  33. package/dist/sandboxes/podman.d.ts +124 -0
  34. package/dist/sandboxes/podman.js +299 -0
  35. package/dist/sandboxes/podman.js.map +1 -0
  36. package/dist/sandboxes/vercel.d.ts +104 -0
  37. package/dist/sandboxes/vercel.js +148 -0
  38. package/dist/sandboxes/vercel.js.map +1 -0
  39. package/dist/templates/blank/main.mts +14 -0
  40. package/dist/templates/blank/prompt.md +12 -0
  41. package/dist/templates/blank/template.json +4 -0
  42. package/dist/templates/parallel-planner/implement-prompt.md +62 -0
  43. package/dist/templates/parallel-planner/main.mts +204 -0
  44. package/dist/templates/parallel-planner/merge-prompt.md +26 -0
  45. package/dist/templates/parallel-planner/plan-prompt.md +37 -0
  46. package/dist/templates/parallel-planner/template.json +4 -0
  47. package/dist/templates/parallel-planner-with-review/CODING_STANDARDS.md +27 -0
  48. package/dist/templates/parallel-planner-with-review/implement-prompt.md +62 -0
  49. package/dist/templates/parallel-planner-with-review/main.mts +226 -0
  50. package/dist/templates/parallel-planner-with-review/merge-prompt.md +26 -0
  51. package/dist/templates/parallel-planner-with-review/plan-prompt.md +37 -0
  52. package/dist/templates/parallel-planner-with-review/review-prompt.md +55 -0
  53. package/dist/templates/parallel-planner-with-review/template.json +4 -0
  54. package/dist/templates/sequential-reviewer/CODING_STANDARDS.md +27 -0
  55. package/dist/templates/sequential-reviewer/implement-prompt.md +53 -0
  56. package/dist/templates/sequential-reviewer/main.mts +119 -0
  57. package/dist/templates/sequential-reviewer/review-prompt.md +55 -0
  58. package/dist/templates/sequential-reviewer/template.json +4 -0
  59. package/dist/templates/simple-loop/main.mts +49 -0
  60. package/dist/templates/simple-loop/prompt.md +53 -0
  61. package/dist/templates/simple-loop/template.json +4 -0
  62. package/package.json +104 -0
package/dist/index.js ADDED
@@ -0,0 +1,2393 @@
1
+ import { createRequire } from 'node:module';
2
+ import { NodeContext_exports, NodeFileSystem_exports, formatErrorMessage } from './chunk-QCLZLPJ7.js';
3
+ export { AGENT_DEFAULT_MODELS, agent, claudeCode, claudeHostSessionPath, claudeSandboxSessionPath, codex, copilot, cursor, encodeProjectPath, findClaudeSessionOnHost, findCodexSessionOnHost, opencode, pi, transferClaudeSession, transferCodexSession } from './chunk-QCLZLPJ7.js';
4
+ import { Context_exports, CwdError, Effect_exports, resolveCwd, getCurrentBranch, generateTempBranchName, Layer_exports, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, pruneStale, create, copyToWorktree, runHostHooks, startSandbox, resolveGitMounts, SANDBOX_REPO_DIR, remove, makeSandboxFromHandle, syncOut, withSandboxLifecycle, hasUncommittedChanges, patchGitMountsForWindows, registerShutdown, SandboxFactory, PromptError, FileSystem_exports, SessionCaptureError, Clock_exports, Duration_exports, Option_exports, PromptExpansionTimeoutError, Deferred_exports, AgentError, Ref_exports, SilentDisplay, AgentIdleTimeoutError, Fiber_exports } from './chunk-VAKEM3U2.js';
5
+ export { createBindMountSandboxProvider, createIsolatedSandboxProvider } from './chunk-BIWNFKGV.js';
6
+ import { noSandbox } from './chunk-72UVAC7B.js';
7
+ import './chunk-NGBM7T3E.js';
8
+ import path, { join } from 'path';
9
+ import { styleText } from 'util';
10
+ import * as clack from '@clack/prompts';
11
+
12
+ createRequire(import.meta.url);
13
+
14
+ // src/resumePrecheck.ts
15
+ var assertResumeSessionExists = async (params) => {
16
+ const { provider, sandboxTag, hostRepoDir, resumeSession } = params;
17
+ if (!provider.sessionStorage) {
18
+ throw new Error(`${provider.name} does not support resumeSession`);
19
+ }
20
+ if (sandboxTag === "none") {
21
+ const found = await provider.sessionStorage.findByIdOnHost(resumeSession);
22
+ if (!found.path) {
23
+ throw new Error(
24
+ `resumeSession "${resumeSession}" not found under ${found.searchedRoot}`
25
+ );
26
+ }
27
+ return;
28
+ }
29
+ const exists = await provider.sessionStorage.existsOnHost(
30
+ hostRepoDir,
31
+ resumeSession
32
+ );
33
+ if (!exists) {
34
+ const sessionPath = provider.sessionStorage.hostSessionFilePath(
35
+ hostRepoDir,
36
+ resumeSession
37
+ );
38
+ throw new Error(
39
+ sessionPath ? `resumeSession "${resumeSession}" not found: expected session file at ${sessionPath}` : `resumeSession "${resumeSession}" not found`
40
+ );
41
+ }
42
+ };
43
+
44
+ // src/AgentStreamEmitter.ts
45
+ var AgentStreamEmitter = class extends Context_exports.Tag("AgentStreamEmitter")() {
46
+ };
47
+ var agentStreamEmitterLayer = (onEvent) => Layer_exports.succeed(AgentStreamEmitter, {
48
+ emit: onEvent ? (event) => Effect_exports.sync(() => {
49
+ try {
50
+ onEvent(event);
51
+ } catch {
52
+ }
53
+ }) : () => Effect_exports.void
54
+ });
55
+
56
+ // src/PromptPreprocessor.ts
57
+ var PROMPT_EXPANSION_TIMEOUT_MS = 3e4;
58
+ var SHELL_BLOCK_MARKER = "";
59
+ var MARKED_SHELL_BLOCK_PATTERN = new RegExp(
60
+ `!${SHELL_BLOCK_MARKER}\`([^\`]+)\``,
61
+ "g"
62
+ );
63
+ var preprocessPrompt = (prompt, sandbox, cwd) => {
64
+ const matches = [...prompt.matchAll(MARKED_SHELL_BLOCK_PATTERN)];
65
+ if (matches.length === 0) {
66
+ return Effect_exports.succeed(prompt.replaceAll(SHELL_BLOCK_MARKER, ""));
67
+ }
68
+ return Effect_exports.gen(function* () {
69
+ const display = yield* Display;
70
+ return yield* display.taskLog(
71
+ "Expanding shell expressions",
72
+ (message) => Effect_exports.gen(function* () {
73
+ const results = yield* Effect_exports.all(
74
+ matches.map((match) => {
75
+ const command = match[1];
76
+ return Effect_exports.gen(function* () {
77
+ const start = yield* Clock_exports.currentTimeMillis;
78
+ const maybeResult = yield* sandbox.exec(command, { cwd }).pipe(
79
+ Effect_exports.timeoutOption(
80
+ Duration_exports.millis(PROMPT_EXPANSION_TIMEOUT_MS)
81
+ )
82
+ );
83
+ if (Option_exports.isNone(maybeResult)) {
84
+ const elapsedMs = (yield* Clock_exports.currentTimeMillis) - start;
85
+ return yield* Effect_exports.fail(
86
+ new PromptExpansionTimeoutError({
87
+ message: `Shell expression \`${command}\` timed out after ${elapsedMs}ms`,
88
+ timeoutMs: PROMPT_EXPANSION_TIMEOUT_MS,
89
+ expression: command,
90
+ elapsedMs
91
+ })
92
+ );
93
+ }
94
+ const execResult = maybeResult.value;
95
+ if (execResult.exitCode !== 0) {
96
+ return yield* Effect_exports.fail(
97
+ new PromptError({
98
+ message: `Command \`${command}\` exited with code ${execResult.exitCode}: ${execResult.stderr}`,
99
+ exitCode: execResult.exitCode
100
+ })
101
+ );
102
+ }
103
+ return execResult.stdout.trimEnd();
104
+ });
105
+ }),
106
+ { concurrency: "unbounded" }
107
+ );
108
+ for (let i = 0; i < matches.length; i++) {
109
+ const command = matches[i][1];
110
+ const tokens = Math.ceil(results[i].length / 4);
111
+ message(`${command} \u2192 ~${tokens} tokens`);
112
+ }
113
+ let result = prompt;
114
+ for (let i = matches.length - 1; i >= 0; i--) {
115
+ const match = matches[i];
116
+ const index = match.index;
117
+ result = result.slice(0, index) + results[i] + result.slice(index + match[0].length);
118
+ }
119
+ return result.replaceAll(SHELL_BLOCK_MARKER, "");
120
+ })
121
+ );
122
+ });
123
+ };
124
+
125
+ // src/TextDeltaBuffer.ts
126
+ var LENGTH_THRESHOLD = 80;
127
+ var DEBOUNCE_MS = 50;
128
+ var SENTENCE_BOUNDARY_RE = /[.!?] $/;
129
+ var TextDeltaBuffer = class {
130
+ buffer = "";
131
+ timer = null;
132
+ onFlush;
133
+ constructor(onFlush) {
134
+ this.onFlush = onFlush;
135
+ }
136
+ write(text2) {
137
+ if (text2.length === 0) return;
138
+ this.buffer += text2;
139
+ this.clearTimer();
140
+ if (this.shouldFlush()) {
141
+ this.doFlush();
142
+ return;
143
+ }
144
+ this.timer = setTimeout(() => {
145
+ this.doFlush();
146
+ }, DEBOUNCE_MS);
147
+ }
148
+ /** Force-flush any buffered text. */
149
+ flush() {
150
+ this.clearTimer();
151
+ this.doFlush();
152
+ }
153
+ /** Flush remaining buffer and clean up. */
154
+ dispose() {
155
+ this.flush();
156
+ }
157
+ shouldFlush() {
158
+ if (this.buffer.includes("\n")) return true;
159
+ if (SENTENCE_BOUNDARY_RE.test(this.buffer)) return true;
160
+ if (this.buffer.length >= LENGTH_THRESHOLD) return true;
161
+ return false;
162
+ }
163
+ doFlush() {
164
+ if (this.buffer.length === 0) return;
165
+ const text2 = this.buffer;
166
+ this.buffer = "";
167
+ this.onFlush(text2);
168
+ }
169
+ clearTimer() {
170
+ if (this.timer !== null) {
171
+ clearTimeout(this.timer);
172
+ this.timer = null;
173
+ }
174
+ }
175
+ };
176
+
177
+ // src/Orchestrator.ts
178
+ var IDLE_WARNING_INTERVAL_MS = 6e4;
179
+ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, completionTimeoutMs, completionSignals, onText, onToolCall, onIdleWarning, onCompletionTimeout, idleWarningIntervalMs = IDLE_WARNING_INTERVAL_MS, resumeSession, forkSession, signal) => Effect_exports.gen(function* () {
180
+ let resultText = "";
181
+ let sessionId;
182
+ let usage;
183
+ let accumulatedOutput = "";
184
+ const timeoutSignal = yield* Deferred_exports.make();
185
+ const completionTimeoutDeferred = yield* Deferred_exports.make();
186
+ let timeoutFiber = null;
187
+ let completionDetected = false;
188
+ let warningFiber = null;
189
+ let idleMinuteCounter = 0;
190
+ const interruptFiber = (fiber) => {
191
+ if (fiber !== null) Effect_exports.runFork(Fiber_exports.interrupt(fiber));
192
+ };
193
+ const startWarningInterval = () => {
194
+ interruptFiber(warningFiber);
195
+ idleMinuteCounter = 0;
196
+ warningFiber = Effect_exports.runFork(
197
+ Effect_exports.gen(function* () {
198
+ while (true) {
199
+ yield* Effect_exports.sleep(Duration_exports.millis(idleWarningIntervalMs));
200
+ idleMinuteCounter++;
201
+ onIdleWarning(idleMinuteCounter);
202
+ }
203
+ })
204
+ );
205
+ };
206
+ const resetTimer = () => {
207
+ interruptFiber(timeoutFiber);
208
+ if (completionDetected) {
209
+ timeoutFiber = Effect_exports.runFork(
210
+ Effect_exports.gen(function* () {
211
+ yield* Effect_exports.sleep(Duration_exports.millis(completionTimeoutMs));
212
+ onCompletionTimeout(completionTimeoutMs);
213
+ yield* Deferred_exports.succeed(completionTimeoutDeferred, {
214
+ result: resultText || accumulatedOutput,
215
+ sessionId,
216
+ usage
217
+ });
218
+ })
219
+ );
220
+ } else {
221
+ timeoutFiber = Effect_exports.runFork(
222
+ Effect_exports.gen(function* () {
223
+ yield* Effect_exports.sleep(Duration_exports.millis(idleTimeoutMs));
224
+ yield* Deferred_exports.fail(
225
+ timeoutSignal,
226
+ new AgentIdleTimeoutError({
227
+ message: `Agent idle for ${idleTimeoutMs / 1e3} seconds \u2014 no output received. Consider increasing the idle timeout with --idle-timeout.`,
228
+ timeoutMs: idleTimeoutMs
229
+ })
230
+ );
231
+ })
232
+ );
233
+ startWarningInterval();
234
+ }
235
+ };
236
+ const abortDeferred = yield* Deferred_exports.make();
237
+ let abortCleanup = null;
238
+ if (signal) {
239
+ if (signal.aborted) {
240
+ return yield* Effect_exports.die(signal.reason);
241
+ }
242
+ const onAbort = () => {
243
+ Effect_exports.runFork(Deferred_exports.die(abortDeferred, signal.reason));
244
+ };
245
+ signal.addEventListener("abort", onAbort, { once: true });
246
+ abortCleanup = () => signal.removeEventListener("abort", onAbort);
247
+ }
248
+ resetTimer();
249
+ const execEffect = Effect_exports.gen(function* () {
250
+ const printCmd = provider.buildPrintCommand({
251
+ prompt,
252
+ dangerouslySkipPermissions: true,
253
+ resumeSession,
254
+ forkSession
255
+ });
256
+ const execResult = yield* sandbox.exec(printCmd.command, {
257
+ onLine: (line) => {
258
+ for (const parsed of provider.parseStreamLine(line)) {
259
+ if (parsed.type === "text") {
260
+ onText(parsed.text);
261
+ accumulatedOutput += parsed.text;
262
+ } else if (parsed.type === "result") {
263
+ resultText = parsed.result;
264
+ accumulatedOutput += parsed.result;
265
+ } else if (parsed.type === "tool_call") {
266
+ onToolCall(parsed.name, parsed.args);
267
+ } else if (parsed.type === "session_id") {
268
+ sessionId = parsed.sessionId;
269
+ } else if (parsed.type === "usage") {
270
+ usage = parsed.usage;
271
+ }
272
+ }
273
+ if (!completionDetected && completionSignals.some((sig) => accumulatedOutput.includes(sig))) {
274
+ completionDetected = true;
275
+ interruptFiber(warningFiber);
276
+ warningFiber = null;
277
+ }
278
+ resetTimer();
279
+ },
280
+ cwd: sandboxRepoDir,
281
+ stdin: printCmd.stdin
282
+ });
283
+ if (execResult.exitCode !== 0) {
284
+ let errorDetail = execResult.stderr;
285
+ if (!errorDetail.trim()) {
286
+ errorDetail = resultText;
287
+ }
288
+ if (!errorDetail.trim()) {
289
+ const lines = execResult.stdout.split("\n").filter((l) => l.trim());
290
+ errorDetail = lines.slice(-20).join("\n");
291
+ }
292
+ return yield* Effect_exports.fail(
293
+ new AgentError({
294
+ message: `${provider.name} exited with code ${execResult.exitCode}:
295
+ ${errorDetail}`
296
+ })
297
+ );
298
+ }
299
+ return { result: resultText || execResult.stdout, sessionId, usage };
300
+ }).pipe(
301
+ Effect_exports.ensuring(
302
+ Effect_exports.sync(() => {
303
+ interruptFiber(timeoutFiber);
304
+ timeoutFiber = null;
305
+ interruptFiber(warningFiber);
306
+ warningFiber = null;
307
+ })
308
+ )
309
+ );
310
+ let raced = Effect_exports.raceFirst(execEffect, Deferred_exports.await(timeoutSignal));
311
+ raced = Effect_exports.raceFirst(raced, Deferred_exports.await(completionTimeoutDeferred));
312
+ if (signal) {
313
+ raced = Effect_exports.raceFirst(
314
+ raced,
315
+ Deferred_exports.await(abortDeferred)
316
+ );
317
+ }
318
+ return yield* raced.pipe(
319
+ Effect_exports.ensuring(
320
+ Effect_exports.sync(() => {
321
+ abortCleanup?.();
322
+ interruptFiber(timeoutFiber);
323
+ timeoutFiber = null;
324
+ interruptFiber(warningFiber);
325
+ warningFiber = null;
326
+ })
327
+ )
328
+ );
329
+ });
330
+ var DEFAULT_COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
331
+ var DEFAULT_IDLE_TIMEOUT_SECONDS = 10 * 60;
332
+ var DEFAULT_COMPLETION_TIMEOUT_SECONDS = 60;
333
+ var orchestrate = (options) => {
334
+ const idleTimeoutMs = (options.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS) * 1e3;
335
+ const completionTimeoutMs = (options.completionTimeoutSeconds ?? DEFAULT_COMPLETION_TIMEOUT_SECONDS) * 1e3;
336
+ return Effect_exports.gen(function* () {
337
+ const factory = yield* SandboxFactory;
338
+ const display = yield* Display;
339
+ const streamEmitter = yield* AgentStreamEmitter;
340
+ const { hostRepoDir, iterations, hooks, prompt, branch, provider } = options;
341
+ let completionSignals;
342
+ if (options.completionSignal === void 0) {
343
+ completionSignals = [DEFAULT_COMPLETION_SIGNAL];
344
+ } else if (Array.isArray(options.completionSignal)) {
345
+ completionSignals = options.completionSignal;
346
+ } else {
347
+ completionSignals = [options.completionSignal];
348
+ }
349
+ const label = (msg) => options.name ? `[${options.name}] ${msg}` : msg;
350
+ const allCommits = [];
351
+ const allIterations = [];
352
+ let allStdout = "";
353
+ let resolvedBranch = "";
354
+ let iterationPreservedPath;
355
+ const checkAbort = () => options.signal?.aborted ? Effect_exports.die(options.signal.reason) : Effect_exports.void;
356
+ for (let i = 1; i <= iterations; i++) {
357
+ yield* checkAbort();
358
+ yield* display.status(label(`Iteration ${i}/${iterations}`), "info");
359
+ const sandboxResult = yield* factory.withSandbox(
360
+ ({ hostWorktreePath, sandboxRepoPath, applyToHost, bindMountHandle }, sandbox) => withSandboxLifecycle(
361
+ {
362
+ hostRepoDir,
363
+ sandboxRepoDir: sandboxRepoPath,
364
+ hooks,
365
+ branch,
366
+ hostWorktreePath,
367
+ applyToHost,
368
+ signal: options.signal,
369
+ timeouts: options.timeouts
370
+ },
371
+ sandbox,
372
+ (ctx) => Effect_exports.gen(function* () {
373
+ const iterationResumeSession = i === 1 ? options.resumeSession : void 0;
374
+ const iterationForkSession = i === 1 ? options.forkSession : void 0;
375
+ if (iterationResumeSession && bindMountHandle && provider.sessionStorage) {
376
+ yield* display.status(label("Resuming session"), "info");
377
+ yield* Effect_exports.tryPromise({
378
+ try: () => provider.sessionStorage.resumeIntoSandbox({
379
+ hostCwd: hostRepoDir,
380
+ sandboxCwd: ctx.sandboxRepoDir,
381
+ sessionId: iterationResumeSession,
382
+ handle: bindMountHandle
383
+ }),
384
+ catch: (e) => new SessionCaptureError({
385
+ message: `Session resume failed: ${e instanceof Error ? e.message : String(e)}`,
386
+ sessionId: iterationResumeSession
387
+ })
388
+ });
389
+ }
390
+ const fullPrompt = options.skipPromptExpansion ? prompt : yield* preprocessPrompt(
391
+ prompt,
392
+ ctx.sandbox,
393
+ ctx.sandboxRepoDir
394
+ );
395
+ yield* display.status(label("Agent started"), "success");
396
+ const textBuffer = new TextDeltaBuffer((chunk) => {
397
+ Effect_exports.runPromise(display.text(chunk));
398
+ Effect_exports.runPromise(
399
+ streamEmitter.emit({
400
+ type: "text",
401
+ message: chunk,
402
+ iteration: i,
403
+ timestamp: /* @__PURE__ */ new Date()
404
+ })
405
+ );
406
+ });
407
+ const onText = (text2) => {
408
+ textBuffer.write(text2);
409
+ };
410
+ const onToolCall = (name, formattedArgs) => {
411
+ textBuffer.flush();
412
+ Effect_exports.runPromise(display.toolCall(name, formattedArgs));
413
+ Effect_exports.runPromise(
414
+ streamEmitter.emit({
415
+ type: "toolCall",
416
+ name,
417
+ formattedArgs,
418
+ iteration: i,
419
+ timestamp: /* @__PURE__ */ new Date()
420
+ })
421
+ );
422
+ };
423
+ const onIdleWarning = (minutes) => {
424
+ const msg = minutes === 1 ? "Agent idle for 1 minute" : `Agent idle for ${minutes} minutes`;
425
+ Effect_exports.runPromise(display.status(label(msg), "warn"));
426
+ };
427
+ const onCompletionTimeout = (timeoutMs) => {
428
+ Effect_exports.runPromise(
429
+ display.status(
430
+ label(
431
+ `Completion signal seen but agent process is hanging \u2014 force-completing after ${timeoutMs / 1e3}s grace window.`
432
+ ),
433
+ "warn"
434
+ )
435
+ );
436
+ };
437
+ const {
438
+ result: agentOutput,
439
+ sessionId,
440
+ usage: streamUsage
441
+ } = yield* invokeAgent(
442
+ ctx.sandbox,
443
+ ctx.sandboxRepoDir,
444
+ fullPrompt,
445
+ provider,
446
+ idleTimeoutMs,
447
+ completionTimeoutMs,
448
+ completionSignals,
449
+ onText,
450
+ onToolCall,
451
+ onIdleWarning,
452
+ onCompletionTimeout,
453
+ options._idleWarningIntervalMs,
454
+ iterationResumeSession,
455
+ iterationForkSession,
456
+ options.signal
457
+ );
458
+ textBuffer.dispose();
459
+ yield* display.status(label("Agent stopped"), "info");
460
+ let sessionFilePath;
461
+ let usage = streamUsage;
462
+ if (provider.captureSessions && provider.sessionStorage && sessionId && bindMountHandle) {
463
+ yield* display.status(label("Capturing session"), "info");
464
+ yield* Effect_exports.tryPromise({
465
+ try: () => provider.sessionStorage.captureToHost({
466
+ hostCwd: hostRepoDir,
467
+ sandboxCwd: ctx.sandboxRepoDir,
468
+ sessionId,
469
+ handle: bindMountHandle
470
+ }),
471
+ catch: (e) => new SessionCaptureError({
472
+ message: `Session capture failed: ${e instanceof Error ? e.message : String(e)}`,
473
+ sessionId
474
+ })
475
+ });
476
+ sessionFilePath = provider.sessionStorage.hostSessionFilePath(
477
+ hostRepoDir,
478
+ sessionId
479
+ );
480
+ if (provider.parseSessionUsage) {
481
+ const content = yield* Effect_exports.promise(
482
+ () => provider.sessionStorage.readHostSession(hostRepoDir, sessionId).catch(() => void 0)
483
+ );
484
+ if (content) {
485
+ const parsedUsage = provider.parseSessionUsage(content);
486
+ if (parsedUsage) usage = parsedUsage;
487
+ }
488
+ }
489
+ }
490
+ const matchedSignal = completionSignals.find(
491
+ (sig) => agentOutput.includes(sig)
492
+ );
493
+ return {
494
+ completionSignal: matchedSignal,
495
+ stdout: agentOutput,
496
+ sessionId,
497
+ sessionFilePath,
498
+ usage
499
+ };
500
+ })
501
+ )
502
+ );
503
+ const lifecycleResult = sandboxResult.value;
504
+ iterationPreservedPath = sandboxResult.preservedWorktreePath;
505
+ allCommits.push(...lifecycleResult.commits);
506
+ allStdout += lifecycleResult.result.stdout;
507
+ resolvedBranch = lifecycleResult.branch;
508
+ allIterations.push({
509
+ sessionId: lifecycleResult.result.sessionId,
510
+ sessionFilePath: lifecycleResult.result.sessionFilePath,
511
+ usage: lifecycleResult.result.usage
512
+ });
513
+ if (lifecycleResult.result.completionSignal !== void 0) {
514
+ yield* display.status(
515
+ label(`Agent signaled completion after ${i} iteration(s).`),
516
+ "success"
517
+ );
518
+ return {
519
+ iterations: allIterations,
520
+ completionSignal: lifecycleResult.result.completionSignal,
521
+ stdout: allStdout,
522
+ commits: allCommits,
523
+ branch: resolvedBranch,
524
+ preservedWorktreePath: iterationPreservedPath
525
+ };
526
+ }
527
+ }
528
+ yield* display.status(
529
+ label(`Reached max iterations (${iterations}).`),
530
+ "info"
531
+ );
532
+ return {
533
+ iterations: allIterations,
534
+ completionSignal: void 0,
535
+ stdout: allStdout,
536
+ commits: allCommits,
537
+ branch: resolvedBranch,
538
+ preservedWorktreePath: iterationPreservedPath
539
+ };
540
+ });
541
+ };
542
+
543
+ // src/PromptResolver.ts
544
+ var resolvePrompt = (options) => {
545
+ const { prompt, promptFile } = options;
546
+ if (prompt !== void 0 && promptFile !== void 0) {
547
+ return Effect_exports.fail(
548
+ new PromptError({
549
+ message: "Cannot provide both --prompt and --prompt-file"
550
+ })
551
+ );
552
+ }
553
+ if (prompt !== void 0) {
554
+ return Effect_exports.succeed({ text: prompt, source: "inline" });
555
+ }
556
+ if (promptFile === void 0) {
557
+ return Effect_exports.fail(
558
+ new PromptError({
559
+ message: "Must provide either prompt or promptFile. Pass prompt: '...' or promptFile: './.sandcastle/prompt.md' to run()."
560
+ })
561
+ );
562
+ }
563
+ return Effect_exports.gen(function* () {
564
+ const fs = yield* FileSystem_exports.FileSystem;
565
+ const text2 = yield* fs.readFileString(promptFile).pipe(
566
+ Effect_exports.catchAll(
567
+ (e) => Effect_exports.fail(
568
+ new PromptError({
569
+ message: `Failed to read prompt from ${promptFile}: ${e}`
570
+ })
571
+ )
572
+ )
573
+ );
574
+ return { text: text2, source: "template" };
575
+ });
576
+ };
577
+ var parseEnvFile = (filePath) => Effect_exports.gen(function* () {
578
+ const fs = yield* FileSystem_exports.FileSystem;
579
+ const content = yield* fs.readFileString(filePath).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(null)));
580
+ if (content === null) return {};
581
+ const vars = {};
582
+ for (const line of content.split("\n")) {
583
+ const trimmed = line.trim();
584
+ if (!trimmed || trimmed.startsWith("#")) continue;
585
+ const eqIndex = trimmed.indexOf("=");
586
+ if (eqIndex === -1) continue;
587
+ const key = trimmed.slice(0, eqIndex).trim();
588
+ let value = trimmed.slice(eqIndex + 1).trim();
589
+ const isDoubleQuoted = value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"';
590
+ const isSingleQuoted = value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'";
591
+ if (isDoubleQuoted || isSingleQuoted) {
592
+ value = value.slice(1, -1);
593
+ }
594
+ if (isDoubleQuoted) {
595
+ value = value.replace(/\\([nrt\\])/g, (_, ch) => {
596
+ const escapes = {
597
+ n: "\n",
598
+ r: "\r",
599
+ t: " ",
600
+ "\\": "\\"
601
+ };
602
+ return escapes[ch] ?? ch;
603
+ });
604
+ }
605
+ vars[key] = value;
606
+ }
607
+ return vars;
608
+ });
609
+ var resolveEnv = (repoDir) => Effect_exports.gen(function* () {
610
+ const sandcastleEnv = yield* parseEnvFile(
611
+ join(repoDir, ".sandcastle", ".env")
612
+ );
613
+ const result = {};
614
+ for (const key of Object.keys(sandcastleEnv)) {
615
+ const value = sandcastleEnv[key] || process.env[key];
616
+ if (value) {
617
+ result[key] = value;
618
+ }
619
+ }
620
+ return result;
621
+ });
622
+
623
+ // src/mergeProviderEnv.ts
624
+ var mergeProviderEnv = (options) => {
625
+ const { resolvedEnv, agentProviderEnv, sandboxProviderEnv } = options;
626
+ const agentKeys = Object.keys(agentProviderEnv);
627
+ const sandboxKeys = new Set(Object.keys(sandboxProviderEnv));
628
+ const overlapping = agentKeys.filter((k) => sandboxKeys.has(k));
629
+ if (overlapping.length > 0) {
630
+ throw new Error(
631
+ `Overlapping env keys between agent provider and sandbox provider: ${overlapping.join(", ")}`
632
+ );
633
+ }
634
+ return {
635
+ ...resolvedEnv,
636
+ ...sandboxProviderEnv,
637
+ ...agentProviderEnv
638
+ };
639
+ };
640
+
641
+ // src/PromptArgumentSubstitution.ts
642
+ var SHELL_BLOCK_PATTERN = /!`([^`]+)`/g;
643
+ var BUILT_IN_PROMPT_ARG_KEYS = [
644
+ "SOURCE_BRANCH",
645
+ "TARGET_BRANCH"
646
+ ];
647
+ var PLACEHOLDER_PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
648
+ var validateNoArgsWithInlinePrompt = (args) => {
649
+ if (Object.keys(args).length === 0) return Effect_exports.void;
650
+ return Effect_exports.fail(
651
+ new PromptError({
652
+ 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.'
653
+ })
654
+ );
655
+ };
656
+ var validateNoBuiltInArgOverride = (args) => {
657
+ for (const key of BUILT_IN_PROMPT_ARG_KEYS) {
658
+ if (key in args) {
659
+ return Effect_exports.fail(
660
+ new PromptError({
661
+ message: `"${key}" is a built-in prompt argument and cannot be overridden via promptArgs`
662
+ })
663
+ );
664
+ }
665
+ }
666
+ return Effect_exports.void;
667
+ };
668
+ var findMissingPromptArgKeys = (prompt, providedArgs) => {
669
+ const matches = [...prompt.matchAll(PLACEHOLDER_PATTERN)];
670
+ const builtInSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
671
+ const seen = /* @__PURE__ */ new Set();
672
+ const missing = [];
673
+ for (const m of matches) {
674
+ const key = m[1];
675
+ if (seen.has(key)) continue;
676
+ seen.add(key);
677
+ if (builtInSet.has(key)) continue;
678
+ if (key in providedArgs) continue;
679
+ missing.push(key);
680
+ }
681
+ return missing;
682
+ };
683
+ var substitutePromptArgs = (prompt, args, silentKeys) => {
684
+ const markedPrompt = prompt.replaceAll(SHELL_BLOCK_MARKER, "").replace(SHELL_BLOCK_PATTERN, `!${SHELL_BLOCK_MARKER}\`$1\``);
685
+ const sanitizedArgs = Object.fromEntries(
686
+ Object.entries(args).map(([key, value]) => [
687
+ key,
688
+ typeof value === "string" ? value.replaceAll(SHELL_BLOCK_MARKER, "") : value
689
+ ])
690
+ );
691
+ const matches = [...markedPrompt.matchAll(PLACEHOLDER_PATTERN)];
692
+ if (matches.length === 0 && Object.keys(sanitizedArgs).length === 0) {
693
+ return Effect_exports.succeed(markedPrompt);
694
+ }
695
+ return Effect_exports.gen(function* () {
696
+ const display = yield* Display;
697
+ const referencedKeys = new Set(matches.map((m) => m[1]));
698
+ for (const key of referencedKeys) {
699
+ if (!(key in sanitizedArgs)) {
700
+ return yield* Effect_exports.fail(
701
+ new PromptError({
702
+ message: `Prompt argument "{{${key}}}" has no matching value in promptArgs`
703
+ })
704
+ );
705
+ }
706
+ }
707
+ for (const key of Object.keys(sanitizedArgs)) {
708
+ if (!referencedKeys.has(key) && !silentKeys?.has(key)) {
709
+ yield* display.status(
710
+ `Prompt argument "${key}" was provided but not referenced in the prompt`,
711
+ "warn"
712
+ );
713
+ }
714
+ }
715
+ const result = markedPrompt.replace(
716
+ PLACEHOLDER_PATTERN,
717
+ (_match, key) => sanitizedArgs[key].toString()
718
+ );
719
+ return result;
720
+ });
721
+ };
722
+
723
+ // src/Output.ts
724
+ var Output = {
725
+ /**
726
+ * Declare an object-typed structured output extracted from an XML tag in
727
+ * the agent's stdout. The tag contents are JSON-parsed (with fence-aware
728
+ * unwrapping) and validated against the provided Standard Schema validator.
729
+ */
730
+ object: (opts) => ({
731
+ _tag: "object",
732
+ tag: opts.tag,
733
+ schema: opts.schema
734
+ }),
735
+ /**
736
+ * Declare a string-typed structured output extracted from an XML tag in
737
+ * the agent's stdout. The tag contents are whitespace-trimmed and returned
738
+ * as a plain string — no JSON parsing, no schema validation.
739
+ */
740
+ string: (opts) => ({
741
+ _tag: "string",
742
+ tag: opts.tag
743
+ })
744
+ };
745
+ var StructuredOutputError = class extends Error {
746
+ tag;
747
+ rawMatched;
748
+ cause;
749
+ commits;
750
+ branch;
751
+ preservedWorktreePath;
752
+ /** Session ID of the iteration that produced the bad output, when available. */
753
+ sessionId;
754
+ /** Host path to the captured session JSONL, when the session was captured. */
755
+ sessionFilePath;
756
+ constructor(message, options) {
757
+ super(message);
758
+ this.name = "StructuredOutputError";
759
+ this.tag = options.tag;
760
+ this.rawMatched = options.rawMatched;
761
+ this.cause = options.cause;
762
+ this.commits = options.commits;
763
+ this.branch = options.branch;
764
+ this.preservedWorktreePath = options.preservedWorktreePath;
765
+ this.sessionId = options.sessionId;
766
+ this.sessionFilePath = options.sessionFilePath;
767
+ }
768
+ };
769
+
770
+ // src/extractStructuredOutput.ts
771
+ var extractStructuredOutput = async (stdout, definition, context) => {
772
+ if (definition._tag === "object") {
773
+ return extractObject(
774
+ stdout,
775
+ definition,
776
+ context
777
+ );
778
+ }
779
+ return extractString(stdout, definition, context);
780
+ };
781
+ var extractObject = async (stdout, definition, context) => {
782
+ const raw = findLastTagContent(stdout, definition.tag);
783
+ if (raw === void 0) {
784
+ throw new StructuredOutputError(
785
+ `Structured output tag <${definition.tag}> not found in agent output`,
786
+ { tag: definition.tag, rawMatched: void 0, ...context }
787
+ );
788
+ }
789
+ const unwrapped = unwrapFences(raw.trim());
790
+ let parsed;
791
+ try {
792
+ parsed = JSON.parse(unwrapped);
793
+ } catch (cause) {
794
+ throw new StructuredOutputError(
795
+ `Structured output tag <${definition.tag}> contains invalid JSON`,
796
+ { tag: definition.tag, rawMatched: raw, cause, ...context }
797
+ );
798
+ }
799
+ const result = await definition.schema["~standard"].validate(parsed);
800
+ if (result.issues) {
801
+ throw new StructuredOutputError(
802
+ `Structured output tag <${definition.tag}> failed schema validation`,
803
+ {
804
+ tag: definition.tag,
805
+ rawMatched: raw,
806
+ cause: result.issues,
807
+ ...context
808
+ }
809
+ );
810
+ }
811
+ return result.value;
812
+ };
813
+ var extractString = (stdout, definition, context) => {
814
+ const raw = findLastTagContent(stdout, definition.tag);
815
+ if (raw === void 0) {
816
+ throw new StructuredOutputError(
817
+ `Structured output tag <${definition.tag}> not found in agent output`,
818
+ { tag: definition.tag, rawMatched: void 0, ...context }
819
+ );
820
+ }
821
+ return raw.trim();
822
+ };
823
+ var findLastTagContent = (text2, tag) => {
824
+ const openTag = `<${tag}>`;
825
+ const closeTag = `</${tag}>`;
826
+ let lastContent;
827
+ let searchFrom = 0;
828
+ while (true) {
829
+ const openIdx = text2.indexOf(openTag, searchFrom);
830
+ if (openIdx === -1) break;
831
+ const contentStart = openIdx + openTag.length;
832
+ const closeIdx = text2.indexOf(closeTag, contentStart);
833
+ if (closeIdx === -1) break;
834
+ lastContent = text2.slice(contentStart, closeIdx);
835
+ searchFrom = closeIdx + closeTag.length;
836
+ }
837
+ return lastContent;
838
+ };
839
+ var unwrapFences = (text2) => {
840
+ const fenceMatch = text2.match(/^```(?:json)?\s*\n([\s\S]*?)\n\s*```\s*$/);
841
+ if (fenceMatch) {
842
+ return fenceMatch[1].trim();
843
+ }
844
+ return text2;
845
+ };
846
+
847
+ // src/run.ts
848
+ var DEFAULT_MAX_ITERATIONS = 1;
849
+ var sanitizeBranchForFilename = (branch) => branch.replace(/[/\\:*?"<>|]/g, "-");
850
+ var printFileDisplayStartup = (options) => {
851
+ const name = options.agentName ?? "Agent";
852
+ const label = styleText("bold", `[${name}]`);
853
+ const branchPart = options.branch ? ` on branch ${options.branch}` : "";
854
+ const hostRepoDir = options.hostRepoDir ?? process.cwd();
855
+ const displayLogPath = hostRepoDir === process.cwd() ? path.relative(process.cwd(), options.logPath) : options.logPath;
856
+ console.log(`${label} Started${branchPart}`);
857
+ console.log(styleText("dim", ` tail -f ${displayLogPath}`));
858
+ };
859
+ var buildLogFilename = (resolvedBranch, targetBranch, name) => {
860
+ const sanitized = sanitizeBranchForFilename(resolvedBranch);
861
+ const nameSuffix = name ? `-${name.toLowerCase().replace(/[^a-z0-9_.-]/g, "-")}` : "";
862
+ if (targetBranch) {
863
+ return `${sanitizeBranchForFilename(targetBranch)}-${sanitized}${nameSuffix}.log`;
864
+ }
865
+ return `${sanitized}${nameSuffix}.log`;
866
+ };
867
+ var buildRunSummaryRows = (options) => ({
868
+ Agent: options.name ?? options.agentName,
869
+ Sandbox: options.sandboxName,
870
+ "Max iterations": String(options.maxIterations),
871
+ Branch: options.branch
872
+ });
873
+ var buildCompletionMessage = (completionSignal, iterationsRun) => {
874
+ if (completionSignal !== void 0) {
875
+ return {
876
+ message: `Run complete: agent finished after ${iterationsRun} iteration(s).`,
877
+ severity: "success"
878
+ };
879
+ }
880
+ return {
881
+ message: `Run complete: reached ${iterationsRun} iteration(s) without completion signal.`,
882
+ severity: "warn"
883
+ };
884
+ };
885
+ var formatContextWindowSize = (usage) => {
886
+ const total = usage.inputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
887
+ return `${Math.ceil(total / 1e3)}k`;
888
+ };
889
+ var buildContextWindowLines = (iterations) => iterations.filter((it) => it.usage !== void 0).map((it) => `Context window: ${formatContextWindowSize(it.usage)}`);
890
+ async function run(options) {
891
+ options.signal?.throwIfAborted();
892
+ const {
893
+ prompt,
894
+ promptFile,
895
+ maxIterations = DEFAULT_MAX_ITERATIONS,
896
+ hooks,
897
+ agent: provider
898
+ } = options;
899
+ const branchStrategy = options.branchStrategy ?? (options.sandbox.tag === "isolated" ? { type: "merge-to-head" } : { type: "head" });
900
+ const effectiveBranchType = branchStrategy.type;
901
+ if (effectiveBranchType === "head" && options.sandbox.tag === "isolated") {
902
+ throw new Error(
903
+ "head branch strategy is not supported with isolated providers"
904
+ );
905
+ }
906
+ if (effectiveBranchType === "head" && options.copyToWorktree && options.copyToWorktree.length > 0) {
907
+ throw new Error(
908
+ "copyToWorktree is not supported with head branch strategy. In head mode the host working directory is bind-mounted directly."
909
+ );
910
+ }
911
+ if (options.resumeSession && maxIterations > 1) {
912
+ throw new Error(
913
+ "resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
914
+ );
915
+ }
916
+ if (options.forkSession && !options.resumeSession) {
917
+ throw new Error(
918
+ "forkSession requires resumeSession. Use RunResult.fork(prompt) to fork the last captured session."
919
+ );
920
+ }
921
+ if (options.output && maxIterations !== 1) {
922
+ throw new Error(
923
+ "output requires maxIterations to be 1. Structured output is only supported for single-iteration runs."
924
+ );
925
+ }
926
+ const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
927
+ const hostRepoDir = await Effect_exports.runPromise(
928
+ resolveCwd(options.cwd).pipe(Effect_exports.provide(NodeContext_exports.layer))
929
+ );
930
+ if (options.resumeSession) {
931
+ await assertResumeSessionExists({
932
+ provider,
933
+ sandboxTag: options.sandbox.tag,
934
+ hostRepoDir,
935
+ resumeSession: options.resumeSession
936
+ });
937
+ }
938
+ const resolved = await Effect_exports.runPromise(
939
+ resolvePrompt({ prompt, promptFile }).pipe(
940
+ Effect_exports.provide(NodeContext_exports.layer)
941
+ )
942
+ );
943
+ const rawPrompt = resolved.text;
944
+ const isInlinePrompt = resolved.source === "inline";
945
+ if (options.output) {
946
+ const openTag = `<${options.output.tag}>`;
947
+ if (!rawPrompt.includes(openTag)) {
948
+ throw new Error(
949
+ `output tag <${options.output.tag}> not found in the resolved prompt. The caller must instruct the agent to emit the configured tag.`
950
+ );
951
+ }
952
+ }
953
+ const agentName = provider.name;
954
+ const resolvedEnv = await Effect_exports.runPromise(
955
+ resolveEnv(hostRepoDir).pipe(Effect_exports.provide(NodeContext_exports.layer))
956
+ );
957
+ const env = mergeProviderEnv({
958
+ resolvedEnv,
959
+ agentProviderEnv: provider.env,
960
+ sandboxProviderEnv: options.sandbox.env
961
+ });
962
+ const currentHostBranch = await Effect_exports.runPromise(
963
+ getCurrentBranch(hostRepoDir)
964
+ );
965
+ const resolvedBranch = effectiveBranchType === "head" ? currentHostBranch : branch ?? generateTempBranchName(options.name);
966
+ const targetBranch = effectiveBranchType === "merge-to-head" ? currentHostBranch : void 0;
967
+ const resolvedLogging = options.logging ?? {
968
+ type: "file",
969
+ path: join(
970
+ hostRepoDir,
971
+ ".sandcastle",
972
+ "logs",
973
+ buildLogFilename(resolvedBranch, targetBranch, options.name)
974
+ )
975
+ };
976
+ const displayLayer = resolvedLogging.type === "file" ? (() => {
977
+ printFileDisplayStartup({
978
+ logPath: resolvedLogging.path,
979
+ agentName: options.name,
980
+ branch: resolvedBranch,
981
+ hostRepoDir
982
+ });
983
+ return Layer_exports.provide(
984
+ FileDisplay.layer(resolvedLogging.path),
985
+ NodeFileSystem_exports.layer
986
+ );
987
+ })() : ClackDisplay.layer;
988
+ const factoryLayer = Layer_exports.provide(
989
+ WorktreeDockerSandboxFactory.layer,
990
+ Layer_exports.mergeAll(
991
+ Layer_exports.succeed(SandboxConfig, {
992
+ env,
993
+ hostRepoDir,
994
+ copyToWorktree: options.copyToWorktree,
995
+ name: options.name,
996
+ sandboxProvider: options.sandbox,
997
+ branchStrategy,
998
+ hooks,
999
+ signal: options.signal,
1000
+ timeouts: options.timeouts
1001
+ }),
1002
+ NodeFileSystem_exports.layer,
1003
+ displayLayer
1004
+ )
1005
+ );
1006
+ const streamEmitterLayer = agentStreamEmitterLayer(
1007
+ resolvedLogging.type === "file" ? resolvedLogging.onAgentStreamEvent : void 0
1008
+ );
1009
+ const runLayer = Layer_exports.mergeAll(
1010
+ factoryLayer,
1011
+ displayLayer,
1012
+ streamEmitterLayer
1013
+ );
1014
+ const baseEffect = Effect_exports.gen(function* () {
1015
+ const d = yield* Display;
1016
+ yield* d.intro(options.name ?? "sandcastle");
1017
+ const rows = buildRunSummaryRows({
1018
+ name: options.name,
1019
+ agentName,
1020
+ sandboxName: options.sandbox.name,
1021
+ maxIterations,
1022
+ branch: resolvedBranch
1023
+ });
1024
+ yield* d.summary("Sandcastle Run", rows);
1025
+ const userArgs = options.promptArgs ?? {};
1026
+ let resolvedPrompt;
1027
+ if (isInlinePrompt) {
1028
+ yield* validateNoArgsWithInlinePrompt(userArgs);
1029
+ resolvedPrompt = rawPrompt;
1030
+ } else {
1031
+ yield* validateNoBuiltInArgOverride(userArgs);
1032
+ const effectiveArgs = {
1033
+ SOURCE_BRANCH: resolvedBranch,
1034
+ TARGET_BRANCH: currentHostBranch,
1035
+ ...userArgs
1036
+ };
1037
+ const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
1038
+ resolvedPrompt = yield* substitutePromptArgs(
1039
+ rawPrompt,
1040
+ effectiveArgs,
1041
+ builtInArgKeysSet
1042
+ );
1043
+ }
1044
+ const orchestrateBranch = effectiveBranchType === "head" ? currentHostBranch : branch;
1045
+ const orchestrateResult = yield* orchestrate({
1046
+ hostRepoDir,
1047
+ iterations: maxIterations,
1048
+ hooks,
1049
+ prompt: resolvedPrompt,
1050
+ branch: orchestrateBranch,
1051
+ provider,
1052
+ completionSignal: options.completionSignal,
1053
+ idleTimeoutSeconds: options.idleTimeoutSeconds,
1054
+ completionTimeoutSeconds: options.completionTimeoutSeconds,
1055
+ name: options.name,
1056
+ resumeSession: options.resumeSession,
1057
+ forkSession: options.forkSession,
1058
+ signal: options.signal,
1059
+ skipPromptExpansion: isInlinePrompt,
1060
+ timeouts: options.timeouts
1061
+ });
1062
+ const completion = buildCompletionMessage(
1063
+ orchestrateResult.completionSignal,
1064
+ orchestrateResult.iterations.length
1065
+ );
1066
+ yield* d.status(completion.message, completion.severity);
1067
+ for (const line of buildContextWindowLines(orchestrateResult.iterations)) {
1068
+ yield* d.text(line);
1069
+ }
1070
+ return orchestrateResult;
1071
+ });
1072
+ const withErrorLog = resolvedLogging.type === "file" ? baseEffect.pipe(
1073
+ Effect_exports.tapError(
1074
+ (error) => Effect_exports.gen(function* () {
1075
+ const d = yield* Display;
1076
+ yield* d.status(
1077
+ formatErrorMessage(error),
1078
+ "error"
1079
+ );
1080
+ })
1081
+ )
1082
+ ) : baseEffect;
1083
+ let result;
1084
+ try {
1085
+ result = await Effect_exports.runPromise(
1086
+ withErrorLog.pipe(Effect_exports.provide(runLayer))
1087
+ );
1088
+ } catch (error) {
1089
+ options.signal?.throwIfAborted();
1090
+ throw error;
1091
+ }
1092
+ const baseResult = {
1093
+ ...result,
1094
+ logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : void 0,
1095
+ resume: async (prompt2, resumeOptions) => {
1096
+ const lastIteration = result.iterations.at(-1);
1097
+ if (!lastIteration?.sessionId) {
1098
+ throw new Error("Cannot resume: no sessionId was captured");
1099
+ }
1100
+ return run({
1101
+ ...options,
1102
+ ...resumeOptions,
1103
+ prompt: prompt2,
1104
+ promptFile: void 0,
1105
+ maxIterations: 1,
1106
+ resumeSession: lastIteration.sessionId
1107
+ });
1108
+ },
1109
+ fork: async (prompt2, forkOptions) => {
1110
+ const lastIteration = result.iterations.at(-1);
1111
+ if (!lastIteration?.sessionId) {
1112
+ throw new Error("Cannot fork: no sessionId was captured");
1113
+ }
1114
+ return run({
1115
+ ...options,
1116
+ ...forkOptions,
1117
+ prompt: prompt2,
1118
+ promptFile: void 0,
1119
+ maxIterations: 1,
1120
+ resumeSession: lastIteration.sessionId,
1121
+ forkSession: true
1122
+ });
1123
+ }
1124
+ };
1125
+ if (options.output) {
1126
+ const lastIteration = baseResult.iterations.at(-1);
1127
+ const output = await extractStructuredOutput(
1128
+ baseResult.stdout,
1129
+ options.output,
1130
+ {
1131
+ commits: baseResult.commits,
1132
+ branch: baseResult.branch,
1133
+ preservedWorktreePath: baseResult.preservedWorktreePath,
1134
+ sessionId: lastIteration?.sessionId,
1135
+ sessionFilePath: lastIteration?.sessionFilePath
1136
+ }
1137
+ );
1138
+ return { ...baseResult, output };
1139
+ }
1140
+ return baseResult;
1141
+ }
1142
+
1143
+ // src/raceAbortSignal.ts
1144
+ var raceAbortSignal = (effect, signal) => {
1145
+ if (!signal) return effect;
1146
+ return Effect_exports.gen(function* () {
1147
+ if (signal.aborted) {
1148
+ return yield* Effect_exports.die(signal.reason);
1149
+ }
1150
+ const abortDeferred = yield* Deferred_exports.make();
1151
+ const onAbort = () => {
1152
+ Effect_exports.runPromise(Deferred_exports.die(abortDeferred, signal.reason)).catch(
1153
+ () => {
1154
+ }
1155
+ );
1156
+ };
1157
+ signal.addEventListener("abort", onAbort, { once: true });
1158
+ return yield* Effect_exports.raceFirst(
1159
+ effect,
1160
+ Deferred_exports.await(abortDeferred)
1161
+ ).pipe(
1162
+ Effect_exports.ensuring(
1163
+ Effect_exports.sync(() => signal.removeEventListener("abort", onAbort))
1164
+ )
1165
+ );
1166
+ });
1167
+ };
1168
+
1169
+ // src/interactive.ts
1170
+ var interactive = async (options) => {
1171
+ options.signal?.throwIfAborted();
1172
+ const { prompt, promptFile, hooks, agent: provider } = options;
1173
+ const resolvedSandbox = options.sandbox ?? noSandbox();
1174
+ const branchStrategy = options.branchStrategy ?? (resolvedSandbox.tag === "isolated" ? { type: "merge-to-head" } : { type: "head" });
1175
+ if (branchStrategy.type === "head" && resolvedSandbox.tag === "isolated") {
1176
+ throw new Error(
1177
+ "head branch strategy is not supported with isolated providers"
1178
+ );
1179
+ }
1180
+ if (branchStrategy.type === "head" && options.copyToWorktree && options.copyToWorktree.length > 0) {
1181
+ throw new Error(
1182
+ "copyToWorktree is not supported with head branch strategy. In head mode the host working directory is bind-mounted directly."
1183
+ );
1184
+ }
1185
+ if (!provider.buildInteractiveArgs) {
1186
+ throw new Error(
1187
+ `Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
1188
+ );
1189
+ }
1190
+ const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
1191
+ const isHeadMode = branchStrategy.type === "head";
1192
+ const sandboxProvider = resolvedSandbox;
1193
+ const inner = Effect_exports.gen(function* () {
1194
+ const hostRepoDir = yield* resolveCwd(options.cwd);
1195
+ const d = yield* Display;
1196
+ const hasPromptSource = prompt !== void 0 || promptFile !== void 0;
1197
+ const resolved = hasPromptSource ? yield* resolvePrompt({ prompt, promptFile }) : void 0;
1198
+ const rawPrompt = resolved?.text ?? "";
1199
+ const isInlinePrompt = resolved?.source === "inline";
1200
+ const resolvedEnv = yield* resolveEnv(hostRepoDir);
1201
+ const env = mergeProviderEnv({
1202
+ resolvedEnv,
1203
+ agentProviderEnv: provider.env,
1204
+ sandboxProviderEnv: sandboxProvider.env
1205
+ });
1206
+ const effectiveEnv = { ...env, ...options.env ?? {} };
1207
+ const currentHostBranch = yield* getCurrentBranch(hostRepoDir);
1208
+ const resolvedBranch = branchStrategy.type === "head" ? currentHostBranch : branch ?? generateTempBranchName(options.name);
1209
+ let substitutedPrompt = rawPrompt;
1210
+ if (hasPromptSource && !isInlinePrompt) {
1211
+ const userArgs = options.promptArgs ?? {};
1212
+ yield* validateNoBuiltInArgOverride(userArgs);
1213
+ const missingKeys = findMissingPromptArgKeys(rawPrompt, userArgs);
1214
+ const collectedArgs = {};
1215
+ for (const key of missingKeys) {
1216
+ const value = yield* Effect_exports.promise(
1217
+ () => clack.text({
1218
+ message: `Enter value for {{${key}}}`,
1219
+ validate: (v) => {
1220
+ if (!v) return `A value is required for {{${key}}}`;
1221
+ }
1222
+ })
1223
+ );
1224
+ if (clack.isCancel(value)) {
1225
+ clack.cancel("Prompt arg collection cancelled.");
1226
+ return yield* Effect_exports.fail(
1227
+ new Error("User cancelled prompt arg collection")
1228
+ );
1229
+ }
1230
+ collectedArgs[key] = value;
1231
+ }
1232
+ const mergedUserArgs = { ...userArgs, ...collectedArgs };
1233
+ const effectiveArgs = {
1234
+ SOURCE_BRANCH: resolvedBranch,
1235
+ TARGET_BRANCH: currentHostBranch,
1236
+ ...mergedUserArgs
1237
+ };
1238
+ const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
1239
+ substitutedPrompt = yield* substitutePromptArgs(
1240
+ rawPrompt,
1241
+ effectiveArgs,
1242
+ builtInArgKeysSet
1243
+ );
1244
+ } else if (isInlinePrompt) {
1245
+ const userArgs = options.promptArgs ?? {};
1246
+ yield* validateNoArgsWithInlinePrompt(userArgs);
1247
+ }
1248
+ const lifecycleBranch = isHeadMode ? currentHostBranch : branch;
1249
+ yield* d.intro(options.name ?? "sandcastle interactive");
1250
+ yield* d.summary("Interactive Session", {
1251
+ Agent: options.name ?? provider.name,
1252
+ Sandbox: sandboxProvider.name,
1253
+ Branch: resolvedBranch
1254
+ });
1255
+ let worktreeInfo;
1256
+ if (!isHeadMode) {
1257
+ worktreeInfo = yield* d.taskLog(
1258
+ "Creating worktree",
1259
+ () => pruneStale(hostRepoDir).pipe(
1260
+ Effect_exports.catchAll(() => Effect_exports.void),
1261
+ Effect_exports.andThen(
1262
+ branch ? create(hostRepoDir, { branch }) : create(hostRepoDir, { name: options.name })
1263
+ )
1264
+ )
1265
+ );
1266
+ }
1267
+ const handle = yield* Effect_exports.gen(function* () {
1268
+ if (!isHeadMode) {
1269
+ if ((sandboxProvider.tag === "bind-mount" || sandboxProvider.tag === "none") && options.copyToWorktree && options.copyToWorktree.length > 0) {
1270
+ yield* d.taskLog(
1271
+ "Copying files to worktree",
1272
+ () => copyToWorktree(
1273
+ options.copyToWorktree,
1274
+ hostRepoDir,
1275
+ worktreeInfo.path,
1276
+ options.timeouts?.copyToWorktreeMs
1277
+ )
1278
+ );
1279
+ }
1280
+ if (hooks?.host?.onWorktreeReady?.length) {
1281
+ yield* runHostHooks(hooks.host.onWorktreeReady, worktreeInfo.path);
1282
+ }
1283
+ } else if (hooks?.host?.onWorktreeReady?.length) {
1284
+ yield* runHostHooks(hooks.host.onWorktreeReady, hostRepoDir);
1285
+ }
1286
+ if (sandboxProvider.tag === "none") {
1287
+ const worktreePath = isHeadMode ? hostRepoDir : worktreeInfo.path;
1288
+ return yield* Effect_exports.promise(
1289
+ () => sandboxProvider.create({
1290
+ worktreePath,
1291
+ env: effectiveEnv
1292
+ })
1293
+ );
1294
+ } else if (sandboxProvider.tag === "isolated") {
1295
+ const startResult = yield* d.taskLog(
1296
+ "Starting sandbox",
1297
+ () => startSandbox({
1298
+ provider: sandboxProvider,
1299
+ hostRepoDir: worktreeInfo.path,
1300
+ env: effectiveEnv,
1301
+ copyPaths: options.copyToWorktree
1302
+ })
1303
+ );
1304
+ return startResult.handle;
1305
+ } else {
1306
+ const gitPath = join(hostRepoDir, ".git");
1307
+ const gitMounts = yield* resolveGitMounts(gitPath);
1308
+ const startResult = yield* d.taskLog(
1309
+ "Starting sandbox",
1310
+ () => startSandbox({
1311
+ provider: sandboxProvider,
1312
+ hostRepoDir,
1313
+ env: effectiveEnv,
1314
+ worktreeOrRepoPath: isHeadMode ? hostRepoDir : worktreeInfo.path,
1315
+ gitMounts,
1316
+ repoDir: SANDBOX_REPO_DIR
1317
+ })
1318
+ );
1319
+ return startResult.handle;
1320
+ }
1321
+ }).pipe(
1322
+ Effect_exports.tapError(
1323
+ () => worktreeInfo ? remove(worktreeInfo.path).pipe(
1324
+ Effect_exports.catchAll(() => Effect_exports.void)
1325
+ ) : Effect_exports.void
1326
+ )
1327
+ );
1328
+ return yield* Effect_exports.gen(function* () {
1329
+ if (!handle.interactiveExec) {
1330
+ throw new Error(
1331
+ `Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
1332
+ );
1333
+ }
1334
+ const interactiveExecFn = handle.interactiveExec.bind(handle);
1335
+ const sandbox = makeSandboxFromHandle(handle);
1336
+ const worktreePath = handle.worktreePath;
1337
+ const applyToHost = sandboxProvider.tag === "isolated" && worktreeInfo ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
1338
+ const lifecycleEffect = withSandboxLifecycle(
1339
+ {
1340
+ hostRepoDir,
1341
+ sandboxRepoDir: worktreePath,
1342
+ hooks,
1343
+ branch: lifecycleBranch,
1344
+ hostWorktreePath: isHeadMode ? hostRepoDir : worktreeInfo?.path,
1345
+ applyToHost,
1346
+ timeouts: options.timeouts
1347
+ },
1348
+ sandbox,
1349
+ (ctx) => Effect_exports.gen(function* () {
1350
+ const fullPrompt = !hasPromptSource || isInlinePrompt ? substitutedPrompt : yield* preprocessPrompt(
1351
+ substitutedPrompt,
1352
+ ctx.sandbox,
1353
+ ctx.sandboxRepoDir
1354
+ );
1355
+ const interactiveArgs = provider.buildInteractiveArgs({
1356
+ prompt: fullPrompt,
1357
+ dangerouslySkipPermissions: sandboxProvider.tag !== "none"
1358
+ });
1359
+ const result2 = yield* raceAbortSignal(
1360
+ Effect_exports.promise(
1361
+ () => interactiveExecFn(interactiveArgs, {
1362
+ stdin: process.stdin,
1363
+ stdout: process.stdout,
1364
+ stderr: process.stderr,
1365
+ cwd: worktreePath
1366
+ })
1367
+ ),
1368
+ options.signal
1369
+ );
1370
+ return result2.exitCode;
1371
+ })
1372
+ );
1373
+ const lifecycleResult = yield* lifecycleEffect;
1374
+ const exitCode = lifecycleResult.result;
1375
+ let preservedWorktreePath;
1376
+ if (worktreeInfo) {
1377
+ const hasUncommitted = yield* hasUncommittedChanges(
1378
+ worktreeInfo.path
1379
+ ).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
1380
+ if (hasUncommitted) {
1381
+ preservedWorktreePath = worktreeInfo.path;
1382
+ }
1383
+ }
1384
+ if (worktreeInfo && !preservedWorktreePath) {
1385
+ yield* remove(worktreeInfo.path).pipe(
1386
+ Effect_exports.catchAll(() => Effect_exports.void)
1387
+ );
1388
+ }
1389
+ yield* d.summary("Session Complete", {
1390
+ Commits: String(lifecycleResult.commits.length),
1391
+ Branch: lifecycleResult.branch,
1392
+ "Exit code": String(exitCode),
1393
+ ...preservedWorktreePath ? { "Preserved worktree": preservedWorktreePath } : {}
1394
+ });
1395
+ return {
1396
+ commits: lifecycleResult.commits,
1397
+ branch: lifecycleResult.branch,
1398
+ preservedWorktreePath,
1399
+ exitCode
1400
+ };
1401
+ }).pipe(
1402
+ // On error, always clean up worktree (on success, handled above with preserve check)
1403
+ Effect_exports.tapError(
1404
+ () => worktreeInfo ? remove(worktreeInfo.path).pipe(
1405
+ Effect_exports.catchAll(() => Effect_exports.void)
1406
+ ) : Effect_exports.void
1407
+ ),
1408
+ // Always close sandbox handle
1409
+ Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
1410
+ })))
1411
+ );
1412
+ });
1413
+ let result;
1414
+ try {
1415
+ result = await Effect_exports.runPromise(
1416
+ inner.pipe(
1417
+ Effect_exports.provide(ClackDisplay.layer),
1418
+ Effect_exports.provide(NodeContext_exports.layer),
1419
+ Effect_exports.provide(NodeFileSystem_exports.layer)
1420
+ )
1421
+ );
1422
+ } catch (error) {
1423
+ options.signal?.throwIfAborted();
1424
+ throw error;
1425
+ }
1426
+ return result;
1427
+ };
1428
+ var buildSandboxHandle = (ctx, close) => {
1429
+ const {
1430
+ branch,
1431
+ worktreePath,
1432
+ hostRepoDir,
1433
+ sandboxRepoDir,
1434
+ sandbox,
1435
+ providerHandle,
1436
+ applyToHost,
1437
+ timeouts
1438
+ } = ctx;
1439
+ const sandboxHandle = {
1440
+ branch,
1441
+ worktreePath,
1442
+ run: async (runOptions) => {
1443
+ runOptions.signal?.throwIfAborted();
1444
+ const {
1445
+ agent: provider,
1446
+ prompt,
1447
+ promptFile,
1448
+ maxIterations = 1
1449
+ } = runOptions;
1450
+ const resolved = await Effect_exports.runPromise(
1451
+ resolvePrompt({ prompt, promptFile }).pipe(
1452
+ Effect_exports.provide(NodeContext_exports.layer)
1453
+ )
1454
+ );
1455
+ const rawPrompt = resolved.text;
1456
+ const isInlinePrompt = resolved.source === "inline";
1457
+ const userArgs = runOptions.promptArgs ?? {};
1458
+ const currentHostBranch = await Effect_exports.runPromise(
1459
+ getCurrentBranch(hostRepoDir)
1460
+ );
1461
+ const displayRef = Ref_exports.unsafeMake([]);
1462
+ const silentDisplayLayer = SilentDisplay.layer(displayRef);
1463
+ const resolvedPrompt = await Effect_exports.runPromise(
1464
+ Effect_exports.gen(function* () {
1465
+ if (isInlinePrompt) {
1466
+ yield* validateNoArgsWithInlinePrompt(userArgs);
1467
+ return rawPrompt;
1468
+ }
1469
+ yield* validateNoBuiltInArgOverride(userArgs);
1470
+ const effectiveArgs = {
1471
+ SOURCE_BRANCH: branch,
1472
+ TARGET_BRANCH: currentHostBranch,
1473
+ ...userArgs
1474
+ };
1475
+ const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
1476
+ return yield* substitutePromptArgs(
1477
+ rawPrompt,
1478
+ effectiveArgs,
1479
+ builtInArgKeysSet
1480
+ );
1481
+ }).pipe(Effect_exports.provide(silentDisplayLayer))
1482
+ );
1483
+ const resolvedLogging = runOptions.logging ?? {
1484
+ type: "file",
1485
+ path: join(
1486
+ hostRepoDir,
1487
+ ".sandcastle",
1488
+ "logs",
1489
+ buildLogFilename(branch, void 0, runOptions.name)
1490
+ )
1491
+ };
1492
+ const runDisplayLayer = resolvedLogging.type === "file" ? (() => {
1493
+ printFileDisplayStartup({
1494
+ logPath: resolvedLogging.path,
1495
+ agentName: runOptions.name,
1496
+ branch
1497
+ });
1498
+ return Layer_exports.provide(
1499
+ FileDisplay.layer(resolvedLogging.path),
1500
+ NodeFileSystem_exports.layer
1501
+ );
1502
+ })() : silentDisplayLayer;
1503
+ const reuseFactoryLayer = Layer_exports.succeed(SandboxFactory, {
1504
+ withSandbox: (makeEffect) => makeEffect(
1505
+ {
1506
+ hostWorktreePath: worktreePath,
1507
+ sandboxRepoPath: sandboxRepoDir,
1508
+ applyToHost
1509
+ },
1510
+ sandbox
1511
+ ).pipe(
1512
+ Effect_exports.map((value) => ({
1513
+ value,
1514
+ preservedWorktreePath: void 0
1515
+ }))
1516
+ )
1517
+ });
1518
+ const streamEmitterLayer = agentStreamEmitterLayer(
1519
+ resolvedLogging.type === "file" ? resolvedLogging.onAgentStreamEvent : void 0
1520
+ );
1521
+ const runLayer = Layer_exports.mergeAll(
1522
+ reuseFactoryLayer,
1523
+ runDisplayLayer,
1524
+ streamEmitterLayer
1525
+ );
1526
+ let result;
1527
+ try {
1528
+ result = await Effect_exports.runPromise(
1529
+ Effect_exports.gen(function* () {
1530
+ const display = yield* Display;
1531
+ yield* display.intro(runOptions.name ?? "sandcastle");
1532
+ const orchestrateResult = yield* orchestrate({
1533
+ hostRepoDir,
1534
+ iterations: maxIterations,
1535
+ prompt: resolvedPrompt,
1536
+ branch,
1537
+ provider,
1538
+ completionSignal: runOptions.completionSignal,
1539
+ idleTimeoutSeconds: runOptions.idleTimeoutSeconds,
1540
+ completionTimeoutSeconds: runOptions.completionTimeoutSeconds,
1541
+ name: runOptions.name,
1542
+ signal: runOptions.signal,
1543
+ skipPromptExpansion: isInlinePrompt,
1544
+ timeouts
1545
+ });
1546
+ const completion = buildCompletionMessage(
1547
+ orchestrateResult.completionSignal,
1548
+ orchestrateResult.iterations.length
1549
+ );
1550
+ yield* display.status(completion.message, completion.severity);
1551
+ for (const line of buildContextWindowLines(
1552
+ orchestrateResult.iterations
1553
+ )) {
1554
+ yield* display.text(line);
1555
+ }
1556
+ return orchestrateResult;
1557
+ }).pipe(Effect_exports.provide(runLayer))
1558
+ );
1559
+ } catch (error) {
1560
+ runOptions.signal?.throwIfAborted();
1561
+ throw error;
1562
+ }
1563
+ return {
1564
+ iterations: result.iterations,
1565
+ completionSignal: result.completionSignal,
1566
+ stdout: result.stdout,
1567
+ commits: result.commits,
1568
+ logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : void 0
1569
+ };
1570
+ },
1571
+ interactive: async (interactiveOptions) => {
1572
+ interactiveOptions.signal?.throwIfAborted();
1573
+ const { agent: provider, prompt, promptFile } = interactiveOptions;
1574
+ if (!provider.buildInteractiveArgs) {
1575
+ throw new Error(
1576
+ `Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
1577
+ );
1578
+ }
1579
+ if (!providerHandle?.interactiveExec) {
1580
+ throw new Error(
1581
+ `Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
1582
+ );
1583
+ }
1584
+ const interactiveExecFn = providerHandle.interactiveExec.bind(providerHandle);
1585
+ let lifecycleResult;
1586
+ try {
1587
+ lifecycleResult = await Effect_exports.runPromise(
1588
+ Effect_exports.gen(function* () {
1589
+ const resolved = yield* resolvePrompt({ prompt, promptFile });
1590
+ const rawPrompt = resolved.text;
1591
+ const isInlinePrompt = resolved.source === "inline";
1592
+ const userArgs = interactiveOptions.promptArgs ?? {};
1593
+ const currentHostBranch = yield* getCurrentBranch(hostRepoDir);
1594
+ let resolvedPrompt;
1595
+ if (isInlinePrompt) {
1596
+ yield* validateNoArgsWithInlinePrompt(userArgs);
1597
+ resolvedPrompt = rawPrompt;
1598
+ } else {
1599
+ yield* validateNoBuiltInArgOverride(userArgs);
1600
+ const effectiveArgs = {
1601
+ SOURCE_BRANCH: branch,
1602
+ TARGET_BRANCH: currentHostBranch,
1603
+ ...userArgs
1604
+ };
1605
+ const builtInArgKeysSet = new Set(
1606
+ BUILT_IN_PROMPT_ARG_KEYS
1607
+ );
1608
+ resolvedPrompt = yield* substitutePromptArgs(
1609
+ rawPrompt,
1610
+ effectiveArgs,
1611
+ builtInArgKeysSet
1612
+ );
1613
+ }
1614
+ return yield* withSandboxLifecycle(
1615
+ {
1616
+ hostRepoDir,
1617
+ sandboxRepoDir,
1618
+ branch,
1619
+ hostWorktreePath: worktreePath,
1620
+ applyToHost,
1621
+ timeouts
1622
+ },
1623
+ sandbox,
1624
+ (ctx2) => Effect_exports.gen(function* () {
1625
+ const fullPrompt = isInlinePrompt ? resolvedPrompt : yield* preprocessPrompt(
1626
+ resolvedPrompt,
1627
+ ctx2.sandbox,
1628
+ ctx2.sandboxRepoDir
1629
+ );
1630
+ const interactiveArgs = provider.buildInteractiveArgs({
1631
+ prompt: fullPrompt,
1632
+ dangerouslySkipPermissions: true
1633
+ });
1634
+ const execPromise = interactiveExecFn(interactiveArgs, {
1635
+ stdin: process.stdin,
1636
+ stdout: process.stdout,
1637
+ stderr: process.stderr,
1638
+ cwd: sandboxRepoDir
1639
+ });
1640
+ const signal = interactiveOptions.signal;
1641
+ const result = yield* Effect_exports.promise(() => {
1642
+ if (!signal) return execPromise;
1643
+ if (signal.aborted) return Promise.reject(signal.reason);
1644
+ return new Promise(
1645
+ (resolve, reject) => {
1646
+ const onAbort = () => reject(signal.reason);
1647
+ signal.addEventListener("abort", onAbort, {
1648
+ once: true
1649
+ });
1650
+ execPromise.then(
1651
+ (r) => {
1652
+ signal.removeEventListener("abort", onAbort);
1653
+ resolve(r);
1654
+ },
1655
+ (e) => {
1656
+ signal.removeEventListener("abort", onAbort);
1657
+ reject(e);
1658
+ }
1659
+ );
1660
+ }
1661
+ );
1662
+ });
1663
+ return result.exitCode;
1664
+ })
1665
+ );
1666
+ }).pipe(
1667
+ Effect_exports.provide(ClackDisplay.layer),
1668
+ Effect_exports.provide(NodeContext_exports.layer)
1669
+ )
1670
+ );
1671
+ } catch (error) {
1672
+ interactiveOptions.signal?.throwIfAborted();
1673
+ throw error;
1674
+ }
1675
+ return {
1676
+ commits: lifecycleResult.commits,
1677
+ exitCode: lifecycleResult.result
1678
+ };
1679
+ },
1680
+ close: async () => close(),
1681
+ [Symbol.asyncDispose]: async () => {
1682
+ await sandboxHandle.close();
1683
+ }
1684
+ };
1685
+ return sandboxHandle;
1686
+ };
1687
+ var createSandboxFromWorktree = async (options) => {
1688
+ const { branch, worktreePath, hostRepoDir } = options;
1689
+ const isTestMode = !!options._test?.buildSandbox;
1690
+ if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
1691
+ await Effect_exports.runPromise(
1692
+ copyToWorktree(
1693
+ options.copyToWorktree,
1694
+ hostRepoDir,
1695
+ worktreePath,
1696
+ options.timeouts?.copyToWorktreeMs
1697
+ )
1698
+ );
1699
+ }
1700
+ let providerHandle;
1701
+ let sandbox;
1702
+ let sandboxRepoDir;
1703
+ const isIsolated = options.sandbox.tag === "isolated";
1704
+ if (isTestMode) {
1705
+ sandbox = options._test.buildSandbox(worktreePath);
1706
+ sandboxRepoDir = worktreePath;
1707
+ } else {
1708
+ const resolvedEnv = await Effect_exports.runPromise(
1709
+ resolveEnv(hostRepoDir).pipe(Effect_exports.provide(NodeContext_exports.layer))
1710
+ );
1711
+ const env = mergeProviderEnv({
1712
+ resolvedEnv,
1713
+ agentProviderEnv: {},
1714
+ sandboxProviderEnv: options.sandbox.env
1715
+ });
1716
+ const provider = options.sandbox;
1717
+ let startEffect;
1718
+ if (provider.tag === "isolated") {
1719
+ startEffect = startSandbox({
1720
+ provider,
1721
+ hostRepoDir: worktreePath,
1722
+ env,
1723
+ copyPaths: options.copyToWorktree
1724
+ });
1725
+ } else if (provider.tag === "none") {
1726
+ startEffect = startSandbox({
1727
+ provider,
1728
+ hostRepoDir,
1729
+ env,
1730
+ worktreeOrRepoPath: worktreePath
1731
+ });
1732
+ } else {
1733
+ startEffect = resolveGitMounts(join(hostRepoDir, ".git")).pipe(
1734
+ Effect_exports.provide(NodeFileSystem_exports.layer),
1735
+ Effect_exports.catchAll(() => Effect_exports.succeed([])),
1736
+ // Patch git mounts for Windows worktree compatibility (ADR-0006)
1737
+ Effect_exports.flatMap(
1738
+ (gitMounts) => Effect_exports.tryPromise({
1739
+ try: () => patchGitMountsForWindows(
1740
+ gitMounts,
1741
+ worktreePath,
1742
+ SANDBOX_REPO_DIR
1743
+ ),
1744
+ catch: (e) => new Error(
1745
+ `Failed to patch git mounts: ${e instanceof Error ? e.message : String(e)}`
1746
+ )
1747
+ })
1748
+ ),
1749
+ Effect_exports.flatMap(
1750
+ (gitMounts) => startSandbox({
1751
+ provider,
1752
+ hostRepoDir,
1753
+ env,
1754
+ worktreeOrRepoPath: worktreePath,
1755
+ gitMounts,
1756
+ repoDir: SANDBOX_REPO_DIR
1757
+ })
1758
+ )
1759
+ );
1760
+ }
1761
+ const startResult = await Effect_exports.runPromise(startEffect);
1762
+ providerHandle = startResult.handle;
1763
+ sandbox = startResult.sandbox;
1764
+ sandboxRepoDir = startResult.worktreePath;
1765
+ }
1766
+ const sandboxOnReady = options.hooks?.sandbox?.onSandboxReady;
1767
+ const hostOnReady = options.hooks?.host?.onSandboxReady;
1768
+ if (sandboxOnReady?.length || hostOnReady?.length) {
1769
+ await Effect_exports.runPromise(
1770
+ Effect_exports.gen(function* () {
1771
+ yield* sandbox.exec(
1772
+ `git config --global --add safe.directory "${sandboxRepoDir}"`
1773
+ );
1774
+ const sandboxEffects = (sandboxOnReady ?? []).map(
1775
+ (hook) => sandbox.exec(hook.command, {
1776
+ cwd: sandboxRepoDir,
1777
+ sudo: hook.sudo
1778
+ })
1779
+ );
1780
+ const allEffects = [...sandboxEffects];
1781
+ if (hostOnReady?.length) {
1782
+ allEffects.push(runHostHooks(hostOnReady, worktreePath));
1783
+ }
1784
+ yield* Effect_exports.all(allEffects, {
1785
+ concurrency: "unbounded"
1786
+ });
1787
+ })
1788
+ );
1789
+ }
1790
+ const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect_exports.void;
1791
+ let closed = false;
1792
+ return buildSandboxHandle(
1793
+ {
1794
+ branch,
1795
+ worktreePath,
1796
+ hostRepoDir,
1797
+ sandboxRepoDir,
1798
+ sandbox,
1799
+ providerHandle,
1800
+ applyToHost,
1801
+ timeouts: options.timeouts
1802
+ },
1803
+ async () => {
1804
+ if (closed) return { preservedWorktreePath: void 0 };
1805
+ closed = true;
1806
+ if (providerHandle) await providerHandle.close();
1807
+ return { preservedWorktreePath: void 0 };
1808
+ }
1809
+ );
1810
+ };
1811
+ var createSandbox = async (options) => {
1812
+ const { branch } = options;
1813
+ const isTestMode = !!options._test?.buildSandbox;
1814
+ const isIsolated = options.sandbox.tag === "isolated";
1815
+ const { hostRepoDir, worktreePath, providerHandle, sandbox, sandboxRepoDir } = await Effect_exports.runPromise(
1816
+ Effect_exports.gen(function* () {
1817
+ const hostRepoDir2 = yield* resolveCwd(options.cwd);
1818
+ yield* pruneStale(hostRepoDir2).pipe(
1819
+ Effect_exports.catchAll(() => Effect_exports.void)
1820
+ );
1821
+ const { path: worktreePath2 } = yield* create(
1822
+ hostRepoDir2,
1823
+ { branch, baseBranch: options.baseBranch }
1824
+ );
1825
+ const prepared = yield* Effect_exports.gen(function* () {
1826
+ if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
1827
+ yield* copyToWorktree(
1828
+ options.copyToWorktree,
1829
+ hostRepoDir2,
1830
+ worktreePath2,
1831
+ options.timeouts?.copyToWorktreeMs
1832
+ );
1833
+ }
1834
+ if (options.hooks?.host?.onWorktreeReady?.length) {
1835
+ yield* runHostHooks(
1836
+ options.hooks.host.onWorktreeReady,
1837
+ worktreePath2
1838
+ );
1839
+ }
1840
+ let providerHandle2;
1841
+ let sandbox2;
1842
+ let sandboxRepoDir2;
1843
+ if (isTestMode) {
1844
+ sandbox2 = options._test.buildSandbox(worktreePath2);
1845
+ sandboxRepoDir2 = worktreePath2;
1846
+ } else {
1847
+ const resolvedEnv = yield* resolveEnv(hostRepoDir2);
1848
+ const env = mergeProviderEnv({
1849
+ resolvedEnv,
1850
+ agentProviderEnv: {},
1851
+ sandboxProviderEnv: options.sandbox.env
1852
+ });
1853
+ const provider = options.sandbox;
1854
+ const startResult = yield* provider.tag === "isolated" ? startSandbox({
1855
+ provider,
1856
+ hostRepoDir: worktreePath2,
1857
+ env,
1858
+ copyPaths: options.copyToWorktree
1859
+ }) : provider.tag === "none" ? startSandbox({
1860
+ provider,
1861
+ hostRepoDir: hostRepoDir2,
1862
+ env,
1863
+ worktreeOrRepoPath: worktreePath2
1864
+ }) : resolveGitMounts(join(hostRepoDir2, ".git")).pipe(
1865
+ Effect_exports.provide(NodeFileSystem_exports.layer),
1866
+ Effect_exports.catchAll(() => Effect_exports.succeed([])),
1867
+ // Patch git mounts for Windows worktree compatibility (ADR-0006)
1868
+ Effect_exports.flatMap(
1869
+ (gitMounts) => Effect_exports.tryPromise({
1870
+ try: () => patchGitMountsForWindows(
1871
+ gitMounts,
1872
+ worktreePath2,
1873
+ SANDBOX_REPO_DIR
1874
+ ),
1875
+ catch: (e) => new Error(
1876
+ `Failed to patch git mounts: ${e instanceof Error ? e.message : String(e)}`
1877
+ )
1878
+ })
1879
+ ),
1880
+ Effect_exports.flatMap(
1881
+ (gitMounts) => startSandbox({
1882
+ provider,
1883
+ hostRepoDir: hostRepoDir2,
1884
+ env,
1885
+ worktreeOrRepoPath: worktreePath2,
1886
+ gitMounts,
1887
+ repoDir: SANDBOX_REPO_DIR
1888
+ })
1889
+ )
1890
+ );
1891
+ providerHandle2 = startResult.handle;
1892
+ sandbox2 = startResult.sandbox;
1893
+ sandboxRepoDir2 = startResult.worktreePath;
1894
+ }
1895
+ const sandboxOnReady = options.hooks?.sandbox?.onSandboxReady;
1896
+ const hostOnReady = options.hooks?.host?.onSandboxReady;
1897
+ if (sandboxOnReady?.length || hostOnReady?.length) {
1898
+ yield* Effect_exports.gen(function* () {
1899
+ yield* sandbox2.exec(
1900
+ `git config --global --add safe.directory "${sandboxRepoDir2}"`
1901
+ );
1902
+ const sandboxEffects = (sandboxOnReady ?? []).map(
1903
+ (hook) => sandbox2.exec(hook.command, {
1904
+ cwd: sandboxRepoDir2,
1905
+ sudo: hook.sudo
1906
+ })
1907
+ );
1908
+ const allEffects = [...sandboxEffects];
1909
+ if (hostOnReady?.length) {
1910
+ allEffects.push(runHostHooks(hostOnReady, worktreePath2));
1911
+ }
1912
+ yield* Effect_exports.all(allEffects, { concurrency: "unbounded" });
1913
+ }).pipe(
1914
+ Effect_exports.onError(
1915
+ () => providerHandle2 ? Effect_exports.promise(
1916
+ () => providerHandle2.close().catch(() => {
1917
+ })
1918
+ ) : Effect_exports.void
1919
+ )
1920
+ );
1921
+ }
1922
+ return { providerHandle: providerHandle2, sandbox: sandbox2, sandboxRepoDir: sandboxRepoDir2 };
1923
+ }).pipe(
1924
+ Effect_exports.onError(
1925
+ () => remove(worktreePath2).pipe(
1926
+ Effect_exports.catchAll(() => Effect_exports.void)
1927
+ )
1928
+ )
1929
+ );
1930
+ return { hostRepoDir: hostRepoDir2, worktreePath: worktreePath2, ...prepared };
1931
+ }).pipe(Effect_exports.provide(NodeContext_exports.layer))
1932
+ );
1933
+ const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect_exports.void;
1934
+ let closed = false;
1935
+ const forceCleanup = () => {
1936
+ console.error(`
1937
+ Worktree preserved at ${worktreePath}`);
1938
+ console.error(` To review: cd ${worktreePath}`);
1939
+ console.error(` To clean up: git worktree remove --force ${worktreePath}`);
1940
+ };
1941
+ const unregisterShutdown = registerShutdown(forceCleanup);
1942
+ const doClose = async () => {
1943
+ if (closed) return { preservedWorktreePath: void 0 };
1944
+ closed = true;
1945
+ return Effect_exports.runPromise(
1946
+ Effect_exports.gen(function* () {
1947
+ if (providerHandle) {
1948
+ yield* Effect_exports.promise(() => providerHandle.close());
1949
+ }
1950
+ const isDirty = yield* hasUncommittedChanges(
1951
+ worktreePath
1952
+ ).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
1953
+ if (isDirty) {
1954
+ return { preservedWorktreePath: worktreePath };
1955
+ }
1956
+ yield* remove(worktreePath).pipe(
1957
+ Effect_exports.catchAll(() => Effect_exports.void)
1958
+ );
1959
+ return { preservedWorktreePath: void 0 };
1960
+ })
1961
+ );
1962
+ };
1963
+ return buildSandboxHandle(
1964
+ {
1965
+ branch,
1966
+ worktreePath,
1967
+ hostRepoDir,
1968
+ sandboxRepoDir,
1969
+ sandbox,
1970
+ providerHandle,
1971
+ applyToHost,
1972
+ timeouts: options.timeouts
1973
+ },
1974
+ async () => {
1975
+ unregisterShutdown();
1976
+ return doClose();
1977
+ }
1978
+ );
1979
+ };
1980
+ var createWorktree = async (options) => {
1981
+ const branch = options.branchStrategy.type === "branch" ? options.branchStrategy.branch : void 0;
1982
+ const baseBranch = options.branchStrategy.type === "branch" ? options.branchStrategy.baseBranch : void 0;
1983
+ const { hostRepoDir, worktreeInfo } = await Effect_exports.gen(function* () {
1984
+ const hostRepoDir2 = yield* resolveCwd(options.cwd);
1985
+ yield* pruneStale(hostRepoDir2).pipe(
1986
+ Effect_exports.catchAll(() => Effect_exports.void)
1987
+ );
1988
+ const info = yield* create(hostRepoDir2, {
1989
+ branch,
1990
+ baseBranch
1991
+ });
1992
+ if (options.copyToWorktree && options.copyToWorktree.length > 0) {
1993
+ yield* copyToWorktree(
1994
+ options.copyToWorktree,
1995
+ hostRepoDir2,
1996
+ info.path,
1997
+ options.timeouts?.copyToWorktreeMs
1998
+ );
1999
+ }
2000
+ if (options.hooks?.host?.onWorktreeReady?.length) {
2001
+ yield* runHostHooks(options.hooks.host.onWorktreeReady, info.path);
2002
+ }
2003
+ return { hostRepoDir: hostRepoDir2, worktreeInfo: info };
2004
+ }).pipe(Effect_exports.provide(NodeContext_exports.layer), Effect_exports.runPromise);
2005
+ let closed = false;
2006
+ const close = async () => {
2007
+ if (closed) return { preservedWorktreePath: void 0 };
2008
+ closed = true;
2009
+ return Effect_exports.gen(function* () {
2010
+ const isDirty = yield* hasUncommittedChanges(
2011
+ worktreeInfo.path
2012
+ ).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
2013
+ if (isDirty) {
2014
+ return { preservedWorktreePath: worktreeInfo.path };
2015
+ }
2016
+ yield* remove(worktreeInfo.path).pipe(
2017
+ Effect_exports.catchAll(() => Effect_exports.void)
2018
+ );
2019
+ return { preservedWorktreePath: void 0 };
2020
+ }).pipe(Effect_exports.runPromise);
2021
+ };
2022
+ const worktreeInteractive = async (opts) => {
2023
+ opts.signal?.throwIfAborted();
2024
+ const { prompt, promptFile, hooks, agent: provider } = opts;
2025
+ const resolvedSandbox = opts.sandbox ?? noSandbox();
2026
+ if (!provider.buildInteractiveArgs) {
2027
+ throw new Error(
2028
+ `Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
2029
+ );
2030
+ }
2031
+ const inner = Effect_exports.gen(function* () {
2032
+ const d = yield* Display;
2033
+ const hasPromptSource = prompt !== void 0 || promptFile !== void 0;
2034
+ const resolved = hasPromptSource ? yield* resolvePrompt({ prompt, promptFile }) : void 0;
2035
+ const rawPrompt = resolved?.text ?? "";
2036
+ const isInlinePrompt = resolved?.source === "inline";
2037
+ const resolvedEnv = yield* resolveEnv(hostRepoDir);
2038
+ const env = mergeProviderEnv({
2039
+ resolvedEnv,
2040
+ agentProviderEnv: provider.env,
2041
+ sandboxProviderEnv: resolvedSandbox.env
2042
+ });
2043
+ const effectiveEnv = { ...env, ...opts.env ?? {} };
2044
+ let substitutedPrompt = rawPrompt;
2045
+ if (hasPromptSource && !isInlinePrompt) {
2046
+ const userArgs = opts.promptArgs ?? {};
2047
+ yield* validateNoBuiltInArgOverride(userArgs);
2048
+ const effectiveArgs = {
2049
+ SOURCE_BRANCH: worktreeInfo.branch,
2050
+ TARGET_BRANCH: worktreeInfo.branch,
2051
+ ...userArgs
2052
+ };
2053
+ const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
2054
+ substitutedPrompt = yield* substitutePromptArgs(
2055
+ rawPrompt,
2056
+ effectiveArgs,
2057
+ builtInArgKeysSet
2058
+ );
2059
+ } else if (isInlinePrompt) {
2060
+ yield* validateNoArgsWithInlinePrompt(opts.promptArgs ?? {});
2061
+ }
2062
+ yield* d.intro(opts.name ?? "sandcastle interactive");
2063
+ yield* d.summary("Interactive Session", {
2064
+ Agent: opts.name ?? provider.name,
2065
+ Sandbox: resolvedSandbox.name,
2066
+ Branch: worktreeInfo.branch
2067
+ });
2068
+ let handle;
2069
+ if (resolvedSandbox.tag === "none") {
2070
+ handle = yield* Effect_exports.promise(
2071
+ () => resolvedSandbox.create({
2072
+ worktreePath: worktreeInfo.path,
2073
+ env: effectiveEnv
2074
+ })
2075
+ );
2076
+ } else if (resolvedSandbox.tag === "isolated") {
2077
+ const startResult = yield* d.taskLog(
2078
+ "Starting sandbox",
2079
+ () => startSandbox({
2080
+ provider: resolvedSandbox,
2081
+ hostRepoDir: worktreeInfo.path,
2082
+ env: effectiveEnv
2083
+ })
2084
+ );
2085
+ handle = startResult.handle;
2086
+ } else {
2087
+ const gitPath = join(hostRepoDir, ".git");
2088
+ const gitMounts = yield* resolveGitMounts(gitPath);
2089
+ const startResult = yield* d.taskLog(
2090
+ "Starting sandbox",
2091
+ () => startSandbox({
2092
+ provider: resolvedSandbox,
2093
+ hostRepoDir,
2094
+ env: effectiveEnv,
2095
+ worktreeOrRepoPath: worktreeInfo.path,
2096
+ gitMounts,
2097
+ repoDir: SANDBOX_REPO_DIR
2098
+ })
2099
+ );
2100
+ handle = startResult.handle;
2101
+ }
2102
+ return yield* Effect_exports.gen(function* () {
2103
+ if (!handle.interactiveExec) {
2104
+ throw new Error(
2105
+ `Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
2106
+ );
2107
+ }
2108
+ const interactiveExecFn = handle.interactiveExec.bind(handle);
2109
+ const sandbox = makeSandboxFromHandle(handle);
2110
+ const worktreePath = handle.worktreePath;
2111
+ const applyToHost = resolvedSandbox.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
2112
+ const lifecycleEffect = withSandboxLifecycle(
2113
+ {
2114
+ hostRepoDir,
2115
+ sandboxRepoDir: worktreePath,
2116
+ hooks,
2117
+ branch: worktreeInfo.branch,
2118
+ hostWorktreePath: worktreeInfo.path,
2119
+ applyToHost,
2120
+ timeouts: options.timeouts
2121
+ },
2122
+ sandbox,
2123
+ (ctx) => Effect_exports.gen(function* () {
2124
+ const fullPrompt = !hasPromptSource || isInlinePrompt ? substitutedPrompt : yield* preprocessPrompt(
2125
+ substitutedPrompt,
2126
+ ctx.sandbox,
2127
+ ctx.sandboxRepoDir
2128
+ );
2129
+ const interactiveArgs = provider.buildInteractiveArgs({
2130
+ prompt: fullPrompt,
2131
+ dangerouslySkipPermissions: resolvedSandbox.tag !== "none"
2132
+ });
2133
+ const result = yield* raceAbortSignal(
2134
+ Effect_exports.promise(
2135
+ () => interactiveExecFn(interactiveArgs, {
2136
+ stdin: process.stdin,
2137
+ stdout: process.stdout,
2138
+ stderr: process.stderr,
2139
+ cwd: worktreePath
2140
+ })
2141
+ ),
2142
+ opts.signal
2143
+ );
2144
+ return result.exitCode;
2145
+ })
2146
+ );
2147
+ const lifecycleResult = yield* lifecycleEffect;
2148
+ const exitCode = lifecycleResult.result;
2149
+ yield* d.summary("Session Complete", {
2150
+ Commits: String(lifecycleResult.commits.length),
2151
+ Branch: lifecycleResult.branch,
2152
+ "Exit code": String(exitCode)
2153
+ });
2154
+ return {
2155
+ commits: lifecycleResult.commits,
2156
+ branch: lifecycleResult.branch,
2157
+ preservedWorktreePath: void 0,
2158
+ exitCode
2159
+ };
2160
+ }).pipe(
2161
+ // Always close sandbox handle
2162
+ Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
2163
+ })))
2164
+ );
2165
+ });
2166
+ try {
2167
+ return await Effect_exports.runPromise(
2168
+ inner.pipe(
2169
+ Effect_exports.provide(ClackDisplay.layer),
2170
+ Effect_exports.provide(NodeContext_exports.layer),
2171
+ Effect_exports.provide(NodeFileSystem_exports.layer)
2172
+ )
2173
+ );
2174
+ } catch (error) {
2175
+ opts.signal?.throwIfAborted();
2176
+ throw error;
2177
+ }
2178
+ };
2179
+ const worktreeRun = async (opts) => {
2180
+ opts.signal?.throwIfAborted();
2181
+ const { prompt, promptFile, hooks, agent: provider } = opts;
2182
+ const sandboxProvider = opts.sandbox;
2183
+ const maxIterations = opts.maxIterations ?? 1;
2184
+ if (opts.resumeSession && maxIterations > 1) {
2185
+ throw new Error(
2186
+ "resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
2187
+ );
2188
+ }
2189
+ if (opts.resumeSession) {
2190
+ await assertResumeSessionExists({
2191
+ provider,
2192
+ sandboxTag: sandboxProvider.tag,
2193
+ hostRepoDir,
2194
+ resumeSession: opts.resumeSession
2195
+ });
2196
+ }
2197
+ const inner = Effect_exports.gen(function* () {
2198
+ const resolved = yield* resolvePrompt({ prompt, promptFile });
2199
+ const rawPrompt = resolved.text;
2200
+ const isInlinePrompt = resolved.source === "inline";
2201
+ const resolvedEnv = yield* resolveEnv(hostRepoDir);
2202
+ const env = mergeProviderEnv({
2203
+ resolvedEnv,
2204
+ agentProviderEnv: provider.env,
2205
+ sandboxProviderEnv: sandboxProvider.env
2206
+ });
2207
+ const effectiveEnv = { ...env, ...opts.env ?? {} };
2208
+ const userArgs = opts.promptArgs ?? {};
2209
+ let resolvedPrompt;
2210
+ if (isInlinePrompt) {
2211
+ yield* validateNoArgsWithInlinePrompt(userArgs);
2212
+ resolvedPrompt = rawPrompt;
2213
+ } else {
2214
+ yield* validateNoBuiltInArgOverride(userArgs);
2215
+ const effectiveArgs = {
2216
+ SOURCE_BRANCH: worktreeInfo.branch,
2217
+ TARGET_BRANCH: worktreeInfo.branch,
2218
+ ...userArgs
2219
+ };
2220
+ const builtInArgKeysSet = new Set(BUILT_IN_PROMPT_ARG_KEYS);
2221
+ resolvedPrompt = yield* substitutePromptArgs(
2222
+ rawPrompt,
2223
+ effectiveArgs,
2224
+ builtInArgKeysSet
2225
+ );
2226
+ }
2227
+ let handle;
2228
+ let sandboxRepoDir;
2229
+ if (sandboxProvider.tag === "isolated") {
2230
+ const startResult = yield* startSandbox({
2231
+ provider: sandboxProvider,
2232
+ hostRepoDir: worktreeInfo.path,
2233
+ env: effectiveEnv
2234
+ });
2235
+ handle = startResult.handle;
2236
+ sandboxRepoDir = startResult.worktreePath;
2237
+ } else if (sandboxProvider.tag === "none") {
2238
+ const startResult = yield* startSandbox({
2239
+ provider: sandboxProvider,
2240
+ hostRepoDir,
2241
+ env: effectiveEnv,
2242
+ worktreeOrRepoPath: worktreeInfo.path
2243
+ });
2244
+ handle = startResult.handle;
2245
+ sandboxRepoDir = startResult.worktreePath;
2246
+ } else {
2247
+ const gitPath = join(hostRepoDir, ".git");
2248
+ const gitMounts = yield* resolveGitMounts(gitPath);
2249
+ const startResult = yield* startSandbox({
2250
+ provider: sandboxProvider,
2251
+ hostRepoDir,
2252
+ env: effectiveEnv,
2253
+ worktreeOrRepoPath: worktreeInfo.path,
2254
+ gitMounts,
2255
+ repoDir: SANDBOX_REPO_DIR
2256
+ });
2257
+ handle = startResult.handle;
2258
+ sandboxRepoDir = startResult.worktreePath;
2259
+ }
2260
+ const sandbox = makeSandboxFromHandle(handle);
2261
+ const applyToHost = sandboxProvider.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
2262
+ const resolvedLogging = opts.logging ?? {
2263
+ type: "file",
2264
+ path: join(
2265
+ hostRepoDir,
2266
+ ".sandcastle",
2267
+ "logs",
2268
+ buildLogFilename(worktreeInfo.branch, void 0, opts.name)
2269
+ )
2270
+ };
2271
+ const runDisplayLayer = resolvedLogging.type === "file" ? (() => {
2272
+ printFileDisplayStartup({
2273
+ logPath: resolvedLogging.path,
2274
+ agentName: opts.name,
2275
+ branch: worktreeInfo.branch
2276
+ });
2277
+ return Layer_exports.provide(
2278
+ FileDisplay.layer(resolvedLogging.path),
2279
+ NodeFileSystem_exports.layer
2280
+ );
2281
+ })() : ClackDisplay.layer;
2282
+ const reuseFactoryLayer = Layer_exports.succeed(SandboxFactory, {
2283
+ withSandbox: (makeEffect) => makeEffect(
2284
+ {
2285
+ hostWorktreePath: worktreeInfo.path,
2286
+ sandboxRepoPath: sandboxRepoDir,
2287
+ applyToHost
2288
+ },
2289
+ sandbox
2290
+ ).pipe(
2291
+ Effect_exports.map((value) => ({
2292
+ value,
2293
+ preservedWorktreePath: void 0
2294
+ }))
2295
+ )
2296
+ });
2297
+ const streamEmitterLayer = agentStreamEmitterLayer(
2298
+ resolvedLogging.type === "file" ? resolvedLogging.onAgentStreamEvent : void 0
2299
+ );
2300
+ const runLayer = Layer_exports.mergeAll(
2301
+ reuseFactoryLayer,
2302
+ runDisplayLayer,
2303
+ streamEmitterLayer
2304
+ );
2305
+ const result = yield* Effect_exports.gen(function* () {
2306
+ const display = yield* Display;
2307
+ yield* display.intro(opts.name ?? "sandcastle");
2308
+ const orchestrateResult = yield* orchestrate({
2309
+ hostRepoDir,
2310
+ iterations: maxIterations,
2311
+ hooks,
2312
+ prompt: resolvedPrompt,
2313
+ branch: worktreeInfo.branch,
2314
+ provider,
2315
+ completionSignal: opts.completionSignal,
2316
+ idleTimeoutSeconds: opts.idleTimeoutSeconds,
2317
+ completionTimeoutSeconds: opts.completionTimeoutSeconds,
2318
+ name: opts.name,
2319
+ resumeSession: opts.resumeSession,
2320
+ signal: opts.signal,
2321
+ skipPromptExpansion: isInlinePrompt,
2322
+ timeouts: options.timeouts
2323
+ });
2324
+ const completion = buildCompletionMessage(
2325
+ orchestrateResult.completionSignal,
2326
+ orchestrateResult.iterations.length
2327
+ );
2328
+ yield* display.status(completion.message, completion.severity);
2329
+ for (const line of buildContextWindowLines(
2330
+ orchestrateResult.iterations
2331
+ )) {
2332
+ yield* display.text(line);
2333
+ }
2334
+ return orchestrateResult;
2335
+ }).pipe(
2336
+ Effect_exports.provide(runLayer),
2337
+ // Always close sandbox handle
2338
+ Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
2339
+ })))
2340
+ );
2341
+ return {
2342
+ iterations: result.iterations,
2343
+ completionSignal: result.completionSignal,
2344
+ stdout: result.stdout,
2345
+ commits: result.commits,
2346
+ branch: result.branch,
2347
+ logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : void 0
2348
+ };
2349
+ });
2350
+ try {
2351
+ return await Effect_exports.runPromise(
2352
+ inner.pipe(
2353
+ Effect_exports.provide(ClackDisplay.layer),
2354
+ Effect_exports.provide(NodeContext_exports.layer),
2355
+ Effect_exports.provide(NodeFileSystem_exports.layer)
2356
+ )
2357
+ );
2358
+ } catch (error) {
2359
+ opts.signal?.throwIfAborted();
2360
+ throw error;
2361
+ }
2362
+ };
2363
+ const worktreeCreateSandbox = async (opts) => {
2364
+ return createSandboxFromWorktree({
2365
+ branch: worktreeInfo.branch,
2366
+ worktreePath: worktreeInfo.path,
2367
+ hostRepoDir,
2368
+ sandbox: opts.sandbox,
2369
+ hooks: opts.hooks,
2370
+ copyToWorktree: opts.copyToWorktree,
2371
+ timeouts: opts.timeouts,
2372
+ _test: opts._test
2373
+ });
2374
+ };
2375
+ return {
2376
+ branch: worktreeInfo.branch,
2377
+ worktreePath: worktreeInfo.path,
2378
+ run: worktreeRun,
2379
+ interactive: worktreeInteractive,
2380
+ createSandbox: worktreeCreateSandbox,
2381
+ close,
2382
+ async [Symbol.asyncDispose]() {
2383
+ await close();
2384
+ }
2385
+ };
2386
+ };
2387
+
2388
+ // src/CwdError.ts
2389
+ var CwdError2 = CwdError;
2390
+
2391
+ export { CwdError2 as CwdError, Output, StructuredOutputError, createSandbox, createWorktree, interactive, run };
2392
+ //# sourceMappingURL=index.js.map
2393
+ //# sourceMappingURL=index.js.map