@aviaratech/ai-delivery 0.3.5 → 0.3.7
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 +65 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +35 -1
- package/dist/cli.js.map +1 -1
- package/dist/dispatch.js +21 -1
- package/dist/dispatch.js.map +1 -1
- package/dist/github/commandIdentityPolicy.js +1 -0
- package/dist/github/commandIdentityPolicy.js.map +1 -1
- package/dist/issue.d.ts +1 -0
- package/dist/issue.js +22 -10
- package/dist/issue.js.map +1 -1
- package/dist/lifecycle.test.js +470 -4
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/tools.d.ts +31 -0
- package/dist/mcp/tools.js +27 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pr.d.ts +1 -1
- package/dist/services/worktreeRegistry.d.ts +12 -0
- package/dist/services/worktreeRegistry.js +146 -4
- package/dist/services/worktreeRegistry.js.map +1 -1
- package/dist/verification.d.ts +34 -0
- package/dist/verification.js +488 -64
- package/dist/verification.js.map +1 -1
- package/dist/worktree.d.ts +3 -0
- package/dist/worktree.js +38 -8
- package/dist/worktree.js.map +1 -1
- package/dist/worktreeTransition.d.ts +173 -0
- package/dist/worktreeTransition.js +1012 -0
- package/dist/worktreeTransition.js.map +1 -0
- package/dist/worktreeTransition.test.d.ts +1 -0
- package/dist/worktreeTransition.test.js +1179 -0
- package/dist/worktreeTransition.test.js.map +1 -0
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
- package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +6 -0
package/dist/verification.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { existsSync, unlinkSync, lstatSync, readFileSync, readdirSync, realpathSync, statfsSync } from 'node:fs';
|
|
4
|
+
import { closeSync, existsSync, fsyncSync, openSync, unlinkSync, lstatSync, mkdirSync, rmdirSync, renameSync, readFileSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
|
|
5
5
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { z } from 'zod';
|
|
@@ -125,6 +125,254 @@ function assertResourceBounds(bounds) {
|
|
|
125
125
|
if (process.platform === 'win32')
|
|
126
126
|
throw new DeliveryError('Resource observation requires POSIX process support.');
|
|
127
127
|
}
|
|
128
|
+
const OUTPUT_GATE_ENV = 'AI_DELIVERY_OUTPUT_OBSERVATION_GATE';
|
|
129
|
+
const GateOwnerSchema = z.strictObject({ pid: z.number().int().positive(), identity: z.string().min(1) });
|
|
130
|
+
const OutputGateSchema = z.strictObject({ directory: z.string().min(1), id: z.uuid(), owner: GateOwnerSchema });
|
|
131
|
+
const GateClaimSchema = z.strictObject({
|
|
132
|
+
gateId: z.uuid(),
|
|
133
|
+
id: z.uuid(),
|
|
134
|
+
kind: z.enum(['scan', 'fixture']),
|
|
135
|
+
owner: GateOwnerSchema,
|
|
136
|
+
});
|
|
137
|
+
function gateOwner() {
|
|
138
|
+
const owner = processSnapshot().get(process.pid);
|
|
139
|
+
if (!owner)
|
|
140
|
+
throw new DeliveryError('Filesystem observation gate owner is unavailable.');
|
|
141
|
+
return { pid: owner.pid, identity: owner.identity };
|
|
142
|
+
}
|
|
143
|
+
function assertGateDirectory(path) {
|
|
144
|
+
const metadata = lstatSync(path);
|
|
145
|
+
if (!metadata.isDirectory() || (metadata.mode & 0o077) !== 0 || realpathSync(path) !== path)
|
|
146
|
+
throw new DeliveryError('Filesystem observation gate directory is invalid.');
|
|
147
|
+
}
|
|
148
|
+
function readOutputGate(value) {
|
|
149
|
+
try {
|
|
150
|
+
const gate = OutputGateSchema.parse(JSON.parse(value));
|
|
151
|
+
assertOutputGate(gate);
|
|
152
|
+
return gate;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new DeliveryError('Filesystem observation gate capability is invalid or stale: ' +
|
|
156
|
+
(error instanceof Error ? error.message : String(error)));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function gateOwnerAlive(owner, snapshot = processSnapshot()) {
|
|
160
|
+
const current = snapshot.get(owner.pid);
|
|
161
|
+
return current?.identity === owner.identity && !current.status.startsWith('Z');
|
|
162
|
+
}
|
|
163
|
+
function assertOutputGate(gate, abandoned = false) {
|
|
164
|
+
assertGateDirectory(gate.directory);
|
|
165
|
+
const manifest = OutputGateSchema.parse(JSON.parse(assertPrivateFile(join(gate.directory, 'gate.json')).toString('utf8')));
|
|
166
|
+
const snapshot = processSnapshot();
|
|
167
|
+
let actor = snapshot.get(process.pid);
|
|
168
|
+
const ancestors = new Set();
|
|
169
|
+
while (actor && actor.pid !== gate.owner.pid && !ancestors.has(actor.pid)) {
|
|
170
|
+
ancestors.add(actor.pid);
|
|
171
|
+
actor = snapshot.get(actor.ppid);
|
|
172
|
+
}
|
|
173
|
+
if (digestValue(manifest) !== digestValue(gate) ||
|
|
174
|
+
(abandoned
|
|
175
|
+
? gateOwnerAlive(gate.owner, snapshot)
|
|
176
|
+
: !gateOwnerAlive(gate.owner, snapshot) || actor?.identity !== gate.owner.identity))
|
|
177
|
+
throw new DeliveryError('Filesystem observation gate ownership changed, was abandoned or is not an inherited ancestor.');
|
|
178
|
+
}
|
|
179
|
+
function readGateClaim(gate, path) {
|
|
180
|
+
const claim = GateClaimSchema.parse(JSON.parse(assertPrivateFile(path).toString('utf8')));
|
|
181
|
+
if (claim.gateId !== gate.id)
|
|
182
|
+
throw new DeliveryError('Filesystem observation gate claim belongs to another run.');
|
|
183
|
+
return claim;
|
|
184
|
+
}
|
|
185
|
+
function gateScanRequests(gate) {
|
|
186
|
+
const directory = join(gate.directory, 'requests');
|
|
187
|
+
assertGateDirectory(directory);
|
|
188
|
+
const requests = readdirSync(directory);
|
|
189
|
+
if (requests.length > 1024 || requests.some((name) => !/^[a-f0-9-]{36}\.json$/u.test(name)))
|
|
190
|
+
throw new DeliveryError('Filesystem observation gate scan requests are invalid.');
|
|
191
|
+
const live = [];
|
|
192
|
+
const snapshot = processSnapshot();
|
|
193
|
+
for (const name of requests) {
|
|
194
|
+
let claim;
|
|
195
|
+
try {
|
|
196
|
+
claim = readGateClaim(gate, join(directory, name));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code === 'ENOENT')
|
|
200
|
+
continue;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
if (claim.kind !== 'scan' || name !== claim.id + '.json' || !gateOwnerAlive(claim.owner, snapshot))
|
|
204
|
+
throw new DeliveryError('Filesystem observation gate scan request was abandoned.');
|
|
205
|
+
live.push(name);
|
|
206
|
+
}
|
|
207
|
+
return live;
|
|
208
|
+
}
|
|
209
|
+
function removeGateClaim(gate, path, claim) {
|
|
210
|
+
if (digestValue(readGateClaim(gate, path)) !== digestValue(claim))
|
|
211
|
+
throw new DeliveryError('Filesystem observation gate claim changed before release.');
|
|
212
|
+
unlinkSync(path);
|
|
213
|
+
}
|
|
214
|
+
async function withOutputGateLock(gate, kind, operation, signal) {
|
|
215
|
+
if (!gate)
|
|
216
|
+
return operation();
|
|
217
|
+
assertOutputGate(gate);
|
|
218
|
+
const claim = { gateId: gate.id, id: randomUUID(), kind, owner: gateOwner() };
|
|
219
|
+
const request = kind === 'scan' ? join(gate.directory, 'requests', claim.id + '.json') : undefined;
|
|
220
|
+
const lock = join(gate.directory, 'held');
|
|
221
|
+
const holder = join(lock, 'owner.json');
|
|
222
|
+
if (request) {
|
|
223
|
+
const staging = join(gate.directory, claim.id + '.pending');
|
|
224
|
+
writePrivateJsonFileAtomically(staging, claim);
|
|
225
|
+
renameSync(staging, request);
|
|
226
|
+
}
|
|
227
|
+
let acquired;
|
|
228
|
+
let incompleteSince;
|
|
229
|
+
let fixtureWait;
|
|
230
|
+
const release = () => {
|
|
231
|
+
if (!acquired)
|
|
232
|
+
return;
|
|
233
|
+
const metadata = lstatSync(lock);
|
|
234
|
+
if (!metadata.isDirectory() || metadata.dev !== acquired.dev || metadata.ino !== acquired.ino)
|
|
235
|
+
throw new DeliveryError('Filesystem observation gate lock changed before release.');
|
|
236
|
+
if (readdirSync(lock).join(',') !== 'owner.json')
|
|
237
|
+
throw new DeliveryError('Filesystem observation gate lock has unrelated metadata.');
|
|
238
|
+
removeGateClaim(gate, holder, claim);
|
|
239
|
+
rmdirSync(lock);
|
|
240
|
+
acquired = undefined;
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
for (;;) {
|
|
244
|
+
if (signal?.aborted)
|
|
245
|
+
throw new DeliveryError('Filesystem observation gate wait cancelled.');
|
|
246
|
+
assertOutputGate(gate);
|
|
247
|
+
const requests = gateScanRequests(gate);
|
|
248
|
+
if (kind === 'scan' || requests.length === 0) {
|
|
249
|
+
let created = false;
|
|
250
|
+
try {
|
|
251
|
+
mkdirSync(lock, { mode: 0o700 });
|
|
252
|
+
created = true;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error.code !== 'EEXIST')
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
if (created) {
|
|
259
|
+
const metadata = lstatSync(lock);
|
|
260
|
+
acquired = { dev: metadata.dev, ino: metadata.ino };
|
|
261
|
+
writePrivateJsonFileAtomically(holder, claim);
|
|
262
|
+
// A scan may queue between the fixture's request check and atomic lock acquisition.
|
|
263
|
+
if (kind === 'scan' || gateScanRequests(gate).length === 0)
|
|
264
|
+
return await operation();
|
|
265
|
+
release();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (existsSync(lock)) {
|
|
269
|
+
try {
|
|
270
|
+
assertGateDirectory(lock);
|
|
271
|
+
const current = readGateClaim(gate, holder);
|
|
272
|
+
incompleteSince = undefined;
|
|
273
|
+
if (!gateOwnerAlive(current.owner))
|
|
274
|
+
throw new DeliveryError('Filesystem observation gate fixture or scanner was abandoned.');
|
|
275
|
+
if (kind === 'scan' && current.kind === 'fixture') {
|
|
276
|
+
if (fixtureWait?.claimId !== current.id)
|
|
277
|
+
fixtureWait = { claimId: current.id, since: Date.now() };
|
|
278
|
+
// A negative reader that blocks prevents strict output observation; terminate this command, never steal its live lease.
|
|
279
|
+
if (Date.now() - fixtureWait.since > 5000)
|
|
280
|
+
throw new DeliveryError('Filesystem-negative fixture did not release a waiting output scan within 5 seconds; reader or teardown is stalled.');
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
fixtureWait = undefined;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error.code !== 'ENOENT')
|
|
287
|
+
throw error;
|
|
288
|
+
incompleteSince ??= Date.now();
|
|
289
|
+
// A concrete publication failure, never an elapsed fixture-work allowance or stale-lock takeover.
|
|
290
|
+
if (Date.now() - incompleteSince > 2000)
|
|
291
|
+
throw new DeliveryError('Filesystem observation gate owner publication is incomplete.');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
try {
|
|
299
|
+
release();
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (request)
|
|
303
|
+
removeGateClaim(gate, request, claim);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Run a real filesystem-negative fixture between strict scans; the callback must clean it up before returning. */
|
|
308
|
+
export async function withVerificationFilesystemFixture(operation) {
|
|
309
|
+
const capability = process.env[OUTPUT_GATE_ENV];
|
|
310
|
+
return withOutputGateLock(capability === undefined ? undefined : readOutputGate(capability), 'fixture', operation);
|
|
311
|
+
}
|
|
312
|
+
function retireOutputGate(gate, abandoned = false) {
|
|
313
|
+
assertOutputGate(gate, abandoned);
|
|
314
|
+
const requests = join(gate.directory, 'requests');
|
|
315
|
+
const held = join(gate.directory, 'held');
|
|
316
|
+
if (readdirSync(gate.directory).some((name) => !['gate.json', 'requests', 'held'].includes(name)))
|
|
317
|
+
throw new DeliveryError('Filesystem observation gate has unrelated or incomplete metadata.');
|
|
318
|
+
if (existsSync(held)) {
|
|
319
|
+
assertGateDirectory(held);
|
|
320
|
+
if (readdirSync(held).join(',') !== 'owner.json')
|
|
321
|
+
throw new DeliveryError('Filesystem observation gate lock has unrelated metadata.');
|
|
322
|
+
const claim = readGateClaim(gate, join(held, 'owner.json'));
|
|
323
|
+
if (gateOwnerAlive(claim.owner))
|
|
324
|
+
throw new DeliveryError('Filesystem observation gate still has a live holder.');
|
|
325
|
+
removeGateClaim(gate, join(held, 'owner.json'), claim);
|
|
326
|
+
rmdirSync(held);
|
|
327
|
+
}
|
|
328
|
+
for (const name of readdirSync(requests)) {
|
|
329
|
+
if (!/^[a-f0-9-]{36}\.json$/u.test(name))
|
|
330
|
+
throw new DeliveryError('Filesystem observation gate has unrelated metadata.');
|
|
331
|
+
const path = join(requests, name);
|
|
332
|
+
const claim = readGateClaim(gate, path);
|
|
333
|
+
if (claim.kind !== 'scan' || name !== claim.id + '.json')
|
|
334
|
+
throw new DeliveryError('Filesystem observation gate has unrelated scan metadata.');
|
|
335
|
+
if (gateOwnerAlive(claim.owner))
|
|
336
|
+
throw new DeliveryError('Filesystem observation gate still has a live scan request.');
|
|
337
|
+
removeGateClaim(gate, path, claim);
|
|
338
|
+
}
|
|
339
|
+
rmdirSync(requests);
|
|
340
|
+
if (digestValue(JSON.parse(assertPrivateFile(join(gate.directory, 'gate.json')).toString('utf8'))) !==
|
|
341
|
+
digestValue(gate))
|
|
342
|
+
throw new DeliveryError('Filesystem observation gate changed before retirement.');
|
|
343
|
+
unlinkSync(join(gate.directory, 'gate.json'));
|
|
344
|
+
rmdirSync(gate.directory);
|
|
345
|
+
}
|
|
346
|
+
async function withOutputObservationGate(root, enabled, operation) {
|
|
347
|
+
const inherited = process.env[OUTPUT_GATE_ENV];
|
|
348
|
+
if (inherited !== undefined)
|
|
349
|
+
return operation(readOutputGate(inherited));
|
|
350
|
+
if (!enabled)
|
|
351
|
+
return operation(undefined);
|
|
352
|
+
const id = randomUUID();
|
|
353
|
+
const namespace = join(gitCommonDir(root), 'ai-delivery', 'output-observation', worktreeDigest(root).slice(7));
|
|
354
|
+
ensurePrivateDirectoryDurably(namespace);
|
|
355
|
+
// The existing writer has recovered and confirmed old command quiescence before this operation.
|
|
356
|
+
for (const priorId of readdirSync(namespace)) {
|
|
357
|
+
z.uuid().parse(priorId);
|
|
358
|
+
const path = join(namespace, priorId);
|
|
359
|
+
assertGateDirectory(path);
|
|
360
|
+
const prior = OutputGateSchema.parse(JSON.parse(assertPrivateFile(join(path, 'gate.json')).toString('utf8')));
|
|
361
|
+
if (prior.id !== priorId || prior.directory !== path || gateOwnerAlive(prior.owner))
|
|
362
|
+
throw new DeliveryError('Prior filesystem observation gate is live or has conflicting ownership.');
|
|
363
|
+
retireOutputGate(prior, true);
|
|
364
|
+
}
|
|
365
|
+
const directory = join(namespace, id);
|
|
366
|
+
ensurePrivateDirectoryDurably(join(directory, 'requests'));
|
|
367
|
+
const gate = { directory, id, owner: gateOwner() };
|
|
368
|
+
writePrivateJsonFileAtomically(join(directory, 'gate.json'), gate);
|
|
369
|
+
try {
|
|
370
|
+
return await operation(gate);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
retireOutputGate(gate);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
128
376
|
const OutputBaselineCheckpointSchema = z.strictObject({
|
|
129
377
|
baselineId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
130
378
|
files: z.array(z.tuple([z.string().min(1), z.number().int().nonnegative().safe()])),
|
|
@@ -187,7 +435,7 @@ function scanOutputRoots(roots) {
|
|
|
187
435
|
}
|
|
188
436
|
return files;
|
|
189
437
|
}
|
|
190
|
-
function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
|
|
438
|
+
async function outputBaseline(repoRoot, common, classificationReceiptId, bounds, gate, signal) {
|
|
191
439
|
if (bounds.outputRoots === undefined)
|
|
192
440
|
return undefined;
|
|
193
441
|
const roots = bounds.outputRoots.map((path) => resolve(repoRoot, path)).sort();
|
|
@@ -236,7 +484,7 @@ function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
|
|
|
236
484
|
let checkpoint;
|
|
237
485
|
if (state?.status !== 'complete') {
|
|
238
486
|
const content = {
|
|
239
|
-
files: [...scanOutputRoots(roots)].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
487
|
+
files: [...(await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots), signal))].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
240
488
|
inputId,
|
|
241
489
|
roots,
|
|
242
490
|
schemaVersion: 'ai-delivery.output-baseline@1',
|
|
@@ -261,11 +509,11 @@ function outputBaseline(repoRoot, common, classificationReceiptId, bounds) {
|
|
|
261
509
|
checkpoint.files.some(([file], index) => (index > 0 && file <= checkpoint.files[index - 1][0]) ||
|
|
262
510
|
!roots.some((root) => file === root || file.startsWith(`${root}${sep}`))))
|
|
263
511
|
throw new DeliveryError('Filesystem output baseline checkpoint is corrupt or belongs to another input.');
|
|
264
|
-
return { baselineId, roots, files: new Map(checkpoint.files) };
|
|
512
|
+
return { baselineId, roots, files: new Map(checkpoint.files), ...(gate === undefined ? {} : { gate }) };
|
|
265
513
|
}
|
|
266
|
-
function positiveNewOutputBytes(baseline) {
|
|
514
|
+
async function positiveNewOutputBytes(baseline, signal) {
|
|
267
515
|
let bytes = 0;
|
|
268
|
-
for (const [path, size] of scanOutputRoots(baseline.roots)) {
|
|
516
|
+
for (const [path, size] of await withOutputGateLock(baseline.gate, 'scan', () => scanOutputRoots(baseline.roots), signal)) {
|
|
269
517
|
bytes += Math.max(0, size - (baseline.files.get(path) ?? 0));
|
|
270
518
|
if (!Number.isSafeInteger(bytes))
|
|
271
519
|
throw new DeliveryError('Filesystem output observation overflowed.');
|
|
@@ -382,7 +630,7 @@ function observeOwnedProcesses(state, snapshot) {
|
|
|
382
630
|
state.sampled = true;
|
|
383
631
|
return [...selected.values()];
|
|
384
632
|
}
|
|
385
|
-
function sampleOwnedTree(state, repoRoot, bounds, baseline) {
|
|
633
|
+
function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes) {
|
|
386
634
|
const owned = observeOwnedProcesses(state, processSnapshot());
|
|
387
635
|
const rssBytes = owned.reduce((total, member) => total + member.rssBytes, 0);
|
|
388
636
|
if (!Number.isSafeInteger(rssBytes))
|
|
@@ -393,7 +641,6 @@ function sampleOwnedTree(state, repoRoot, bounds, baseline) {
|
|
|
393
641
|
const available = freeDiskBytes(repoRoot, baseline);
|
|
394
642
|
if (available < bounds.minFreeDiskBytes)
|
|
395
643
|
throw new DeliveryError(`Free disk fell below limit ${bounds.minFreeDiskBytes}.`);
|
|
396
|
-
const newOutputBytes = baseline === undefined ? undefined : positiveNewOutputBytes(baseline);
|
|
397
644
|
if (newOutputBytes !== undefined && !Number.isSafeInteger(newOutputBytes)) {
|
|
398
645
|
throw new DeliveryError('Filesystem output observation overflowed.');
|
|
399
646
|
}
|
|
@@ -492,9 +739,30 @@ const WriterStateSchema = z
|
|
|
492
739
|
if (stateId !== digestValue(content))
|
|
493
740
|
ctx.addIssue({ code: 'custom', message: 'Writer state is corrupt.' });
|
|
494
741
|
});
|
|
495
|
-
|
|
742
|
+
/** @internal Read-only closure check shared by inspection and the locked transition lease. */
|
|
743
|
+
export function assertWorktreeTransitionWriterQuiescent(worktreePath, bytes) {
|
|
744
|
+
if (bytes === undefined)
|
|
745
|
+
return;
|
|
746
|
+
const previous = WriterStateSchema.parse(JSON.parse(bytes.toString('utf8')));
|
|
747
|
+
if (previous.worktreeDigest !== worktreeDigest(worktreePath))
|
|
748
|
+
throw new DeliveryError('Verification writer belongs to another worktree.');
|
|
749
|
+
const snapshot = processSnapshot();
|
|
750
|
+
const owner = snapshot.get(previous.owner.pid);
|
|
751
|
+
if (owner?.identity === previous.owner.identity && !owner.status.startsWith('Z'))
|
|
752
|
+
throw new DeliveryError('A live verification writer still owns this worktree.');
|
|
753
|
+
if (previous.command.phase === 'starting')
|
|
754
|
+
throw new DeliveryError('Interrupted writer has unknown command ownership; reconcile before retrying.');
|
|
755
|
+
if (previous.command.phase === 'running') {
|
|
756
|
+
const identities = [previous.command.root, ...previous.command.tracked];
|
|
757
|
+
const groups = new Set([previous.command.root.pid, ...previous.command.tracked.map((member) => member.pgid)]);
|
|
758
|
+
if (identities.some((member) => snapshot.has(member.pid)) ||
|
|
759
|
+
[...snapshot.values()].some((member) => groups.has(member.pgid)))
|
|
760
|
+
throw new DeliveryError('Transition requires identity-bound command and process-group absence.');
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function withVerificationWriter(root, operation, transition, commonDirectory) {
|
|
496
764
|
const digest = worktreeDigest(root);
|
|
497
|
-
const path = join(gitCommonDir(root), 'ai-delivery', 'writers@1', `${digest.slice(7)}.json`);
|
|
765
|
+
const path = join(commonDirectory ?? gitCommonDir(root), 'ai-delivery', 'writers@1', `${digest.slice(7)}.json`);
|
|
498
766
|
ensurePrivateDirectoryDurably(dirname(path));
|
|
499
767
|
return withLock(path, {
|
|
500
768
|
projectRoot: root,
|
|
@@ -504,8 +772,23 @@ async function withVerificationWriter(root, operation) {
|
|
|
504
772
|
const owner = snapshot.get(process.pid);
|
|
505
773
|
if (owner === undefined || owner.status.startsWith('Z'))
|
|
506
774
|
throw new DeliveryError('Verification writer identity is unknown.');
|
|
507
|
-
|
|
508
|
-
|
|
775
|
+
let previousBytes;
|
|
776
|
+
if (transition) {
|
|
777
|
+
try {
|
|
778
|
+
lstatSync(path);
|
|
779
|
+
previousBytes = assertPrivateFile(path);
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
if (error.code !== 'ENOENT')
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
else if (existsSync(path))
|
|
787
|
+
previousBytes = assertPrivateFile(path);
|
|
788
|
+
if (transition)
|
|
789
|
+
assertWorktreeTransitionWriterQuiescent(root, previousBytes);
|
|
790
|
+
if (previousBytes !== undefined && !transition) {
|
|
791
|
+
const previous = WriterStateSchema.parse(JSON.parse(previousBytes.toString('utf8')));
|
|
509
792
|
if (previous.worktreeDigest !== digest)
|
|
510
793
|
throw new DeliveryError('Verification writer belongs to another worktree.');
|
|
511
794
|
const priorOwner = snapshot.get(previous.owner.pid);
|
|
@@ -533,9 +816,15 @@ async function withVerificationWriter(root, operation) {
|
|
|
533
816
|
});
|
|
534
817
|
}
|
|
535
818
|
}
|
|
819
|
+
if (transition) {
|
|
820
|
+
await transition.beforeClaim({
|
|
821
|
+
previousBytes: previousBytes === undefined ? undefined : Buffer.from(previousBytes),
|
|
822
|
+
owner: { pid: owner.pid, identity: owner.identity },
|
|
823
|
+
});
|
|
824
|
+
}
|
|
536
825
|
let state = {
|
|
537
826
|
schemaVersion: 'ai-delivery.verification-writer@1',
|
|
538
|
-
writerId: randomUUID(),
|
|
827
|
+
writerId: transition?.writerId ?? randomUUID(),
|
|
539
828
|
worktreeDigest: digest,
|
|
540
829
|
owner: { pid: owner.pid, identity: owner.identity },
|
|
541
830
|
command: { phase: 'idle' },
|
|
@@ -544,6 +833,22 @@ async function withVerificationWriter(root, operation) {
|
|
|
544
833
|
state = { ...state, command };
|
|
545
834
|
writePrivateJsonFileAtomically(path, { ...state, stateId: digestValue(state) });
|
|
546
835
|
};
|
|
836
|
+
if (transition) {
|
|
837
|
+
const bytes = Buffer.from(`${JSON.stringify({ ...state, stateId: digestValue(state) }, null, 2)}\n`);
|
|
838
|
+
await transition.recordClaim(Buffer.from(bytes));
|
|
839
|
+
let actual;
|
|
840
|
+
try {
|
|
841
|
+
lstatSync(path);
|
|
842
|
+
actual = assertPrivateFile(path);
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
if (error.code !== 'ENOENT')
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
if ((actual === undefined) !== (previousBytes === undefined) ||
|
|
849
|
+
(actual !== undefined && previousBytes !== undefined && !actual.equals(previousBytes)))
|
|
850
|
+
throw new DeliveryError('Writer changed after transition intent preservation.');
|
|
851
|
+
}
|
|
547
852
|
persist({ phase: 'idle' });
|
|
548
853
|
const writer = {
|
|
549
854
|
starting: () => persist({ phase: 'starting' }),
|
|
@@ -563,22 +868,80 @@ async function withVerificationWriter(root, operation) {
|
|
|
563
868
|
throw new DeliveryError('Verification writer command quiescence is not confirmed.');
|
|
564
869
|
},
|
|
565
870
|
};
|
|
566
|
-
const releaseWriter = () => {
|
|
567
|
-
const
|
|
871
|
+
const releaseWriter = async () => {
|
|
872
|
+
const bytes = assertPrivateFile(path);
|
|
873
|
+
const current = WriterStateSchema.parse(JSON.parse(bytes.toString('utf8')));
|
|
568
874
|
if (current.writerId !== state.writerId)
|
|
569
875
|
throw new DeliveryError('Verification writer ownership changed.');
|
|
570
|
-
if (current.command.phase === 'idle')
|
|
876
|
+
if (current.command.phase === 'idle') {
|
|
877
|
+
if (transition) {
|
|
878
|
+
await transition.recordRelease(Buffer.from(bytes));
|
|
879
|
+
if (!assertPrivateFile(path).equals(bytes))
|
|
880
|
+
throw new DeliveryError('Writer changed before recorded release.');
|
|
881
|
+
}
|
|
571
882
|
unlinkSync(path);
|
|
883
|
+
if (transition) {
|
|
884
|
+
const descriptor = openSync(dirname(path), 'r');
|
|
885
|
+
try {
|
|
886
|
+
fsyncSync(descriptor);
|
|
887
|
+
}
|
|
888
|
+
finally {
|
|
889
|
+
closeSync(descriptor);
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
lstatSync(path);
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
if (error.code !== 'ENOENT')
|
|
896
|
+
throw error;
|
|
897
|
+
await transition.afterRelease?.();
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
throw new DeliveryError('Writer absence is not confirmed after recorded release.');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
572
903
|
};
|
|
573
904
|
try {
|
|
905
|
+
await transition?.afterClaim?.(Buffer.from(assertPrivateFile(path)));
|
|
574
906
|
return await operation(writer);
|
|
575
907
|
}
|
|
576
908
|
finally {
|
|
577
|
-
releaseWriter();
|
|
909
|
+
await releaseWriter();
|
|
578
910
|
}
|
|
579
911
|
},
|
|
580
912
|
});
|
|
581
913
|
}
|
|
914
|
+
/** @internal Hold the existing writer lock while reading a sealed transition; never claim or recover its slot. */
|
|
915
|
+
export async function withWorktreeTransitionWriterAbsent(repoRoot, worktreePath, operation) {
|
|
916
|
+
const path = join(gitCommonDir(repoRoot), 'ai-delivery', 'writers@1', `${worktreeDigest(worktreePath).slice(7)}.json`);
|
|
917
|
+
return withLock(path, {
|
|
918
|
+
projectRoot: repoRoot,
|
|
919
|
+
timeout: 200,
|
|
920
|
+
operation: async () => {
|
|
921
|
+
try {
|
|
922
|
+
lstatSync(path);
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
if (error.code !== 'ENOENT')
|
|
926
|
+
throw error;
|
|
927
|
+
return operation();
|
|
928
|
+
}
|
|
929
|
+
throw new DeliveryError('Sealed transition writer absence postcondition drifted.');
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
/** @internal Check-only lease for the explicit transition owner; never recovers or signals old commands. */
|
|
934
|
+
export async function withWorktreeTransitionWriterLease(input, operation) {
|
|
935
|
+
z.uuid().parse(input.writerId);
|
|
936
|
+
const repository = gitRoot(input.repoRoot);
|
|
937
|
+
const subject = input.worktreePath === undefined ? repository : resolve(input.worktreePath);
|
|
938
|
+
return withVerificationWriter(subject, async (writer) => {
|
|
939
|
+
writer.assertQuiescent();
|
|
940
|
+
const result = await operation();
|
|
941
|
+
writer.assertQuiescent();
|
|
942
|
+
return result;
|
|
943
|
+
}, input, gitCommonDir(repository));
|
|
944
|
+
}
|
|
582
945
|
function primaryRoot(worktreeRoot) {
|
|
583
946
|
const common = gitCommonDir(worktreeRoot);
|
|
584
947
|
if (basename(common) !== '.git')
|
|
@@ -597,8 +960,7 @@ function assertRegisteredIssue(worktreeRoot, issueNumber, allowMerged = false) {
|
|
|
597
960
|
function worktreeDigest(root) {
|
|
598
961
|
return digestValue(resolve(root));
|
|
599
962
|
}
|
|
600
|
-
function
|
|
601
|
-
const root = dirname(fileURLToPath(import.meta.url));
|
|
963
|
+
function producerContent(root) {
|
|
602
964
|
const code = [];
|
|
603
965
|
const collect = (directory) => {
|
|
604
966
|
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
@@ -646,7 +1008,22 @@ function producerDigest() {
|
|
|
646
1008
|
}
|
|
647
1009
|
};
|
|
648
1010
|
collectDependencies(join(dirname(root), 'package.json'));
|
|
649
|
-
return
|
|
1011
|
+
return { code, dependencies: [...dependencies].sort(([a], [b]) => a.localeCompare(b)) };
|
|
1012
|
+
}
|
|
1013
|
+
function producerDigest(root = dirname(fileURLToPath(import.meta.url))) {
|
|
1014
|
+
return digestValue(producerContent(root));
|
|
1015
|
+
}
|
|
1016
|
+
/** @internal Retained public code is hashed as data; it is never imported or executed. */
|
|
1017
|
+
export function retainedWorktreeTransitionProducerDigest(distDirectory) {
|
|
1018
|
+
return producerDigest(realpathSync(distDirectory));
|
|
1019
|
+
}
|
|
1020
|
+
/** @internal Preserve the same dependency manifests that participate in the existing producer digest. */
|
|
1021
|
+
export function retainedWorktreeTransitionDependencyManifests(distDirectory) {
|
|
1022
|
+
return producerContent(realpathSync(distDirectory)).dependencies.map(([source, digest]) => ({ source, digest }));
|
|
1023
|
+
}
|
|
1024
|
+
/** @internal Original manifests validate as history and cannot authorize current verification. */
|
|
1025
|
+
export function readWorktreeTransitionRun(bytes) {
|
|
1026
|
+
return parseRun(JSON.parse(bytes.toString('utf8')));
|
|
650
1027
|
}
|
|
651
1028
|
function manifestPath(common, headSha, root) {
|
|
652
1029
|
return root === undefined
|
|
@@ -756,6 +1133,9 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
756
1133
|
cwd: repoRoot,
|
|
757
1134
|
detached: process.platform !== 'win32',
|
|
758
1135
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1136
|
+
...(baseline?.gate === undefined
|
|
1137
|
+
? {}
|
|
1138
|
+
: { env: { ...process.env, [OUTPUT_GATE_ENV]: JSON.stringify(baseline.gate) } }),
|
|
759
1139
|
});
|
|
760
1140
|
const stdout = [];
|
|
761
1141
|
const stderr = [];
|
|
@@ -769,6 +1149,8 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
769
1149
|
let closed = false;
|
|
770
1150
|
let settled = false;
|
|
771
1151
|
let commandReleased = false;
|
|
1152
|
+
let pendingOutput;
|
|
1153
|
+
const scanCancellation = new AbortController();
|
|
772
1154
|
let closeTimer;
|
|
773
1155
|
let exitPipeTimer;
|
|
774
1156
|
let resourceTimer;
|
|
@@ -825,6 +1207,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
825
1207
|
if (failure !== undefined)
|
|
826
1208
|
return;
|
|
827
1209
|
failure = reason;
|
|
1210
|
+
scanCancellation.abort();
|
|
828
1211
|
killOwned();
|
|
829
1212
|
if (!closed) {
|
|
830
1213
|
closeTimer = setTimeout(() => {
|
|
@@ -881,10 +1264,26 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
881
1264
|
onResourceSample?.(result);
|
|
882
1265
|
onRunning?.(outputBytes, result);
|
|
883
1266
|
}
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1267
|
+
if (closed || pendingOutput !== undefined)
|
|
1268
|
+
return;
|
|
1269
|
+
pendingOutput = (async () => {
|
|
1270
|
+
if (resourceBounds !== undefined && baseline !== undefined) {
|
|
1271
|
+
const bytes = await positiveNewOutputBytes(baseline, scanCancellation.signal);
|
|
1272
|
+
if (failure !== undefined)
|
|
1273
|
+
return;
|
|
1274
|
+
const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, bytes);
|
|
1275
|
+
onResourceSample?.(result);
|
|
1276
|
+
onRunning?.(outputBytes, result);
|
|
1277
|
+
}
|
|
1278
|
+
if (failure === undefined && !commandReleased) {
|
|
1279
|
+
commandReleased = true;
|
|
1280
|
+
child.stdin.end('run\n');
|
|
1281
|
+
}
|
|
1282
|
+
})()
|
|
1283
|
+
.catch((error) => stop(error instanceof Error ? error.message : String(error)))
|
|
1284
|
+
.finally(() => {
|
|
1285
|
+
pendingOutput = undefined;
|
|
1286
|
+
});
|
|
888
1287
|
}
|
|
889
1288
|
catch (error) {
|
|
890
1289
|
stop(error instanceof Error ? error.message : String(error));
|
|
@@ -908,11 +1307,11 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
908
1307
|
closed = true;
|
|
909
1308
|
if (settled)
|
|
910
1309
|
return;
|
|
911
|
-
|
|
1310
|
+
await pendingOutput;
|
|
912
1311
|
if (failure === undefined && owned !== undefined) {
|
|
913
1312
|
try {
|
|
914
1313
|
if (resourceBounds !== undefined) {
|
|
915
|
-
const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline);
|
|
1314
|
+
const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, baseline === undefined ? undefined : await positiveNewOutputBytes(baseline, scanCancellation.signal));
|
|
916
1315
|
onResourceSample?.(sample);
|
|
917
1316
|
onRunning?.(outputBytes, sample);
|
|
918
1317
|
}
|
|
@@ -955,18 +1354,20 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
955
1354
|
resolve(Buffer.concat([...stdout, ...stderr], outputBytes));
|
|
956
1355
|
};
|
|
957
1356
|
child.once('close', (code, signal) => {
|
|
958
|
-
void handleClose(code, signal)
|
|
1357
|
+
void handleClose(code, signal)
|
|
1358
|
+
.catch((error) => {
|
|
959
1359
|
settled = true;
|
|
960
1360
|
reject(error);
|
|
961
|
-
})
|
|
1361
|
+
})
|
|
1362
|
+
.finally(clearWatchers);
|
|
962
1363
|
});
|
|
963
1364
|
});
|
|
964
1365
|
}
|
|
965
1366
|
export async function verifyIssue(input) {
|
|
966
1367
|
const root = gitRoot(input.repoRoot);
|
|
967
|
-
return withVerificationWriter(root, (writer) => verifyIssueOwned(input, writer));
|
|
1368
|
+
return withVerificationWriter(root, (writer) => withOutputObservationGate(root, input.resourceBounds?.outputRoots !== undefined, (gate) => verifyIssueOwned(input, writer, gate)));
|
|
968
1369
|
}
|
|
969
|
-
async function verifyIssueOwned(input, writer) {
|
|
1370
|
+
async function verifyIssueOwned(input, writer, gate) {
|
|
970
1371
|
const assertNotCancelled = () => {
|
|
971
1372
|
if (input.signal?.aborted)
|
|
972
1373
|
throw new DeliveryError('Verification cancelled.');
|
|
@@ -996,7 +1397,7 @@ async function verifyIssueOwned(input, writer) {
|
|
|
996
1397
|
});
|
|
997
1398
|
const baseline = input.resourceBounds === undefined
|
|
998
1399
|
? undefined
|
|
999
|
-
: outputBaseline(root, common, classification.receiptId, input.resourceBounds);
|
|
1400
|
+
: await outputBaseline(root, common, classification.receiptId, input.resourceBounds, gate, input.signal);
|
|
1000
1401
|
if (input.resourceBounds !== undefined)
|
|
1001
1402
|
assertDiskHeadroom(root, input.resourceBounds, baseline);
|
|
1002
1403
|
const receipts = [];
|
|
@@ -1149,6 +1550,9 @@ async function verifyIssueOwned(input, writer) {
|
|
|
1149
1550
|
sampledFreeDiskBytes: sample.freeDiskBytes,
|
|
1150
1551
|
...(sample.newOutputBytes === undefined ? {} : { sampledNewOutputBytes: sample.newOutputBytes }),
|
|
1151
1552
|
ownedProcessCount: sample.ownedProcessCount,
|
|
1553
|
+
...(baseline !== undefined && sample.newOutputBytes === undefined
|
|
1554
|
+
? { reason: 'waiting for strict filesystem output scan' }
|
|
1555
|
+
: {}),
|
|
1152
1556
|
}),
|
|
1153
1557
|
});
|
|
1154
1558
|
progress(0);
|
|
@@ -1211,6 +1615,19 @@ async function verifyIssueOwned(input, writer) {
|
|
|
1211
1615
|
});
|
|
1212
1616
|
}
|
|
1213
1617
|
assertNotCancelled();
|
|
1618
|
+
if (baseline !== undefined && input.resourceBounds !== undefined) {
|
|
1619
|
+
const bytes = await positiveNewOutputBytes(baseline, input.signal);
|
|
1620
|
+
if (bytes > input.resourceBounds.maxNewOutputBytes)
|
|
1621
|
+
throw new DeliveryError('Positive new filesystem output ' + bytes + ' exceeded limit ' + input.resourceBounds.maxNewOutputBytes + '.');
|
|
1622
|
+
assertDiskHeadroom(root, input.resourceBounds, baseline);
|
|
1623
|
+
recordResourceSample({
|
|
1624
|
+
aggregateRssBytes: 0,
|
|
1625
|
+
freeDiskBytes: freeDiskBytes(root, baseline),
|
|
1626
|
+
newOutputBytes: bytes,
|
|
1627
|
+
ownedProcessCount: 0,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
assertNotCancelled();
|
|
1214
1631
|
assertRepositoryClassificationCurrent({
|
|
1215
1632
|
classification,
|
|
1216
1633
|
configDigest: loaded.configDigest,
|
|
@@ -1299,41 +1716,48 @@ export async function withRuntimeSetupWriter(root, operation) {
|
|
|
1299
1716
|
if (roots[i] === roots[i - 1] || roots[i].startsWith(`${roots[i - 1]}${sep}`))
|
|
1300
1717
|
throw new DeliveryError('Filesystem output roots must not overlap.');
|
|
1301
1718
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
baselineId: digestValue([...files]),
|
|
1309
|
-
};
|
|
1310
|
-
assertDiskHeadroom(root, bounds, baseline);
|
|
1311
|
-
const started = Date.now();
|
|
1312
|
-
const output = await runStageCommand(root, argv, signal, (capturedOutputBytes, sample) => reportVerificationProgress({
|
|
1313
|
-
state: 'running',
|
|
1314
|
-
stageId: 'runtime-setup',
|
|
1315
|
-
completedStages: 0,
|
|
1316
|
-
reusedStages: 0,
|
|
1317
|
-
remainingStages: 1,
|
|
1318
|
-
elapsedMs: Date.now() - started,
|
|
1319
|
-
capturedOutputBytes,
|
|
1320
|
-
...(sample === undefined
|
|
1321
|
-
? {}
|
|
1719
|
+
return withOutputObservationGate(root, roots !== undefined, async (gate) => {
|
|
1720
|
+
const files = roots === undefined
|
|
1721
|
+
? undefined
|
|
1722
|
+
: await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots), signal);
|
|
1723
|
+
const baseline = roots === undefined || files === undefined
|
|
1724
|
+
? undefined
|
|
1322
1725
|
: {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1726
|
+
roots,
|
|
1727
|
+
files,
|
|
1728
|
+
baselineId: digestValue([...files]),
|
|
1729
|
+
...(gate === undefined ? {} : { gate }),
|
|
1730
|
+
};
|
|
1731
|
+
assertDiskHeadroom(root, bounds, baseline);
|
|
1732
|
+
const started = Date.now();
|
|
1733
|
+
const output = await runStageCommand(root, argv, signal, (capturedOutputBytes, sample) => reportVerificationProgress({
|
|
1734
|
+
state: 'running',
|
|
1735
|
+
stageId: 'runtime-setup',
|
|
1736
|
+
completedStages: 0,
|
|
1737
|
+
reusedStages: 0,
|
|
1738
|
+
remainingStages: 1,
|
|
1739
|
+
elapsedMs: Date.now() - started,
|
|
1740
|
+
capturedOutputBytes,
|
|
1741
|
+
...(sample === undefined
|
|
1742
|
+
? {}
|
|
1743
|
+
: {
|
|
1744
|
+
sampledAggregateRssBytes: sample.aggregateRssBytes,
|
|
1745
|
+
sampledFreeDiskBytes: sample.freeDiskBytes,
|
|
1746
|
+
sampledNewOutputBytes: sample.newOutputBytes,
|
|
1747
|
+
}),
|
|
1748
|
+
}), bounds, baseline, undefined, writer);
|
|
1749
|
+
if (signal?.aborted)
|
|
1750
|
+
throw new DeliveryError('Runtime setup cancelled.');
|
|
1751
|
+
reportVerificationProgress({
|
|
1752
|
+
state: 'completed',
|
|
1753
|
+
stageId: 'runtime-setup',
|
|
1754
|
+
completedStages: 1,
|
|
1755
|
+
reusedStages: 0,
|
|
1756
|
+
remainingStages: 0,
|
|
1757
|
+
elapsedMs: Date.now() - started,
|
|
1758
|
+
});
|
|
1759
|
+
return output;
|
|
1335
1760
|
});
|
|
1336
|
-
return output;
|
|
1337
1761
|
},
|
|
1338
1762
|
}));
|
|
1339
1763
|
}
|