@snappedly-tools/shipyard 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +14 -11
  2. package/dist/{SandboxProvider-KeFp_nju.d.ts → SandboxProvider-XJQqEdSf.d.ts} +4 -0
  3. package/dist/chunk-2GT3OPO5.js +2620 -0
  4. package/dist/chunk-2GT3OPO5.js.map +1 -0
  5. package/dist/{chunk-CP6YEQKU.js → chunk-KMGNFXKN.js} +2 -6
  6. package/dist/chunk-KMGNFXKN.js.map +1 -0
  7. package/dist/{chunk-OAHPDSOJ.js → chunk-OCJKONZ7.js} +90 -22
  8. package/dist/chunk-OCJKONZ7.js.map +1 -0
  9. package/dist/chunk-VDHZO5PP.js +113 -0
  10. package/dist/chunk-VDHZO5PP.js.map +1 -0
  11. package/dist/{chunk-YVJHSJUW.js → chunk-VW4MI5GJ.js} +5 -6
  12. package/dist/chunk-VW4MI5GJ.js.map +1 -0
  13. package/dist/chunk-WA7QPAXO.js +50 -0
  14. package/dist/chunk-WA7QPAXO.js.map +1 -0
  15. package/dist/index.d.ts +28 -5
  16. package/dist/index.js +471 -297
  17. package/dist/index.js.map +1 -1
  18. package/dist/main.js +242 -19347
  19. package/dist/main.js.map +1 -1
  20. package/dist/sandboxes/docker.d.ts +1 -1
  21. package/dist/sandboxes/docker.js +4 -7
  22. package/dist/sandboxes/no-sandbox.d.ts +1 -1
  23. package/dist/sandboxes/no-sandbox.js +2 -5
  24. package/dist/sandboxes/vercel.d.ts +1 -1
  25. package/dist/sandboxes/vercel.js +2 -4
  26. package/dist/sandboxes/vercel.js.map +1 -1
  27. package/package.json +15 -12
  28. package/dist/chunk-4VDXZY7T.js +0 -27624
  29. package/dist/chunk-4VDXZY7T.js.map +0 -1
  30. package/dist/chunk-BU6XJFJ2.js +0 -90
  31. package/dist/chunk-BU6XJFJ2.js.map +0 -1
  32. package/dist/chunk-CP6YEQKU.js.map +0 -1
  33. package/dist/chunk-OAHPDSOJ.js.map +0 -1
  34. package/dist/chunk-SRUH232X.js +0 -26712
  35. package/dist/chunk-SRUH232X.js.map +0 -1
  36. package/dist/chunk-YVJHSJUW.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,21 +1,21 @@
1
- import { createRequire } from 'node:module';
2
- import { CODEX_MODELS, NodeContext_exports, NodeFileSystem_exports, formatErrorMessage } from './chunk-SRUH232X.js';
3
- export { CODEX_MODELS, CODEX_REASONING_EFFORTS } from './chunk-SRUH232X.js';
4
- import { Context_exports, CwdError, Effect_exports, resolveCwd, getCurrentBranch, generateTempBranchName, CONFIG_DIR, LOGS_DIR, assertNoSymlinkComponents, Layer_exports, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, CLI_NAME, PRODUCT_NAME, pruneStale, create, copyToWorktree, runHostHooks, startSandbox, resolveGitMounts, patchGitMountsForWindows, SANDBOX_REPO_DIR, remove, makeSandboxFromHandle, syncOut, withSandboxLifecycle, hasUncommittedChanges, registerShutdown, SandboxFactory, PromptError, FileSystem_exports, resolveCanonicalConfigDir, ConfigDirError, AgentError, SessionCaptureError, Clock_exports, Duration_exports, Option_exports, PromptExpansionTimeoutError, Deferred_exports, Ref_exports, SilentDisplay, AgentIdleTimeoutError, Fiber_exports } from './chunk-4VDXZY7T.js';
5
- import { shellQuote, toSessionTransferHandle } from './chunk-CP6YEQKU.js';
6
- export { createBindMountSandboxProvider, createIsolatedSandboxProvider } from './chunk-CP6YEQKU.js';
7
- import { noSandbox } from './chunk-YVJHSJUW.js';
8
- import './chunk-BU6XJFJ2.js';
1
+ import { CODEX_MODELS, formatErrorMessage } from './chunk-VDHZO5PP.js';
2
+ export { CODEX_MODELS, CODEX_REASONING_EFFORTS } from './chunk-VDHZO5PP.js';
3
+ import { CwdError, resolveCwd, getCurrentBranch, generateTempBranchName, CONFIG_DIR, LOGS_DIR, assertNoSymlinkComponents, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, CLI_NAME, PRODUCT_NAME, pruneStale, create, copyToWorktree, runHostHooks, startSandbox, resolveGitMounts, patchGitMountsForWindows, SANDBOX_REPO_DIR, remove, makeSandboxFromHandle, syncOut, withSandboxLifecycle, hasUncommittedChanges, registerShutdown, SandboxFactory, PromptError, ConfigDirError, AgentError, SessionCaptureError, PromptExpansionTimeoutError, SilentDisplay, AgentIdleTimeoutError } from './chunk-2GT3OPO5.js';
4
+ import { shellQuote, toSessionTransferHandle } from './chunk-KMGNFXKN.js';
5
+ export { createBindMountSandboxProvider, createIsolatedSandboxProvider } from './chunk-KMGNFXKN.js';
6
+ import { noSandbox } from './chunk-VW4MI5GJ.js';
7
+ import './chunk-WA7QPAXO.js';
8
+ import { NodeContext, NodeFileSystem } from '@effect/platform-node';
9
9
  import { mkdirSync, chmodSync, appendFileSync, constants } from 'fs';
10
10
  import path, { join, posix, dirname, relative } from 'path';
11
11
  import { styleText } from 'util';
12
+ import { Context, Effect, Layer, Clock, Duration, Option, Deferred, Ref, Fiber } from 'effect';
13
+ import { FileSystem } from '@effect/platform';
12
14
  import * as clack from '@clack/prompts';
13
15
  import { readdir, access, readFile, mkdir, mkdtemp, writeFile, rm, lstat, open, chmod } from 'fs/promises';
14
16
  import { homedir, tmpdir } from 'os';
15
17
  import { createHmac, timingSafeEqual, createHash, randomUUID } from 'crypto';
16
18
 
17
- createRequire(import.meta.url);
18
-
19
19
  // src/resumePrecheck.ts
20
20
  var assertResumeSessionExists = async (params) => {
21
21
  const { provider, sandboxTag, hostRepoDir, resumeSession } = params;
@@ -45,20 +45,16 @@ var assertResumeSessionExists = async (params) => {
45
45
  );
46
46
  }
47
47
  };
