@aviaratech/ai-delivery 0.3.22 → 0.3.24
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/CONTRIBUTING.md +9 -0
- package/README.md +77 -442
- package/dist/agent.d.ts +5 -11
- package/dist/agent.js +4 -8
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +59 -107
- package/dist/cli.js.map +1 -1
- package/dist/config/deliveryConfig.d.ts +44 -55
- package/dist/config/deliveryConfig.js +85 -88
- package/dist/config/deliveryConfig.js.map +1 -1
- package/dist/config/deliveryConfig.test.js +13 -5
- package/dist/config/deliveryConfig.test.js.map +1 -1
- package/dist/delivery/common.d.ts +4 -1
- package/dist/delivery/common.js +56 -8
- package/dist/delivery/common.js.map +1 -1
- package/dist/delivery/delivery.test.js +2 -3
- package/dist/delivery/delivery.test.js.map +1 -1
- package/dist/delivery/index.d.ts +0 -6
- package/dist/delivery/index.js +0 -3
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/legacy.d.ts +8 -0
- package/dist/delivery/legacy.js +5 -0
- package/dist/delivery/legacy.js.map +1 -0
- package/dist/delivery/policy.js +2 -13
- package/dist/delivery/policy.js.map +1 -1
- package/dist/delivery/stage.d.ts +6 -0
- package/dist/delivery/stage.js +11 -2
- package/dist/delivery/stage.js.map +1 -1
- package/dist/directoryIndependent.test.d.ts +1 -0
- package/dist/directoryIndependent.test.js +262 -0
- package/dist/directoryIndependent.test.js.map +1 -0
- package/dist/dispatch.d.ts +29 -3
- package/dist/dispatch.js +160 -457
- package/dist/dispatch.js.map +1 -1
- package/dist/genericCompatibility.test.js +0 -35
- package/dist/genericCompatibility.test.js.map +1 -1
- package/dist/git.js +10 -3
- package/dist/git.js.map +1 -1
- package/dist/gitProcess.d.ts +6 -0
- package/dist/gitProcess.js +56 -0
- package/dist/gitProcess.js.map +1 -0
- package/dist/github/discovery.d.ts +1 -0
- package/dist/github/discovery.js +1 -1
- package/dist/github/discovery.js.map +1 -1
- package/dist/github/discovery.test.js +25 -20
- package/dist/github/discovery.test.js.map +1 -1
- package/dist/github/nativeIssueMetadata.d.ts +2 -2
- package/dist/github/nativeIssueMetadata.js.map +1 -1
- package/dist/github/projectDelivery.d.ts +1 -1
- package/dist/github/projectDelivery.js.map +1 -1
- package/dist/github/repo.d.ts +7 -1
- package/dist/github/repo.js +58 -2
- package/dist/github/repo.js.map +1 -1
- package/dist/issue.d.ts +11 -6
- package/dist/issue.js +136 -319
- package/dist/issue.js.map +1 -1
- package/dist/issueJournal.d.ts +1 -1
- package/dist/lifecycle.test.js +40 -7371
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/logger.js +4 -4
- package/dist/logger.js.map +1 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +27 -42
- package/dist/mcp/tools.js +34 -42
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pluginInstaller.d.ts +51 -0
- package/dist/pluginInstaller.js +774 -0
- package/dist/pluginInstaller.js.map +1 -0
- package/dist/pluginInstaller.test.d.ts +1 -0
- package/dist/pluginInstaller.test.js +543 -0
- package/dist/pluginInstaller.test.js.map +1 -0
- package/dist/pluginPackaging.test.d.ts +1 -0
- package/dist/pluginPackaging.test.js +245 -0
- package/dist/pluginPackaging.test.js.map +1 -0
- package/dist/pr.d.ts +66 -114
- package/dist/pr.js +288 -1108
- package/dist/pr.js.map +1 -1
- package/dist/pr.test.js +295 -237
- package/dist/pr.test.js.map +1 -1
- package/dist/releaseReconciliation.test.d.ts +1 -0
- package/dist/releaseReconciliation.test.js +314 -0
- package/dist/releaseReconciliation.test.js.map +1 -0
- package/dist/review.d.ts +37 -7
- package/dist/review.js +106 -45
- package/dist/review.js.map +1 -1
- package/dist/review.test.js +30 -71
- package/dist/review.test.js.map +1 -1
- package/dist/services/agentReadinessService.d.ts +1 -1
- package/dist/services/agentReadinessService.js +2 -0
- package/dist/services/agentReadinessService.js.map +1 -1
- package/dist/services/deliveryAdmission.d.ts +4 -4
- package/dist/services/deliveryAdmission.js.map +1 -1
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +71 -10
- package/dist/setup.js.map +1 -1
- package/dist/setup.test.js +594 -204
- package/dist/setup.test.js.map +1 -1
- package/dist/start.test.d.ts +1 -0
- package/dist/start.test.js +72 -0
- package/dist/start.test.js.map +1 -0
- package/dist/stdoutRegression.test.d.ts +1 -0
- package/dist/stdoutRegression.test.js +326 -0
- package/dist/stdoutRegression.test.js.map +1 -0
- package/dist/verification.d.ts +62 -27
- package/dist/verification.js +172 -1337
- package/dist/verification.js.map +1 -1
- package/dist/worktree.d.ts +1 -31
- package/dist/worktree.js +5 -200
- package/dist/worktree.js.map +1 -1
- package/dist/worktreeTransition.d.ts +1 -1
- package/dist/worktreeTransition.js +10 -5
- package/dist/worktreeTransition.js.map +1 -1
- package/dist/worktreeTransition.test.js +2 -3
- package/dist/worktreeTransition.test.js.map +1 -1
- package/package.json +4 -4
- package/plugins/ai-delivery/.claude-plugin/plugin.json +6 -1
- package/plugins/ai-delivery/LICENSE +21 -0
- package/plugins/ai-delivery/README.md +4 -2
- package/plugins/ai-delivery/dist/mcp-launcher.js +19 -8
- package/plugins/ai-delivery/mcp.json +10 -0
- package/plugins/ai-delivery/plugin.json +10 -0
- package/plugins/ai-delivery/runtime/dist/THIRD-PARTY-NOTICES.md +1010 -0
- package/plugins/ai-delivery/runtime/dist/cli.js +276 -0
- package/plugins/ai-delivery/runtime/package.json +13 -0
- package/plugins/ai-delivery/skills/intake-create/SKILL.md +1 -1
- package/plugins/ai-delivery/skills/pr-handoff/SKILL.md +14 -21
- package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +12 -23
- package/dist/services/deliveryRecordService.d.ts +0 -47
- package/dist/services/deliveryRecordService.js +0 -199
- package/dist/services/deliveryRecordService.js.map +0 -1
package/dist/verification.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { closeSync, existsSync, fsyncSync,
|
|
5
|
-
import {
|
|
4
|
+
import { closeSync, existsSync, fsyncSync, openSync, unlinkSync, lstatSync, mkdirSync, rmdirSync, renameSync, readFileSync, readlinkSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import { loadDeliveryConfig } from './config/deliveryConfig.js';
|
|
9
8
|
import { assertPrivateFile } from './delivery/common.js';
|
|
10
|
-
import {
|
|
9
|
+
import { readRepositoryCommandOutput } from './delivery/stage.js';
|
|
10
|
+
import { createRepositoryStageAggregate, digestBytes, digestValue, RepositoryClassificationReceiptSchema, RepositoryStageAggregateSchema, RepositoryStageReceiptSchema, writeRepositoryCommandOutput, } from './delivery/legacy.js';
|
|
11
11
|
import { DeliveryError } from './errors.js';
|
|
12
|
-
import {
|
|
13
|
-
import { assertClean, changedPaths, coordinate, defaultBaseRef, git, gitCommonDir, gitRoot } from './git.js';
|
|
14
|
-
import { assertAiDeliveryWorktreeOwner, getIssueWorktreeStrict } from './services/worktreeRegistry.js';
|
|
15
|
-
import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
|
|
12
|
+
import { gitCommonDir, gitRoot } from './git.js';
|
|
16
13
|
import { withLock } from './utils/lockfile.js';
|
|
17
14
|
import { ensurePrivateDirectoryDurably, writePrivateJsonFileAtomically } from './utils/atomicJson.js';
|
|
18
15
|
const ResourceSummarySchema = z
|
|
@@ -49,63 +46,6 @@ const ResourceSummarySchema = z
|
|
|
49
46
|
(value.bounds.maxNewOutputBytes !== undefined) !== (value.outputBaselineId !== undefined))
|
|
50
47
|
ctx.addIssue({ code: 'custom', message: 'Resource samples do not satisfy their declared bounds.' });
|
|
51
48
|
});
|
|
52
|
-
const ResourceStageCheckpointSchema = z.strictObject({
|
|
53
|
-
checkpointId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
54
|
-
receipt: RepositoryStageReceiptSchema,
|
|
55
|
-
resources: ResourceSummarySchema,
|
|
56
|
-
schemaVersion: z.literal('ai-delivery.resource-stage@1'),
|
|
57
|
-
});
|
|
58
|
-
function resourceStagePath(common, stageInput) {
|
|
59
|
-
return join(common, 'ai-delivery', 'resource-stages@1', `${stageInput.inputId.slice(7)}.json`);
|
|
60
|
-
}
|
|
61
|
-
function loadResourceStageCheckpoint(common, repoRoot, stageInput, bounds, baselineId) {
|
|
62
|
-
let raw;
|
|
63
|
-
try {
|
|
64
|
-
raw = JSON.parse(readFileSync(resourceStagePath(common, stageInput), 'utf8'));
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
if (error.code === 'ENOENT')
|
|
68
|
-
return undefined;
|
|
69
|
-
throw new DeliveryError('Resource stage checkpoint is corrupt.');
|
|
70
|
-
}
|
|
71
|
-
const checkpoint = ResourceStageCheckpointSchema.parse(raw);
|
|
72
|
-
const { checkpointId, ...content } = checkpoint;
|
|
73
|
-
if (checkpointId !== digestValue(content) ||
|
|
74
|
-
checkpoint.receipt.input.inputId !== stageInput.inputId ||
|
|
75
|
-
digestValue(checkpoint.resources.bounds) !== digestValue(bounds) ||
|
|
76
|
-
checkpoint.resources.outputBaselineId !== baselineId ||
|
|
77
|
-
checkpoint.resources.sampleCount === 0)
|
|
78
|
-
throw new DeliveryError('Resource stage checkpoint is corrupt or belongs to another input.');
|
|
79
|
-
assertRepositoryStageProof({ gitCommonDir: common, receipt: checkpoint.receipt, repoRoot });
|
|
80
|
-
return checkpoint;
|
|
81
|
-
}
|
|
82
|
-
function writeResourceStageCheckpoint(common, receipt, resources) {
|
|
83
|
-
if (resources.sampleCount === 0)
|
|
84
|
-
throw new DeliveryError('Completed bounded stage lacks resource samples.');
|
|
85
|
-
const content = {
|
|
86
|
-
receipt,
|
|
87
|
-
resources: ResourceSummarySchema.parse(resources),
|
|
88
|
-
schemaVersion: 'ai-delivery.resource-stage@1',
|
|
89
|
-
};
|
|
90
|
-
writePrivateJsonFileAtomically(resourceStagePath(common, receipt.input), {
|
|
91
|
-
...content,
|
|
92
|
-
checkpointId: digestValue(content),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
function mergeResourceSummary(target, source) {
|
|
96
|
-
target.sampleCount += source.sampleCount;
|
|
97
|
-
if (!Number.isSafeInteger(target.sampleCount))
|
|
98
|
-
throw new DeliveryError('Resource sample count overflowed.');
|
|
99
|
-
if (source.maxSampledAggregateRssBytes !== null) {
|
|
100
|
-
target.maxSampledAggregateRssBytes = Math.max(target.maxSampledAggregateRssBytes ?? 0, source.maxSampledAggregateRssBytes);
|
|
101
|
-
}
|
|
102
|
-
if (source.maxSampledNewOutputBytes !== null) {
|
|
103
|
-
target.maxSampledNewOutputBytes = Math.max(target.maxSampledNewOutputBytes ?? 0, source.maxSampledNewOutputBytes);
|
|
104
|
-
}
|
|
105
|
-
if (source.minSampledFreeDiskBytes !== null) {
|
|
106
|
-
target.minSampledFreeDiskBytes = Math.min(target.minSampledFreeDiskBytes ?? source.minSampledFreeDiskBytes, source.minSampledFreeDiskBytes);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
49
|
function assertResourceBounds(bounds) {
|
|
110
50
|
const limits = [
|
|
111
51
|
['maxAggregateRssBytes', bounds.maxAggregateRssBytes],
|
|
@@ -375,23 +315,6 @@ async function withOutputObservationGate(root, enabled, operation) {
|
|
|
375
315
|
retireOutputGate(gate);
|
|
376
316
|
}
|
|
377
317
|
}
|
|
378
|
-
const OutputBaselineCheckpointSchema = z.strictObject({
|
|
379
|
-
baselineId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
380
|
-
files: z.array(z.tuple([z.string().min(1), z.number().int().nonnegative().safe()])),
|
|
381
|
-
inputId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
382
|
-
roots: z.array(z.string().min(1)).min(1),
|
|
383
|
-
schemaVersion: z.literal('ai-delivery.output-baseline@1'),
|
|
384
|
-
});
|
|
385
|
-
const OutputBaselineStateSchema = z.strictObject({
|
|
386
|
-
baselineId: z
|
|
387
|
-
.string()
|
|
388
|
-
.regex(/^sha256:[a-f0-9]{64}$/u)
|
|
389
|
-
.optional(),
|
|
390
|
-
inputId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
391
|
-
schemaVersion: z.literal('ai-delivery.output-baseline-state@1'),
|
|
392
|
-
stateId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
393
|
-
status: z.enum(['preparing', 'complete']),
|
|
394
|
-
});
|
|
395
318
|
class OutputObservationError extends DeliveryError {
|
|
396
319
|
cause;
|
|
397
320
|
code;
|
|
@@ -563,83 +486,6 @@ function scanOutputRoots(roots, links, commandRunning = false, onMissingTarget)
|
|
|
563
486
|
links.set(path, link);
|
|
564
487
|
return files;
|
|
565
488
|
}
|
|
566
|
-
async function outputBaseline(repoRoot, common, classificationReceiptId, bounds, gate, signal) {
|
|
567
|
-
if (bounds.outputRoots === undefined)
|
|
568
|
-
return undefined;
|
|
569
|
-
const roots = bounds.outputRoots.map((path) => resolve(repoRoot, path)).sort();
|
|
570
|
-
const links = new Map();
|
|
571
|
-
for (let i = 1; i < roots.length; i++) {
|
|
572
|
-
if (roots[i] === roots[i - 1] || roots[i].startsWith(`${roots[i - 1]}${sep}`)) {
|
|
573
|
-
throw new DeliveryError('Filesystem output roots must not overlap.');
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
const inputId = digestValue({
|
|
577
|
-
bounds: { ...bounds, outputRoots: roots },
|
|
578
|
-
classificationReceiptId,
|
|
579
|
-
resourceRunner: digestBytes(readFileSync(fileURLToPath(import.meta.url))),
|
|
580
|
-
});
|
|
581
|
-
const path = join(common, 'ai-delivery', 'output-baselines@1', `${inputId.slice(7)}.json`);
|
|
582
|
-
const statePath = join(common, 'ai-delivery', 'output-baselines@1', `${inputId.slice(7)}.state.json`);
|
|
583
|
-
const writeState = (status, baselineId) => {
|
|
584
|
-
const content = {
|
|
585
|
-
inputId,
|
|
586
|
-
schemaVersion: 'ai-delivery.output-baseline-state@1',
|
|
587
|
-
status,
|
|
588
|
-
...(baselineId === undefined ? {} : { baselineId }),
|
|
589
|
-
};
|
|
590
|
-
writePrivateJsonFileAtomically(statePath, { ...content, stateId: digestValue(content) });
|
|
591
|
-
};
|
|
592
|
-
let state;
|
|
593
|
-
try {
|
|
594
|
-
state = OutputBaselineStateSchema.parse(JSON.parse(readFileSync(statePath, 'utf8')));
|
|
595
|
-
}
|
|
596
|
-
catch (error) {
|
|
597
|
-
if (error.code !== 'ENOENT') {
|
|
598
|
-
throw new DeliveryError('Filesystem output baseline state is corrupt.');
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (state === undefined) {
|
|
602
|
-
if (existsSync(path))
|
|
603
|
-
throw new DeliveryError('Filesystem output baseline state is missing.');
|
|
604
|
-
writeState('preparing');
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
const { stateId, ...content } = state;
|
|
608
|
-
if (stateId !== digestValue(content) ||
|
|
609
|
-
state.inputId !== inputId ||
|
|
610
|
-
(state.status === 'complete') !== (state.baselineId !== undefined))
|
|
611
|
-
throw new DeliveryError('Filesystem output baseline state is corrupt.');
|
|
612
|
-
}
|
|
613
|
-
let checkpoint;
|
|
614
|
-
if (state?.status !== 'complete') {
|
|
615
|
-
const content = {
|
|
616
|
-
files: [...(await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots, links), signal))].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
617
|
-
inputId,
|
|
618
|
-
roots,
|
|
619
|
-
schemaVersion: 'ai-delivery.output-baseline@1',
|
|
620
|
-
};
|
|
621
|
-
checkpoint = { ...content, baselineId: digestValue(content) };
|
|
622
|
-
writePrivateJsonFileAtomically(path, checkpoint);
|
|
623
|
-
writeState('complete', checkpoint.baselineId);
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
try {
|
|
627
|
-
checkpoint = OutputBaselineCheckpointSchema.parse(JSON.parse(readFileSync(path, 'utf8')));
|
|
628
|
-
}
|
|
629
|
-
catch {
|
|
630
|
-
throw new DeliveryError('Filesystem output baseline checkpoint is missing or corrupt.');
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
const { baselineId, ...content } = checkpoint;
|
|
634
|
-
if (baselineId !== digestValue(content) ||
|
|
635
|
-
(state?.status === 'complete' && state.baselineId !== baselineId) ||
|
|
636
|
-
checkpoint.inputId !== inputId ||
|
|
637
|
-
digestValue(checkpoint.roots) !== digestValue(roots) ||
|
|
638
|
-
checkpoint.files.some(([file], index) => (index > 0 && file <= checkpoint.files[index - 1][0]) ||
|
|
639
|
-
!roots.some((root) => file === root || file.startsWith(`${root}${sep}`))))
|
|
640
|
-
throw new DeliveryError('Filesystem output baseline checkpoint is corrupt or belongs to another input.');
|
|
641
|
-
return { baselineId, roots, files: new Map(checkpoint.files), links, ...(gate === undefined ? {} : { gate }) };
|
|
642
|
-
}
|
|
643
489
|
async function positiveNewOutputBytes(baseline, signal, commandRunning = false, onUnavailable) {
|
|
644
490
|
let files;
|
|
645
491
|
let firstFailure;
|
|
@@ -711,7 +557,7 @@ function assertDiskHeadroom(repoRoot, bounds, baseline) {
|
|
|
711
557
|
throw new DeliveryError(`Free disk fell below limit ${bounds.minFreeDiskBytes}.`);
|
|
712
558
|
}
|
|
713
559
|
}
|
|
714
|
-
function processSnapshot() {
|
|
560
|
+
export function processSnapshot() {
|
|
715
561
|
const psPath = existsSync('/usr/bin/ps') ? '/usr/bin/ps' : '/bin/ps';
|
|
716
562
|
const result = spawnSync(psPath, ['-A', '-o', 'pid=,ppid=,pgid=,rss=,stat=,lstart='], {
|
|
717
563
|
encoding: 'utf8',
|
|
@@ -840,7 +686,7 @@ function controllerTreeRssBytes(snapshot = processSnapshot(), excluded = new Set
|
|
|
840
686
|
throw new DeliveryError('Controller aggregate RSS observation overflowed.');
|
|
841
687
|
return bytes;
|
|
842
688
|
}
|
|
843
|
-
async function confirmOwnedCleanup(state) {
|
|
689
|
+
export async function confirmOwnedCleanup(state) {
|
|
844
690
|
if (!state.sampled || state.rootIdentity === undefined) {
|
|
845
691
|
throw new DeliveryError('Owned process cleanup cannot be verified without a root identity.');
|
|
846
692
|
}
|
|
@@ -868,7 +714,7 @@ async function confirmOwnedCleanup(state) {
|
|
|
868
714
|
}
|
|
869
715
|
}
|
|
870
716
|
/** Signal only birth identities established by this command, including observed detached children. */
|
|
871
|
-
function terminateOwnedProcesses(state) {
|
|
717
|
+
export function terminateOwnedProcesses(state) {
|
|
872
718
|
if (!state.sampled || state.rootIdentity === undefined)
|
|
873
719
|
throw new DeliveryError('Owned process identity is unknown.');
|
|
874
720
|
const snapshot = processSnapshot();
|
|
@@ -1150,21 +996,6 @@ export async function withWorktreeTransitionWriterLease(input, operation) {
|
|
|
1150
996
|
return result;
|
|
1151
997
|
}, input, gitCommonDir(repository));
|
|
1152
998
|
}
|
|
1153
|
-
function primaryRoot(worktreeRoot) {
|
|
1154
|
-
const common = gitCommonDir(worktreeRoot);
|
|
1155
|
-
if (basename(common) !== '.git')
|
|
1156
|
-
throw new DeliveryError('Expected a conventional non-bare Git repository.');
|
|
1157
|
-
return dirname(common);
|
|
1158
|
-
}
|
|
1159
|
-
function assertRegisteredIssue(worktreeRoot, issueNumber, allowMerged = false) {
|
|
1160
|
-
const row = getIssueWorktreeStrict(issueNumber, primaryRoot(worktreeRoot));
|
|
1161
|
-
if (row.path !== worktreeRoot ||
|
|
1162
|
-
row.branch !== `issue/${issueNumber}` ||
|
|
1163
|
-
(row.status !== 'active' && row.status !== 'pr-published' && !(allowMerged && row.status === 'merged'))) {
|
|
1164
|
-
throw new DeliveryError('Verification requires the exact registered issue worktree.');
|
|
1165
|
-
}
|
|
1166
|
-
assertClean(worktreeRoot);
|
|
1167
|
-
}
|
|
1168
999
|
function worktreeDigest(root) {
|
|
1169
1000
|
return digestValue(resolve(root));
|
|
1170
1001
|
}
|
|
@@ -1233,11 +1064,6 @@ export function retainedWorktreeTransitionDependencyManifests(distDirectory) {
|
|
|
1233
1064
|
export function readWorktreeTransitionRun(bytes) {
|
|
1234
1065
|
return parseRun(JSON.parse(bytes.toString('utf8')));
|
|
1235
1066
|
}
|
|
1236
|
-
function manifestPath(common, headSha, root) {
|
|
1237
|
-
return root === undefined
|
|
1238
|
-
? join(common, 'ai-delivery', 'runs', `${headSha}.json`)
|
|
1239
|
-
: join(common, 'ai-delivery', 'runs@2', worktreeDigest(root).slice(7), `${headSha}.json`);
|
|
1240
|
-
}
|
|
1241
1067
|
function parseRun(value) {
|
|
1242
1068
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1243
1069
|
throw new DeliveryError('Invalid run manifest.');
|
|
@@ -1275,54 +1101,10 @@ function parseRun(value) {
|
|
|
1275
1101
|
throw new DeliveryError('Run manifest identity or aggregate is invalid.');
|
|
1276
1102
|
return { ...content, completedAt: input.completedAt, manifestId: input.manifestId };
|
|
1277
1103
|
}
|
|
1278
|
-
function loadRun(common, headSha, root, historical = false) {
|
|
1279
|
-
let raw;
|
|
1280
|
-
try {
|
|
1281
|
-
raw = JSON.parse(readFileSync(manifestPath(common, headSha, root), 'utf8'));
|
|
1282
|
-
}
|
|
1283
|
-
catch (error) {
|
|
1284
|
-
if (!historical || error.code !== 'ENOENT')
|
|
1285
|
-
throw new DeliveryError('Exact-head worktree verification run is missing or corrupt; verify with the current producer.');
|
|
1286
|
-
raw = JSON.parse(readFileSync(manifestPath(common, headSha), 'utf8'));
|
|
1287
|
-
}
|
|
1288
|
-
const run = parseRun(raw);
|
|
1289
|
-
if (run.classification.head.sha !== headSha ||
|
|
1290
|
-
(run.writer !== undefined &&
|
|
1291
|
-
(run.writer.worktreeDigest !== worktreeDigest(root) ||
|
|
1292
|
-
(!historical && run.writer.producerDigest !== producerDigest()))) ||
|
|
1293
|
-
(!historical && run.writer === undefined))
|
|
1294
|
-
throw new DeliveryError('Verification run belongs to another writer or producer.');
|
|
1295
|
-
const expectedEnvironment = historical ? undefined : verificationEnvironmentDigest(root, run.resources);
|
|
1296
|
-
if (expectedEnvironment !== undefined &&
|
|
1297
|
-
run.stageReceipts.some((receipt) => receipt.input.environmentDigest !== expectedEnvironment))
|
|
1298
|
-
throw new DeliveryError('Verification stage environment belongs to a foreign worktree or producer.');
|
|
1299
|
-
return run;
|
|
1300
|
-
}
|
|
1301
|
-
function stageEnvironmentDigest(root, producer = producerDigest()) {
|
|
1302
|
-
return digestValue({
|
|
1303
|
-
arch: process.arch,
|
|
1304
|
-
node: process.version,
|
|
1305
|
-
path: process.env.PATH ?? '',
|
|
1306
|
-
platform: process.platform,
|
|
1307
|
-
worktreeDigest: worktreeDigest(root),
|
|
1308
|
-
producerDigest: producer,
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
function verificationEnvironmentDigest(root, resources, producer = producerDigest()) {
|
|
1312
|
-
const environment = stageEnvironmentDigest(root, producer);
|
|
1313
|
-
return resources === undefined
|
|
1314
|
-
? environment
|
|
1315
|
-
: digestValue({
|
|
1316
|
-
environment,
|
|
1317
|
-
...(resources.outputBaselineId === undefined ? {} : { outputBaselineId: resources.outputBaselineId }),
|
|
1318
|
-
resourceBounds: resources.bounds,
|
|
1319
|
-
resourceRunner: digestBytes(readFileSync(fileURLToPath(import.meta.url))),
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
1104
|
function reportVerificationProgress(input) {
|
|
1323
1105
|
process.stderr.write(`ai-delivery.verify ${JSON.stringify(input)}\n`);
|
|
1324
1106
|
}
|
|
1325
|
-
function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds, baseline, onResourceSample, writer, execution) {
|
|
1107
|
+
export function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds, baseline, onResourceSample, writer, execution) {
|
|
1326
1108
|
const [executable, ...args] = argv;
|
|
1327
1109
|
if (!executable)
|
|
1328
1110
|
throw new DeliveryError('Policy selected an empty stage command.');
|
|
@@ -1375,6 +1157,8 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1375
1157
|
: ` First filesystem observation: ${firstOutputObservationFailure} Last filesystem observation: ${lastOutputObservationFailure}`;
|
|
1376
1158
|
try {
|
|
1377
1159
|
const bytes = Buffer.concat([...stdout, ...stderr]);
|
|
1160
|
+
if (execution?.failedOutput)
|
|
1161
|
+
return execution.failedOutput(bytes);
|
|
1378
1162
|
return ` Command output ${writeRepositoryCommandOutput({ bytes, gitCommonDir: gitCommonDir(repoRoot) })}.${observation}`;
|
|
1379
1163
|
}
|
|
1380
1164
|
catch (error) {
|
|
@@ -1607,7 +1391,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1607
1391
|
}
|
|
1608
1392
|
writer?.idle();
|
|
1609
1393
|
settled = true;
|
|
1610
|
-
resolve(Buffer.concat([...stdout, ...stderr], outputBytes));
|
|
1394
|
+
resolve(execution?.stdoutOnly ? Buffer.concat(stdout) : Buffer.concat([...stdout, ...stderr], outputBytes));
|
|
1611
1395
|
};
|
|
1612
1396
|
child.once('close', (code, signal) => {
|
|
1613
1397
|
void handleClose(code, signal)
|
|
@@ -1619,359 +1403,6 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1619
1403
|
});
|
|
1620
1404
|
});
|
|
1621
1405
|
}
|
|
1622
|
-
export async function verifyIssue(input) {
|
|
1623
|
-
const root = gitRoot(input.repoRoot);
|
|
1624
|
-
return withVerificationWriter(root, (writer) => withOutputObservationGate(root, input.resourceBounds?.outputRoots !== undefined, (gate) => verifyIssueOwned(input, writer, gate)));
|
|
1625
|
-
}
|
|
1626
|
-
async function verifyIssueOwned(input, writer, gate) {
|
|
1627
|
-
const assertNotCancelled = () => {
|
|
1628
|
-
if (input.signal?.aborted)
|
|
1629
|
-
throw new DeliveryError('Verification cancelled.');
|
|
1630
|
-
};
|
|
1631
|
-
assertNotCancelled();
|
|
1632
|
-
const root = gitRoot(input.repoRoot);
|
|
1633
|
-
if (input.resourceBounds !== undefined)
|
|
1634
|
-
assertResourceBounds(input.resourceBounds);
|
|
1635
|
-
assertRegisteredIssue(root, input.issueNumber);
|
|
1636
|
-
const common = gitCommonDir(root);
|
|
1637
|
-
if (existsSync(join(common, 'ai-delivery', 'merges', String(input.issueNumber), `${coordinate(root).sha}.json`))) {
|
|
1638
|
-
throw new DeliveryError('Commit continuation work before verifying an already-merged issue head; retain its receipts.');
|
|
1639
|
-
}
|
|
1640
|
-
const loaded = await loadDeliveryConfig(root, input.personalAuth === undefined ? {} : { personalAuth: input.personalAuth });
|
|
1641
|
-
const selected = await loadSelectedRepositoryPolicy({
|
|
1642
|
-
repoRoot: root,
|
|
1643
|
-
policySourcePath: loaded.config.policy.module,
|
|
1644
|
-
});
|
|
1645
|
-
const base = coordinate(root, defaultBaseRef(root, loaded.remote));
|
|
1646
|
-
const head = coordinate(root);
|
|
1647
|
-
const classification = classifyRepositoryExactRange({
|
|
1648
|
-
base,
|
|
1649
|
-
changedPaths: changedPaths(root, base.sha, head.sha),
|
|
1650
|
-
configDigest: loaded.configDigest,
|
|
1651
|
-
head,
|
|
1652
|
-
policy: selected.policy,
|
|
1653
|
-
policySourcePath: loaded.config.policy.module,
|
|
1654
|
-
repoRoot: root,
|
|
1655
|
-
repository: loaded.config.repository,
|
|
1656
|
-
});
|
|
1657
|
-
const baseline = input.resourceBounds === undefined
|
|
1658
|
-
? undefined
|
|
1659
|
-
: await outputBaseline(root, common, classification.receiptId, input.resourceBounds, gate, input.signal);
|
|
1660
|
-
if (input.resourceBounds !== undefined)
|
|
1661
|
-
assertDiskHeadroom(root, input.resourceBounds, baseline);
|
|
1662
|
-
const receipts = [];
|
|
1663
|
-
const admitted = new Set(input.admittedResourceClasses ?? ['source_only']);
|
|
1664
|
-
const startedAtMs = Date.now();
|
|
1665
|
-
let reusedStages = 0;
|
|
1666
|
-
let completedCommands = 0;
|
|
1667
|
-
let reusedCommands = 0;
|
|
1668
|
-
let lastCompletedAtMs = startedAtMs;
|
|
1669
|
-
const totalCommands = classification.requiredStages.reduce((total, stage) => total + stage.commands.length, 0);
|
|
1670
|
-
const report = (progress) => {
|
|
1671
|
-
const elapsedMs = Date.now() - startedAtMs;
|
|
1672
|
-
reportVerificationProgress({
|
|
1673
|
-
...progress,
|
|
1674
|
-
completedCommands,
|
|
1675
|
-
remainingCommands: totalCommands - completedCommands,
|
|
1676
|
-
reusedCommands,
|
|
1677
|
-
executedCommandsPerSecond: elapsedMs === 0 ? 0 : ((completedCommands - reusedCommands) * 1000) / elapsedMs,
|
|
1678
|
-
lastCompletedWorkAgeMs: Date.now() - lastCompletedAtMs,
|
|
1679
|
-
});
|
|
1680
|
-
};
|
|
1681
|
-
const normalizedBounds = input.resourceBounds === undefined
|
|
1682
|
-
? undefined
|
|
1683
|
-
: {
|
|
1684
|
-
maxAggregateRssBytes: input.resourceBounds.maxAggregateRssBytes,
|
|
1685
|
-
minFreeDiskBytes: input.resourceBounds.minFreeDiskBytes,
|
|
1686
|
-
...(input.resourceBounds.maxNewOutputBytes === undefined
|
|
1687
|
-
? {}
|
|
1688
|
-
: { maxNewOutputBytes: input.resourceBounds.maxNewOutputBytes }),
|
|
1689
|
-
...(baseline === undefined ? {} : { outputRoots: baseline.roots }),
|
|
1690
|
-
};
|
|
1691
|
-
const emptyResources = () => ({
|
|
1692
|
-
bounds: normalizedBounds,
|
|
1693
|
-
maxSampledAggregateRssBytes: null,
|
|
1694
|
-
maxSampledNewOutputBytes: null,
|
|
1695
|
-
minSampledFreeDiskBytes: null,
|
|
1696
|
-
observation: 'sampled',
|
|
1697
|
-
...(baseline === undefined ? {} : { outputBaselineId: baseline.baselineId }),
|
|
1698
|
-
processCoverage: 'observed-processes-only',
|
|
1699
|
-
sampleCount: 0,
|
|
1700
|
-
});
|
|
1701
|
-
const resources = normalizedBounds === undefined ? undefined : emptyResources();
|
|
1702
|
-
const producer = producerDigest();
|
|
1703
|
-
const environmentDigest = verificationEnvironmentDigest(root, resources, producer);
|
|
1704
|
-
let stageResources;
|
|
1705
|
-
const recordResourceSample = (sample) => {
|
|
1706
|
-
if (resources === undefined)
|
|
1707
|
-
return;
|
|
1708
|
-
const measured = {
|
|
1709
|
-
...emptyResources(),
|
|
1710
|
-
sampleCount: 1,
|
|
1711
|
-
maxSampledAggregateRssBytes: sample.aggregateRssBytes,
|
|
1712
|
-
maxSampledNewOutputBytes: sample.newOutputBytes ?? null,
|
|
1713
|
-
minSampledFreeDiskBytes: sample.freeDiskBytes,
|
|
1714
|
-
};
|
|
1715
|
-
mergeResourceSummary(resources, measured);
|
|
1716
|
-
if (stageResources !== undefined)
|
|
1717
|
-
mergeResourceSummary(stageResources, measured);
|
|
1718
|
-
};
|
|
1719
|
-
for (const stage of classification.requiredStages) {
|
|
1720
|
-
assertNotCancelled();
|
|
1721
|
-
const stageInput = createRepositoryStageInput({
|
|
1722
|
-
classification,
|
|
1723
|
-
environmentDigest,
|
|
1724
|
-
semanticInputs: stage.semanticInputs ??
|
|
1725
|
-
stage.semanticInputKeys.map((key) => ({
|
|
1726
|
-
digest: digestValue({ key, evidenceId: classification.policyEvidence.evidenceId }),
|
|
1727
|
-
key,
|
|
1728
|
-
})),
|
|
1729
|
-
stageId: stage.id,
|
|
1730
|
-
upstream: stage.dependsOn.map((stageId) => {
|
|
1731
|
-
const receipt = receipts.find((value) => value.input.stageId === stageId);
|
|
1732
|
-
if (!receipt)
|
|
1733
|
-
throw new DeliveryError(`Missing upstream stage '${stageId}'.`);
|
|
1734
|
-
return { receiptId: receipt.receiptId, stageId };
|
|
1735
|
-
}),
|
|
1736
|
-
});
|
|
1737
|
-
let cached = loadRepositoryStageCheckpoint({
|
|
1738
|
-
classification,
|
|
1739
|
-
configDigest: loaded.configDigest,
|
|
1740
|
-
gitCommonDir: common,
|
|
1741
|
-
policySourcePath: loaded.config.policy.module,
|
|
1742
|
-
repoRoot: root,
|
|
1743
|
-
stageInput,
|
|
1744
|
-
});
|
|
1745
|
-
if (normalizedBounds !== undefined) {
|
|
1746
|
-
const measured = loadResourceStageCheckpoint(common, root, stageInput, normalizedBounds, baseline?.baselineId);
|
|
1747
|
-
if (measured !== undefined) {
|
|
1748
|
-
if (cached === undefined) {
|
|
1749
|
-
writeRepositoryStageCheckpoint({ gitCommonDir: common, receipt: measured.receipt, repoRoot: root });
|
|
1750
|
-
cached = loadRepositoryStageCheckpoint({
|
|
1751
|
-
classification,
|
|
1752
|
-
configDigest: loaded.configDigest,
|
|
1753
|
-
gitCommonDir: common,
|
|
1754
|
-
policySourcePath: loaded.config.policy.module,
|
|
1755
|
-
repoRoot: root,
|
|
1756
|
-
stageInput,
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
if (cached?.receiptId !== measured.receipt.receiptId) {
|
|
1760
|
-
throw new DeliveryError('Resource stage checkpoint disagrees with its stage receipt.');
|
|
1761
|
-
}
|
|
1762
|
-
mergeResourceSummary(resources, measured.resources);
|
|
1763
|
-
}
|
|
1764
|
-
else if (cached !== undefined) {
|
|
1765
|
-
throw new DeliveryError('Bounded stage receipt lacks its resource checkpoint.');
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
if (cached) {
|
|
1769
|
-
receipts.push(cached);
|
|
1770
|
-
reusedStages += 1;
|
|
1771
|
-
completedCommands += cached.commands.length;
|
|
1772
|
-
reusedCommands += cached.commands.length;
|
|
1773
|
-
lastCompletedAtMs = Date.now();
|
|
1774
|
-
report({
|
|
1775
|
-
state: 'reused',
|
|
1776
|
-
stageId: stage.id,
|
|
1777
|
-
completedStages: receipts.length,
|
|
1778
|
-
remainingStages: classification.requiredStages.length - receipts.length,
|
|
1779
|
-
reusedStages,
|
|
1780
|
-
elapsedMs: Date.now() - startedAtMs,
|
|
1781
|
-
});
|
|
1782
|
-
continue;
|
|
1783
|
-
}
|
|
1784
|
-
assertNotCancelled();
|
|
1785
|
-
if (!admitted.has(stage.resourceClass)) {
|
|
1786
|
-
throw new DeliveryError(`Stage '${stage.id}' requires explicit ${stage.resourceClass} admission.`);
|
|
1787
|
-
}
|
|
1788
|
-
stageResources = normalizedBounds === undefined ? undefined : emptyResources();
|
|
1789
|
-
const startedAt = new Date().toISOString();
|
|
1790
|
-
const commands = [];
|
|
1791
|
-
for (const command of stage.commands) {
|
|
1792
|
-
const commandStartedAtMs = Date.now();
|
|
1793
|
-
if (input.resourceBounds !== undefined)
|
|
1794
|
-
assertDiskHeadroom(root, input.resourceBounds, baseline);
|
|
1795
|
-
const progress = (capturedOutputBytes, sample, reason) => report({
|
|
1796
|
-
state: 'running',
|
|
1797
|
-
stageId: stage.id,
|
|
1798
|
-
commandLabel: command.label,
|
|
1799
|
-
completedStages: receipts.length,
|
|
1800
|
-
remainingStages: classification.requiredStages.length - receipts.length,
|
|
1801
|
-
reusedStages,
|
|
1802
|
-
elapsedMs: Date.now() - startedAtMs,
|
|
1803
|
-
capturedOutputBytes,
|
|
1804
|
-
...(reason === undefined ? {} : { reason }),
|
|
1805
|
-
commandElapsedMs: Date.now() - commandStartedAtMs,
|
|
1806
|
-
...(sample === undefined
|
|
1807
|
-
? {}
|
|
1808
|
-
: {
|
|
1809
|
-
sampledAggregateRssBytes: sample.aggregateRssBytes,
|
|
1810
|
-
sampledFreeDiskBytes: sample.freeDiskBytes,
|
|
1811
|
-
...(sample.newOutputBytes === undefined ? {} : { sampledNewOutputBytes: sample.newOutputBytes }),
|
|
1812
|
-
ownedProcessCount: sample.ownedProcessCount,
|
|
1813
|
-
...(baseline !== undefined && sample.newOutputBytes === undefined
|
|
1814
|
-
? { reason: 'waiting for strict filesystem output scan' }
|
|
1815
|
-
: {}),
|
|
1816
|
-
}),
|
|
1817
|
-
});
|
|
1818
|
-
progress(0);
|
|
1819
|
-
let bytes;
|
|
1820
|
-
try {
|
|
1821
|
-
bytes = await runStageCommand(root, command.argv, input.signal, progress, input.resourceBounds, baseline, recordResourceSample, writer);
|
|
1822
|
-
}
|
|
1823
|
-
catch (error) {
|
|
1824
|
-
report({
|
|
1825
|
-
state: 'failed',
|
|
1826
|
-
stageId: stage.id,
|
|
1827
|
-
commandLabel: command.label,
|
|
1828
|
-
completedStages: receipts.length,
|
|
1829
|
-
remainingStages: classification.requiredStages.length - receipts.length,
|
|
1830
|
-
reusedStages,
|
|
1831
|
-
elapsedMs: Date.now() - startedAtMs,
|
|
1832
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1833
|
-
});
|
|
1834
|
-
throw error;
|
|
1835
|
-
}
|
|
1836
|
-
assertNotCancelled();
|
|
1837
|
-
completedCommands += 1;
|
|
1838
|
-
lastCompletedAtMs = Date.now();
|
|
1839
|
-
commands.push({
|
|
1840
|
-
exitCode: 0,
|
|
1841
|
-
label: command.label,
|
|
1842
|
-
outputDigest: writeRepositoryCommandOutput({ bytes, gitCommonDir: common }),
|
|
1843
|
-
});
|
|
1844
|
-
}
|
|
1845
|
-
const artifacts = stage.attestationKey === undefined
|
|
1846
|
-
? []
|
|
1847
|
-
: classification.policyEvidence.artifacts
|
|
1848
|
-
.filter((artifact) => artifact.path === stage.attestationKey)
|
|
1849
|
-
.map((artifact) => ({ digest: artifact.digest, path: artifact.path }));
|
|
1850
|
-
if (stage.attestationKey !== undefined && artifacts.length === 0) {
|
|
1851
|
-
throw new DeliveryError(`Selected attestation '${stage.attestationKey}' lacks a bound artifact.`);
|
|
1852
|
-
}
|
|
1853
|
-
const receipt = createRepositoryStageReceipt({
|
|
1854
|
-
artifacts,
|
|
1855
|
-
classification,
|
|
1856
|
-
commands,
|
|
1857
|
-
completedAt: new Date().toISOString(),
|
|
1858
|
-
stageInput,
|
|
1859
|
-
startedAt,
|
|
1860
|
-
});
|
|
1861
|
-
assertNotCancelled();
|
|
1862
|
-
if (stageResources !== undefined)
|
|
1863
|
-
writeResourceStageCheckpoint(common, receipt, stageResources);
|
|
1864
|
-
writeRepositoryStageCheckpoint({ gitCommonDir: common, receipt, repoRoot: root });
|
|
1865
|
-
stageResources = undefined;
|
|
1866
|
-
receipts.push(receipt);
|
|
1867
|
-
lastCompletedAtMs = Date.now();
|
|
1868
|
-
report({
|
|
1869
|
-
state: 'completed',
|
|
1870
|
-
stageId: stage.id,
|
|
1871
|
-
completedStages: receipts.length,
|
|
1872
|
-
remainingStages: classification.requiredStages.length - receipts.length,
|
|
1873
|
-
reusedStages,
|
|
1874
|
-
elapsedMs: Date.now() - startedAtMs,
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
assertNotCancelled();
|
|
1878
|
-
if (baseline !== undefined && input.resourceBounds !== undefined) {
|
|
1879
|
-
const bytes = await positiveNewOutputBytes(baseline, input.signal);
|
|
1880
|
-
if (bytes > input.resourceBounds.maxNewOutputBytes)
|
|
1881
|
-
throw new DeliveryError('Positive new filesystem output ' + bytes + ' exceeded limit ' + input.resourceBounds.maxNewOutputBytes + '.');
|
|
1882
|
-
assertDiskHeadroom(root, input.resourceBounds, baseline);
|
|
1883
|
-
recordResourceSample({
|
|
1884
|
-
aggregateRssBytes: 0,
|
|
1885
|
-
freeDiskBytes: freeDiskBytes(root, baseline),
|
|
1886
|
-
newOutputBytes: bytes,
|
|
1887
|
-
ownedProcessCount: 0,
|
|
1888
|
-
});
|
|
1889
|
-
}
|
|
1890
|
-
assertNotCancelled();
|
|
1891
|
-
assertRepositoryClassificationCurrent({
|
|
1892
|
-
classification,
|
|
1893
|
-
configDigest: loaded.configDigest,
|
|
1894
|
-
policySourcePath: loaded.config.policy.module,
|
|
1895
|
-
repoRoot: root,
|
|
1896
|
-
});
|
|
1897
|
-
if (coordinate(root).sha !== head.sha || coordinate(root, defaultBaseRef(root, loaded.remote)).sha !== base.sha)
|
|
1898
|
-
throw new DeliveryError('Verification source changed during execution; classify and resume the current source.');
|
|
1899
|
-
if (producerDigest() !== producer)
|
|
1900
|
-
throw new DeliveryError('Verification producer changed during execution.');
|
|
1901
|
-
const aggregate = createRepositoryStageAggregate({ classification, receipts });
|
|
1902
|
-
writeRepositoryStageAggregate({ gitCommonDir: common, aggregate });
|
|
1903
|
-
const content = {
|
|
1904
|
-
aggregate,
|
|
1905
|
-
classification,
|
|
1906
|
-
completedAt: new Date().toISOString(),
|
|
1907
|
-
...(resources === undefined ? {} : { resources: ResourceSummarySchema.parse(resources) }),
|
|
1908
|
-
schemaVersion: 'ai-delivery.run@3',
|
|
1909
|
-
writer: { worktreeDigest: worktreeDigest(root), producerDigest: producer },
|
|
1910
|
-
stageReceipts: receipts,
|
|
1911
|
-
};
|
|
1912
|
-
const run = { ...content, manifestId: digestValue(content) };
|
|
1913
|
-
writePrivateJsonFileAtomically(manifestPath(common, head.sha, root), run);
|
|
1914
|
-
return run;
|
|
1915
|
-
}
|
|
1916
|
-
export async function createIssuePhaseEvidence(input) {
|
|
1917
|
-
const root = gitRoot(input.repoRoot);
|
|
1918
|
-
assertRegisteredIssue(root, input.issueNumber);
|
|
1919
|
-
const loaded = await loadDeliveryConfig(root, input.personalAuth === undefined ? {} : { personalAuth: input.personalAuth });
|
|
1920
|
-
const selected = await loadSelectedRepositoryPolicy({
|
|
1921
|
-
repoRoot: root,
|
|
1922
|
-
policySourcePath: loaded.config.policy.module,
|
|
1923
|
-
});
|
|
1924
|
-
const run = loadRun(gitCommonDir(root), coordinate(root).sha, root);
|
|
1925
|
-
const evidence = createRepositoryDeliveryEvidence({
|
|
1926
|
-
aggregate: run.aggregate,
|
|
1927
|
-
...(input.approval === undefined ? {} : { approval: input.approval }),
|
|
1928
|
-
approvalRoles: {
|
|
1929
|
-
authorIdentity: loaded.config.roles.author.identity,
|
|
1930
|
-
reviewerIdentity: loaded.config.roles.reviewer.identity,
|
|
1931
|
-
},
|
|
1932
|
-
classification: run.classification,
|
|
1933
|
-
configDigest: loaded.configDigest,
|
|
1934
|
-
currentBase: coordinate(root, run.classification.base.sha),
|
|
1935
|
-
currentHead: coordinate(root),
|
|
1936
|
-
gitCommonDir: gitCommonDir(root),
|
|
1937
|
-
...(input.mergeReadback === undefined ? {} : { mergeReadback: input.mergeReadback }),
|
|
1938
|
-
phase: input.phase,
|
|
1939
|
-
policy: selected.policy,
|
|
1940
|
-
policySourcePath: loaded.config.policy.module,
|
|
1941
|
-
repoRoot: root,
|
|
1942
|
-
stageReceipts: run.stageReceipts,
|
|
1943
|
-
});
|
|
1944
|
-
writeRepositoryDeliveryEvidence({ evidence, gitCommonDir: gitCommonDir(root) });
|
|
1945
|
-
return evidence;
|
|
1946
|
-
}
|
|
1947
|
-
export function loadVerifiedRun(repoRoot, issueNumber) {
|
|
1948
|
-
const root = gitRoot(repoRoot);
|
|
1949
|
-
assertRegisteredIssue(root, issueNumber, true);
|
|
1950
|
-
const historical = getIssueWorktreeStrict(issueNumber, primaryRoot(root)).status === 'merged';
|
|
1951
|
-
return loadRun(gitCommonDir(root), coordinate(root).sha, root, historical);
|
|
1952
|
-
}
|
|
1953
|
-
/** @internal Preservation-only read; the continuation owner validates custody and terminal lineage. */
|
|
1954
|
-
export function loadHistoricalMergedRun(repoRoot, headSha) {
|
|
1955
|
-
const root = gitRoot(repoRoot);
|
|
1956
|
-
const common = gitCommonDir(root);
|
|
1957
|
-
const run = loadRun(common, headSha, root, true);
|
|
1958
|
-
const current = manifestPath(common, headSha, root);
|
|
1959
|
-
return { run, path: existsSync(current) ? current : manifestPath(common, headSha) };
|
|
1960
|
-
}
|
|
1961
|
-
/** Recover an exact merged run after worktree removal interrupted terminal recording. */
|
|
1962
|
-
export function loadRemovedMergedRun(primaryRepoRoot, issueNumber) {
|
|
1963
|
-
const root = gitRoot(primaryRepoRoot);
|
|
1964
|
-
const row = getIssueWorktreeStrict(issueNumber, root);
|
|
1965
|
-
if (row.status !== 'merged' || row.path !== join(root, '.worktrees', `issue-${issueNumber}`)) {
|
|
1966
|
-
throw new DeliveryError('Removed-run recovery requires the exact registered merged issue.');
|
|
1967
|
-
}
|
|
1968
|
-
const headSha = git(root, 'rev-parse', `refs/heads/${row.branch}`);
|
|
1969
|
-
const run = loadRun(gitCommonDir(root), headSha, row.path, true);
|
|
1970
|
-
if (run.classification.head.sha !== headSha) {
|
|
1971
|
-
throw new DeliveryError('Recovered merged run disagrees with the registered branch.');
|
|
1972
|
-
}
|
|
1973
|
-
return run;
|
|
1974
|
-
}
|
|
1975
1406
|
const SourceDigest = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
|
|
1976
1407
|
const SourceSha = z.string().regex(/^[a-f0-9]{40}$/u);
|
|
1977
1408
|
const SourceArtifactSchema = z.strictObject({ path: z.string().min(1), digest: SourceDigest });
|
|
@@ -2170,761 +1601,42 @@ const IssueSourcePhaseReceiptSchema = z.strictObject({
|
|
|
2170
1601
|
source: SourceSnapshotSchema,
|
|
2171
1602
|
commands: z.array(SourcePhaseCommandResultSchema),
|
|
2172
1603
|
});
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
mode: stat.mode,
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
function sourceDirty(root) {
|
|
2211
|
-
const paths = [
|
|
2212
|
-
...new Set([
|
|
2213
|
-
...git(root, 'diff', '--cached', '--name-only', '-z').split('\0'),
|
|
2214
|
-
...git(root, 'ls-files', '-m', '-o', '--exclude-standard', '-z').split('\0'),
|
|
2215
|
-
].filter(Boolean)),
|
|
2216
|
-
].sort();
|
|
2217
|
-
return paths.map((path) => {
|
|
2218
|
-
const absolute = resolve(root, path);
|
|
2219
|
-
if (!absolute.startsWith(`${root}${sep}`))
|
|
2220
|
-
throw new DeliveryError('Source-phase dirty path escapes the worktree.');
|
|
2221
|
-
try {
|
|
2222
|
-
lstatSync(absolute);
|
|
2223
|
-
}
|
|
2224
|
-
catch (error) {
|
|
2225
|
-
if (error.code === 'ENOENT')
|
|
2226
|
-
return { path, digest: null };
|
|
2227
|
-
throw error;
|
|
2228
|
-
}
|
|
2229
|
-
return { path, digest: sourceFile(absolute).digest };
|
|
2230
|
-
});
|
|
2231
|
-
}
|
|
2232
|
-
function sourceReceipt(record) {
|
|
2233
|
-
if (record.status !== 'complete' || record.result === undefined)
|
|
2234
|
-
throw new DeliveryError('Source phase is not complete.');
|
|
2235
|
-
return IssueSourcePhaseReceiptSchema.parse({
|
|
2236
|
-
status: 'complete',
|
|
2237
|
-
phaseId: record.phaseId,
|
|
2238
|
-
recordId: record.recordId,
|
|
2239
|
-
source: record.result.source,
|
|
2240
|
-
commands: record.commands,
|
|
2241
|
-
});
|
|
2242
|
-
}
|
|
2243
|
-
/** Run an authorized, frozen source graph under the registered issue's existing writer fence. */
|
|
2244
|
-
export async function withIssueSourcePhase(supplied, operation) {
|
|
2245
|
-
const { signal, ...values } = supplied;
|
|
2246
|
-
const input = IssueSourcePhaseInputSchema.parse(values);
|
|
2247
|
-
assertResourceBounds(input.bounds);
|
|
2248
|
-
if (signal?.aborted)
|
|
2249
|
-
throw new DeliveryError('Source phase cancelled before preparation.');
|
|
2250
|
-
const root = gitRoot(input.repoRoot);
|
|
2251
|
-
if (root !== input.repoRoot ||
|
|
2252
|
-
root !== primaryRoot(root) ||
|
|
2253
|
-
realpathSync(input.source.path) !== input.source.path ||
|
|
2254
|
-
gitCommonDir(root) !== gitCommonDir(input.source.path))
|
|
2255
|
-
throw new DeliveryError('Source phase requires canonical primary controller and issue worktree roots.');
|
|
2256
|
-
const row = getIssueWorktreeStrict(input.issueNumber, root);
|
|
2257
|
-
assertAiDeliveryWorktreeOwner(row, root);
|
|
2258
|
-
if (!row.identity ||
|
|
2259
|
-
row.path !== input.source.path ||
|
|
2260
|
-
row.branch !== input.source.branch ||
|
|
2261
|
-
!['active', 'pr-published'].includes(row.status) ||
|
|
2262
|
-
digestValue(row) !== input.rowDigest)
|
|
2263
|
-
throw new DeliveryError('Source phase disagrees with the exact registered issue owner.');
|
|
2264
|
-
const environment = { ...process.env };
|
|
2265
|
-
delete environment[OUTPUT_GATE_ENV];
|
|
2266
|
-
for (const [name, value] of Object.entries(input.environment.overrides)) {
|
|
2267
|
-
if (name === OUTPUT_GATE_ENV)
|
|
2268
|
-
throw new DeliveryError('Source phase cannot supply an output observer capability.');
|
|
2269
|
-
if (value === null)
|
|
2270
|
-
delete environment[name];
|
|
2271
|
-
else
|
|
2272
|
-
environment[name] = value;
|
|
2273
|
-
}
|
|
2274
|
-
Object.freeze(environment);
|
|
2275
|
-
const { environment: _environment, ...bound } = input;
|
|
2276
|
-
const binding = SourceBindingSchema.parse({ ...bound, environmentDigest: input.environment.digest });
|
|
2277
|
-
const phaseId = digestValue(binding);
|
|
2278
|
-
const subject = worktreeDigest(row.path);
|
|
2279
|
-
const common = gitCommonDir(root);
|
|
2280
|
-
const directory = join(common, 'ai-delivery', 'receipts', 'source-phase@1', subject.slice(7));
|
|
2281
|
-
const path = join(directory, `${phaseId.slice(7)}.json`);
|
|
2282
|
-
const writerPath = join(common, 'ai-delivery', 'writers@1', `${subject.slice(7)}.json`);
|
|
2283
|
-
const roots = input.bounds.outputRoots.map((value) => resolve(root, value)).sort();
|
|
2284
|
-
if (roots.some((value, index) => index > 0 && (value === roots[index - 1] || value.startsWith(`${roots[index - 1]}${sep}`))))
|
|
2285
|
-
throw new DeliveryError('Source-phase output roots must not overlap.');
|
|
2286
|
-
for (const required of [
|
|
2287
|
-
directory,
|
|
2288
|
-
writerPath,
|
|
2289
|
-
join(common, 'ai-delivery', 'receipts'),
|
|
2290
|
-
join(common, 'ai-delivery', 'output-observation', subject.slice(7)),
|
|
2291
|
-
]) {
|
|
2292
|
-
if (!roots.some((value) => required === value || required.startsWith(`${value}${sep}`)))
|
|
2293
|
-
throw new DeliveryError('Source-phase roots must account for writer, observer and SDK receipt metadata.');
|
|
2294
|
-
}
|
|
2295
|
-
const cancelled = new AbortController();
|
|
2296
|
-
const combined = signal === undefined ? cancelled.signal : AbortSignal.any([signal, cancelled.signal]);
|
|
2297
|
-
let record;
|
|
2298
|
-
let baseline;
|
|
2299
|
-
let reused = false;
|
|
2300
|
-
let created = false;
|
|
2301
|
-
let entered = false;
|
|
2302
|
-
let startedCommand = false;
|
|
2303
|
-
let validated = false;
|
|
2304
|
-
let writerReleased = false;
|
|
2305
|
-
let lockFailure;
|
|
2306
|
-
let releaseFailure;
|
|
2307
|
-
let operationFailure;
|
|
2308
|
-
let accountingFailure;
|
|
2309
|
-
let cleanupFailure;
|
|
2310
|
-
let active;
|
|
2311
|
-
let expired = false;
|
|
2312
|
-
let captured = 0;
|
|
2313
|
-
let bootstrapCharge = 0;
|
|
2314
|
-
let previousBootstrap = 0;
|
|
2315
|
-
let writerBootstrap = 0;
|
|
2316
|
-
let predecessorChain = [];
|
|
2317
|
-
let accountingBounds = input.bounds;
|
|
2318
|
-
let protocolFailure;
|
|
2319
|
-
let commandFailure;
|
|
2320
|
-
const attempted = new Set();
|
|
2321
|
-
let finalSource;
|
|
2322
|
-
let artifacts = [];
|
|
2323
|
-
let newBytes = 0;
|
|
2324
|
-
const writerId = randomUUID();
|
|
2325
|
-
const started = Date.now();
|
|
2326
|
-
const message = (error) => (error instanceof Error ? error.message : String(error));
|
|
2327
|
-
const persist = () => {
|
|
2328
|
-
if (record === undefined)
|
|
2329
|
-
throw new DeliveryError('Source-phase intent is unavailable.');
|
|
2330
|
-
const { recordId: _recordId, ...content } = record;
|
|
2331
|
-
record = SourcePhaseRecordSchema.parse({ ...content, recordId: digestValue(content) });
|
|
2332
|
-
writePrivateJsonFileAtomically(path, record);
|
|
2333
|
-
};
|
|
2334
|
-
const readRecord = (file) => {
|
|
2335
|
-
const value = SourcePhaseRecordSchema.parse(JSON.parse(assertPrivateFile(file).toString('utf8')));
|
|
2336
|
-
if (value.worktreeDigest !== subject || basename(file) !== `${value.phaseId.slice(7)}.json`)
|
|
2337
|
-
throw new DeliveryError('Source-phase record belongs to another input or worktree.');
|
|
2338
|
-
return value;
|
|
2339
|
-
};
|
|
2340
|
-
const assertArtifact = (artifact) => {
|
|
2341
|
-
if (sourceFile(artifact.path).digest !== artifact.digest)
|
|
2342
|
-
throw new DeliveryError('Frozen source-phase input artifact changed.');
|
|
2343
|
-
};
|
|
2344
|
-
const sourceState = async (allowFinal = false) => {
|
|
2345
|
-
const head = git(row.path, 'rev-parse', 'HEAD');
|
|
2346
|
-
const branch = git(row.path, 'branch', '--show-current');
|
|
2347
|
-
const dirty = sourceDirty(row.path);
|
|
2348
|
-
const candidate = await loadDeliveryConfig(row.path);
|
|
2349
|
-
const configDigest = head === input.source.head || input.source.effect.kind !== 'commitOnce'
|
|
2350
|
-
? input.source.configDigest
|
|
2351
|
-
: (input.source.effect.configDigest ?? input.source.configDigest);
|
|
2352
|
-
if (branch !== input.source.branch || candidate.configDigest !== configDigest)
|
|
2353
|
-
throw new DeliveryError('Source-phase candidate branch or configuration changed.');
|
|
2354
|
-
if (head === input.source.head) {
|
|
2355
|
-
if (git(row.path, 'diff', '--cached', '--name-only', input.source.indexTree) !== '' ||
|
|
2356
|
-
digestValue(dirty) !== digestValue(input.source.dirty))
|
|
2357
|
-
throw new DeliveryError('Source-phase frozen index or dirty source changed.');
|
|
2358
|
-
return { head, branch, indexTree: input.source.indexTree, configDigest: candidate.configDigest, dirty };
|
|
2359
|
-
}
|
|
2360
|
-
if (!allowFinal)
|
|
2361
|
-
throw new DeliveryError('Source phase requires its exact initial source before new work.');
|
|
2362
|
-
if (input.source.effect.kind !== 'commitOnce' ||
|
|
2363
|
-
input.source.effect.parent !== input.source.head ||
|
|
2364
|
-
git(row.path, 'rev-list', '--parents', '-n', '1', head) !== `${head} ${input.source.effect.parent}` ||
|
|
2365
|
-
git(row.path, 'rev-parse', 'HEAD^{tree}') !== input.source.effect.tree ||
|
|
2366
|
-
git(row.path, 'diff', '--cached', '--name-only', head) !== '' ||
|
|
2367
|
-
dirty.length !== 0)
|
|
2368
|
-
throw new DeliveryError('Source-phase commit does not match its exact parent, tree and clean postconditions.');
|
|
2369
|
-
return { head, branch, indexTree: input.source.effect.tree, configDigest: candidate.configDigest, dirty };
|
|
2370
|
-
};
|
|
2371
|
-
let actor;
|
|
2372
|
-
const validate = async (allowFinal = false) => {
|
|
2373
|
-
const currentRow = getIssueWorktreeStrict(input.issueNumber, root);
|
|
2374
|
-
assertAiDeliveryWorktreeOwner(currentRow, root);
|
|
2375
|
-
const configuration = await loadDeliveryConfig(root);
|
|
2376
|
-
const admission = await assertDeliveryRuntimeAdmitted({
|
|
2377
|
-
repoRoot: root,
|
|
2378
|
-
runtimeEntryPath: input.controller.runtimeEntryPath,
|
|
2379
|
-
configuration,
|
|
2380
|
-
});
|
|
2381
|
-
if (dirname(realpathSync(input.controller.runtimeEntryPath)) !== dirname(realpathSync(fileURLToPath(import.meta.url))))
|
|
2382
|
-
throw new DeliveryError('Source-phase executing SDK installation is not the selected admitted runtime.');
|
|
2383
|
-
if (digestValue(currentRow) !== input.rowDigest ||
|
|
2384
|
-
currentRow.identity !== row.identity ||
|
|
2385
|
-
configuration.config.roles.author.identity !== input.identity ||
|
|
2386
|
-
git(root, 'rev-parse', 'HEAD') !== input.controller.head ||
|
|
2387
|
-
configuration.configDigest !== input.controller.configDigest ||
|
|
2388
|
-
admission.admissionId !== input.controller.admissionId ||
|
|
2389
|
-
admission.sourceArchiveSha256 !== input.controller.archiveDigest ||
|
|
2390
|
-
admission.packageVersion !== input.controller.packageVersion ||
|
|
2391
|
-
digestValue(environment) !== input.environment.digest ||
|
|
2392
|
-
digestValue(input.commands) !== input.commandGraphDigest)
|
|
2393
|
-
throw new DeliveryError('Source-phase controller, selected runtime, actor or command environment binding changed.');
|
|
2394
|
-
const clients = await createDeliveryGitHubClients({
|
|
2395
|
-
config: configuration.config,
|
|
2396
|
-
identity: input.identity,
|
|
2397
|
-
role: 'author',
|
|
2398
|
-
env: environment,
|
|
1604
|
+
const RuntimeOutputLedgerSchema = z
|
|
1605
|
+
.strictObject({
|
|
1606
|
+
schemaVersion: z.literal('ai-delivery.runtime-output@1'),
|
|
1607
|
+
stageId: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
1608
|
+
worktreeDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
1609
|
+
producerDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
1610
|
+
limit: z
|
|
1611
|
+
.number()
|
|
1612
|
+
.int()
|
|
1613
|
+
.positive()
|
|
1614
|
+
.safe()
|
|
1615
|
+
.max(8 * 1024 ** 2),
|
|
1616
|
+
outputs: z
|
|
1617
|
+
.array(z.strictObject({
|
|
1618
|
+
digest: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
1619
|
+
bytes: z
|
|
1620
|
+
.number()
|
|
1621
|
+
.int()
|
|
1622
|
+
.nonnegative()
|
|
1623
|
+
.safe()
|
|
1624
|
+
.max(8 * 1024 ** 2),
|
|
1625
|
+
status: z.enum(['reserved', 'complete']),
|
|
1626
|
+
}))
|
|
1627
|
+
.max(4096),
|
|
1628
|
+
contentDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
1629
|
+
})
|
|
1630
|
+
.superRefine((value, context) => {
|
|
1631
|
+
const { contentDigest, ...content } = value;
|
|
1632
|
+
if (contentDigest !== digestValue(content) ||
|
|
1633
|
+
new Set(value.outputs.map((output) => output.digest)).size !== value.outputs.length ||
|
|
1634
|
+
value.outputs.reduce((total, output) => total + output.bytes, 0) > value.limit)
|
|
1635
|
+
context.addIssue({
|
|
1636
|
+
code: 'custom',
|
|
1637
|
+
message: 'Runtime retained-output ledger is corrupt.',
|
|
2399
1638
|
});
|
|
2400
|
-
|
|
2401
|
-
throw new DeliveryError('Source-phase configured author authentication is unavailable.');
|
|
2402
|
-
const authenticated = await clients.authenticatedAuthor();
|
|
2403
|
-
if (!authenticated.actorLogin || !authenticated.credentialIdentity)
|
|
2404
|
-
throw new DeliveryError('Source-phase configured author identity readback is incomplete.');
|
|
2405
|
-
const currentActor = { identity: input.identity, ...authenticated };
|
|
2406
|
-
if (actor !== undefined && digestValue(actor) !== digestValue(currentActor))
|
|
2407
|
-
throw new DeliveryError('Source-phase authenticated author changed.');
|
|
2408
|
-
actor = currentActor;
|
|
2409
|
-
for (const artifact of [input.caller, input.authorization, ...input.inputs])
|
|
2410
|
-
assertArtifact(artifact);
|
|
2411
|
-
for (const command of input.commands) {
|
|
2412
|
-
if (command.cwd !== row.path ||
|
|
2413
|
-
command.argv[0] !== command.executable.path ||
|
|
2414
|
-
digestValue(sourceFile(command.executable.path)) !== digestValue(command.executable))
|
|
2415
|
-
throw new DeliveryError('Source-phase command or executable identity changed.');
|
|
2416
|
-
}
|
|
2417
|
-
await sourceState(allowFinal);
|
|
2418
|
-
};
|
|
2419
|
-
const observe = async (finishing = false) => {
|
|
2420
|
-
if (baseline === undefined)
|
|
2421
|
-
throw new DeliveryError('Source-phase output baseline is unavailable.');
|
|
2422
|
-
assertDiskHeadroom(row.path, accountingBounds, baseline);
|
|
2423
|
-
newBytes = bootstrapCharge + captured + (await positiveNewOutputBytes(baseline, finishing ? undefined : combined));
|
|
2424
|
-
if (newBytes > accountingBounds.maxNewOutputBytes)
|
|
2425
|
-
throw new DeliveryError('Source-phase cumulative output exceeded its original allowance.');
|
|
2426
|
-
if (process.memoryUsage().rss > accountingBounds.maxAggregateRssBytes)
|
|
2427
|
-
throw new DeliveryError('Source-phase controller RSS exceeded its bound.');
|
|
2428
|
-
};
|
|
2429
|
-
try {
|
|
2430
|
-
await withVerificationWriter(row.path, async (writer) => {
|
|
2431
|
-
try {
|
|
2432
|
-
ensurePrivateDirectoryDurably(directory);
|
|
2433
|
-
if (created && record !== undefined) {
|
|
2434
|
-
record.binding = binding;
|
|
2435
|
-
record.allocation = input.bounds;
|
|
2436
|
-
}
|
|
2437
|
-
const names = readdirSync(directory);
|
|
2438
|
-
if (names.length > 100_000)
|
|
2439
|
-
throw new DeliveryError('Source-phase record observation exceeded its entry bound.');
|
|
2440
|
-
const history = names
|
|
2441
|
-
.map((name) => readRecord(join(directory, name)))
|
|
2442
|
-
.filter((value) => value.phaseId !== phaseId);
|
|
2443
|
-
if (history.some((value) => ['preparing', 'intent', 'unresolved'].includes(value.status)))
|
|
2444
|
-
throw new DeliveryError('Unresolved source phase requires owning reconciliation before another attempt.');
|
|
2445
|
-
const previous = input.reconciliation === undefined
|
|
2446
|
-
? undefined
|
|
2447
|
-
: history.find((value) => value.phaseId === input.reconciliation?.phaseId && value.recordId === input.reconciliation.recordId);
|
|
2448
|
-
const failed = history.filter((value) => ['failed-quiescent', 'rejected-before-work'].includes(value.status) &&
|
|
2449
|
-
!history.some((later) => later.retainedPredecessors.some((retained) => retained.phaseId === value.phaseId && retained.recordId === value.recordId)));
|
|
2450
|
-
predecessorChain = [];
|
|
2451
|
-
const visiting = new Set();
|
|
2452
|
-
const retained = new Set();
|
|
2453
|
-
const retain = (value) => {
|
|
2454
|
-
if (visiting.has(value))
|
|
2455
|
-
throw new DeliveryError('Source-phase predecessor cycle is invalid.');
|
|
2456
|
-
if (retained.has(value))
|
|
2457
|
-
return;
|
|
2458
|
-
visiting.add(value);
|
|
2459
|
-
predecessorChain.push(value);
|
|
2460
|
-
for (const link of value.retainedPredecessors) {
|
|
2461
|
-
const predecessor = history.find((candidate) => candidate.phaseId === link.phaseId && candidate.recordId === link.recordId);
|
|
2462
|
-
if (predecessor === undefined)
|
|
2463
|
-
throw new DeliveryError('Source-phase retained predecessor is unavailable.');
|
|
2464
|
-
retain(predecessor);
|
|
2465
|
-
}
|
|
2466
|
-
visiting.delete(value);
|
|
2467
|
-
retained.add(value);
|
|
2468
|
-
};
|
|
2469
|
-
for (const value of failed)
|
|
2470
|
-
retain(value);
|
|
2471
|
-
if (record !== undefined && created)
|
|
2472
|
-
record.retainedPredecessors = failed.map(({ phaseId: retainedPhaseId, recordId }) => ({
|
|
2473
|
-
phaseId: retainedPhaseId,
|
|
2474
|
-
recordId,
|
|
2475
|
-
}));
|
|
2476
|
-
previousBootstrap = predecessorChain.reduce((sum, value) => sum + value.bootstrapBytes, 0);
|
|
2477
|
-
captured = predecessorChain.reduce((sum, value) => sum + value.capturedBytes, 0);
|
|
2478
|
-
const allocations = predecessorChain.map((value) => {
|
|
2479
|
-
if (value.allocation === undefined)
|
|
2480
|
-
throw new DeliveryError('Source-phase retained predecessor lacks its original allocation.');
|
|
2481
|
-
return value.allocation;
|
|
2482
|
-
});
|
|
2483
|
-
const allocation = { ...(allocations[0] ?? input.bounds) };
|
|
2484
|
-
for (const value of allocations) {
|
|
2485
|
-
allocation.maxAggregateRssBytes = Math.min(allocation.maxAggregateRssBytes, value.maxAggregateRssBytes);
|
|
2486
|
-
allocation.maxNewOutputBytes = Math.min(allocation.maxNewOutputBytes, value.maxNewOutputBytes);
|
|
2487
|
-
allocation.minFreeDiskBytes = Math.max(allocation.minFreeDiskBytes, value.minFreeDiskBytes);
|
|
2488
|
-
}
|
|
2489
|
-
accountingBounds = {
|
|
2490
|
-
...allocation,
|
|
2491
|
-
maxAggregateRssBytes: Math.min(allocation.maxAggregateRssBytes, input.bounds.maxAggregateRssBytes),
|
|
2492
|
-
maxNewOutputBytes: Math.min(allocation.maxNewOutputBytes, input.bounds.maxNewOutputBytes),
|
|
2493
|
-
minFreeDiskBytes: Math.max(allocation.minFreeDiskBytes, input.bounds.minFreeDiskBytes),
|
|
2494
|
-
};
|
|
2495
|
-
const priorBaselines = predecessorChain.flatMap((value) => value.baseline === undefined ? [] : [value.baseline]);
|
|
2496
|
-
const prior = priorBaselines[0];
|
|
2497
|
-
if (created && record !== undefined) {
|
|
2498
|
-
record.allocation = allocation;
|
|
2499
|
-
if (prior !== undefined) {
|
|
2500
|
-
record.baseline = prior;
|
|
2501
|
-
baseline = {
|
|
2502
|
-
roots: prior.roots,
|
|
2503
|
-
files: new Map(prior.files),
|
|
2504
|
-
links: new Map(prior.links),
|
|
2505
|
-
baselineId: prior.baselineId,
|
|
2506
|
-
};
|
|
2507
|
-
for (const retained of [...predecessorChain, record])
|
|
2508
|
-
for (const [file, size] of retained.bootstrapFiles)
|
|
2509
|
-
baseline.files.set(file, Math.max(baseline.files.get(file) ?? 0, size));
|
|
2510
|
-
}
|
|
2511
|
-
bootstrapCharge = previousBootstrap + record.bootstrapBytes;
|
|
2512
|
-
}
|
|
2513
|
-
if (allocations.some((value) => digestValue(value.outputRoots) !== digestValue(allocation.outputRoots))) {
|
|
2514
|
-
accountingFailure = 'Source-phase retained original output roots disagree.';
|
|
2515
|
-
throw new DeliveryError(accountingFailure);
|
|
2516
|
-
}
|
|
2517
|
-
if (input.bounds.maxNewOutputBytes > allocation.maxNewOutputBytes ||
|
|
2518
|
-
input.bounds.maxAggregateRssBytes > allocation.maxAggregateRssBytes ||
|
|
2519
|
-
input.bounds.minFreeDiskBytes < allocation.minFreeDiskBytes ||
|
|
2520
|
-
digestValue(input.bounds.outputRoots) !== digestValue(allocation.outputRoots))
|
|
2521
|
-
throw new DeliveryError('Source-phase reconciliation cannot increase its original resource allowance.');
|
|
2522
|
-
if (failed.length > 0 &&
|
|
2523
|
-
(previous === undefined ||
|
|
2524
|
-
!predecessorChain.includes(previous) ||
|
|
2525
|
-
input.reconciliation?.authorizationDigest !== input.authorization.digest))
|
|
2526
|
-
throw new DeliveryError('A failed source phase requires exact authorized retained-output reconciliation.');
|
|
2527
|
-
if (input.reconciliation !== undefined && (previous === undefined || !predecessorChain.includes(previous)))
|
|
2528
|
-
throw new DeliveryError('Source-phase reconciliation does not name its exact terminal predecessor.');
|
|
2529
|
-
if (prior !== undefined &&
|
|
2530
|
-
(priorBaselines.some((value) => value.baselineId !== prior.baselineId) ||
|
|
2531
|
-
input.reconciliation?.baselineId !== prior.baselineId ||
|
|
2532
|
-
digestValue(prior.roots) !== digestValue(roots)))
|
|
2533
|
-
throw new DeliveryError('Source-phase reconciliation cannot replace its original output baseline.');
|
|
2534
|
-
for (const retained of predecessorChain.filter((value) => value.status === 'failed-quiescent')) {
|
|
2535
|
-
const old = retained.binding;
|
|
2536
|
-
if (old === undefined ||
|
|
2537
|
-
digestValue(old.controller) !== digestValue(input.controller) ||
|
|
2538
|
-
digestValue(old.source) !== digestValue(input.source) ||
|
|
2539
|
-
old.repoRoot !== root ||
|
|
2540
|
-
old.issueNumber !== input.issueNumber ||
|
|
2541
|
-
old.rowDigest !== input.rowDigest ||
|
|
2542
|
-
old.identity !== input.identity ||
|
|
2543
|
-
old.inputs.some((artifact) => !input.inputs.some((current) => digestValue(current) === digestValue(artifact))))
|
|
2544
|
-
throw new DeliveryError('Source-phase reconciliation requires compatible source/runtime and retained frozen inputs.');
|
|
2545
|
-
for (const artifact of [old.caller, old.authorization, ...old.inputs])
|
|
2546
|
-
assertArtifact(artifact);
|
|
2547
|
-
}
|
|
2548
|
-
if (predecessorChain.some((retained) => retained.retainedArtifacts?.some((artifact) => digestValue(sourceFile(artifact.path)) !== digestValue(artifact))))
|
|
2549
|
-
throw new DeliveryError('Source-phase retained output artifact identity changed before reconciliation.');
|
|
2550
|
-
await validate(!created);
|
|
2551
|
-
const executionProducer = producerDigest();
|
|
2552
|
-
if (predecessorChain.some((value) => value.status === 'failed-quiescent' &&
|
|
2553
|
-
(value.producerDigest !== executionProducer || digestValue(value.actor) !== digestValue(actor))))
|
|
2554
|
-
throw new DeliveryError('Source-phase reconciliation requires its original authenticated actor and producer.');
|
|
2555
|
-
if (!created) {
|
|
2556
|
-
record = readRecord(path);
|
|
2557
|
-
if (record.status !== 'complete' ||
|
|
2558
|
-
record.bindingDigest !== phaseId ||
|
|
2559
|
-
record.producerDigest !== executionProducer ||
|
|
2560
|
-
record.baseline === undefined ||
|
|
2561
|
-
record.result === undefined)
|
|
2562
|
-
throw new DeliveryError('Existing source phase is incomplete or belongs to another producer.');
|
|
2563
|
-
if (digestValue(actor) !== digestValue(record.actor) ||
|
|
2564
|
-
digestValue(await sourceState(true)) !== digestValue(record.result.source) ||
|
|
2565
|
-
record.result.artifacts.some((artifact) => digestValue(sourceFile(artifact.path)) !== digestValue(artifact)))
|
|
2566
|
-
throw new DeliveryError('Completed source-phase source or artifacts changed.');
|
|
2567
|
-
reused = true;
|
|
2568
|
-
return;
|
|
2569
|
-
}
|
|
2570
|
-
validated = true;
|
|
2571
|
-
if (record === undefined)
|
|
2572
|
-
throw new DeliveryError('Source phase lacks preparation intent.');
|
|
2573
|
-
record.binding = binding;
|
|
2574
|
-
record.producerDigest = executionProducer;
|
|
2575
|
-
record.actor = actor;
|
|
2576
|
-
await withOutputObservationGate(row.path, true, async (gate) => {
|
|
2577
|
-
const gatePath = gate === undefined ? undefined : join(gate.directory, 'gate.json');
|
|
2578
|
-
if (gatePath !== undefined) {
|
|
2579
|
-
const gateBytes = lstatSync(gatePath).size;
|
|
2580
|
-
record.bootstrapBytes += gateBytes;
|
|
2581
|
-
record.bootstrapFiles.push([gatePath, gateBytes]);
|
|
2582
|
-
}
|
|
2583
|
-
bootstrapCharge = previousBootstrap + record.bootstrapBytes;
|
|
2584
|
-
const links = new Map();
|
|
2585
|
-
const files = prior === undefined
|
|
2586
|
-
? await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots, links), combined)
|
|
2587
|
-
: new Map(prior.files);
|
|
2588
|
-
if (prior !== undefined)
|
|
2589
|
-
for (const [name, alias] of prior.links)
|
|
2590
|
-
links.set(name, alias);
|
|
2591
|
-
const content = { roots, files: [...files], links: [...links] };
|
|
2592
|
-
const persistedBaseline = prior ?? { ...content, baselineId: digestValue(content) };
|
|
2593
|
-
baseline = {
|
|
2594
|
-
roots,
|
|
2595
|
-
files,
|
|
2596
|
-
links,
|
|
2597
|
-
baselineId: persistedBaseline.baselineId,
|
|
2598
|
-
...(gate === undefined ? {} : { gate }),
|
|
2599
|
-
};
|
|
2600
|
-
record.baseline = persistedBaseline;
|
|
2601
|
-
for (const retained of [...predecessorChain, record])
|
|
2602
|
-
for (const [file, size] of retained.bootstrapFiles)
|
|
2603
|
-
baseline.files.set(file, Math.max(baseline.files.get(file) ?? 0, size));
|
|
2604
|
-
record.status = 'intent';
|
|
2605
|
-
persist();
|
|
2606
|
-
await observe();
|
|
2607
|
-
let callbackFailure;
|
|
2608
|
-
let callbackRejected = false;
|
|
2609
|
-
const run = (index) => {
|
|
2610
|
-
if (expired)
|
|
2611
|
-
return Promise.reject(new DeliveryError('Source-phase command capability has expired.'));
|
|
2612
|
-
if (!Number.isSafeInteger(index) ||
|
|
2613
|
-
index !== record.nextCommandIndex ||
|
|
2614
|
-
attempted.has(index) ||
|
|
2615
|
-
active !== undefined ||
|
|
2616
|
-
index >= input.commands.length) {
|
|
2617
|
-
protocolFailure = 'Source-phase commands must be ordered, single-use and awaited.';
|
|
2618
|
-
cancelled.abort(new DeliveryError(protocolFailure));
|
|
2619
|
-
return Promise.reject(new DeliveryError(protocolFailure));
|
|
2620
|
-
}
|
|
2621
|
-
attempted.add(index);
|
|
2622
|
-
const command = input.commands[index];
|
|
2623
|
-
active = (async () => {
|
|
2624
|
-
let commandCaptured = 0;
|
|
2625
|
-
try {
|
|
2626
|
-
await validate(true);
|
|
2627
|
-
await observe();
|
|
2628
|
-
writer.assertQuiescent();
|
|
2629
|
-
startedCommand = true;
|
|
2630
|
-
const output = await runStageCommand(command.cwd, command.argv, combined, (bytes, sample, reason) => reportVerificationProgress({
|
|
2631
|
-
state: 'running',
|
|
2632
|
-
stageId: 'source-phase',
|
|
2633
|
-
completedStages: index,
|
|
2634
|
-
remainingStages: input.commands.length - index,
|
|
2635
|
-
reusedStages: 0,
|
|
2636
|
-
elapsedMs: Date.now() - started,
|
|
2637
|
-
capturedOutputBytes: bytes,
|
|
2638
|
-
...(sample === undefined ? {} : { sampledNewOutputBytes: sample.newOutputBytes }),
|
|
2639
|
-
...(reason === undefined ? {} : { reason }),
|
|
2640
|
-
}), { ...input.bounds, maxNewOutputBytes: input.bounds.maxNewOutputBytes - bootstrapCharge - captured }, baseline, undefined, writer, {
|
|
2641
|
-
environment,
|
|
2642
|
-
onCaptured: (bytes) => {
|
|
2643
|
-
commandCaptured = bytes;
|
|
2644
|
-
},
|
|
2645
|
-
onCleanupFailure: (reason) => {
|
|
2646
|
-
cleanupFailure = reason;
|
|
2647
|
-
},
|
|
2648
|
-
});
|
|
2649
|
-
record.commands.push({
|
|
2650
|
-
index,
|
|
2651
|
-
outputDigest: digestBytes(output),
|
|
2652
|
-
outputBytes: output.length,
|
|
2653
|
-
status: 'complete',
|
|
2654
|
-
});
|
|
2655
|
-
record.nextCommandIndex += 1;
|
|
2656
|
-
return output;
|
|
2657
|
-
}
|
|
2658
|
-
catch (error) {
|
|
2659
|
-
commandFailure = message(error);
|
|
2660
|
-
record.commands.push({ index, outputDigest: null, outputBytes: commandCaptured, status: 'failed' });
|
|
2661
|
-
throw error;
|
|
2662
|
-
}
|
|
2663
|
-
finally {
|
|
2664
|
-
captured += commandCaptured;
|
|
2665
|
-
record.capturedBytes += commandCaptured;
|
|
2666
|
-
active = undefined;
|
|
2667
|
-
persist();
|
|
2668
|
-
}
|
|
2669
|
-
})();
|
|
2670
|
-
return active;
|
|
2671
|
-
};
|
|
2672
|
-
entered = true;
|
|
2673
|
-
try {
|
|
2674
|
-
if (actor === undefined)
|
|
2675
|
-
throw new DeliveryError('Source-phase authenticated author is unavailable.');
|
|
2676
|
-
const source = await sourceState();
|
|
2677
|
-
await operation(Object.freeze({
|
|
2678
|
-
phaseId,
|
|
2679
|
-
signal: combined,
|
|
2680
|
-
source: Object.freeze({
|
|
2681
|
-
...source,
|
|
2682
|
-
dirty: Object.freeze(source.dirty.map((entry) => Object.freeze(entry))),
|
|
2683
|
-
}),
|
|
2684
|
-
controller: Object.freeze({ ...input.controller }),
|
|
2685
|
-
actor: Object.freeze({ ...actor }),
|
|
2686
|
-
inputs: Object.freeze([input.caller, input.authorization, ...input.inputs].map((artifact) => Object.freeze({ ...artifact }))),
|
|
2687
|
-
run,
|
|
2688
|
-
}));
|
|
2689
|
-
}
|
|
2690
|
-
catch (error) {
|
|
2691
|
-
callbackRejected = true;
|
|
2692
|
-
callbackFailure = error;
|
|
2693
|
-
}
|
|
2694
|
-
expired = true;
|
|
2695
|
-
if (active !== undefined) {
|
|
2696
|
-
protocolFailure = 'Source-phase callback settled with an unawaited command.';
|
|
2697
|
-
cancelled.abort(new DeliveryError(protocolFailure));
|
|
2698
|
-
await active.catch(() => undefined);
|
|
2699
|
-
}
|
|
2700
|
-
if (callbackRejected)
|
|
2701
|
-
throw callbackFailure;
|
|
2702
|
-
if (protocolFailure !== undefined)
|
|
2703
|
-
throw new DeliveryError(protocolFailure);
|
|
2704
|
-
if (commandFailure !== undefined)
|
|
2705
|
-
throw new DeliveryError(commandFailure);
|
|
2706
|
-
if (combined.aborted)
|
|
2707
|
-
throw new DeliveryError('Source phase cancelled.');
|
|
2708
|
-
if (record.nextCommandIndex !== input.commands.length)
|
|
2709
|
-
throw new DeliveryError('Source-phase callback did not complete its frozen graph.');
|
|
2710
|
-
await validate(true);
|
|
2711
|
-
finalSource = await sourceState(true);
|
|
2712
|
-
if (input.source.effect.kind === 'commitOnce' && finalSource.head === input.source.head)
|
|
2713
|
-
throw new DeliveryError('Source phase did not publish its required commit.');
|
|
2714
|
-
artifacts = input.completionArtifacts.map(sourceFile);
|
|
2715
|
-
writer.assertQuiescent();
|
|
2716
|
-
await observe();
|
|
2717
|
-
}).finally(() => {
|
|
2718
|
-
if (baseline !== undefined)
|
|
2719
|
-
delete baseline.gate;
|
|
2720
|
-
});
|
|
2721
|
-
}
|
|
2722
|
-
catch (error) {
|
|
2723
|
-
operationFailure = message(error);
|
|
2724
|
-
}
|
|
2725
|
-
finally {
|
|
2726
|
-
expired = true;
|
|
2727
|
-
if (active !== undefined) {
|
|
2728
|
-
cancelled.abort(new DeliveryError('Source phase is releasing its command capability.'));
|
|
2729
|
-
await active.catch((error) => {
|
|
2730
|
-
operationFailure ??= message(error);
|
|
2731
|
-
});
|
|
2732
|
-
}
|
|
2733
|
-
if (created && baseline !== undefined) {
|
|
2734
|
-
try {
|
|
2735
|
-
await observe(true);
|
|
2736
|
-
}
|
|
2737
|
-
catch (error) {
|
|
2738
|
-
accountingFailure = message(error);
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
}, {
|
|
2743
|
-
repoRoot: root,
|
|
2744
|
-
worktreePath: row.path,
|
|
2745
|
-
writerId,
|
|
2746
|
-
beforeClaim: ({ previousBytes }) => {
|
|
2747
|
-
if (previousBytes !== undefined)
|
|
2748
|
-
throw new DeliveryError('Source phase requires an absent writer; it never recovers a prior command.');
|
|
2749
|
-
},
|
|
2750
|
-
recordClaim: (bytes) => {
|
|
2751
|
-
ensurePrivateDirectoryDurably(directory);
|
|
2752
|
-
if (existsSync(path))
|
|
2753
|
-
return;
|
|
2754
|
-
created = true;
|
|
2755
|
-
writerBootstrap = bytes.length;
|
|
2756
|
-
const content = {
|
|
2757
|
-
schemaVersion: 'ai-delivery.source-phase@1',
|
|
2758
|
-
phaseId,
|
|
2759
|
-
bindingDigest: phaseId,
|
|
2760
|
-
authorizationDigest: input.authorization.digest,
|
|
2761
|
-
worktreeDigest: subject,
|
|
2762
|
-
status: 'preparing',
|
|
2763
|
-
writerId,
|
|
2764
|
-
bootstrapBytes: bytes.length,
|
|
2765
|
-
allocation: input.bounds,
|
|
2766
|
-
capturedBytes: 0,
|
|
2767
|
-
nextCommandIndex: 0,
|
|
2768
|
-
owner: JSON.parse(bytes.toString('utf8')).owner,
|
|
2769
|
-
bootstrapFiles: [],
|
|
2770
|
-
commands: [],
|
|
2771
|
-
retainedPredecessors: [],
|
|
2772
|
-
claimDigest: digestBytes(bytes),
|
|
2773
|
-
...(input.reconciliation === undefined ? {} : { reconciliation: input.reconciliation }),
|
|
2774
|
-
};
|
|
2775
|
-
record = { ...content, recordId: digestValue(content) };
|
|
2776
|
-
persist();
|
|
2777
|
-
record.preparationArtifact = sourceFile(path);
|
|
2778
|
-
record.bootstrapFiles.push([path, lstatSync(path).size]);
|
|
2779
|
-
for (let index = 0; index < 4; index++) {
|
|
2780
|
-
const size = Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`);
|
|
2781
|
-
record.bootstrapFiles[0] = [path, size];
|
|
2782
|
-
record.bootstrapBytes = writerBootstrap + size;
|
|
2783
|
-
}
|
|
2784
|
-
persist();
|
|
2785
|
-
},
|
|
2786
|
-
afterClaim: () => {
|
|
2787
|
-
if (record !== undefined && created)
|
|
2788
|
-
record.writerArtifact = sourceFile(writerPath);
|
|
2789
|
-
},
|
|
2790
|
-
recordRelease: (bytes) => {
|
|
2791
|
-
if (record !== undefined && !reused)
|
|
2792
|
-
record.releaseDigest = digestBytes(bytes);
|
|
2793
|
-
},
|
|
2794
|
-
afterRelease: () => {
|
|
2795
|
-
writerReleased = true;
|
|
2796
|
-
},
|
|
2797
|
-
onLockReleaseError: (error) => {
|
|
2798
|
-
lockFailure = message(error);
|
|
2799
|
-
},
|
|
2800
|
-
onLockCompromised: (error) => {
|
|
2801
|
-
lockFailure = message(error);
|
|
2802
|
-
cancelled.abort(error);
|
|
2803
|
-
},
|
|
2804
|
-
}, common, true);
|
|
2805
|
-
}
|
|
2806
|
-
catch (error) {
|
|
2807
|
-
releaseFailure = message(error);
|
|
2808
|
-
}
|
|
2809
|
-
if (reused &&
|
|
2810
|
-
record !== undefined &&
|
|
2811
|
-
writerReleased &&
|
|
2812
|
-
lockFailure === undefined &&
|
|
2813
|
-
releaseFailure === undefined &&
|
|
2814
|
-
operationFailure === undefined)
|
|
2815
|
-
return sourceReceipt(record);
|
|
2816
|
-
if (record === undefined || !created)
|
|
2817
|
-
throw new DeliveryError(operationFailure ?? releaseFailure ?? 'Source-phase ownership could not be established.');
|
|
2818
|
-
const failures = {
|
|
2819
|
-
...(operationFailure === undefined ? {} : { operation: operationFailure }),
|
|
2820
|
-
...(accountingFailure === undefined ? {} : { accounting: accountingFailure }),
|
|
2821
|
-
...(cleanupFailure === undefined ? {} : { cleanup: cleanupFailure }),
|
|
2822
|
-
...(writerReleased && releaseFailure === undefined
|
|
2823
|
-
? {}
|
|
2824
|
-
: { writerRelease: releaseFailure ?? 'Writer absence was not confirmed.' }),
|
|
2825
|
-
...(lockFailure === undefined ? {} : { lockRelease: lockFailure }),
|
|
2826
|
-
};
|
|
2827
|
-
const quiescent = writerReleased && releaseFailure === undefined && lockFailure === undefined && cleanupFailure === undefined;
|
|
2828
|
-
let unchanged = false;
|
|
2829
|
-
if (validated) {
|
|
2830
|
-
try {
|
|
2831
|
-
await validate(finalSource !== undefined);
|
|
2832
|
-
unchanged = (await sourceState(finalSource !== undefined)).head === input.source.head;
|
|
2833
|
-
if (finalSource !== undefined &&
|
|
2834
|
-
(existsSync(writerPath) ||
|
|
2835
|
-
artifacts.some((artifact) => digestValue(sourceFile(artifact.path)) !== digestValue(artifact))))
|
|
2836
|
-
throw new DeliveryError('Source-phase writer absence or completion artifact changed before sealing.');
|
|
2837
|
-
}
|
|
2838
|
-
catch (error) {
|
|
2839
|
-
unchanged = false;
|
|
2840
|
-
if (finalSource !== undefined)
|
|
2841
|
-
failures.operation ??= message(error);
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
const successful = Object.keys(failures).length === 0 && finalSource !== undefined;
|
|
2845
|
-
record.status = successful
|
|
2846
|
-
? 'complete'
|
|
2847
|
-
: !entered && !startedCommand && quiescent && accountingFailure === undefined
|
|
2848
|
-
? 'rejected-before-work'
|
|
2849
|
-
: quiescent && unchanged && accountingFailure === undefined && baseline !== undefined
|
|
2850
|
-
? 'failed-quiescent'
|
|
2851
|
-
: 'unresolved';
|
|
2852
|
-
if (successful && finalSource !== undefined)
|
|
2853
|
-
record.result = {
|
|
2854
|
-
source: finalSource,
|
|
2855
|
-
artifacts,
|
|
2856
|
-
newOutputBytes: newBytes,
|
|
2857
|
-
writerReleased: true,
|
|
2858
|
-
lockReleased: true,
|
|
2859
|
-
};
|
|
2860
|
-
else
|
|
2861
|
-
record.failure = failures;
|
|
2862
|
-
if (record.status === 'failed-quiescent' && baseline !== undefined) {
|
|
2863
|
-
try {
|
|
2864
|
-
const files = scanOutputRoots(baseline.roots, new Map());
|
|
2865
|
-
record.retainedArtifacts = [...files]
|
|
2866
|
-
.filter(([file, bytes]) => file !== path && bytes > (baseline.files.get(file) ?? 0))
|
|
2867
|
-
.map(([file]) => sourceFile(file));
|
|
2868
|
-
}
|
|
2869
|
-
catch (error) {
|
|
2870
|
-
record.status = 'unresolved';
|
|
2871
|
-
record.failure = { ...failures, accounting: message(error) };
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
if (baseline !== undefined) {
|
|
2875
|
-
try {
|
|
2876
|
-
await observe(true);
|
|
2877
|
-
const originalSize = baseline.files.get(path) ?? 0;
|
|
2878
|
-
const currentGrowth = Math.max(0, lstatSync(path).size - originalSize);
|
|
2879
|
-
const observed = newBytes;
|
|
2880
|
-
let projected = observed;
|
|
2881
|
-
for (let index = 0; index < 4; index++) {
|
|
2882
|
-
if (record.result !== undefined)
|
|
2883
|
-
record.result.newOutputBytes = projected;
|
|
2884
|
-
const { recordId: _recordId, ...content } = record;
|
|
2885
|
-
record.recordId = digestValue(content);
|
|
2886
|
-
projected =
|
|
2887
|
-
observed -
|
|
2888
|
-
currentGrowth +
|
|
2889
|
-
Math.max(0, Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`) - originalSize);
|
|
2890
|
-
}
|
|
2891
|
-
if (projected > accountingBounds.maxNewOutputBytes)
|
|
2892
|
-
throw new DeliveryError('Source-phase final receipt exceeds its original output allowance.');
|
|
2893
|
-
if (record.result !== undefined)
|
|
2894
|
-
record.result.newOutputBytes = projected;
|
|
2895
|
-
}
|
|
2896
|
-
catch (error) {
|
|
2897
|
-
record.status = 'unresolved';
|
|
2898
|
-
delete record.result;
|
|
2899
|
-
record.failure = { ...failures, accounting: message(error) };
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
else {
|
|
2903
|
-
for (let index = 0; index < 4; index++) {
|
|
2904
|
-
const size = Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`);
|
|
2905
|
-
record.bootstrapFiles[0] = [path, size];
|
|
2906
|
-
record.bootstrapBytes =
|
|
2907
|
-
writerBootstrap + size + record.bootstrapFiles.slice(1).reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
2908
|
-
}
|
|
2909
|
-
if (previousBootstrap + record.bootstrapBytes + captured > accountingBounds.maxNewOutputBytes) {
|
|
2910
|
-
record.status = 'unresolved';
|
|
2911
|
-
record.failure = {
|
|
2912
|
-
...failures,
|
|
2913
|
-
accounting: 'Retained rejected bootstrap exceeded the original output allowance.',
|
|
2914
|
-
};
|
|
2915
|
-
for (let index = 0; index < 4; index++) {
|
|
2916
|
-
const size = Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`);
|
|
2917
|
-
record.bootstrapFiles[0] = [path, size];
|
|
2918
|
-
record.bootstrapBytes =
|
|
2919
|
-
writerBootstrap + size + record.bootstrapFiles.slice(1).reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
persist();
|
|
2924
|
-
if (record.status !== 'complete')
|
|
2925
|
-
throw new DeliveryError(`Source phase ${record.status}: ${operationFailure ?? accountingFailure ?? releaseFailure ?? lockFailure ?? failures.operation ?? 'postconditions failed'}.`, path);
|
|
2926
|
-
return sourceReceipt(record);
|
|
2927
|
-
}
|
|
1639
|
+
});
|
|
2928
1640
|
/** @internal Runtime setup shares the existing writer, ownership handshake and bounded command runner. */
|
|
2929
1641
|
export async function withRuntimeSetupWriter(root, operation, scope) {
|
|
2930
1642
|
const cancellation = new AbortController();
|
|
@@ -2956,6 +1668,121 @@ export async function withRuntimeSetupWriter(root, operation, scope) {
|
|
|
2956
1668
|
}
|
|
2957
1669
|
root = gitRoot(root);
|
|
2958
1670
|
return withVerificationWriter(root, async (writer) => withOutputObservationGate(root, scope !== undefined, async (scopeGate) => {
|
|
1671
|
+
let ledger;
|
|
1672
|
+
let ledgerPath;
|
|
1673
|
+
const common = gitCommonDir(root);
|
|
1674
|
+
const saveLedger = () => {
|
|
1675
|
+
if (ledger === undefined || ledgerPath === undefined)
|
|
1676
|
+
throw new DeliveryError('Runtime output allowance is unbound.');
|
|
1677
|
+
const { contentDigest: _previous, ...content } = ledger;
|
|
1678
|
+
ledger = RuntimeOutputLedgerSchema.parse({
|
|
1679
|
+
...content,
|
|
1680
|
+
contentDigest: digestValue(content),
|
|
1681
|
+
});
|
|
1682
|
+
if (Buffer.byteLength(JSON.stringify(ledger, null, 2)) > 1024 ** 2)
|
|
1683
|
+
throw new DeliveryError('Runtime retained-output ledger exceeds its evidence bound.');
|
|
1684
|
+
writePrivateJsonFileAtomically(ledgerPath, ledger);
|
|
1685
|
+
};
|
|
1686
|
+
const assertOutput = (output) => {
|
|
1687
|
+
try {
|
|
1688
|
+
const bytes = readRepositoryCommandOutput({
|
|
1689
|
+
gitCommonDir: common,
|
|
1690
|
+
digest: output.digest,
|
|
1691
|
+
expectedBytes: output.bytes,
|
|
1692
|
+
});
|
|
1693
|
+
if (bytes.length !== output.bytes)
|
|
1694
|
+
throw new DeliveryError('Runtime retained-output evidence size changed.');
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
catch (error) {
|
|
1698
|
+
if (output.status === 'reserved' && error.code === 'ENOENT')
|
|
1699
|
+
return false;
|
|
1700
|
+
throw error;
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
const bindCapturedOutput = (stageId) => {
|
|
1704
|
+
if (scope === undefined)
|
|
1705
|
+
return;
|
|
1706
|
+
if (ledger !== undefined)
|
|
1707
|
+
throw new DeliveryError('Runtime output allowance is already bound.');
|
|
1708
|
+
const identity = {
|
|
1709
|
+
stageId,
|
|
1710
|
+
worktreeDigest: worktreeDigest(root),
|
|
1711
|
+
producerDigest: producerDigest(),
|
|
1712
|
+
limit: scope.maxCapturedOutputBytes,
|
|
1713
|
+
};
|
|
1714
|
+
ledgerPath = join(common, 'ai-delivery', 'verification@1', 'runtime-output', identity.worktreeDigest.slice(7), `${stageId.slice(7)}.json`);
|
|
1715
|
+
let retained;
|
|
1716
|
+
try {
|
|
1717
|
+
retained = assertPrivateFile(ledgerPath, { maxBytes: 1024 ** 2 });
|
|
1718
|
+
}
|
|
1719
|
+
catch (error) {
|
|
1720
|
+
if (error.code !== 'ENOENT')
|
|
1721
|
+
throw error;
|
|
1722
|
+
}
|
|
1723
|
+
if (retained !== undefined) {
|
|
1724
|
+
ledger = RuntimeOutputLedgerSchema.parse(JSON.parse(retained.toString('utf8')));
|
|
1725
|
+
if (ledger.stageId !== identity.stageId ||
|
|
1726
|
+
ledger.worktreeDigest !== identity.worktreeDigest ||
|
|
1727
|
+
ledger.producerDigest !== identity.producerDigest ||
|
|
1728
|
+
ledger.limit !== identity.limit)
|
|
1729
|
+
throw new DeliveryError('Runtime retained-output ledger belongs to an incompatible stage or controller.');
|
|
1730
|
+
let recovered = false;
|
|
1731
|
+
for (const output of ledger.outputs) {
|
|
1732
|
+
if (assertOutput(output) && output.status === 'reserved') {
|
|
1733
|
+
output.status = 'complete';
|
|
1734
|
+
recovered = true;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (recovered)
|
|
1738
|
+
saveLedger();
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
const content = {
|
|
1742
|
+
schemaVersion: 'ai-delivery.runtime-output@1',
|
|
1743
|
+
...identity,
|
|
1744
|
+
outputs: [],
|
|
1745
|
+
};
|
|
1746
|
+
ledger = RuntimeOutputLedgerSchema.parse({
|
|
1747
|
+
...content,
|
|
1748
|
+
contentDigest: digestValue(content),
|
|
1749
|
+
});
|
|
1750
|
+
saveLedger();
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
const remainingOutput = () => {
|
|
1754
|
+
if (scope === undefined)
|
|
1755
|
+
return 8 * 1024 ** 2;
|
|
1756
|
+
if (ledger === undefined)
|
|
1757
|
+
throw new DeliveryError('Runtime output allowance is unbound.');
|
|
1758
|
+
return ledger.limit - ledger.outputs.reduce((total, output) => total + output.bytes, 0);
|
|
1759
|
+
};
|
|
1760
|
+
const persistFailedOutput = (bytes) => {
|
|
1761
|
+
if (ledger === undefined)
|
|
1762
|
+
throw new DeliveryError('Runtime output allowance is unbound.');
|
|
1763
|
+
const digest = digestBytes(bytes);
|
|
1764
|
+
const prior = ledger.outputs.find((output) => output.digest === digest);
|
|
1765
|
+
if (prior !== undefined) {
|
|
1766
|
+
if (prior.bytes !== bytes.length || !assertOutput(prior))
|
|
1767
|
+
throw new DeliveryError('Runtime retained-output reservation is unresolved; identical output cannot be rewritten.');
|
|
1768
|
+
return ` Command output ${digest}.`;
|
|
1769
|
+
}
|
|
1770
|
+
if (bytes.length > remainingOutput())
|
|
1771
|
+
throw new DeliveryError('Runtime retained-output allowance exceeded.');
|
|
1772
|
+
const output = {
|
|
1773
|
+
digest,
|
|
1774
|
+
bytes: bytes.length,
|
|
1775
|
+
status: 'reserved',
|
|
1776
|
+
};
|
|
1777
|
+
ledger.outputs.push(output);
|
|
1778
|
+
saveLedger();
|
|
1779
|
+
writeRepositoryCommandOutput({ bytes, gitCommonDir: common });
|
|
1780
|
+
if (!assertOutput(output))
|
|
1781
|
+
throw new DeliveryError('Runtime retained-output publication is unresolved.');
|
|
1782
|
+
ledger.outputs[ledger.outputs.length - 1].status = 'complete';
|
|
1783
|
+
saveLedger();
|
|
1784
|
+
return ` Command output ${digest}.`;
|
|
1785
|
+
};
|
|
2959
1786
|
if (scope !== undefined) {
|
|
2960
1787
|
const paths = [...(scope.bounds.outputRoots ?? []), join(gitCommonDir(root), 'ai-delivery')]
|
|
2961
1788
|
.map((path) => resolve(root, path))
|
|
@@ -3008,6 +1835,7 @@ export async function withRuntimeSetupWriter(root, operation, scope) {
|
|
|
3008
1835
|
try {
|
|
3009
1836
|
const result = await operation({
|
|
3010
1837
|
assertQuiescent: () => writer.assertQuiescent(),
|
|
1838
|
+
bindCapturedOutput,
|
|
3011
1839
|
checkResources,
|
|
3012
1840
|
signal,
|
|
3013
1841
|
run: async (argv, bounds, commandSignal, environment, cwd) => {
|
|
@@ -3020,6 +1848,9 @@ export async function withRuntimeSetupWriter(root, operation, scope) {
|
|
|
3020
1848
|
throw new DeliveryError('Filesystem output roots must not overlap.');
|
|
3021
1849
|
}
|
|
3022
1850
|
const run = async (gate) => {
|
|
1851
|
+
const remaining = remainingOutput();
|
|
1852
|
+
if (remaining <= 0)
|
|
1853
|
+
throw new DeliveryError('Runtime retained-output allowance is exhausted; installer was not started.');
|
|
3023
1854
|
const links = new Map();
|
|
3024
1855
|
const files = roots === undefined
|
|
3025
1856
|
? undefined
|
|
@@ -3058,7 +1889,11 @@ export async function withRuntimeSetupWriter(root, operation, scope) {
|
|
|
3058
1889
|
...(cwd === undefined ? {} : { cwd }),
|
|
3059
1890
|
...(scope === undefined
|
|
3060
1891
|
? {}
|
|
3061
|
-
: {
|
|
1892
|
+
: {
|
|
1893
|
+
maxCapturedOutputBytes: remaining,
|
|
1894
|
+
includeControllerTree: true,
|
|
1895
|
+
failedOutput: persistFailedOutput,
|
|
1896
|
+
}),
|
|
3062
1897
|
onCaptured: (bytes) => {
|
|
3063
1898
|
captured += bytes - commandCaptured;
|
|
3064
1899
|
commandCaptured = bytes;
|