@snappedly-tools/shipyard 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{SandboxProvider-KeFp_nju.d.ts → SandboxProvider-XJQqEdSf.d.ts} +4 -0
- package/dist/chunk-2GT3OPO5.js +2620 -0
- package/dist/chunk-2GT3OPO5.js.map +1 -0
- package/dist/{chunk-CP6YEQKU.js → chunk-KMGNFXKN.js} +2 -6
- package/dist/chunk-KMGNFXKN.js.map +1 -0
- package/dist/{chunk-OAHPDSOJ.js → chunk-OCJKONZ7.js} +90 -22
- package/dist/chunk-OCJKONZ7.js.map +1 -0
- package/dist/chunk-VDHZO5PP.js +113 -0
- package/dist/chunk-VDHZO5PP.js.map +1 -0
- package/dist/{chunk-YVJHSJUW.js → chunk-VW4MI5GJ.js} +5 -6
- package/dist/chunk-VW4MI5GJ.js.map +1 -0
- package/dist/chunk-WA7QPAXO.js +50 -0
- package/dist/chunk-WA7QPAXO.js.map +1 -0
- package/dist/index.d.ts +28 -5
- package/dist/index.js +471 -297
- package/dist/index.js.map +1 -1
- package/dist/main.js +224 -19339
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.d.ts +1 -1
- package/dist/sandboxes/docker.js +4 -7
- package/dist/sandboxes/no-sandbox.d.ts +1 -1
- package/dist/sandboxes/no-sandbox.js +2 -5
- package/dist/sandboxes/vercel.d.ts +1 -1
- package/dist/sandboxes/vercel.js +2 -4
- package/dist/sandboxes/vercel.js.map +1 -1
- package/package.json +15 -12
- package/dist/chunk-4VDXZY7T.js +0 -27624
- package/dist/chunk-4VDXZY7T.js.map +0 -1
- package/dist/chunk-BU6XJFJ2.js +0 -90
- package/dist/chunk-BU6XJFJ2.js.map +0 -1
- package/dist/chunk-CP6YEQKU.js.map +0 -1
- package/dist/chunk-OAHPDSOJ.js.map +0 -1
- package/dist/chunk-SRUH232X.js +0 -26712
- package/dist/chunk-SRUH232X.js.map +0 -1
- package/dist/chunk-YVJHSJUW.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import '
|
|
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) =>
|
|
53
|
-
emit: onEvent ? (event) =>
|
|
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
|
-
}) : () =>
|
|
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
|
|
71
|
+
return Effect.succeed(prompt.replaceAll(SHELL_BLOCK_MARKER, ""));
|
|
76
72
|
}
|
|
77
73
|
if (matches.length > MAX_PROMPT_EXPANSIONS) {
|
|
78
|
-
return
|
|
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
|
|
80
|
+
return Effect.gen(function* () {
|
|
85
81
|
const display = yield* Display;
|
|
86
82
|
return yield* display.taskLog(
|
|
87
83
|
"Expanding shell expressions",
|
|
88
|
-
(message) =>
|
|
89
|
-
const results = yield*
|
|
84
|
+
(message) => Effect.gen(function* () {
|
|
85
|
+
const results = yield* Effect.all(
|
|
90
86
|
matches.map((match) => {
|
|
91
87
|
const command = match[1];
|
|
92
|
-
return
|
|
93
|
-
const start = yield*
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
102
|
+
Effect.ensuring(
|
|
103
|
+
Effect.sync(() => {
|
|
108
104
|
controller.abort();
|
|
109
105
|
})
|
|
110
106
|
)
|
|
111
107
|
);
|
|
112
|
-
if (
|
|
113
|
-
const elapsedMs = (yield*
|
|
114
|
-
return yield*
|
|
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*
|
|
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*
|
|
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*
|
|
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) =>
|
|
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*
|
|
237
|
-
const completionTimeoutDeferred = yield*
|
|
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)
|
|
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 =
|
|
249
|
-
|
|
244
|
+
warningFiber = Effect.runFork(
|
|
245
|
+
Effect.gen(function* () {
|
|
250
246
|
while (true) {
|
|
251
|
-
yield*
|
|
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 =
|
|
262
|
-
|
|
263
|
-
yield*
|
|
257
|
+
timeoutFiber = Effect.runFork(
|
|
258
|
+
Effect.gen(function* () {
|
|
259
|
+
yield* Effect.sleep(Duration.millis(completionTimeoutMs));
|
|
264
260
|
onCompletionTimeout(completionTimeoutMs);
|
|
265
|
-
yield*
|
|
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 =
|
|
274
|
-
|
|
275
|
-
yield*
|
|
276
|
-
yield*
|
|
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*
|
|
284
|
+
const abortDeferred = yield* Deferred.make();
|
|
289
285
|
let abortCleanup = null;
|
|
290
286
|
if (signal) {
|
|
291
287
|
if (signal.aborted) {
|
|
292
|
-
return yield*
|
|
288
|
+
return yield* Effect.die(signal.reason);
|
|
293
289
|
}
|
|
294
290
|
const onAbort = () => {
|
|
295
|
-
|
|
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 =
|
|
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*
|
|
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
|
-
|
|
366
|
-
|
|
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 =
|
|
375
|
-
raced =
|
|
370
|
+
let raced = Effect.raceFirst(execEffect, Deferred.await(timeoutSignal));
|
|
371
|
+
raced = Effect.raceFirst(raced, Deferred.await(completionTimeoutDeferred));
|
|
376
372
|
if (signal) {
|
|
377
|
-
raced =
|
|
373
|
+
raced = Effect.raceFirst(
|
|
378
374
|
raced,
|
|
379
|
-
|
|
375
|
+
Deferred.await(abortDeferred)
|
|
380
376
|
);
|
|
381
377
|
}
|
|
382
378
|
return yield* raced.pipe(
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
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*
|
|
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 ?
|
|
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) =>
|
|
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*
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
503
|
+
Effect.runPromise(display.status(label(msg), "warn"));
|
|
508
504
|
};
|
|
509
505
|
const onCompletionTimeout = (timeoutMs) => {
|
|
510
|
-
|
|
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*
|
|
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*
|
|
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
|
|
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
|
|
632
|
+
return Effect.succeed({ text: prompt, source: "inline" });
|
|
639
633
|
}
|
|
640
634
|
if (promptFile === void 0) {
|
|
641
|
-
return
|
|
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
|
|
648
|
-
const fs = yield*
|
|
641
|
+
return Effect.gen(function* () {
|
|
642
|
+
const fs = yield* FileSystem.FileSystem;
|
|
649
643
|
const text2 = yield* fs.readFileString(promptFile).pipe(
|
|
650
|
-
|
|
651
|
-
(e) =>
|
|
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) =>
|
|
662
|
-
const fs = yield*
|
|
655
|
+
var parseEnvFile = (filePath) => Effect.gen(function* () {
|
|
656
|
+
const fs = yield* FileSystem.FileSystem;
|
|
663
657
|
const symlink = yield* fs.readLink(filePath).pipe(
|
|
664
|
-
|
|
665
|
-
|
|
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(
|
|
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) =>
|
|
699
|
-
const configDir =
|
|
692
|
+
var resolveEnv = (repoDir) => Effect.gen(function* () {
|
|
693
|
+
const configDir = join(repoDir, CONFIG_DIR);
|
|
700
694
|
const envPath = join(configDir, ".env");
|
|
701
|
-
yield*
|
|
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
|
|
745
|
-
return
|
|
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
|
|
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
|
|
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
|
|
780
|
+
return Effect.succeed(markedPrompt);
|
|
789
781
|
}
|
|
790
|
-
return
|
|
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*
|
|
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*
|
|
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
|
|
1109
|
-
resolveCwd(options.cwd).pipe(
|
|
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
|
|
1121
|
+
const resolved = await Effect.runPromise(
|
|
1120
1122
|
resolvePrompt({ prompt, promptFile }).pipe(
|
|
1121
|
-
|
|
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
|
|
1136
|
-
resolveEnv(hostRepoDir).pipe(
|
|
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
|
|
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
|
|
1173
|
+
return Layer.provide(
|
|
1172
1174
|
FileDisplay.layer(resolvedLogging.path),
|
|
1173
|
-
|
|
1175
|
+
NodeFileSystem.layer
|
|
1174
1176
|
);
|
|
1175
1177
|
})() : ClackDisplay.layer;
|
|
1176
|
-
const factoryLayer =
|
|
1178
|
+
const factoryLayer = Layer.provide(
|
|
1177
1179
|
WorktreeDockerSandboxFactory.layer,
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1192
|
+
NodeFileSystem.layer,
|
|
1191
1193
|
displayLayer
|
|
1192
1194
|
)
|
|
1193
1195
|
);
|
|
1194
1196
|
const streamEmitterLayer = agentStreamEmitterLayer(
|
|
1195
1197
|
buildAgentStreamHandler(resolvedLogging)
|
|
1196
1198
|
);
|
|
1197
|
-
const runLayer =
|
|
1199
|
+
const runLayer = Layer.mergeAll(
|
|
1198
1200
|
factoryLayer,
|
|
1199
1201
|
displayLayer,
|
|
1200
1202
|
streamEmitterLayer
|
|
1201
1203
|
);
|
|
1202
|
-
const baseEffect =
|
|
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
|
-
|
|
1263
|
-
(error) =>
|
|
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
|
|
1275
|
-
withErrorLog.pipe(
|
|
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
|
|
1358
|
+
return Effect.gen(function* () {
|
|
1359
1359
|
if (signal.aborted) {
|
|
1360
|
-
return yield*
|
|
1360
|
+
return yield* Effect.die(signal.reason);
|
|
1361
1361
|
}
|
|
1362
|
-
const abortDeferred = yield*
|
|
1362
|
+
const abortDeferred = yield* Deferred.make();
|
|
1363
1363
|
const onAbort = () => {
|
|
1364
|
-
|
|
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*
|
|
1370
|
+
return yield* Effect.raceFirst(
|
|
1371
1371
|
effect,
|
|
1372
|
-
|
|
1372
|
+
Deferred.await(abortDeferred)
|
|
1373
1373
|
).pipe(
|
|
1374
|
-
|
|
1375
|
-
|
|
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 =
|
|
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*
|
|
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*
|
|
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
|
-
|
|
1473
|
-
|
|
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*
|
|
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*
|
|
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
|
-
|
|
1541
|
+
Effect.tapError(
|
|
1542
1542
|
() => worktreeInfo ? remove(worktreeInfo.path).pipe(
|
|
1543
|
-
|
|
1544
|
-
) :
|
|
1543
|
+
Effect.catchAll(() => Effect.void)
|
|
1544
|
+
) : Effect.void
|
|
1545
1545
|
)
|
|
1546
1546
|
);
|
|
1547
|
-
return yield*
|
|
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) : () =>
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
1622
|
-
|
|
1623
|
-
() => worktreeInfo ? remove(worktreeInfo.path).pipe(
|
|
1624
|
-
|
|
1625
|
-
) :
|
|
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
|
-
|
|
1629
|
+
Effect.ensuring(Effect.promise(() => handle.close().catch(() => {
|
|
1629
1630
|
})))
|
|
1630
1631
|
);
|
|
1631
1632
|
});
|
|
1632
1633
|
let result3;
|
|
1633
1634
|
try {
|
|
1634
|
-
result3 = await
|
|
1635
|
+
result3 = await Effect.runPromise(
|
|
1635
1636
|
inner.pipe(
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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
|
|
1692
|
+
const resolved = await Effect.runPromise(
|
|
1691
1693
|
resolvePrompt({ prompt, promptFile }).pipe(
|
|
1692
|
-
|
|
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
|
|
1700
|
+
const currentHostBranch = await Effect.runPromise(
|
|
1699
1701
|
getCurrentBranch(hostRepoDir)
|
|
1700
1702
|
);
|
|
1701
|
-
const displayRef =
|
|
1703
|
+
const displayRef = Ref.unsafeMake([]);
|
|
1702
1704
|
const silentDisplayLayer = SilentDisplay.layer(displayRef);
|
|
1703
|
-
const resolvedPrompt = await
|
|
1704
|
-
|
|
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(
|
|
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
|
|
1747
|
+
return Layer.provide(
|
|
1746
1748
|
FileDisplay.layer(resolvedLogging.path),
|
|
1747
|
-
|
|
1749
|
+
NodeFileSystem.layer
|
|
1748
1750
|
);
|
|
1749
1751
|
})() : silentDisplayLayer;
|
|
1750
|
-
const reuseFactoryLayer =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1777
|
-
|
|
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(
|
|
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
|
|
1865
|
-
|
|
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) =>
|
|
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*
|
|
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
|
-
|
|
1946
|
-
|
|
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
|
|
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
|
|
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
|
|
1995
|
-
resolveEnv(hostRepoDir).pipe(
|
|
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
|
-
|
|
2022
|
-
|
|
2024
|
+
Effect.provide(NodeFileSystem.layer),
|
|
2025
|
+
Effect.catchAll(() => Effect.succeed([])),
|
|
2023
2026
|
// Patch git mounts for Windows worktree compatibility (ADR-0006)
|
|
2024
|
-
|
|
2027
|
+
Effect.flatMap(
|
|
2025
2028
|
(gitMounts) => patchGitMountsForWindows(gitMounts, worktreePath, SANDBOX_REPO_DIR)
|
|
2026
2029
|
),
|
|
2027
|
-
|
|
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
|
|
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
|
|
2048
|
-
|
|
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*
|
|
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) : () =>
|
|
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
|
|
2098
|
-
|
|
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
|
-
|
|
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*
|
|
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
|
-
|
|
2150
|
-
|
|
2152
|
+
Effect.provide(NodeFileSystem.layer),
|
|
2153
|
+
Effect.catchAll(() => Effect.succeed([])),
|
|
2151
2154
|
// Patch git mounts for Windows worktree compatibility (ADR-0006)
|
|
2152
|
-
|
|
2155
|
+
Effect.flatMap(
|
|
2153
2156
|
(gitMounts) => patchGitMountsForWindows(
|
|
2154
2157
|
gitMounts,
|
|
2155
2158
|
worktreePath2,
|
|
2156
2159
|
SANDBOX_REPO_DIR
|
|
2157
2160
|
)
|
|
2158
2161
|
),
|
|
2159
|
-
|
|
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*
|
|
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*
|
|
2194
|
+
yield* Effect.all(allEffects, { concurrency: "unbounded" });
|
|
2192
2195
|
}).pipe(
|
|
2193
|
-
|
|
2194
|
-
() => providerHandle2 ?
|
|
2196
|
+
Effect.onError(
|
|
2197
|
+
() => providerHandle2 ? Effect.promise(
|
|
2195
2198
|
() => providerHandle2.close().catch(() => {
|
|
2196
2199
|
})
|
|
2197
|
-
) :
|
|
2200
|
+
) : Effect.void
|
|
2198
2201
|
)
|
|
2199
2202
|
);
|
|
2200
2203
|
}
|
|
2201
2204
|
return { providerHandle: providerHandle2, sandbox: sandbox2, sandboxRepoDir: sandboxRepoDir2 };
|
|
2202
2205
|
}).pipe(
|
|
2203
|
-
|
|
2206
|
+
Effect.onError(
|
|
2204
2207
|
() => remove(worktreePath2).pipe(
|
|
2205
|
-
|
|
2208
|
+
Effect.catchAll(() => Effect.void)
|
|
2206
2209
|
)
|
|
2207
2210
|
)
|
|
2208
2211
|
);
|
|
2209
2212
|
return { hostRepoDir: hostRepoDir2, worktreePath: worktreePath2, ...prepared };
|
|
2210
|
-
}).pipe(
|
|
2213
|
+
}).pipe(Effect.provide(NodeContext.layer))
|
|
2211
2214
|
);
|
|
2212
|
-
const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () =>
|
|
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
|
|
2225
|
-
|
|
2227
|
+
return Effect.runPromise(
|
|
2228
|
+
Effect.gen(function* () {
|
|
2226
2229
|
if (providerHandle) {
|
|
2227
|
-
yield*
|
|
2230
|
+
yield* Effect.promise(() => providerHandle.close());
|
|
2228
2231
|
}
|
|
2229
2232
|
const isDirty = yield* hasUncommittedChanges(
|
|
2230
2233
|
worktreePath
|
|
2231
|
-
).pipe(
|
|
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
|
-
|
|
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
|
|
2269
|
+
const { hostRepoDir, worktreeInfo } = await Effect.gen(function* () {
|
|
2267
2270
|
const hostRepoDir2 = yield* resolveCwd(options.cwd);
|
|
2268
2271
|
yield* pruneStale(hostRepoDir2).pipe(
|
|
2269
|
-
|
|
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(
|
|
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
|
|
2295
|
+
return Effect.gen(function* () {
|
|
2293
2296
|
const isDirty = yield* hasUncommittedChanges(
|
|
2294
2297
|
worktreeInfo.path
|
|
2295
|
-
).pipe(
|
|
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
|
-
|
|
2303
|
+
Effect.catchAll(() => Effect.void)
|
|
2301
2304
|
);
|
|
2302
2305
|
return { preservedWorktreePath: void 0 };
|
|
2303
|
-
}).pipe(
|
|
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 =
|
|
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*
|
|
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*
|
|
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) : () =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
2459
|
+
Effect.ensuring(Effect.promise(() => handle.close().catch(() => {
|
|
2456
2460
|
})))
|
|
2457
2461
|
);
|
|
2458
2462
|
});
|
|
2459
2463
|
try {
|
|
2460
|
-
return await
|
|
2464
|
+
return await Effect.runPromise(
|
|
2461
2465
|
inner.pipe(
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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 =
|
|
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) : () =>
|
|
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*
|
|
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
|
|
2593
|
+
return Layer.provide(
|
|
2589
2594
|
FileDisplay.layer(resolvedLogging.path),
|
|
2590
|
-
|
|
2595
|
+
NodeFileSystem.layer
|
|
2591
2596
|
);
|
|
2592
2597
|
})() : ClackDisplay.layer;
|
|
2593
2598
|
const bindMountHandle = toSessionTransferHandle(handle);
|
|
2594
|
-
const reuseFactoryLayer =
|
|
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
|
-
|
|
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 =
|
|
2618
|
+
const runLayer = Layer.mergeAll(
|
|
2614
2619
|
reuseFactoryLayer,
|
|
2615
2620
|
runDisplayLayer,
|
|
2616
2621
|
streamEmitterLayer
|
|
2617
2622
|
);
|
|
2618
|
-
const result3 = yield*
|
|
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
|
-
|
|
2658
|
+
Effect.provide(runLayer),
|
|
2654
2659
|
// Always close sandbox handle
|
|
2655
|
-
|
|
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
|
|
2673
|
+
return await Effect.runPromise(
|
|
2669
2674
|
inner.pipe(
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
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 = (
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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 ===
|
|
5427
|
-
throw new Error("
|
|
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:
|
|
5469
|
-
briefHash:
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
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
|
|
8877
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|