48
-
49
- // src/AgentStreamEmitter.ts
50
- var AgentStreamEmitter = class extends Context_exports.Tag("AgentStreamEmitter")() {
48
+ var AgentStreamEmitter = class extends Context.Tag("AgentStreamEmitter")() {
51
49
  };
52
- var agentStreamEmitterLayer = (onEvent) => Layer_exports.succeed(AgentStreamEmitter, {
53
- emit: onEvent ? (event) => Effect_exports.sync(() => {
50
+ var agentStreamEmitterLayer = (onEvent) => Layer.succeed(AgentStreamEmitter, {
51
+ emit: onEvent ? (event) => Effect.sync(() => {
54
52
  try {
55
53
  onEvent(event);
56
54
  } catch {
57
55
  }
58
- }) : () => Effect_exports.void
56
+ }) : () => Effect.void
59
57
  });
60
-
61
- // src/PromptPreprocessor.ts
62
58
  var PROMPT_EXPANSION_TIMEOUT_MS = 3e4;
63
59
  var MAX_PROMPT_EXPANSIONS = 64;
64
60
  var MAX_EXPANSION_OUTPUT_BYTES = 256 * 1024;
@@ -72,46 +68,46 @@ var MARKED_SHELL_BLOCK_PATTERN = new RegExp(
72
68
  var preprocessPrompt = (prompt, sandbox, cwd) => {
73
69
  const matches = [...prompt.matchAll(MARKED_SHELL_BLOCK_PATTERN)];
74
70
  if (matches.length === 0) {
75
- return Effect_exports.succeed(prompt.replaceAll(SHELL_BLOCK_MARKER, ""));
71
+ return Effect.succeed(prompt.replaceAll(SHELL_BLOCK_MARKER, ""));
76
72
  }
77
73
  if (matches.length > MAX_PROMPT_EXPANSIONS) {
78
- return Effect_exports.fail(
74
+ return Effect.fail(
79
75
  new PromptError({
80
76
  message: `Prompt contains ${matches.length} shell expressions; the maximum is ${MAX_PROMPT_EXPANSIONS}`
81
77
  })
82
78
  );
83
79
  }
84
- return Effect_exports.gen(function* () {
80
+ return Effect.gen(function* () {
85
81
  const display = yield* Display;
86
82
  return yield* display.taskLog(
87
83
  "Expanding shell expressions",
88
- (message) => Effect_exports.gen(function* () {
89
- const results = yield* Effect_exports.all(
84
+ (message) => Effect.gen(function* () {
85
+ const results = yield* Effect.all(
90
86
  matches.map((match) => {
91
87
  const command = match[1];
92
- return Effect_exports.gen(function* () {
93
- const start = yield* Clock_exports.currentTimeMillis;
88
+ return Effect.gen(function* () {
89
+ const start = yield* Clock.currentTimeMillis;
94
90
  const controller = new AbortController();
95
91
  const maybeResult = yield* sandbox.exec(command, {
96
92
  cwd,
97
93
  signal: controller.signal,
98
94
  maxOutputBytes: MAX_EXPANSION_OUTPUT_BYTES
99
95
  }).pipe(
100
- Effect_exports.timeoutOption(
101
- Duration_exports.millis(PROMPT_EXPANSION_TIMEOUT_MS)
96
+ Effect.timeoutOption(
97
+ Duration.millis(PROMPT_EXPANSION_TIMEOUT_MS)
102
98
  ),
103
99
  // A timeout must stop the underlying local process too;
104
100
  // interrupting a Promise alone would leave a host-side
105
101
  // prompt command running after the request has failed.
106
- Effect_exports.ensuring(
107
- Effect_exports.sync(() => {
102
+ Effect.ensuring(
103
+ Effect.sync(() => {
108
104
  controller.abort();
109
105
  })
110
106
  )
111
107
  );
112
- if (Option_exports.isNone(maybeResult)) {
113
- const elapsedMs = (yield* Clock_exports.currentTimeMillis) - start;
114
- return yield* Effect_exports.fail(
108
+ if (Option.isNone(maybeResult)) {
109
+ const elapsedMs = (yield* Clock.currentTimeMillis) - start;
110
+ return yield* Effect.fail(
115
111
  new PromptExpansionTimeoutError({
116
112
  message: `Shell expression \`${command}\` timed out after ${elapsedMs}ms`,
117
113
  timeoutMs: PROMPT_EXPANSION_TIMEOUT_MS,
@@ -122,7 +118,7 @@ var preprocessPrompt = (prompt, sandbox, cwd) => {
122
118
  }
123
119
  const execResult = maybeResult.value;
124
120
  if (execResult.exitCode !== 0) {
125
- return yield* Effect_exports.fail(
121
+ return yield* Effect.fail(
126
122
  new PromptError({
127
123
  message: `Command \`${command}\` exited with code ${execResult.exitCode}: ${execResult.stderr}`,
128
124
  exitCode: execResult.exitCode
@@ -131,7 +127,7 @@ var preprocessPrompt = (prompt, sandbox, cwd) => {
131
127
  }
132
128
  const output = execResult.stdout.trimEnd();
133
129
  if (Buffer.byteLength(output, "utf8") > MAX_EXPANSION_OUTPUT_BYTES) {
134
- return yield* Effect_exports.fail(
130
+ return yield* Effect.fail(
135
131
  new PromptError({
136
132
  message: `Shell expression output exceeds the ${MAX_EXPANSION_OUTPUT_BYTES}-byte limit`,
137
133
  exitCode: execResult.exitCode
@@ -156,7 +152,7 @@ var preprocessPrompt = (prompt, sandbox, cwd) => {
156
152
  }
157
153
  const expanded = result3.replaceAll(SHELL_BLOCK_MARKER, "");
158
154
  if (Buffer.byteLength(expanded, "utf8") > MAX_EXPANDED_PROMPT_BYTES) {
159
- return yield* Effect_exports.fail(
155
+ return yield* Effect.fail(
160
156
  new PromptError({
161
157
  message: `Expanded prompt exceeds the ${MAX_EXPANDED_PROMPT_BYTES}-byte limit`
162
158
  })
@@ -228,27 +224,27 @@ var appendBounded = (current, addition) => {
228
224
  if (current.length >= MAX_AGENT_RETAINED_OUTPUT_CHARS) return current;
229
225
  return current + addition.slice(0, MAX_AGENT_RETAINED_OUTPUT_CHARS - current.length);
230
226
  };
231
- var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, completionTimeoutMs, completionSignals, onText, onToolCall, onRawLine, onIdleWarning, onCompletionTimeout, idleWarningIntervalMs = IDLE_WARNING_INTERVAL_MS, resumeSession, forkSession, signal, toolAllowlist) => Effect_exports.gen(function* () {
227
+ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, completionTimeoutMs, completionSignals, onText, onToolCall, onRawLine, onIdleWarning, onCompletionTimeout, idleWarningIntervalMs = IDLE_WARNING_INTERVAL_MS, resumeSession, forkSession, signal, toolAllowlist) => Effect.gen(function* () {
232
228
  let resultText = "";
233
229
  let sessionId;
234
230
  let usage;
235
231
  let accumulatedOutput = "";
236
- const timeoutSignal = yield* Deferred_exports.make();
237
- const completionTimeoutDeferred = yield* Deferred_exports.make();
232
+ const timeoutSignal = yield* Deferred.make();
233
+ const completionTimeoutDeferred = yield* Deferred.make();
238
234
  let timeoutFiber = null;
239
235
  let completionDetected = false;
240
236
  let warningFiber = null;
241
237
  let idleMinuteCounter = 0;
242
238
  const interruptFiber = (fiber) => {
243
- if (fiber !== null) Effect_exports.runFork(Fiber_exports.interrupt(fiber));
239
+ if (fiber !== null) Effect.runFork(Fiber.interrupt(fiber));
244
240
  };
245
241
  const startWarningInterval = () => {
246
242
  interruptFiber(warningFiber);
247
243
  idleMinuteCounter = 0;
248
- warningFiber = Effect_exports.runFork(
249
- Effect_exports.gen(function* () {
244
+ warningFiber = Effect.runFork(
245
+ Effect.gen(function* () {
250
246
  while (true) {
251
- yield* Effect_exports.sleep(Duration_exports.millis(idleWarningIntervalMs));
247
+ yield* Effect.sleep(Duration.millis(idleWarningIntervalMs));
252
248
  idleMinuteCounter++;
253
249
  onIdleWarning(idleMinuteCounter);
254
250
  }
@@ -258,11 +254,11 @@ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, com
258
254
  const resetTimer = () => {
259
255
  interruptFiber(timeoutFiber);
260
256
  if (completionDetected) {
261
- timeoutFiber = Effect_exports.runFork(
262
- Effect_exports.gen(function* () {
263
- yield* Effect_exports.sleep(Duration_exports.millis(completionTimeoutMs));
257
+ timeoutFiber = Effect.runFork(
258
+ Effect.gen(function* () {
259
+ yield* Effect.sleep(Duration.millis(completionTimeoutMs));
264
260
  onCompletionTimeout(completionTimeoutMs);
265
- yield* Deferred_exports.succeed(completionTimeoutDeferred, {
261
+ yield* Deferred.succeed(completionTimeoutDeferred, {
266
262
  result: resultText || accumulatedOutput,
267
263
  sessionId,
268
264
  usage
@@ -270,10 +266,10 @@ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, com
270
266
  })
271
267
  );
272
268
  } else {
273
- timeoutFiber = Effect_exports.runFork(
274
- Effect_exports.gen(function* () {
275
- yield* Effect_exports.sleep(Duration_exports.millis(idleTimeoutMs));
276
- yield* Deferred_exports.fail(
269
+ timeoutFiber = Effect.runFork(
270
+ Effect.gen(function* () {
271
+ yield* Effect.sleep(Duration.millis(idleTimeoutMs));
272
+ yield* Deferred.fail(
277
273
  timeoutSignal,
278
274
  new AgentIdleTimeoutError({
279
275
  message: `Agent idle for ${idleTimeoutMs / 1e3} seconds \u2014 no output received. Consider increasing the idle timeout with --idle-timeout.`,
@@ -285,20 +281,20 @@ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, com
285
281
  startWarningInterval();
286
282
  }
287
283
  };
288
- const abortDeferred = yield* Deferred_exports.make();
284
+ const abortDeferred = yield* Deferred.make();
289
285
  let abortCleanup = null;
290
286
  if (signal) {
291
287
  if (signal.aborted) {
292
- return yield* Effect_exports.die(signal.reason);
288
+ return yield* Effect.die(signal.reason);
293
289
  }
294
290
  const onAbort = () => {
295
- Effect_exports.runFork(Deferred_exports.die(abortDeferred, signal.reason));
291
+ Effect.runFork(Deferred.die(abortDeferred, signal.reason));
296
292
  };
297
293
  signal.addEventListener("abort", onAbort, { once: true });
298
294
  abortCleanup = () => signal.removeEventListener("abort", onAbort);
299
295
  }
300
296
  resetTimer();
301
- const execEffect = Effect_exports.gen(function* () {
297
+ const execEffect = Effect.gen(function* () {
302
298
  const printCmd = provider.buildPrintCommand({
303
299
  prompt,
304
300
  dangerouslySkipPermissions: true,
@@ -353,7 +349,7 @@ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, com
353
349
  const lines = execResult.stdout.split("\n").filter((l) => l.trim());
354
350
  errorDetail = lines.slice(-20).join("\n");
355
351
  }
356
- return yield* Effect_exports.fail(
352
+ return yield* Effect.fail(
357
353
  new AgentError({
358
354
  message: `${provider.name} exited with code ${execResult.exitCode}:
359
355
  ${errorDetail}`
@@ -362,8 +358,8 @@ ${errorDetail}`
362
358
  }
363
359
  return { result: resultText || execResult.stdout, sessionId, usage };
364
360
  }).pipe(
365
- Effect_exports.ensuring(
366
- Effect_exports.sync(() => {
361
+ Effect.ensuring(
362
+ Effect.sync(() => {
367
363
  interruptFiber(timeoutFiber);
368
364
  timeoutFiber = null;
369
365
  interruptFiber(warningFiber);
@@ -371,17 +367,17 @@ ${errorDetail}`
371
367
  })
372
368
  )
373
369
  );
374
- let raced = Effect_exports.raceFirst(execEffect, Deferred_exports.await(timeoutSignal));
375
- raced = Effect_exports.raceFirst(raced, Deferred_exports.await(completionTimeoutDeferred));
370
+ let raced = Effect.raceFirst(execEffect, Deferred.await(timeoutSignal));
371
+ raced = Effect.raceFirst(raced, Deferred.await(completionTimeoutDeferred));
376
372
  if (signal) {
377
- raced = Effect_exports.raceFirst(
373
+ raced = Effect.raceFirst(
378
374
  raced,
379
- Deferred_exports.await(abortDeferred)
375
+ Deferred.await(abortDeferred)
380
376
  );
381
377
  }
382
378
  return yield* raced.pipe(
383
- Effect_exports.ensuring(
384
- Effect_exports.sync(() => {
379
+ Effect.ensuring(
380
+ Effect.sync(() => {
385
381
  abortCleanup?.();
386
382
  interruptFiber(timeoutFiber);
387
383
  timeoutFiber = null;
@@ -397,13 +393,13 @@ var DEFAULT_COMPLETION_TIMEOUT_SECONDS = 60;
397
393
  var orchestrate = (options) => {
398
394
  const idleTimeoutMs = (options.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS) * 1e3;
399
395
  const completionTimeoutMs = (options.completionTimeoutSeconds ?? DEFAULT_COMPLETION_TIMEOUT_SECONDS) * 1e3;
400
- return Effect_exports.gen(function* () {
396
+ return Effect.gen(function* () {
401
397
  const factory = yield* SandboxFactory;
402
398
  const display = yield* Display;
403
399
  const streamEmitter = yield* AgentStreamEmitter;
404
400
  const { hostRepoDir, iterations, hooks, prompt, branch, provider } = options;
405
401
  if (options.toolAllowlist !== void 0 && provider.supportsToolAllowlist !== true) {
406
- return yield* Effect_exports.fail(
402
+ return yield* Effect.fail(
407
403
  new AgentError({
408
404
  message: `Agent provider "${provider.name}" does not enforce the phase tool allowlist`
409
405
  })
@@ -423,7 +419,7 @@ var orchestrate = (options) => {
423
419
  let allStdout = "";
424
420
  let resolvedBranch = "";
425
421
  let iterationPreservedPath;
426
- const checkAbort = () => options.signal?.aborted ? Effect_exports.die(options.signal.reason) : Effect_exports.void;
422
+ const checkAbort = () => options.signal?.aborted ? Effect.die(options.signal.reason) : Effect.void;
427
423
  for (let i = 1; i <= iterations; i++) {
428
424
  yield* checkAbort();
429
425
  yield* display.status(label(`Iteration ${i}/${iterations}`), "info");
@@ -441,12 +437,12 @@ var orchestrate = (options) => {
441
437
  keepSourceBranch: options.keepSourceBranch
442
438
  },
443
439
  sandbox,
444
- (ctx) => Effect_exports.gen(function* () {
440
+ (ctx) => Effect.gen(function* () {
445
441
  const iterationResumeSession = i === 1 ? options.resumeSession : void 0;
446
442
  const iterationForkSession = i === 1 ? options.forkSession : void 0;
447
443
  if (iterationResumeSession && bindMountHandle && provider.sessionStorage) {
448
444
  yield* display.status(label("Resuming session"), "info");
449
- yield* Effect_exports.tryPromise({
445
+ yield* Effect.tryPromise({
450
446
  try: () => provider.sessionStorage.resumeIntoSandbox({
451
447
  hostCwd: hostRepoDir,
452
448
  sandboxCwd: ctx.sandboxRepoDir,
@@ -466,8 +462,8 @@ var orchestrate = (options) => {
466
462
  );
467
463
  yield* display.status(label("Agent started"), "success");
468
464
  const textBuffer = new TextDeltaBuffer((chunk) => {
469
- Effect_exports.runPromise(display.textChunk(chunk));
470
- Effect_exports.runPromise(
465
+ Effect.runPromise(display.textChunk(chunk));
466
+ Effect.runPromise(
471
467
  streamEmitter.emit({
472
468
  type: "text",
473
469
  message: chunk,
@@ -481,8 +477,8 @@ var orchestrate = (options) => {
481
477
  };
482
478
  const onToolCall = (name, formattedArgs) => {
483
479
  textBuffer.flush();
484
- Effect_exports.runPromise(display.toolCall(name, formattedArgs));
485
- Effect_exports.runPromise(
480
+ Effect.runPromise(display.toolCall(name, formattedArgs));
481
+ Effect.runPromise(
486
482
  streamEmitter.emit({
487
483
  type: "toolCall",
488
484
  name,
@@ -493,7 +489,7 @@ var orchestrate = (options) => {
493
489
  );
494
490
  };
495
491
  const onRawLine = (line) => {
496
- Effect_exports.runPromise(
492
+ Effect.runPromise(
497
493
  streamEmitter.emit({
498
494
  type: "raw",
499
495
  line,
@@ -504,10 +500,10 @@ var orchestrate = (options) => {
504
500
  };
505
501
  const onIdleWarning = (minutes) => {
506
502
  const msg = minutes === 1 ? "Agent idle for 1 minute" : `Agent idle for ${minutes} minutes`;
507
- Effect_exports.runPromise(display.status(label(msg), "warn"));
503
+ Effect.runPromise(display.status(label(msg), "warn"));
508
504
  };
509
505
  const onCompletionTimeout = (timeoutMs) => {
510
- Effect_exports.runPromise(
506
+ Effect.runPromise(
511
507
  display.status(
512
508
  label(
513
509
  `Completion signal seen but agent process is hanging \u2014 force-completing after ${timeoutMs / 1e3}s grace window.`
@@ -545,7 +541,7 @@ var orchestrate = (options) => {
545
541
  let usage = streamUsage;
546
542
  if (provider.captureSessions && provider.sessionStorage && sessionId && bindMountHandle) {
547
543
  yield* display.status(label("Capturing session"), "info");
548
- yield* Effect_exports.tryPromise({
544
+ yield* Effect.tryPromise({
549
545
  try: () => provider.sessionStorage.captureToHost({
550
546
  hostCwd: hostRepoDir,
551
547
  sandboxCwd: ctx.sandboxRepoDir,
@@ -562,7 +558,7 @@ var orchestrate = (options) => {
562
558
  sessionId
563
559
  );
564
560
  if (provider.parseSessionUsage) {
565
- const content = yield* Effect_exports.promise(
561
+ const content = yield* Effect.promise(
566
562
  () => provider.sessionStorage.readHostSession(hostRepoDir, sessionId).catch(() => void 0)
567
563
  );
568
564
  if (content) {
@@ -623,32 +619,30 @@ var orchestrate = (options) => {
623
619
  };
624
620
  });
625
621
  };
626
-
627
- // src/PromptResolver.ts
628
622
  var resolvePrompt = (options) => {
629
623
  const { prompt, promptFile } = options;
630
624
  if (prompt !== void 0 && promptFile !== void 0) {
631
- return Effect_exports.fail(
625
+ return Effect.fail(
632
626
  new PromptError({
633
627
  message: "Cannot provide both --prompt and --prompt-file"
634
628
  })
635
629
  );
636
630
  }
637
631
  if (prompt !== void 0) {
638
- return Effect_exports.succeed({ text: prompt, source: "inline" });
632
+ return Effect.succeed({ text: prompt, source: "inline" });
639
633
  }
640
634
  if (promptFile === void 0) {
641
- return Effect_exports.fail(
635
+ return Effect.fail(
642
636
  new PromptError({
643
637
  message: "Must provide either prompt or promptFile. Pass prompt: '...' or promptFile: './.shipyard/prompt.md' to run()."
644
638
  })
645
639
  );
646
640
  }
647
- return Effect_exports.gen(function* () {
648
- const fs = yield* FileSystem_exports.FileSystem;
641
+ return Effect.gen(function* () {
642
+ const fs = yield* FileSystem.FileSystem;
649
643
  const text2 = yield* fs.readFileString(promptFile).pipe(
650
- Effect_exports.catchAll(
651
- (e) => Effect_exports.fail(
644
+ Effect.catchAll(
645
+ (e) => Effect.fail(
652
646
  new PromptError({
653
647
  message: `Failed to read prompt from ${promptFile}: ${e}`
654
648
  })
@@ -658,14 +652,14 @@ var resolvePrompt = (options) => {
658
652
  return { text: text2, source: "template" };
659
653
  });
660
654
  };
661
- var parseEnvFile = (filePath) => Effect_exports.gen(function* () {
662
- const fs = yield* FileSystem_exports.FileSystem;
655
+ var parseEnvFile = (filePath) => Effect.gen(function* () {
656
+ const fs = yield* FileSystem.FileSystem;
663
657
  const symlink = yield* fs.readLink(filePath).pipe(
664
- Effect_exports.map(() => true),
665
- Effect_exports.catchAll(() => Effect_exports.succeed(false))
658
+ Effect.map(() => true),
659
+ Effect.catchAll(() => Effect.succeed(false))
666
660
  );
667
661
  if (symlink) return {};
668
- const content = yield* fs.readFileString(filePath).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(null)));
662
+ const content = yield* fs.readFileString(filePath).pipe(Effect.catchAll(() => Effect.succeed(null)));
669
663
  if (content === null) return {};
670
664
  const vars = {};
671
665
  for (const line of content.split("\n")) {
@@ -695,10 +689,10 @@ var parseEnvFile = (filePath) => Effect_exports.gen(function* () {
695
689
  }
696
690
  return vars;
697
691
  });
698
- var resolveEnv = (repoDir) => Effect_exports.gen(function* () {
699
- const configDir = yield* resolveCanonicalConfigDir(repoDir);
692
+ var resolveEnv = (repoDir) => Effect.gen(function* () {
693
+ const configDir = join(repoDir, CONFIG_DIR);
700
694
  const envPath = join(configDir, ".env");
701
- yield* Effect_exports.tryPromise({
695
+ yield* Effect.tryPromise({
702
696
  try: () => assertNoSymlinkComponents(repoDir, envPath, "environment file"),
703
697
  catch: (e) => new ConfigDirError({
704
698
  message: `Refusing symlinked environment path: ${e instanceof Error ? e.message : String(e)}`
@@ -732,8 +726,6 @@ var mergeProviderEnv = (options) => {
732
726
  ...agentProviderEnv
733
727
  };
734
728
  };
735
-
736
- // src/PromptArgumentSubstitution.ts
737
729
  var SHELL_BLOCK_PATTERN = /!`([^`]+)`/g;
738
730
  var BUILT_IN_PROMPT_ARG_KEYS = [
739
731
  "SOURCE_BRANCH",
@@ -741,8 +733,8 @@ var BUILT_IN_PROMPT_ARG_KEYS = [
741
733
  ];
742
734
  var PLACEHOLDER_PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
743
735
  var validateNoArgsWithInlinePrompt = (args) => {
744
- if (Object.keys(args).length === 0) return Effect_exports.void;
745
- return Effect_exports.fail(
736
+ if (Object.keys(args).length === 0) return Effect.void;
737
+ return Effect.fail(
746
738
  new PromptError({
747
739
  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.'
748
740
  })
@@ -751,14 +743,14 @@ var validateNoArgsWithInlinePrompt = (args) => {
751
743
  var validateNoBuiltInArgOverride = (args) => {
752
744
  for (const key of BUILT_IN_PROMPT_ARG_KEYS) {
753
745
  if (key in args) {
754
- return Effect_exports.fail(
746
+ return Effect.fail(
755
747
  new PromptError({
756
748
  message: `"${key}" is a built-in prompt argument and cannot be overridden via promptArgs`
757
749
  })
758
750
  );
759
751
  }
760
752
  }
761
- return Effect_exports.void;
753
+ return Effect.void;
762
754
  };
763
755
  var findMissingPromptArgKeys = (prompt, providedArgs) => {
764
756
  const matches = [...prompt.matchAll(PLACEHOLDER_PATTERN)];
@@ -785,14 +777,14 @@ var substitutePromptArgs = (prompt, args, silentKeys) => {
785
777
  );
786
778
  const matches = [...markedPrompt.matchAll(PLACEHOLDER_PATTERN)];
787
779
  if (matches.length === 0 && Object.keys(sanitizedArgs).length === 0) {
788
- return Effect_exports.succeed(markedPrompt);
780
+ return Effect.succeed(markedPrompt);
789
781
  }
790
- return Effect_exports.gen(function* () {
782
+ return Effect.gen(function* () {
791
783
  const display = yield* Display;
792
784
  const referencedKeys = new Set(matches.map((m) => m[1]));
793
785
  for (const key of referencedKeys) {
794
786
  if (!(key in sanitizedArgs)) {
795
- return yield* Effect_exports.fail(
787
+ return yield* Effect.fail(
796
788
  new PromptError({
797
789
  message: `Prompt argument "{{${key}}}" has no matching value in promptArgs`
798
790
  })
@@ -800,7 +792,7 @@ var substitutePromptArgs = (prompt, args, silentKeys) => {
800
792
  }
801
793
  const value = sanitizedArgs[key];
802
794
  if (value == null) {
803
- return yield* Effect_exports.fail(
795
+ return yield* Effect.fail(
804
796
  new PromptError({
805
797
  message: `Prompt argument "{{${key}}}" has value ${value === null ? "null" : "undefined"} in promptArgs`
806
798
  })
@@ -957,6 +949,15 @@ var unwrapFences = (text2) => {
957
949
  return text2;
958
950
  };
959
951
 
952
+ // src/validateMaxIterations.ts
953
+ var validateMaxIterations = (value) => {
954
+ if (!Number.isSafeInteger(value) || value <= 0) {
955
+ throw new Error(
956
+ `maxIterations must be a positive safe integer. Received: ${value}`
957
+ );
958
+ }
959
+ };
960
+
960
961
  // src/run.ts
961
962
  var buildStructuredOutputRetryFeedback = (error, retriesRemaining) => {
962
963
  const raw = error.rawMatched === void 0 ? "(no matching tag was emitted)" : error.rawMatched;
@@ -1066,6 +1067,7 @@ async function run(options) {
1066
1067
  hooks,
1067
1068
  agent: provider
1068
1069
  } = options;
1070
+ validateMaxIterations(maxIterations);
1069
1071
  const branchStrategy = options.branchStrategy ?? (options.sandbox.tag === "isolated" ? { type: "merge-to-head" } : { type: "head" });
1070
1072
  const effectiveBranchType = branchStrategy.type;
1071
1073
  if (effectiveBranchType === "head" && options.sandbox.tag === "isolated") {
@@ -1105,8 +1107,8 @@ async function run(options) {
1105
1107
  );
1106
1108
  }
1107
1109
  const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
1108
- const hostRepoDir = await Effect_exports.runPromise(
1109
- resolveCwd(options.cwd).pipe(Effect_exports.provide(NodeContext_exports.layer))
1110
+ const hostRepoDir = await Effect.runPromise(
1111
+ resolveCwd(options.cwd).pipe(Effect.provide(NodeContext.layer))
1110
1112
  );
1111
1113
  if (options.resumeSession) {
1112
1114
  await assertResumeSessionExists({
@@ -1116,9 +1118,9 @@ async function run(options) {
1116
1118
  resumeSession: options.resumeSession
1117
1119
  });
1118
1120
  }
1119
- const resolved = await Effect_exports.runPromise(
1121
+ const resolved = await Effect.runPromise(
1120
1122
  resolvePrompt({ prompt, promptFile }).pipe(
1121
- Effect_exports.provide(NodeContext_exports.layer)
1123
+ Effect.provide(NodeContext.layer)
1122
1124
  )
1123
1125
  );
1124
1126
  const rawPrompt = resolved.text;
@@ -1132,15 +1134,15 @@ async function run(options) {
1132
1134
  }
1133
1135
  }
1134
1136
  const agentName = provider.name;
1135
- const resolvedEnv = await Effect_exports.runPromise(
1136
- resolveEnv(hostRepoDir).pipe(Effect_exports.provide(NodeContext_exports.layer))
1137
+ const resolvedEnv = await Effect.runPromise(
1138
+ resolveEnv(hostRepoDir).pipe(Effect.provide(NodeContext.layer))
1137
1139
  );
1138
1140
  const env = mergeProviderEnv({
1139
1141
  resolvedEnv,
1140
1142
  agentProviderEnv: provider.env,
1141
1143
  sandboxProviderEnv: options.sandbox.env
1142
1144
  });
1143
- const currentHostBranch = await Effect_exports.runPromise(
1145
+ const currentHostBranch = await Effect.runPromise(
1144
1146
  getCurrentBranch(hostRepoDir)
1145
1147
  );
1146
1148
  const resolvedBranch = effectiveBranchType === "head" ? currentHostBranch : branch ?? generateTempBranchName(options.name);
@@ -1168,15 +1170,15 @@ async function run(options) {
1168
1170
  branch: resolvedBranch,
1169
1171
  hostRepoDir
1170
1172
  });
1171
- return Layer_exports.provide(
1173
+ return Layer.provide(
1172
1174
  FileDisplay.layer(resolvedLogging.path),
1173
- NodeFileSystem_exports.layer
1175
+ NodeFileSystem.layer
1174
1176
  );
1175
1177
  })() : ClackDisplay.layer;
1176
- const factoryLayer = Layer_exports.provide(
1178
+ const factoryLayer = Layer.provide(
1177
1179
  WorktreeDockerSandboxFactory.layer,
1178
- Layer_exports.mergeAll(
1179
- Layer_exports.succeed(SandboxConfig, {
1180
+ Layer.mergeAll(
1181
+ Layer.succeed(SandboxConfig, {
1180
1182
  env,
1181
1183
  hostRepoDir,
1182
1184
  copyToWorktree: options.copyToWorktree,
@@ -1187,19 +1189,19 @@ async function run(options) {
1187
1189
  signal: options.signal,
1188
1190
  timeouts: options.timeouts
1189
1191
  }),
1190
- NodeFileSystem_exports.layer,
1192
+ NodeFileSystem.layer,
1191
1193
  displayLayer
1192
1194
  )
1193
1195
  );
1194
1196
  const streamEmitterLayer = agentStreamEmitterLayer(
1195
1197
  buildAgentStreamHandler(resolvedLogging)
1196
1198
  );
1197
- const runLayer = Layer_exports.mergeAll(
1199
+ const runLayer = Layer.mergeAll(
1198
1200
  factoryLayer,
1199
1201
  displayLayer,
1200
1202
  streamEmitterLayer
1201
1203
  );
1202
- const baseEffect = Effect_exports.gen(function* () {
1204
+ const baseEffect = Effect.gen(function* () {
1203
1205
  const d = yield* Display;
1204
1206
  yield* d.intro(options.name ?? CLI_NAME);
1205
1207
  const rows = buildRunSummaryRows({
@@ -1259,8 +1261,8 @@ async function run(options) {
1259
1261
  return orchestrateResult;
1260
1262
  });
1261
1263
  const withErrorLog = resolvedLogging.type === "file" ? baseEffect.pipe(
1262
- Effect_exports.tapError(
1263
- (error) => Effect_exports.gen(function* () {
1264
+ Effect.tapError(
1265
+ (error) => Effect.gen(function* () {
1264
1266
  const d = yield* Display;
1265
1267
  yield* d.status(
1266
1268
  formatErrorMessage(error),
@@ -1271,8 +1273,8 @@ async function run(options) {
1271
1273
  ) : baseEffect;
1272
1274
  let result3;
1273
1275
  try {
1274
- result3 = await Effect_exports.runPromise(
1275
- withErrorLog.pipe(Effect_exports.provide(runLayer))
1276
+ result3 = await Effect.runPromise(
1277
+ withErrorLog.pipe(Effect.provide(runLayer))
1276
1278
  );
1277
1279
  } catch (error) {
1278
1280
  options.signal?.throwIfAborted();
@@ -1351,28 +1353,26 @@ async function run(options) {
1351
1353
  }
1352
1354
  return baseResult;
1353
1355
  }
1354
-
1355
- // src/raceAbortSignal.ts
1356
1356
  var raceAbortSignal = (effect, signal) => {
1357
1357
  if (!signal) return effect;
1358
- return Effect_exports.gen(function* () {
1358
+ return Effect.gen(function* () {
1359
1359
  if (signal.aborted) {
1360
- return yield* Effect_exports.die(signal.reason);
1360
+ return yield* Effect.die(signal.reason);
1361
1361
  }
1362
- const abortDeferred = yield* Deferred_exports.make();
1362
+ const abortDeferred = yield* Deferred.make();
1363
1363
  const onAbort = () => {
1364
- Effect_exports.runPromise(Deferred_exports.die(abortDeferred, signal.reason)).catch(
1364
+ Effect.runPromise(Deferred.die(abortDeferred, signal.reason)).catch(
1365
1365
  () => {
1366
1366
  }
1367
1367
  );
1368
1368
  };
1369
1369
  signal.addEventListener("abort", onAbort, { once: true });
1370
- return yield* Effect_exports.raceFirst(
1370
+ return yield* Effect.raceFirst(
1371
1371
  effect,
1372
- Deferred_exports.await(abortDeferred)
1372
+ Deferred.await(abortDeferred)
1373
1373
  ).pipe(
1374
- Effect_exports.ensuring(
1375
- Effect_exports.sync(() => signal.removeEventListener("abort", onAbort))
1374
+ Effect.ensuring(
1375
+ Effect.sync(() => signal.removeEventListener("abort", onAbort))
1376
1376
  )
1377
1377
  );
1378
1378
  });
@@ -1402,7 +1402,7 @@ var interactive = async (options) => {
1402
1402
  const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
1403
1403
  const isHeadMode = branchStrategy.type === "head";
1404
1404
  const sandboxProvider = resolvedSandbox;
1405
- const inner = Effect_exports.gen(function* () {
1405
+ const inner = Effect.gen(function* () {
1406
1406
  const hostRepoDir = yield* resolveCwd(options.cwd);
1407
1407
  const d = yield* Display;
1408
1408
  const hasPromptSource = prompt !== void 0 || promptFile !== void 0;
@@ -1425,7 +1425,7 @@ var interactive = async (options) => {
1425
1425
  const missingKeys = findMissingPromptArgKeys(rawPrompt, userArgs);
1426
1426
  const collectedArgs = {};
1427
1427
  for (const key of missingKeys) {
1428
- const value = yield* Effect_exports.promise(
1428
+ const value = yield* Effect.promise(
1429
1429
  () => clack.text({
1430
1430
  message: `Enter value for {{${key}}}`,
1431
1431
  validate: (v) => {
@@ -1435,7 +1435,7 @@ var interactive = async (options) => {
1435
1435
  );
1436
1436
  if (clack.isCancel(value)) {
1437
1437
  clack.cancel("Prompt arg collection cancelled.");
1438
- return yield* Effect_exports.fail(
1438
+ return yield* Effect.fail(
1439
1439
  new Error("User cancelled prompt arg collection")
1440
1440
  );
1441
1441
  }
@@ -1469,14 +1469,14 @@ var interactive = async (options) => {
1469
1469
  worktreeInfo = yield* d.taskLog(
1470
1470
  "Creating worktree",
1471
1471
  () => pruneStale(hostRepoDir).pipe(
1472
- Effect_exports.catchAll(() => Effect_exports.void),
1473
- Effect_exports.andThen(
1472
+ Effect.catchAll(() => Effect.void),
1473
+ Effect.andThen(
1474
1474
  branch ? create(hostRepoDir, { branch }) : create(hostRepoDir, { name: options.name })
1475
1475
  )
1476
1476
  )
1477
1477
  );
1478
1478
  }
1479
- const handle = yield* Effect_exports.gen(function* () {
1479
+ const handle = yield* Effect.gen(function* () {
1480
1480
  if (!isHeadMode) {
1481
1481
  if ((sandboxProvider.tag === "bind-mount" || sandboxProvider.tag === "none") && options.copyToWorktree && options.copyToWorktree.length > 0) {
1482
1482
  yield* d.taskLog(
@@ -1497,7 +1497,7 @@ var interactive = async (options) => {
1497
1497
  }
1498
1498
  if (sandboxProvider.tag === "none") {
1499
1499
  const worktreePath = isHeadMode ? hostRepoDir : worktreeInfo.path;
1500
- return yield* Effect_exports.promise(
1500
+ return yield* Effect.promise(
1501
1501
  () => sandboxProvider.create({
1502
1502
  worktreePath,
1503
1503
  env: effectiveEnv
@@ -1538,13 +1538,13 @@ var interactive = async (options) => {
1538
1538
  return startResult.handle;
1539
1539
  }
1540
1540
  }).pipe(
1541
- Effect_exports.tapError(
1541
+ Effect.tapError(
1542
1542
  () => worktreeInfo ? remove(worktreeInfo.path).pipe(
1543
- Effect_exports.catchAll(() => Effect_exports.void)
1544
- ) : Effect_exports.void
1543
+ Effect.catchAll(() => Effect.void)
1544
+ ) : Effect.void
1545
1545
  )
1546
1546
  );
1547
- return yield* Effect_exports.gen(function* () {
1547
+ return yield* Effect.gen(function* () {
1548
1548
  if (!handle.interactiveExec) {
1549
1549
  throw new Error(
1550
1550
  `Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
@@ -1553,7 +1553,7 @@ var interactive = async (options) => {
1553
1553
  const interactiveExecFn = handle.interactiveExec.bind(handle);
1554
1554
  const sandbox = makeSandboxFromHandle(handle);
1555
1555
  const worktreePath = handle.worktreePath;
1556
- const applyToHost = sandboxProvider.tag === "isolated" && worktreeInfo ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
1556
+ const applyToHost = sandboxProvider.tag === "isolated" && worktreeInfo ? () => syncOut(worktreeInfo.path, handle) : () => Effect.void;
1557
1557
  const lifecycleEffect = withSandboxLifecycle(
1558
1558
  {
1559
1559
  hostRepoDir,
@@ -1565,7 +1565,7 @@ var interactive = async (options) => {
1565
1565
  timeouts: options.timeouts
1566
1566
  },
1567
1567
  sandbox,
1568
- (ctx) => Effect_exports.gen(function* () {
1568
+ (ctx) => Effect.gen(function* () {
1569
1569
  const fullPrompt = !hasPromptSource || isInlinePrompt ? substitutedPrompt : yield* preprocessPrompt(
1570
1570
  substitutedPrompt,
1571
1571
  ctx.sandbox,
@@ -1576,12 +1576,13 @@ var interactive = async (options) => {
1576
1576
  dangerouslySkipPermissions: sandboxProvider.tag !== "none"
1577
1577
  });
1578
1578
  const result4 = yield* raceAbortSignal(
1579
- Effect_exports.promise(
1579
+ Effect.promise(
1580
1580
  () => interactiveExecFn(interactiveArgs, {
1581
1581
  stdin: process.stdin,
1582
1582
  stdout: process.stdout,
1583
1583
  stderr: process.stderr,
1584
- cwd: worktreePath
1584
+ cwd: worktreePath,
1585
+ signal: options.signal
1585
1586
  })
1586
1587
  ),
1587
1588
  options.signal
@@ -1595,14 +1596,14 @@ var interactive = async (options) => {
1595
1596
  if (worktreeInfo) {
1596
1597
  const hasUncommitted = yield* hasUncommittedChanges(
1597
1598
  worktreeInfo.path
1598
- ).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
1599
+ ).pipe(Effect.catchAll(() => Effect.succeed(false)));
1599
1600
  if (hasUncommitted) {
1600
1601
  preservedWorktreePath = worktreeInfo.path;
1601
1602
  }
1602
1603
  }
1603
1604
  if (worktreeInfo && !preservedWorktreePath) {
1604
1605
  yield* remove(worktreeInfo.path).pipe(
1605
- Effect_exports.catchAll(() => Effect_exports.void)
1606
+ Effect.catchAll(() => Effect.void)
1606
1607
  );
1607
1608
  }
1608
1609
  yield* d.summary("Session Complete", {
@@ -1618,24 +1619,24 @@ var interactive = async (options) => {
1618
1619
  exitCode
1619
1620
  };
1620
1621
  }).pipe(
1621
- // On error, always clean up worktree (on success, handled above with preserve check)
1622
- Effect_exports.tapError(
1623
- () => worktreeInfo ? remove(worktreeInfo.path).pipe(
1624
- Effect_exports.catchAll(() => Effect_exports.void)
1625
- ) : Effect_exports.void
1622
+ // Cancellation preserves the worktree for inspection and recovery.
1623
+ Effect.tapError(
1624
+ () => worktreeInfo && !options.signal?.aborted ? remove(worktreeInfo.path).pipe(
1625
+ Effect.catchAll(() => Effect.void)
1626
+ ) : Effect.void
1626
1627
  ),
1627
1628
  // Always close sandbox handle
1628
- Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
1629
+ Effect.ensuring(Effect.promise(() => handle.close().catch(() => {
1629
1630
  })))
1630
1631
  );
1631
1632
  });
1632
1633
  let result3;
1633
1634
  try {
1634
- result3 = await Effect_exports.runPromise(
1635
+ result3 = await Effect.runPromise(
1635
1636
  inner.pipe(
1636
- Effect_exports.provide(ClackDisplay.layer),
1637
- Effect_exports.provide(NodeContext_exports.layer),
1638
- Effect_exports.provide(NodeFileSystem_exports.layer)
1637
+ Effect.provide(ClackDisplay.layer),
1638
+ Effect.provide(NodeContext.layer),
1639
+ Effect.provide(NodeFileSystem.layer)
1639
1640
  )
1640
1641
  );
1641
1642
  } catch (error) {
@@ -1669,6 +1670,7 @@ var buildSandboxHandle = (ctx, close) => {
1669
1670
  promptFile,
1670
1671
  maxIterations = 1
1671
1672
  } = runOptions;
1673
+ validateMaxIterations(maxIterations);
1672
1674
  if (runOptions.resumeSession && maxIterations > 1) {
1673
1675
  throw new Error(
1674
1676
  "resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
@@ -1687,21 +1689,21 @@ var buildSandboxHandle = (ctx, close) => {
1687
1689
  resumeSession: runOptions.resumeSession
1688
1690
  });
1689
1691
  }
1690
- const resolved = await Effect_exports.runPromise(
1692
+ const resolved = await Effect.runPromise(
1691
1693
  resolvePrompt({ prompt, promptFile }).pipe(
1692
- Effect_exports.provide(NodeContext_exports.layer)
1694
+ Effect.provide(NodeContext.layer)
1693
1695
  )
1694
1696
  );
1695
1697
  const rawPrompt = resolved.text;
1696
1698
  const isInlinePrompt = resolved.source === "inline";
1697
1699
  const userArgs = runOptions.promptArgs ?? {};
1698
- const currentHostBranch = await Effect_exports.runPromise(
1700
+ const currentHostBranch = await Effect.runPromise(
1699
1701
  getCurrentBranch(hostRepoDir)
1700
1702
  );
1701
- const displayRef = Ref_exports.unsafeMake([]);
1703
+ const displayRef = Ref.unsafeMake([]);
1702
1704
  const silentDisplayLayer = SilentDisplay.layer(displayRef);
1703
- const resolvedPrompt = await Effect_exports.runPromise(
1704
- Effect_exports.gen(function* () {
1705
+ const resolvedPrompt = await Effect.runPromise(
1706
+ Effect.gen(function* () {
1705
1707
  if (isInlinePrompt) {
1706
1708
  yield* validateNoArgsWithInlinePrompt(userArgs);
1707
1709
  return rawPrompt;
@@ -1718,7 +1720,7 @@ var buildSandboxHandle = (ctx, close) => {
1718
1720
  effectiveArgs,
1719
1721
  builtInArgKeysSet
1720
1722
  );
1721
- }).pipe(Effect_exports.provide(silentDisplayLayer))
1723
+ }).pipe(Effect.provide(silentDisplayLayer))
1722
1724
  );
1723
1725
  const resolvedLogging = runOptions.logging ?? {
1724
1726
  type: "file",
@@ -1742,12 +1744,12 @@ var buildSandboxHandle = (ctx, close) => {
1742
1744
  agentName: runOptions.name,
1743
1745
  branch
1744
1746
  });
1745
- return Layer_exports.provide(
1747
+ return Layer.provide(
1746
1748
  FileDisplay.layer(resolvedLogging.path),
1747
- NodeFileSystem_exports.layer
1749
+ NodeFileSystem.layer
1748
1750
  );
1749
1751
  })() : silentDisplayLayer;
1750
- const reuseFactoryLayer = Layer_exports.succeed(SandboxFactory, {
1752
+ const reuseFactoryLayer = Layer.succeed(SandboxFactory, {
1751
1753
  withSandbox: (makeEffect) => makeEffect(
1752
1754
  {
1753
1755
  hostWorktreePath: worktreePath,
@@ -1757,7 +1759,7 @@ var buildSandboxHandle = (ctx, close) => {
1757
1759
  },
1758
1760
  sandbox
1759
1761
  ).pipe(
1760
- Effect_exports.map((value) => ({
1762
+ Effect.map((value) => ({
1761
1763
  value,
1762
1764
  preservedWorktreePath: void 0
1763
1765
  }))
@@ -1766,15 +1768,15 @@ var buildSandboxHandle = (ctx, close) => {
1766
1768
  const streamEmitterLayer = agentStreamEmitterLayer(
1767
1769
  buildAgentStreamHandler(resolvedLogging)
1768
1770
  );
1769
- const runLayer = Layer_exports.mergeAll(
1771
+ const runLayer = Layer.mergeAll(
1770
1772
  reuseFactoryLayer,
1771
1773
  runDisplayLayer,
1772
1774
  streamEmitterLayer
1773
1775
  );
1774
1776
  let result3;
1775
1777
  try {
1776
- result3 = await Effect_exports.runPromise(
1777
- Effect_exports.gen(function* () {
1778
+ result3 = await Effect.runPromise(
1779
+ Effect.gen(function* () {
1778
1780
  const display = yield* Display;
1779
1781
  yield* display.intro(runOptions.name ?? CLI_NAME);
1780
1782
  const orchestrateResult = yield* orchestrate({
@@ -1806,7 +1808,7 @@ var buildSandboxHandle = (ctx, close) => {
1806
1808
  yield* display.text(line);
1807
1809
  }
1808
1810
  return orchestrateResult;
1809
- }).pipe(Effect_exports.provide(runLayer))
1811
+ }).pipe(Effect.provide(runLayer))
1810
1812
  );
1811
1813
  } catch (error) {
1812
1814
  runOptions.signal?.throwIfAborted();
@@ -1861,8 +1863,8 @@ var buildSandboxHandle = (ctx, close) => {
1861
1863
  const interactiveExecFn = providerHandle.interactiveExec.bind(providerHandle);
1862
1864
  let lifecycleResult;
1863
1865
  try {
1864
- lifecycleResult = await Effect_exports.runPromise(
1865
- Effect_exports.gen(function* () {
1866
+ lifecycleResult = await Effect.runPromise(
1867
+ Effect.gen(function* () {
1866
1868
  const resolved = yield* resolvePrompt({ prompt, promptFile });
1867
1869
  const rawPrompt = resolved.text;
1868
1870
  const isInlinePrompt = resolved.source === "inline";
@@ -1899,7 +1901,7 @@ var buildSandboxHandle = (ctx, close) => {
1899
1901
  keepSourceBranch: mergeToHead
1900
1902
  },
1901
1903
  sandbox,
1902
- (ctx2) => Effect_exports.gen(function* () {
1904
+ (ctx2) => Effect.gen(function* () {
1903
1905
  const fullPrompt = isInlinePrompt ? resolvedPrompt : yield* preprocessPrompt(
1904
1906
  resolvedPrompt,
1905
1907
  ctx2.sandbox,
@@ -1913,10 +1915,11 @@ var buildSandboxHandle = (ctx, close) => {
1913
1915
  stdin: process.stdin,
1914
1916
  stdout: process.stdout,
1915
1917
  stderr: process.stderr,
1916
- cwd: sandboxRepoDir
1918
+ cwd: sandboxRepoDir,
1919
+ signal: interactiveOptions.signal
1917
1920
  });
1918
1921
  const signal = interactiveOptions.signal;
1919
- const result3 = yield* Effect_exports.promise(() => {
1922
+ const result3 = yield* Effect.promise(() => {
1920
1923
  if (!signal) return execPromise;
1921
1924
  if (signal.aborted) return Promise.reject(signal.reason);
1922
1925
  return new Promise(
@@ -1942,8 +1945,8 @@ var buildSandboxHandle = (ctx, close) => {
1942
1945
  })
1943
1946
  );
1944
1947
  }).pipe(
1945
- Effect_exports.provide(ClackDisplay.layer),
1946
- Effect_exports.provide(NodeContext_exports.layer)
1948
+ Effect.provide(ClackDisplay.layer),
1949
+ Effect.provide(NodeContext.layer)
1947
1950
  )
1948
1951
  );
1949
1952
  } catch (error) {
@@ -1960,7 +1963,7 @@ var buildSandboxHandle = (ctx, close) => {
1960
1963
  if (providerHandle) {
1961
1964
  return providerHandle.exec(command, mergedOptions);
1962
1965
  }
1963
- return Effect_exports.runPromise(sandbox.exec(command, mergedOptions));
1966
+ return Effect.runPromise(sandbox.exec(command, mergedOptions));
1964
1967
  },
1965
1968
  close: async () => close(),
1966
1969
  [Symbol.asyncDispose]: async () => {
@@ -1973,7 +1976,7 @@ var createSandboxFromWorktree = async (options) => {
1973
1976
  const { branch, worktreePath, hostRepoDir } = options;
1974
1977
  const isTestMode = !!options._test?.buildSandbox;
1975
1978
  if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
1976
- await Effect_exports.runPromise(
1979
+ await Effect.runPromise(
1977
1980
  copyToWorktree(
1978
1981
  options.copyToWorktree,
1979
1982
  hostRepoDir,
@@ -1991,8 +1994,8 @@ var createSandboxFromWorktree = async (options) => {
1991
1994
  sandboxRepoDir = worktreePath;
1992
1995
  providerHandle = options._test.bindMountHandle;
1993
1996
  } else {
1994
- const resolvedEnv = await Effect_exports.runPromise(
1995
- resolveEnv(hostRepoDir).pipe(Effect_exports.provide(NodeContext_exports.layer))
1997
+ const resolvedEnv = await Effect.runPromise(
1998
+ resolveEnv(hostRepoDir).pipe(Effect.provide(NodeContext.layer))
1996
1999
  );
1997
2000
  const env = mergeProviderEnv({
1998
2001
  resolvedEnv,
@@ -2018,13 +2021,13 @@ var createSandboxFromWorktree = async (options) => {
2018
2021
  });
2019
2022
  } else {
2020
2023
  startEffect = resolveGitMounts(join(hostRepoDir, ".git")).pipe(
2021
- Effect_exports.provide(NodeFileSystem_exports.layer),
2022
- Effect_exports.catchAll(() => Effect_exports.succeed([])),
2024
+ Effect.provide(NodeFileSystem.layer),
2025
+ Effect.catchAll(() => Effect.succeed([])),
2023
2026
  // Patch git mounts for Windows worktree compatibility (ADR-0006)
2024
- Effect_exports.flatMap(
2027
+ Effect.flatMap(
2025
2028
  (gitMounts) => patchGitMountsForWindows(gitMounts, worktreePath, SANDBOX_REPO_DIR)
2026
2029
  ),
2027
- Effect_exports.flatMap(
2030
+ Effect.flatMap(
2028
2031
  (gitMounts) => startSandbox({
2029
2032
  provider,
2030
2033
  hostRepoDir,
@@ -2036,7 +2039,7 @@ var createSandboxFromWorktree = async (options) => {
2036
2039
  )
2037
2040
  );
2038
2041
  }
2039
- const startResult = await Effect_exports.runPromise(startEffect);
2042
+ const startResult = await Effect.runPromise(startEffect);
2040
2043
  providerHandle = startResult.handle;
2041
2044
  sandbox = startResult.sandbox;
2042
2045
  sandboxRepoDir = startResult.worktreePath;
@@ -2044,8 +2047,8 @@ var createSandboxFromWorktree = async (options) => {
2044
2047
  const sandboxOnReady = options.hooks?.sandbox?.onSandboxReady;
2045
2048
  const hostOnReady = options.hooks?.host?.onSandboxReady;
2046
2049
  if (sandboxOnReady?.length || hostOnReady?.length) {
2047
- await Effect_exports.runPromise(
2048
- Effect_exports.gen(function* () {
2050
+ await Effect.runPromise(
2051
+ Effect.gen(function* () {
2049
2052
  yield* sandbox.exec(
2050
2053
  `git config --global --add safe.directory ${shellQuote(sandboxRepoDir)}`
2051
2054
  );
@@ -2059,13 +2062,13 @@ var createSandboxFromWorktree = async (options) => {
2059
2062
  if (hostOnReady?.length) {
2060
2063
  allEffects.push(runHostHooks(hostOnReady, worktreePath));
2061
2064
  }
2062
- yield* Effect_exports.all(allEffects, {
2065
+ yield* Effect.all(allEffects, {
2063
2066
  concurrency: "unbounded"
2064
2067
  });
2065
2068
  })
2066
2069
  );
2067
2070
  }
2068
- const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect_exports.void;
2071
+ const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect.void;
2069
2072
  let closed = false;
2070
2073
  const bindMountHandle = toSessionTransferHandle(providerHandle);
2071
2074
  return buildSandboxHandle(
@@ -2094,17 +2097,17 @@ var createSandbox = async (options) => {
2094
2097
  const { branch } = options;
2095
2098
  const isTestMode = !!options._test?.buildSandbox;
2096
2099
  const isIsolated = options.sandbox.tag === "isolated";
2097
- const { hostRepoDir, worktreePath, providerHandle, sandbox, sandboxRepoDir } = await Effect_exports.runPromise(
2098
- Effect_exports.gen(function* () {
2100
+ const { hostRepoDir, worktreePath, providerHandle, sandbox, sandboxRepoDir } = await Effect.runPromise(
2101
+ Effect.gen(function* () {
2099
2102
  const hostRepoDir2 = yield* resolveCwd(options.cwd);
2100
2103
  yield* pruneStale(hostRepoDir2).pipe(
2101
- Effect_exports.catchAll(() => Effect_exports.void)
2104
+ Effect.catchAll(() => Effect.void)
2102
2105
  );
2103
2106
  const { path: worktreePath2 } = yield* create(
2104
2107
  hostRepoDir2,
2105
2108
  { branch, baseBranch: options.baseBranch }
2106
2109
  );
2107
- const prepared = yield* Effect_exports.gen(function* () {
2110
+ const prepared = yield* Effect.gen(function* () {
2108
2111
  if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
2109
2112
  yield* copyToWorktree(
2110
2113
  options.copyToWorktree,
@@ -2146,17 +2149,17 @@ var createSandbox = async (options) => {
2146
2149
  env,
2147
2150
  worktreeOrRepoPath: worktreePath2
2148
2151
  }) : resolveGitMounts(join(hostRepoDir2, ".git")).pipe(
2149
- Effect_exports.provide(NodeFileSystem_exports.layer),
2150
- Effect_exports.catchAll(() => Effect_exports.succeed([])),
2152
+ Effect.provide(NodeFileSystem.layer),
2153
+ Effect.catchAll(() => Effect.succeed([])),
2151
2154
  // Patch git mounts for Windows worktree compatibility (ADR-0006)
2152
- Effect_exports.flatMap(
2155
+ Effect.flatMap(
2153
2156
  (gitMounts) => patchGitMountsForWindows(
2154
2157
  gitMounts,
2155
2158
  worktreePath2,
2156
2159
  SANDBOX_REPO_DIR
2157
2160
  )
2158
2161
  ),
2159
- Effect_exports.flatMap(
2162
+ Effect.flatMap(
2160
2163
  (gitMounts) => startSandbox({
2161
2164
  provider,
2162
2165
  hostRepoDir: hostRepoDir2,
@@ -2174,7 +2177,7 @@ var createSandbox = async (options) => {
2174
2177
  const sandboxOnReady = options.hooks?.sandbox?.onSandboxReady;
2175
2178
  const hostOnReady = options.hooks?.host?.onSandboxReady;
2176
2179
  if (sandboxOnReady?.length || hostOnReady?.length) {
2177
- yield* Effect_exports.gen(function* () {
2180
+ yield* Effect.gen(function* () {
2178
2181
  yield* sandbox2.exec(
2179
2182
  `git config --global --add safe.directory ${shellQuote(sandboxRepoDir2)}`
2180
2183
  );
@@ -2188,28 +2191,28 @@ var createSandbox = async (options) => {
2188
2191
  if (hostOnReady?.length) {
2189
2192
  allEffects.push(runHostHooks(hostOnReady, worktreePath2));
2190
2193
  }
2191
- yield* Effect_exports.all(allEffects, { concurrency: "unbounded" });
2194
+ yield* Effect.all(allEffects, { concurrency: "unbounded" });
2192
2195
  }).pipe(
2193
- Effect_exports.onError(
2194
- () => providerHandle2 ? Effect_exports.promise(
2196
+ Effect.onError(
2197
+ () => providerHandle2 ? Effect.promise(
2195
2198
  () => providerHandle2.close().catch(() => {
2196
2199
  })
2197
- ) : Effect_exports.void
2200
+ ) : Effect.void
2198
2201
  )
2199
2202
  );
2200
2203
  }
2201
2204
  return { providerHandle: providerHandle2, sandbox: sandbox2, sandboxRepoDir: sandboxRepoDir2 };
2202
2205
  }).pipe(
2203
- Effect_exports.onError(
2206
+ Effect.onError(
2204
2207
  () => remove(worktreePath2).pipe(
2205
- Effect_exports.catchAll(() => Effect_exports.void)
2208
+ Effect.catchAll(() => Effect.void)
2206
2209
  )
2207
2210
  )
2208
2211
  );
2209
2212
  return { hostRepoDir: hostRepoDir2, worktreePath: worktreePath2, ...prepared };
2210
- }).pipe(Effect_exports.provide(NodeContext_exports.layer))
2213
+ }).pipe(Effect.provide(NodeContext.layer))
2211
2214
  );
2212
- const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect_exports.void;
2215
+ const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect.void;
2213
2216
  let closed = false;
2214
2217
  const forceCleanup = () => {
2215
2218
  console.error(`
@@ -2221,19 +2224,19 @@ Worktree preserved at ${worktreePath}`);
2221
2224
  const doClose = async () => {
2222
2225
  if (closed) return { preservedWorktreePath: void 0 };
2223
2226
  closed = true;
2224
- return Effect_exports.runPromise(
2225
- Effect_exports.gen(function* () {
2227
+ return Effect.runPromise(
2228
+ Effect.gen(function* () {
2226
2229
  if (providerHandle) {
2227
- yield* Effect_exports.promise(() => providerHandle.close());
2230
+ yield* Effect.promise(() => providerHandle.close());
2228
2231
  }
2229
2232
  const isDirty = yield* hasUncommittedChanges(
2230
2233
  worktreePath
2231
- ).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
2234
+ ).pipe(Effect.catchAll(() => Effect.succeed(false)));
2232
2235
  if (isDirty) {
2233
2236
  return { preservedWorktreePath: worktreePath };
2234
2237
  }
2235
2238
  yield* remove(worktreePath).pipe(
2236
- Effect_exports.catchAll(() => Effect_exports.void)
2239
+ Effect.catchAll(() => Effect.void)
2237
2240
  );
2238
2241
  return { preservedWorktreePath: void 0 };
2239
2242
  })
@@ -2263,10 +2266,10 @@ var createWorktree = async (options) => {
2263
2266
  const branch = options.branchStrategy.type === "branch" ? options.branchStrategy.branch : void 0;
2264
2267
  const baseBranch = options.branchStrategy.type === "branch" ? options.branchStrategy.baseBranch : void 0;
2265
2268
  const isMergeToHead = options.branchStrategy.type === "merge-to-head";
2266
- const { hostRepoDir, worktreeInfo } = await Effect_exports.gen(function* () {
2269
+ const { hostRepoDir, worktreeInfo } = await Effect.gen(function* () {
2267
2270
  const hostRepoDir2 = yield* resolveCwd(options.cwd);
2268
2271
  yield* pruneStale(hostRepoDir2).pipe(
2269
- Effect_exports.catchAll(() => Effect_exports.void)
2272
+ Effect.catchAll(() => Effect.void)
2270
2273
  );
2271
2274
  const info = yield* create(hostRepoDir2, {
2272
2275
  branch,
@@ -2284,23 +2287,23 @@ var createWorktree = async (options) => {
2284
2287
  yield* runHostHooks(options.hooks.host.onWorktreeReady, info.path);
2285
2288
  }
2286
2289
  return { hostRepoDir: hostRepoDir2, worktreeInfo: info };
2287
- }).pipe(Effect_exports.provide(NodeContext_exports.layer), Effect_exports.runPromise);
2290
+ }).pipe(Effect.provide(NodeContext.layer), Effect.runPromise);
2288
2291
  let closed = false;
2289
2292
  const close = async () => {
2290
2293
  if (closed) return { preservedWorktreePath: void 0 };
2291
2294
  closed = true;
2292
- return Effect_exports.gen(function* () {
2295
+ return Effect.gen(function* () {
2293
2296
  const isDirty = yield* hasUncommittedChanges(
2294
2297
  worktreeInfo.path
2295
- ).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(false)));
2298
+ ).pipe(Effect.catchAll(() => Effect.succeed(false)));
2296
2299
  if (isDirty) {
2297
2300
  return { preservedWorktreePath: worktreeInfo.path };
2298
2301
  }
2299
2302
  yield* remove(worktreeInfo.path).pipe(
2300
- Effect_exports.catchAll(() => Effect_exports.void)
2303
+ Effect.catchAll(() => Effect.void)
2301
2304
  );
2302
2305
  return { preservedWorktreePath: void 0 };
2303
- }).pipe(Effect_exports.runPromise);
2306
+ }).pipe(Effect.runPromise);
2304
2307
  };
2305
2308
  const worktreeInteractive = async (opts) => {
2306
2309
  opts.signal?.throwIfAborted();
@@ -2311,7 +2314,7 @@ var createWorktree = async (options) => {
2311
2314
  `Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
2312
2315
  );
2313
2316
  }
2314
- const inner = Effect_exports.gen(function* () {
2317
+ const inner = Effect.gen(function* () {
2315
2318
  const d = yield* Display;
2316
2319
  const hasPromptSource = prompt !== void 0 || promptFile !== void 0;
2317
2320
  const resolved = hasPromptSource ? yield* resolvePrompt({ prompt, promptFile }) : void 0;
@@ -2350,7 +2353,7 @@ var createWorktree = async (options) => {
2350
2353
  });
2351
2354
  let handle;
2352
2355
  if (resolvedSandbox.tag === "none") {
2353
- handle = yield* Effect_exports.promise(
2356
+ handle = yield* Effect.promise(
2354
2357
  () => resolvedSandbox.create({
2355
2358
  worktreePath: worktreeInfo.path,
2356
2359
  env: effectiveEnv
@@ -2388,7 +2391,7 @@ var createWorktree = async (options) => {
2388
2391
  );
2389
2392
  handle = startResult.handle;
2390
2393
  }
2391
- return yield* Effect_exports.gen(function* () {
2394
+ return yield* Effect.gen(function* () {
2392
2395
  if (!handle.interactiveExec) {
2393
2396
  throw new Error(
2394
2397
  `Sandbox provider does not support interactiveExec. The provider must implement the optional interactiveExec method to use interactive().`
@@ -2397,7 +2400,7 @@ var createWorktree = async (options) => {
2397
2400
  const interactiveExecFn = handle.interactiveExec.bind(handle);
2398
2401
  const sandbox = makeSandboxFromHandle(handle);
2399
2402
  const worktreePath = handle.worktreePath;
2400
- const applyToHost = resolvedSandbox.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
2403
+ const applyToHost = resolvedSandbox.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect.void;
2401
2404
  const lifecycleEffect = withSandboxLifecycle(
2402
2405
  {
2403
2406
  hostRepoDir,
@@ -2413,7 +2416,7 @@ var createWorktree = async (options) => {
2413
2416
  keepSourceBranch: isMergeToHead
2414
2417
  },
2415
2418
  sandbox,
2416
- (ctx) => Effect_exports.gen(function* () {
2419
+ (ctx) => Effect.gen(function* () {
2417
2420
  const fullPrompt = !hasPromptSource || isInlinePrompt ? substitutedPrompt : yield* preprocessPrompt(
2418
2421
  substitutedPrompt,
2419
2422
  ctx.sandbox,
@@ -2424,12 +2427,13 @@ var createWorktree = async (options) => {
2424
2427
  dangerouslySkipPermissions: resolvedSandbox.tag !== "none"
2425
2428
  });
2426
2429
  const result3 = yield* raceAbortSignal(
2427
- Effect_exports.promise(
2430
+ Effect.promise(
2428
2431
  () => interactiveExecFn(interactiveArgs, {
2429
2432
  stdin: process.stdin,
2430
2433
  stdout: process.stdout,
2431
2434
  stderr: process.stderr,
2432
- cwd: worktreePath
2435
+ cwd: worktreePath,
2436
+ signal: opts.signal
2433
2437
  })
2434
2438
  ),
2435
2439
  opts.signal
@@ -2452,16 +2456,16 @@ var createWorktree = async (options) => {
2452
2456
  };
2453
2457
  }).pipe(
2454
2458
  // Always close sandbox handle
2455
- Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
2459
+ Effect.ensuring(Effect.promise(() => handle.close().catch(() => {
2456
2460
  })))
2457
2461
  );
2458
2462
  });
2459
2463
  try {
2460
- return await Effect_exports.runPromise(
2464
+ return await Effect.runPromise(
2461
2465
  inner.pipe(
2462
- Effect_exports.provide(ClackDisplay.layer),
2463
- Effect_exports.provide(NodeContext_exports.layer),
2464
- Effect_exports.provide(NodeFileSystem_exports.layer)
2466
+ Effect.provide(ClackDisplay.layer),
2467
+ Effect.provide(NodeContext.layer),
2468
+ Effect.provide(NodeFileSystem.layer)
2465
2469
  )
2466
2470
  );
2467
2471
  } catch (error) {
@@ -2474,6 +2478,7 @@ var createWorktree = async (options) => {
2474
2478
  const { prompt, promptFile, hooks, agent: provider } = opts;
2475
2479
  const sandboxProvider = opts.sandbox;
2476
2480
  const maxIterations = opts.maxIterations ?? 1;
2481
+ validateMaxIterations(maxIterations);
2477
2482
  if (opts.resumeSession && maxIterations > 1) {
2478
2483
  throw new Error(
2479
2484
  "resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
@@ -2487,7 +2492,7 @@ var createWorktree = async (options) => {
2487
2492
  resumeSession: opts.resumeSession
2488
2493
  });
2489
2494
  }
2490
- const inner = Effect_exports.gen(function* () {
2495
+ const inner = Effect.gen(function* () {
2491
2496
  const resolved = yield* resolvePrompt({ prompt, promptFile });
2492
2497
  const rawPrompt = resolved.text;
2493
2498
  const isInlinePrompt = resolved.source === "inline";
@@ -2557,7 +2562,7 @@ var createWorktree = async (options) => {
2557
2562
  sandboxRepoDir = startResult.worktreePath;
2558
2563
  }
2559
2564
  const sandbox = makeSandboxFromHandle(handle);
2560
- const applyToHost = sandboxProvider.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect_exports.void;
2565
+ const applyToHost = sandboxProvider.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect.void;
2561
2566
  const resolvedLogging = opts.logging ?? {
2562
2567
  type: "file",
2563
2568
  path: join(
@@ -2568,7 +2573,7 @@ var createWorktree = async (options) => {
2568
2573
  )
2569
2574
  };
2570
2575
  if (opts.logging === void 0 && resolvedLogging.type === "file") {
2571
- yield* Effect_exports.tryPromise({
2576
+ yield* Effect.tryPromise({
2572
2577
  try: () => assertNoSymlinkComponents(
2573
2578
  hostRepoDir,
2574
2579
  resolvedLogging.path,
@@ -2585,13 +2590,13 @@ var createWorktree = async (options) => {
2585
2590
  agentName: opts.name,
2586
2591
  branch: worktreeInfo.branch
2587
2592
  });
2588
- return Layer_exports.provide(
2593
+ return Layer.provide(
2589
2594
  FileDisplay.layer(resolvedLogging.path),
2590
- NodeFileSystem_exports.layer
2595
+ NodeFileSystem.layer
2591
2596
  );
2592
2597
  })() : ClackDisplay.layer;
2593
2598
  const bindMountHandle = toSessionTransferHandle(handle);
2594
- const reuseFactoryLayer = Layer_exports.succeed(SandboxFactory, {
2599
+ const reuseFactoryLayer = Layer.succeed(SandboxFactory, {
2595
2600
  withSandbox: (makeEffect) => makeEffect(
2596
2601
  {
2597
2602
  hostWorktreePath: worktreeInfo.path,
@@ -2601,7 +2606,7 @@ var createWorktree = async (options) => {
2601
2606
  },
2602
2607
  sandbox
2603
2608
  ).pipe(
2604
- Effect_exports.map((value) => ({
2609
+ Effect.map((value) => ({
2605
2610
  value,
2606
2611
  preservedWorktreePath: void 0
2607
2612
  }))
@@ -2610,12 +2615,12 @@ var createWorktree = async (options) => {
2610
2615
  const streamEmitterLayer = agentStreamEmitterLayer(
2611
2616
  buildAgentStreamHandler(resolvedLogging)
2612
2617
  );
2613
- const runLayer = Layer_exports.mergeAll(
2618
+ const runLayer = Layer.mergeAll(
2614
2619
  reuseFactoryLayer,
2615
2620
  runDisplayLayer,
2616
2621
  streamEmitterLayer
2617
2622
  );
2618
- const result3 = yield* Effect_exports.gen(function* () {
2623
+ const result3 = yield* Effect.gen(function* () {
2619
2624
  const display = yield* Display;
2620
2625
  yield* display.intro(opts.name ?? CLI_NAME);
2621
2626
  const orchestrateResult = yield* orchestrate({
@@ -2650,9 +2655,9 @@ var createWorktree = async (options) => {
2650
2655
  }
2651
2656
  return orchestrateResult;
2652
2657
  }).pipe(
2653
- Effect_exports.provide(runLayer),
2658
+ Effect.provide(runLayer),
2654
2659
  // Always close sandbox handle
2655
- Effect_exports.ensuring(Effect_exports.promise(() => handle.close().catch(() => {
2660
+ Effect.ensuring(Effect.promise(() => handle.close().catch(() => {
2656
2661
  })))
2657
2662
  );
2658
2663
  return {
@@ -2665,11 +2670,11 @@ var createWorktree = async (options) => {
2665
2670
  };
2666
2671
  });
2667
2672
  try {
2668
- return await Effect_exports.runPromise(
2673
+ return await Effect.runPromise(
2669
2674
  inner.pipe(
2670
- Effect_exports.provide(ClackDisplay.layer),
2671
- Effect_exports.provide(NodeContext_exports.layer),
2672
- Effect_exports.provide(NodeFileSystem_exports.layer)
2675
+ Effect.provide(ClackDisplay.layer),
2676
+ Effect.provide(NodeContext.layer),
2677
+ Effect.provide(NodeFileSystem.layer)
2673
2678
  )
2674
2679
  );
2675
2680
  } catch (error) {
@@ -3648,7 +3653,21 @@ var allowedTransitions = {
3648
3653
  blocked: [],
3649
3654
  cancelled: []
3650
3655
  };
3651
- var allRequiredChecksPassed = (checks) => checks !== void 0 && checks.length > 0 && checks.every((check) => check.status === "passed");
3656
+ var allRequiredChecksPassed = (context) => {
3657
+ const checks = context.checks;
3658
+ if (checks === void 0) return false;
3659
+ if (context.requiredCheckNames === void 0) {
3660
+ return checks.length > 0 && checks.every((check) => check.status === "passed");
3661
+ }
3662
+ if (context.requiredCheckNames.length > 0 && context.checkCandidate === void 0) {
3663
+ return false;
3664
+ }
3665
+ return context.requiredCheckNames.every(
3666
+ (name) => checks.some(
3667
+ (check) => check.name === name && check.status === "passed" && (context.checkCandidate === void 0 || check.baseSha === context.checkCandidate.baseSha && check.headSha === context.checkCandidate.headSha && check.briefHash === context.checkCandidate.briefHash)
3668
+ )
3669
+ );
3670
+ };
3652
3671
  var requireTransition = (from, to, context) => {
3653
3672
  if (!allowedTransitions[from].includes(to)) {
3654
3673
  throw new ContractValidationError(
@@ -3667,20 +3686,20 @@ var requireTransition = (from, to, context) => {
3667
3686
  );
3668
3687
  }
3669
3688
  }
3670
- if (to === "reviewing" && !allRequiredChecksPassed(context.checks)) {
3689
+ if (to === "reviewing" && !allRequiredChecksPassed(context)) {
3671
3690
  const unknown = context.checks?.find((check) => check.status === "unknown");
3672
3691
  throw new ContractValidationError(
3673
3692
  unknown ? `Cannot review while check ${unknown.name} is unknown` : "Cannot review until all required checks pass"
3674
3693
  );
3675
3694
  }
3676
3695
  if (to === "human-review") {
3677
- if (!allRequiredChecksPassed(context.checks)) {
3696
+ if (!allRequiredChecksPassed(context)) {
3678
3697
  throw new ContractValidationError(
3679
3698
  "Human review requires a complete set of passing checks"
3680
3699
  );
3681
3700
  }
3682
3701
  if (context.review?.outcome !== "passed" || context.review.axes.length === 0 || context.review.findings.some(
3683
- (finding) => finding.disposition === "open" || finding.severity !== "info" && finding.disposition === "deferred"
3702
+ (finding) => finding.severity !== "info" && (finding.disposition === "open" || finding.disposition === "deferred")
3684
3703
  )) {
3685
3704
  throw new ContractValidationError(
3686
3705
  "Human review requires every review axis to pass and blocking findings to be disposed"
@@ -3715,6 +3734,11 @@ var createAssignment = (input) => {
3715
3734
  "Assignment policy repository does not match work identity"
3716
3735
  );
3717
3736
  }
3737
+ if (["checking", "review", "handoff", "merge"].includes(input.phase) && input.head === void 0) {
3738
+ throw new ContractValidationError(
3739
+ `${input.phase} assignments require an immutable candidate head`
3740
+ );
3741
+ }
3718
3742
  const assignment = {
3719
3743
  contractVersion: WORKFLOW_CONTRACT_VERSION,
3720
3744
  id: nonEmptyString(input.id, "assignment.id"),
@@ -3776,6 +3800,7 @@ var parsePhaseResult = (value) => {
3776
3800
  ),
3777
3801
  outcome,
3778
3802
  identity: parseIdentity(value.identity, "phase result.identity"),
3803
+ briefHash: nonEmptyString(value.briefHash, "phase result.briefHash"),
3779
3804
  base: value.base === void 0 ? void 0 : parseRevision(value.base, "phase result.base"),
3780
3805
  head: value.head === void 0 ? void 0 : parseRevision(value.head, "phase result.head"),
3781
3806
  summary: nonEmptyString(value.summary, "phase result.summary"),
@@ -3870,6 +3895,8 @@ var MemoryTransaction = class {
3870
3895
  async saveEvent(event) {
3871
3896
  this.state.events.set(event.deliveryId, clone(event));
3872
3897
  }
3898
+ async lockWorkIdentity(_identity) {
3899
+ }
3873
3900
  async getJob(jobId) {
3874
3901
  const job = this.state.jobs.get(jobId);
3875
3902
  return job === void 0 ? void 0 : clone(job);
@@ -3899,9 +3926,13 @@ var MemoryTransaction = class {
3899
3926
  this.state.jobs.set(job.id, clone(job));
3900
3927
  }
3901
3928
  async saveJob(job) {
3902
- if (!this.state.jobs.has(job.id)) {
3929
+ const current = this.state.jobs.get(job.id);
3930
+ if (current === void 0) {
3903
3931
  throw new Error(`Workflow job ${job.id} does not exist`);
3904
3932
  }
3933
+ if (current.version !== job.version - 1) {
3934
+ throw new Error(`Workflow job ${job.id} was updated concurrently`);
3935
+ }
3905
3936
  this.state.jobs.set(job.id, clone(job));
3906
3937
  }
3907
3938
  async findDispatchByDedupeKey(dedupeKey) {
@@ -3961,6 +3992,8 @@ var MemoryTransaction = class {
3961
3992
  const lease = this.state.leases.get(resourceKey(repository, branch));
3962
3993
  return lease === void 0 ? void 0 : clone(lease);
3963
3994
  }
3995
+ async lockLeaseResource(_repository, _branch) {
3996
+ }
3964
3997
  async saveLease(lease) {
3965
3998
  this.state.leases.set(lease.resourceKey, clone(lease));
3966
3999
  }
@@ -4382,9 +4415,15 @@ var PostgresTransaction = class {
4382
4415
  ]
4383
4416
  );
4384
4417
  }
4418
+ async lockWorkIdentity(identity) {
4419
+ await this.client.query(
4420
+ "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))",
4421
+ [`${identity.repository}\0${identity.itemId}\0${identity.kind}`]
4422
+ );
4423
+ }
4385
4424
  async getJob(jobId) {
4386
4425
  const result3 = await this.client.query(
4387
- "SELECT * FROM shipyard_jobs WHERE id = $1",
4426
+ "SELECT * FROM shipyard_jobs WHERE id = $1 FOR UPDATE",
4388
4427
  [jobId]
4389
4428
  );
4390
4429
  const value = optionalRow(result3);
@@ -4396,7 +4435,7 @@ var PostgresTransaction = class {
4396
4435
  WHERE repository = $1 AND item_id = $2 AND brief_revision = $3
4397
4436
  AND phase = $4 AND relevant_revision = $5
4398
4437
  AND brief->>'hash' = $6
4399
- ORDER BY updated_at DESC LIMIT 1`,
4438
+ ORDER BY updated_at DESC LIMIT 1 FOR UPDATE`,
4400
4439
  [
4401
4440
  key.repository,
4402
4441
  key.itemId,
@@ -4414,7 +4453,7 @@ var PostgresTransaction = class {
4414
4453
  `SELECT * FROM shipyard_jobs
4415
4454
  WHERE repository = $1 AND item_id = $2 AND item_kind = $3
4416
4455
  AND control <> 'superseded'
4417
- ORDER BY latest_observed_at DESC, updated_at DESC LIMIT 1`,
4456
+ ORDER BY latest_observed_at DESC, updated_at DESC LIMIT 1 FOR UPDATE`,
4418
4457
  [identity.repository, identity.itemId, identity.kind]
4419
4458
  );
4420
4459
  const value = optionalRow(result3);
@@ -4435,7 +4474,7 @@ var PostgresTransaction = class {
4435
4474
  );
4436
4475
  }
4437
4476
  async saveJob(job) {
4438
- await this.client.query(
4477
+ const result3 = await this.client.query(
4439
4478
  `UPDATE shipyard_jobs SET
4440
4479
  repository = $2, item_id = $3, item_kind = $4, brief_revision = $5,
4441
4480
  phase = $6, relevant_revision = $7, brief = $8::jsonb,
@@ -4445,9 +4484,13 @@ var PostgresTransaction = class {
4445
4484
  assignments = $17::jsonb, phase_results = $18::jsonb,
4446
4485
  active_assignment_id = $19, latest_observed_at = $20,
4447
4486
  created_at = $21, updated_at = $22, version = $23
4448
- WHERE id = $1`,
4449
- jobValues(job)
4487
+ WHERE id = $1 AND version = $24
4488
+ RETURNING id`,
4489
+ [...jobValues(job), job.version - 1]
4450
4490
  );
4491
+ if (result3.rows.length !== 1) {
4492
+ throw new Error(`Workflow job ${job.id} was updated concurrently`);
4493
+ }
4451
4494
  }
4452
4495
  async findDispatchByDedupeKey(dedupeKey) {
4453
4496
  const result3 = await this.client.query(
@@ -4568,6 +4611,12 @@ var PostgresTransaction = class {
4568
4611
  const value = optionalRow(result3);
4569
4612
  return value === void 0 ? void 0 : leaseFromRow(value);
4570
4613
  }
4614
+ async lockLeaseResource(repository, branch) {
4615
+ await this.client.query(
4616
+ "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))",
4617
+ [`${repository}\0${branch}`]
4618
+ );
4619
+ }
4571
4620
  async saveLease(lease) {
4572
4621
  await this.client.query(
4573
4622
  `INSERT INTO shipyard_branch_leases (
@@ -4773,7 +4822,9 @@ var resultState = (result3) => {
4773
4822
  case "checking":
4774
4823
  return "reviewing";
4775
4824
  case "review":
4776
- return "human-review";
4825
+ return result3.findings.some(
4826
+ (finding) => finding.severity !== "info" && (finding.disposition === "open" || finding.disposition === "deferred")
4827
+ ) ? "repairing" : "human-review";
4777
4828
  case "repair":
4778
4829
  return "checking";
4779
4830
  case "handoff":
@@ -4787,6 +4838,29 @@ var resultState = (result3) => {
4787
4838
  }
4788
4839
  }
4789
4840
  };
4841
+ var normalizeWorkerReviewFindings = (findings) => {
4842
+ const byId = /* @__PURE__ */ new Map();
4843
+ const byContent = /* @__PURE__ */ new Map();
4844
+ for (const finding of findings) {
4845
+ if (finding.disposition !== "open") {
4846
+ throw new Error("Review workers may only submit open findings");
4847
+ }
4848
+ const contentKey = JSON.stringify([
4849
+ finding.axis,
4850
+ finding.title,
4851
+ finding.location ?? "",
4852
+ finding.requirement ?? "",
4853
+ finding.evidence
4854
+ ]);
4855
+ const previous = byId.get(finding.id);
4856
+ if (previous !== void 0 && previous !== contentKey) {
4857
+ throw new Error(`Finding id ${finding.id} identifies different findings`);
4858
+ }
4859
+ byId.set(finding.id, contentKey);
4860
+ if (!byContent.has(contentKey)) byContent.set(contentKey, finding);
4861
+ }
4862
+ return [...byContent.values()];
4863
+ };
4790
4864
  var phaseAttempts = () => ({
4791
4865
  triage: 0,
4792
4866
  implementation: 0,
@@ -4876,6 +4950,7 @@ var WorkflowCoordinator = class {
4876
4950
  job: job2
4877
4951
  };
4878
4952
  }
4953
+ await transaction.lockWorkIdentity(brief.identity);
4879
4954
  const current = await transaction.findCurrentJob(brief.identity);
4880
4955
  if (input.sourceState === "closed") {
4881
4956
  const cancelled = current ? await this.cancelStoredJob(
@@ -5072,7 +5147,7 @@ var WorkflowCoordinator = class {
5072
5147
  brief: job.brief,
5073
5148
  policy: job.policy,
5074
5149
  attempt,
5075
- head: phase2 === "review" ? this.latestHeadForRevision(job, job.key.relevantRevision) : void 0,
5150
+ head: ["checking", "review", "handoff", "merge"].includes(phase2) ? this.latestHeadForRevision(job, job.key.relevantRevision) : void 0,
5076
5151
  createdAt: this.clock.now()
5077
5152
  });
5078
5153
  const attempts = { ...job.phaseAttempts, [phase2]: attempt };
@@ -5158,6 +5233,7 @@ var WorkflowCoordinator = class {
5158
5233
  }
5159
5234
  const now = this.clock.nowMilliseconds();
5160
5235
  const resourceKey2 = `${input.repository}\0${input.branch}`;
5236
+ await transaction.lockLeaseResource(input.repository, input.branch);
5161
5237
  const existing = await transaction.getLease(
5162
5238
  input.repository,
5163
5239
  input.branch
@@ -5399,7 +5475,11 @@ var WorkflowCoordinator = class {
5399
5475
  });
5400
5476
  }
5401
5477
  async recordPhaseResult(input) {
5402
- const result3 = parsePhaseResult(input.result);
5478
+ const parsedResult = parsePhaseResult(input.result);
5479
+ const result3 = parsedResult.phase === "review" ? {
5480
+ ...parsedResult,
5481
+ findings: normalizeWorkerReviewFindings(parsedResult.findings)
5482
+ } : parsedResult;
5403
5483
  return this.storage.transaction(async (transaction) => {
5404
5484
  let job = await transaction.getJob(input.jobId);
5405
5485
  if (job === void 0)
@@ -5417,14 +5497,14 @@ var WorkflowCoordinator = class {
5417
5497
  `Phase result ${result3.assignmentId} is not assigned to job ${job.id}`
5418
5498
  );
5419
5499
  }
5420
- if (assignment.phase !== result3.phase || !sameIdentity3(assignment.identity, result3.identity)) {
5500
+ if (assignment.phase !== result3.phase || !sameIdentity3(assignment.identity, result3.identity) || assignment.briefHash !== result3.briefHash) {
5421
5501
  throw new Error("Phase result does not match its assignment");
5422
5502
  }
5423
5503
  if (result3.base !== void 0 && !sameRevision(result3.base, assignment.base)) {
5424
5504
  throw new Error("Phase result base revision does not match assignment");
5425
5505
  }
5426
- if (assignment.phase === "review" && assignment.head !== void 0 && (result3.head === void 0 || !sameRevision(result3.head, assignment.head))) {
5427
- throw new Error("Review result is not bound to the assigned candidate");
5506
+ if (["checking", "review", "handoff", "merge"].includes(assignment.phase) && (result3.base === void 0 || !sameRevision(result3.base, assignment.base) || assignment.head === void 0 || result3.head === void 0 || !sameRevision(result3.head, assignment.head))) {
5507
+ throw new Error("Phase result is not bound to the assigned candidate");
5428
5508
  }
5429
5509
  const dispatch = await transaction.findDispatchByAssignmentId(
5430
5510
  result3.assignmentId
@@ -5461,12 +5541,18 @@ var WorkflowCoordinator = class {
5461
5541
  kind: job.brief.identity.kind,
5462
5542
  authorization: job.brief.authorization.status,
5463
5543
  checks: result3.checks,
5544
+ requiredCheckNames: job.policy.checks.filter((check) => check.required).map((check) => check.name),
5545
+ checkCandidate: assignment.head === void 0 ? void 0 : {
5546
+ baseSha: assignment.base.sha,
5547
+ headSha: assignment.head.sha,
5548
+ briefHash: assignment.briefHash
5549
+ },
5464
5550
  review: {
5465
5551
  outcome: result3.outcome === "completed" && result3.phase === "review" ? "passed" : "incomplete",
5466
5552
  axes: result3.reviewAxes ?? result3.findings.map((finding) => finding.axis),
5467
5553
  findings: result3.findings,
5468
- headSha: result3.head?.sha ?? "",
5469
- briefHash: job.brief.hash
5554
+ headSha: assignment.head?.sha ?? "",
5555
+ briefHash: assignment.briefHash
5470
5556
  }
5471
5557
  });
5472
5558
  state2 = nextState;
@@ -5626,6 +5712,17 @@ var WorkflowCoordinator = class {
5626
5712
  if (input.head !== void 0 && input.head.sha !== input.relevantRevision) {
5627
5713
  throw new Error("Scheduled phase head does not match its revision");
5628
5714
  }
5715
+ if (["checking", "review", "handoff", "merge"].includes(input.phase)) {
5716
+ const currentCandidate = this.latestHeadForRevision(
5717
+ job,
5718
+ input.relevantRevision
5719
+ );
5720
+ if (input.head === void 0 || currentCandidate === void 0 || !sameRevision(input.head, currentCandidate)) {
5721
+ throw new Error(
5722
+ "Scheduled phase head does not match the current candidate"
5723
+ );
5724
+ }
5725
+ }
5629
5726
  if (input.phase !== "triage" && !isAuthorized(job.brief, job.policy)) {
5630
5727
  return {
5631
5728
  status: "blocked",
@@ -5657,7 +5754,13 @@ var WorkflowCoordinator = class {
5657
5754
  requireTransition(state2, targetState, {
5658
5755
  kind: job.brief.identity.kind,
5659
5756
  authorization: job.brief.authorization.status,
5660
- checks: latestChecks2
5757
+ checks: latestChecks2,
5758
+ requiredCheckNames: job.policy.checks.filter((check) => check.required).map((check) => check.name),
5759
+ checkCandidate: input.head === void 0 ? void 0 : {
5760
+ baseSha: job.brief.base.sha,
5761
+ headSha: input.head.sha,
5762
+ briefHash: job.brief.hash
5763
+ }
5661
5764
  });
5662
5765
  state2 = targetState;
5663
5766
  } catch (error) {
@@ -5869,6 +5972,7 @@ var toPhaseResult = (input) => {
5869
5972
  phase: assignment.phase,
5870
5973
  outcome: input.outcome,
5871
5974
  identity: assignment.identity,
5975
+ briefHash: assignment.briefHash,
5872
5976
  base: assignment.base,
5873
5977
  head,
5874
5978
  summary: input.summary ?? report.summary,
@@ -5877,7 +5981,10 @@ var toPhaseResult = (input) => {
5877
5981
  commits: isReview ? [] : input.commits,
5878
5982
  artifacts: input.artifacts,
5879
5983
  questions: input.questions ?? report.questions,
5880
- findings: report.findings,
5984
+ findings: isReview ? report.findings.map((finding) => ({
5985
+ ...finding,
5986
+ disposition: "open"
5987
+ })) : report.findings,
5881
5988
  reviewAxes: report.reviewAxes,
5882
5989
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
5883
5990
  });
@@ -6009,6 +6116,7 @@ var executePhase = async (options) => {
6009
6116
  let response;
6010
6117
  try {
6011
6118
  response = await options.adapter.execute(request);
6119
+ controller.signal.throwIfAborted();
6012
6120
  } catch (error) {
6013
6121
  clearTimeout(timeout);
6014
6122
  options.signal?.removeEventListener("abort", abortFromCaller);
@@ -6334,6 +6442,9 @@ var isFreshTimestamp = (value, now, freshnessWindowSeconds) => {
6334
6442
  };
6335
6443
  var requiredChecks = (policy) => policy.checks.filter((check) => check.required).map((check) => check.name);
6336
6444
  var blockingFinding = (finding) => finding.severity !== "info" && (finding.disposition === "open" || finding.disposition === "deferred");
6445
+ var implementationEvidence = (job, candidate) => [...job.phaseResults].reverse().find(
6446
+ (result3) => result3.outcome === "completed" && (result3.phase === "implementation" || result3.phase === "repair") && result3.base !== void 0 && result3.head !== void 0 && sameRevision2(result3.base, candidate.base) && sameRevision2(result3.head, candidate.head) && result3.briefHash === candidate.briefHash
6447
+ )?.evidence ?? [];
6337
6448
  var allRequiredChecksPassed2 = (policy, checks, expected) => {
6338
6449
  const reasons = [];
6339
6450
  for (const name of requiredChecks(policy)) {
@@ -6385,13 +6496,18 @@ var candidateReasons = (job, candidate, review, requiredAxes) => {
6385
6496
  if (review.briefHash !== candidate.briefHash || review.briefHash !== brief.hash) {
6386
6497
  reasons.push("Review evidence is bound to a different brief revision");
6387
6498
  }
6388
- const implementationHead = [...job.phaseResults].reverse().find((result3) => result3.phase === "implementation")?.head;
6499
+ const implementationHead = [...job.phaseResults].reverse().find(
6500
+ (result3) => result3.phase === "implementation" || result3.phase === "repair"
6501
+ )?.head;
6389
6502
  if (implementationHead !== void 0 && !sameRevision2(implementationHead, candidate.head)) {
6390
6503
  reasons.push("Candidate head does not match the implementation result");
6391
6504
  }
6392
6505
  if (review.outcome !== "passed") {
6393
6506
  reasons.push(`Independent review outcome is ${review.outcome}`);
6394
6507
  }
6508
+ if (implementationEvidence(job, candidate).length < brief.acceptanceCriteria.length) {
6509
+ reasons.push("Implementation acceptance evidence is missing");
6510
+ }
6395
6511
  for (const axis of requiredAxes) {
6396
6512
  if (!review.axes.includes(axis))
6397
6513
  reasons.push(`Review axis is missing: ${axis}`);
@@ -6411,7 +6527,7 @@ var packetFor = (input, brief) => ({
6411
6527
  change: brief.problem,
6412
6528
  risk: brief.risk,
6413
6529
  acceptanceCriteria: brief.acceptanceCriteria,
6414
- acceptanceEvidence: brief.evidence,
6530
+ acceptanceEvidence: implementationEvidence(input.job, input.candidate),
6415
6531
  checks: input.checks,
6416
6532
  reviewAxes: input.review.axes,
6417
6533
  findings: input.review.findings,
@@ -6803,7 +6919,6 @@ var checkConclusion = (check) => {
6803
6919
  return "neutral";
6804
6920
  }
6805
6921
  };
6806
- var checkStatus = (check) => "completed";
6807
6922
  var checkSummary = (check) => `${check.summary}${check.exitCode === void 0 ? "" : ` (exit ${check.exitCode})`}`;
6808
6923
  var publicPrBody = (brief, result3, readinessIssue, policy) => [
6809
6924
  "## Shipyard implementation",
@@ -7098,7 +7213,7 @@ var runAuthorizedImplementation = async (options) => {
7098
7213
  branch,
7099
7214
  name: check.name,
7100
7215
  headSha: phaseResult.head.sha,
7101
- status: checkStatus(),
7216
+ status: "completed",
7102
7217
  conclusion: checkConclusion(check),
7103
7218
  summary: checkSummary(check)
7104
7219
  });
@@ -7687,13 +7802,6 @@ var validSeverities = [
7687
7802
  "high",
7688
7803
  "critical"
7689
7804
  ];
7690
- var validDispositions = [
7691
- "open",
7692
- "fixed",
7693
- "rejected",
7694
- "accepted",
7695
- "deferred"
7696
- ];
7697
7805
  var nonEmpty = (value, path2) => {
7698
7806
  if (typeof value !== "string" || value.trim().length === 0) {
7699
7807
  throw new Error(`${path2} must be a non-empty string`);
@@ -7732,11 +7840,9 @@ var normalizeFinding = (value, index) => ({
7732
7840
  `findings[${index}].severity`
7733
7841
  ),
7734
7842
  axis: enumValue2(value.axis, validAxes, `findings[${index}].axis`),
7735
- disposition: enumValue2(
7736
- value.disposition,
7737
- validDispositions,
7738
- `findings[${index}].disposition`
7739
- ),
7843
+ // A reviewer reports evidence. Only the repair/authorization workflow may
7844
+ // dispose of a finding after independent verification.
7845
+ disposition: "open",
7740
7846
  title: nonEmpty(value.title, `findings[${index}].title`),
7741
7847
  evidence: nonEmpty(value.evidence, `findings[${index}].evidence`),
7742
7848
  location: value.location === void 0 ? void 0 : nonEmpty(value.location, `findings[${index}].location`),
@@ -8873,8 +8979,15 @@ var GitHubIntegration = class {
8873
8979
  if (!trustedReconciliation && isBot(effectiveSource, this.options.authorization)) {
8874
8980
  return this.ignoreStored(effectiveDelivery, "bot-originated");
8875
8981
  }
8876
- if (!trustedReconciliation && !allowedSender(this.options.authorization, effectiveSource)) {
8877
- return this.rejectStored(effectiveDelivery, "disallowed-sender");
8982
+ if (!trustedReconciliation) {
8983
+ const isReview = effectiveEnvelope.eventName === "pull_request_review";
8984
+ const authorized = isReview ? allowedReviewer(this.options.authorization, effectiveSource) : allowedSender(this.options.authorization, effectiveSource);
8985
+ if (!authorized) {
8986
+ return this.rejectStored(
8987
+ effectiveDelivery,
8988
+ isReview ? "disallowed-reviewer" : "disallowed-sender"
8989
+ );
8990
+ }
8878
8991
  }
8879
8992
  const normalized = await this.normalize(effectiveEnvelope);
8880
8993
  if (normalized.disposition === "ignored") {
@@ -9172,6 +9285,9 @@ var allowedRepository = (policy, repository) => policy.allowedRepositories.some(
9172
9285
  var allowedSender = (policy, source) => policy.allowedSenders.some(
9173
9286
  (candidate) => candidate.toLowerCase() === source.login.toLowerCase()
9174
9287
  );
9288
+ var allowedReviewer = (policy, source) => policy.allowedReviewers.some(
9289
+ (candidate) => candidate.toLowerCase() === source.login.toLowerCase()
9290
+ );
9175
9291
  var isBot = (source, policy) => source.type === "Bot" || policy.botLogins?.some(
9176
9292
  (candidate) => candidate.toLowerCase() === source.login.toLowerCase()
9177
9293
  ) === true;
@@ -9429,7 +9545,7 @@ var isFreshTimestamp2 = (value, now, maxAgeSeconds) => {
9429
9545
  return Number.isFinite(timestamp2) && Number.isFinite(current) && timestamp2 <= current && current - timestamp2 <= maxAgeSeconds * 1e3;
9430
9546
  };
9431
9547
  var approvalRoles = (policy) => policy.approvalRoles ?? ["owner", "maintainer"];
9432
- var validCandidate = (candidate) => {
9548
+ var validCandidate = (candidate, validateArtifactRef) => {
9433
9549
  const fields = [
9434
9550
  ["sourceSha", candidate.sourceSha],
9435
9551
  ["reviewedHeadSha", candidate.reviewedHeadSha],
@@ -9440,8 +9556,40 @@ var validCandidate = (candidate) => {
9440
9556
  const missing = fields.find(
9441
9557
  ([, value]) => typeof value !== "string" || value.trim().length === 0
9442
9558
  );
9443
- return missing === void 0 ? void 0 : `${missing[0]} is required`;
9559
+ if (missing !== void 0) return `${missing[0]} is required`;
9560
+ const gitObject = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i;
9561
+ if (!gitObject.test(candidate.sourceSha)) {
9562
+ return "sourceSha must be a full Git object id";
9563
+ }
9564
+ if (!gitObject.test(candidate.reviewedHeadSha)) {
9565
+ return "reviewedHeadSha must be a full Git object id";
9566
+ }
9567
+ if (!/^[0-9a-f]{64}$/i.test(candidate.briefHash)) {
9568
+ return "briefHash must be a SHA-256 hash";
9569
+ }
9570
+ if (!/^sha256:[0-9a-f]{64}$/i.test(candidate.artifactDigest)) {
9571
+ return "artifactDigest must be a SHA-256 digest";
9572
+ }
9573
+ if (/[\s?#]/.test(candidate.artifactRef)) {
9574
+ return "artifactRef must not contain whitespace, a query, or a fragment";
9575
+ }
9576
+ const digestReference = candidate.artifactRef.match(
9577
+ /@sha256:([0-9a-f]{64})$/i
9578
+ );
9579
+ if (digestReference !== null) {
9580
+ return digestReference[1].toLowerCase() === candidate.artifactDigest.slice("sha256:".length).toLowerCase() ? void 0 : "digest-qualified artifactRef must match artifactDigest";
9581
+ }
9582
+ const immutableNpmReference = /^npm:(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*@v?\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i;
9583
+ const immutableGitReference = /^(?:git|github):[^\s/@]+\/[^\s@]+@[0-9a-f]{40}(?:[0-9a-f]{24})?$/i;
9584
+ if (immutableNpmReference.test(candidate.artifactRef) || immutableGitReference.test(candidate.artifactRef)) {
9585
+ return void 0;
9586
+ }
9587
+ if (validateArtifactRef !== void 0) {
9588
+ return validateArtifactRef(candidate);
9589
+ }
9590
+ return "artifactRef format is not proven immutable";
9444
9591
  };
9592
+ var isIntentDeploymentId = (deploymentId) => deploymentId.startsWith("intent:");
9445
9593
  var requiredChecksPassed = (policy, checks) => {
9446
9594
  const reasons = [];
9447
9595
  for (const required of policy.requiredStagingChecks) {
@@ -9470,6 +9618,7 @@ var ReleaseIntegration = class {
9470
9618
  store;
9471
9619
  transport;
9472
9620
  actor;
9621
+ validateArtifactRef;
9473
9622
  now;
9474
9623
  constructor(options) {
9475
9624
  if (options.policy.repository.trim().length === 0) {
@@ -9493,6 +9642,7 @@ var ReleaseIntegration = class {
9493
9642
  this.store = options.store;
9494
9643
  this.transport = options.transport;
9495
9644
  this.actor = options.actor;
9645
+ this.validateArtifactRef = options.validateArtifactRef;
9496
9646
  this.now = options.now ?? defaultNow5;
9497
9647
  }
9498
9648
  // Bind evidence and remote idempotency to the deployment target and gate.
@@ -9513,7 +9663,7 @@ var ReleaseIntegration = class {
9513
9663
  ).digest("hex");
9514
9664
  }
9515
9665
  async requestStaging(candidate) {
9516
- const invalid = validCandidate(candidate);
9666
+ const invalid = validCandidate(candidate, this.validateArtifactRef);
9517
9667
  if (invalid !== void 0)
9518
9668
  return blocked3(`Invalid release candidate: ${invalid}`);
9519
9669
  const key = this.stateKey(candidate);
@@ -9565,7 +9715,10 @@ var ReleaseIntegration = class {
9565
9715
  return { outcome: "requested", state: updated };
9566
9716
  }
9567
9717
  recordStagingVerification(verification) {
9568
- const invalid = validCandidate(verification.candidate);
9718
+ const invalid = validCandidate(
9719
+ verification.candidate,
9720
+ this.validateArtifactRef
9721
+ );
9569
9722
  if (invalid !== void 0)
9570
9723
  return blocked3(`Invalid release candidate: ${invalid}`);
9571
9724
  if (verification.environment !== "staging")
@@ -9574,6 +9727,12 @@ var ReleaseIntegration = class {
9574
9727
  const state2 = this.store.get(key);
9575
9728
  if (state2 === void 0)
9576
9729
  return blocked3("Staging deployment was not requested");
9730
+ if (state2.staging === void 0 || isIntentDeploymentId(state2.staging.deploymentId)) {
9731
+ return blocked3(
9732
+ "Staging deployment request has not been confirmed",
9733
+ state2
9734
+ );
9735
+ }
9577
9736
  if (state2.staging?.deploymentId !== verification.deploymentId) {
9578
9737
  return blocked3(
9579
9738
  "Staging verification references a different deployment",
@@ -9603,7 +9762,7 @@ var ReleaseIntegration = class {
9603
9762
  return reasons.length === 0 ? { outcome: "verified", state: updated } : { outcome: "failed", reason: reasons.join("; "), state: updated };
9604
9763
  }
9605
9764
  approveProduction(input) {
9606
- const invalid = validCandidate(input.candidate);
9765
+ const invalid = validCandidate(input.candidate, this.validateArtifactRef);
9607
9766
  if (invalid !== void 0)
9608
9767
  return blocked3(`Invalid release candidate: ${invalid}`);
9609
9768
  const key = this.stateKey(input.candidate);
@@ -9668,7 +9827,7 @@ var ReleaseIntegration = class {
9668
9827
  return { outcome: "approved", state: updated };
9669
9828
  }
9670
9829
  async promoteProduction(candidate) {
9671
- const invalid = validCandidate(candidate);
9830
+ const invalid = validCandidate(candidate, this.validateArtifactRef);
9672
9831
  if (invalid !== void 0)
9673
9832
  return blocked3(`Invalid release candidate: ${invalid}`);
9674
9833
  const key = this.stateKey(candidate);
@@ -9781,7 +9940,10 @@ var ReleaseIntegration = class {
9781
9940
  }
9782
9941
  }
9783
9942
  recordProductionVerification(verification) {
9784
- const invalid = validCandidate(verification.candidate);
9943
+ const invalid = validCandidate(
9944
+ verification.candidate,
9945
+ this.validateArtifactRef
9946
+ );
9785
9947
  if (invalid !== void 0)
9786
9948
  return blocked3(`Invalid release candidate: ${invalid}`);
9787
9949
  if (verification.environment !== "production")
@@ -9791,6 +9953,12 @@ var ReleaseIntegration = class {
9791
9953
  if (state2 === void 0 || state2.production === void 0) {
9792
9954
  return blocked3("Production deployment was not requested");
9793
9955
  }
9956
+ if (isIntentDeploymentId(state2.production.deploymentId)) {
9957
+ return blocked3(
9958
+ "Production deployment request has not been confirmed",
9959
+ state2
9960
+ );
9961
+ }
9794
9962
  if (state2.status === "production-failed") {
9795
9963
  return blocked3(
9796
9964
  "Production verification already failed; record recovery before any new candidate",
@@ -9825,6 +9993,12 @@ var ReleaseIntegration = class {
9825
9993
  "Recovery can only be recorded for a failed production candidate"
9826
9994
  );
9827
9995
  }
9996
+ if (input.reason.trim().length === 0) {
9997
+ throw new Error("Recovery reason is required");
9998
+ }
9999
+ if (input.authorizedBy.id.trim().length === 0 || input.authorizedBy.role === "release-coordinator" || !approvalRoles(this.policy).includes(input.authorizedBy.role)) {
10000
+ throw new Error("Recovery requires an authorized owner or maintainer");
10001
+ }
9828
10002
  const recovery = {
9829
10003
  candidate: structuredClone(input.candidate),
9830
10004
  action: this.policy.recoveryMode,