@open-agent-toolkit/cli 0.2.29 → 0.2.31
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +52 -1
- package/assets/docs/contributing/code.md +19 -7
- package/assets/docs/contributing/explainer-kit-verification.md +9 -1
- package/assets/docs/contributing/skills.md +9 -0
- package/assets/docs/workflows/projects/artifacts.md +5 -0
- package/assets/docs/workflows/projects/index.md +1 -0
- package/assets/docs/workflows/projects/lifecycle.md +23 -0
- package/assets/docs/workflows/projects/retro.md +261 -0
- package/assets/docs/workflows/skills/explainer-kit-providers.md +16 -5
- package/assets/docs/workflows/skills/explainer-kit.md +98 -38
- package/assets/docs/workflows/skills/index.md +6 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +33 -8
- package/assets/skills/explainer-kit/briefs/project-recap.md +25 -7
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +72 -0
- package/assets/skills/explainer-kit/references/contracts.md +49 -17
- package/assets/skills/explainer-kit/references/destination-contract.md +141 -25
- package/assets/skills/explainer-kit/references/extension-contract.md +19 -10
- package/assets/skills/explainer-kit/references/visual-authoring.md +24 -0
- package/assets/skills/explainer-kit/references/visual-review.md +19 -5
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +241 -0
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +215 -0
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +34 -0
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -1
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +81 -0
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +66 -0
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +109 -3
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +550 -17
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +90 -8
- package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +5 -0
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +538 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +129 -11
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +54 -0
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +2 -1
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +139 -22
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +353 -0
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +237 -107
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +1 -0
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +157 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +26 -6
- package/assets/skills/explainer-kit/scripts/run.mjs +1006 -144
- package/assets/skills/oat-explainer-kit/SKILL.md +16 -3
- package/assets/skills/oat-explainer-kit/references/config-contract.md +13 -8
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +50 -6
- package/assets/skills/oat-explainer-kit/references/migration.md +2 -1
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +11 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +37 -15
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +83 -0
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +91 -0
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +66 -10
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +60 -21
- package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +52 -8
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +271 -36
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +38 -38
- package/assets/skills/oat-project-complete/SKILL.md +60 -8
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +38 -38
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +28 -6
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +38 -38
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +38 -38
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +38 -38
- package/assets/skills/oat-project-retro/SKILL.md +310 -0
- package/assets/skills/oat-project-retro/references/apply-procedure.md +212 -0
- package/assets/skills/oat-project-retro/references/evidence-and-lanes.md +91 -0
- package/assets/skills/oat-project-retro/references/retro-quality-bar.md +100 -0
- package/assets/skills/oat-project-retro-file/SKILL.md +387 -0
- package/assets/templates/project-retro.md +236 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +99 -0
- package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
- package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
- package/dist/commands/init/tools/shared/skill-manifest.js +3 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +57 -7
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +29 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.js +37 -0
- package/dist/config/oat-config.d.ts +14 -1
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +42 -2
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +9 -0
- package/package.json +2 -2
- /package/assets/skills/explainer-kit/recipes/{project-recap.json → project-recap.v1.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-receipt.schema.json → publish-receipt.v1.schema.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-request.schema.json → publish-request.v1.schema.json} +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readdir, realpath, rm } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
2
4
|
|
|
3
5
|
import { browserCaptureIdentity } from './browser-runtime.mjs';
|
|
4
6
|
import { canonicalHash, validateContract } from './contracts.mjs';
|
|
@@ -17,6 +19,94 @@ const REVIEW_VIEWPORTS = Object.freeze(['mobile', 'tablet', 'desktop']);
|
|
|
17
19
|
const SUCCESSFUL_OUTCOMES = new Set(['built-not-durable', 'built-durable']);
|
|
18
20
|
const PARTIAL_REVIEW_OUTCOME = 'built-needs-review';
|
|
19
21
|
|
|
22
|
+
export function permissibleRunPackagePaths(
|
|
23
|
+
manifest,
|
|
24
|
+
{ includeManifest = true, includeTerminalEvidence = false } = {},
|
|
25
|
+
) {
|
|
26
|
+
if (!isObject(manifest) || !isObject(manifest.immutableHashes)) {
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
'Run package inventory requires a manifest with immutable hashes.',
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const paths = new Set([
|
|
32
|
+
...Object.keys(manifest.immutableHashes),
|
|
33
|
+
'build-record.json',
|
|
34
|
+
...(includeManifest ? ['manifest.json'] : []),
|
|
35
|
+
...(includeTerminalEvidence ? ['terminal-evidence.json'] : []),
|
|
36
|
+
]);
|
|
37
|
+
for (const path of paths) assertInventoryPath(path);
|
|
38
|
+
return [...paths].sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function enforceRunPackageInventory(
|
|
42
|
+
runRoot,
|
|
43
|
+
manifest,
|
|
44
|
+
{
|
|
45
|
+
includeManifest = true,
|
|
46
|
+
includeTerminalEvidence = false,
|
|
47
|
+
removeUnexpected = false,
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
50
|
+
if (typeof runRoot !== 'string' || runRoot.length === 0) {
|
|
51
|
+
throw new TypeError('Run package inventory requires a run root.');
|
|
52
|
+
}
|
|
53
|
+
const rootStats = await lstat(runRoot);
|
|
54
|
+
const canonicalRoot = await realpath(runRoot);
|
|
55
|
+
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
|
|
56
|
+
throw new Error('Run package inventory root is not a real directory.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const allowedFiles = new Set(
|
|
60
|
+
permissibleRunPackagePaths(manifest, {
|
|
61
|
+
includeManifest,
|
|
62
|
+
includeTerminalEvidence,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
const allowedDirectories = new Set();
|
|
66
|
+
for (const path of allowedFiles) {
|
|
67
|
+
let parent = dirname(path);
|
|
68
|
+
while (parent !== '.') {
|
|
69
|
+
allowedDirectories.add(parent);
|
|
70
|
+
parent = dirname(parent);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const presentFiles = new Set();
|
|
75
|
+
let unexpected = false;
|
|
76
|
+
async function inspect(relativeRoot = '') {
|
|
77
|
+
const entries = await readdir(join(canonicalRoot, relativeRoot));
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const relativePath = relativeRoot ? `${relativeRoot}/${entry}` : entry;
|
|
80
|
+
const absolutePath = join(canonicalRoot, relativePath);
|
|
81
|
+
const stats = await lstat(absolutePath);
|
|
82
|
+
const allowed =
|
|
83
|
+
(stats.isDirectory() && allowedDirectories.has(relativePath)) ||
|
|
84
|
+
(stats.isFile() && allowedFiles.has(relativePath));
|
|
85
|
+
if (stats.isSymbolicLink() || !allowed) {
|
|
86
|
+
unexpected = true;
|
|
87
|
+
if (removeUnexpected) {
|
|
88
|
+
await rm(absolutePath, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (stats.isDirectory()) {
|
|
93
|
+
await inspect(relativePath);
|
|
94
|
+
} else {
|
|
95
|
+
presentFiles.add(relativePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await inspect();
|
|
100
|
+
|
|
101
|
+
const missing = [...allowedFiles].some((path) => !presentFiles.has(path));
|
|
102
|
+
if (unexpected || missing) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'Run package inventory does not match the exact permissible tree.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return [...presentFiles].sort();
|
|
108
|
+
}
|
|
109
|
+
|
|
20
110
|
export function requiredImmutablePackagePaths(manifest, { runMode } = {}) {
|
|
21
111
|
if (!isObject(manifest)) {
|
|
22
112
|
throw new TypeError(
|
|
@@ -93,6 +183,11 @@ export async function validateImmutablePackageEvidence(
|
|
|
93
183
|
if (manifest?.recipe?.id !== 'project-recap') return;
|
|
94
184
|
|
|
95
185
|
const recorded = Object.keys(manifest.immutableHashes ?? {});
|
|
186
|
+
if (recorded.some(isLegacyReviewMaterial)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
'Legacy review-gate evidence is not part of the canonical package.',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
96
191
|
const successful = SUCCESSFUL_OUTCOMES.has(manifest.outcome);
|
|
97
192
|
const retainsReviewMaterial = recorded.some(isReviewMaterial);
|
|
98
193
|
if (manifest.outcome === PARTIAL_REVIEW_OUTCOME) return;
|
|
@@ -274,12 +369,17 @@ export async function validateImmutablePackageEvidence(
|
|
|
274
369
|
|
|
275
370
|
const resultPath = `${root}/result.json`;
|
|
276
371
|
const result = parseJson(await readVerified(resultPath), resultPath);
|
|
277
|
-
const resultValidation = validateContract(
|
|
278
|
-
|
|
279
|
-
|
|
372
|
+
const resultValidation = validateContract(
|
|
373
|
+
'visual-review-evidence',
|
|
374
|
+
result,
|
|
375
|
+
{
|
|
376
|
+
visualReviewRequest: request,
|
|
377
|
+
attempt,
|
|
378
|
+
},
|
|
379
|
+
);
|
|
280
380
|
if (!resultValidation.valid) {
|
|
281
381
|
throw new Error(
|
|
282
|
-
`Immutable visual-review
|
|
382
|
+
`Immutable visual-review evidence ${resultPath} is invalid: ${resultValidation.errors
|
|
283
383
|
.map(({ code }) => code)
|
|
284
384
|
.join(', ')}.`,
|
|
285
385
|
);
|
|
@@ -309,18 +409,17 @@ function addVisualReviewAttemptPaths(required, manifest, attempt) {
|
|
|
309
409
|
}
|
|
310
410
|
|
|
311
411
|
function isReviewMaterial(path) {
|
|
312
|
-
return (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
);
|
|
412
|
+
return path.startsWith('qa/browser/') || path.startsWith('qa/visual-review/');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isLegacyReviewMaterial(path) {
|
|
416
|
+
return path.startsWith('qa/review-gate/');
|
|
317
417
|
}
|
|
318
418
|
|
|
319
419
|
function isAttemptTwoMaterial(path) {
|
|
320
420
|
return (
|
|
321
421
|
path === VISUAL_REVISION_PATH ||
|
|
322
|
-
path.startsWith('qa/visual-review/attempt-2/')
|
|
323
|
-
path.startsWith('qa/review-gate/attempt-2')
|
|
422
|
+
path.startsWith('qa/visual-review/attempt-2/')
|
|
324
423
|
);
|
|
325
424
|
}
|
|
326
425
|
|
|
@@ -328,6 +427,25 @@ function isObject(value) {
|
|
|
328
427
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
329
428
|
}
|
|
330
429
|
|
|
430
|
+
function assertInventoryPath(path) {
|
|
431
|
+
if (
|
|
432
|
+
typeof path !== 'string' ||
|
|
433
|
+
path.length === 0 ||
|
|
434
|
+
path.includes('\\') ||
|
|
435
|
+
isAbsolute(path) ||
|
|
436
|
+
path
|
|
437
|
+
.split('/')
|
|
438
|
+
.some(
|
|
439
|
+
(segment) => segment === '' || segment === '.' || segment === '..',
|
|
440
|
+
) ||
|
|
441
|
+
relative('.', path).startsWith(`..${sep}`)
|
|
442
|
+
) {
|
|
443
|
+
throw new TypeError(
|
|
444
|
+
'Run package inventory paths must be normalized relative paths.',
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
331
449
|
function isPng(bytes) {
|
|
332
450
|
return (
|
|
333
451
|
bytes.length >= 8 &&
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const SUCCESSFUL_STAGE_STATUSES = new Set(['passed', 'warned', 'skipped']);
|
|
2
|
+
|
|
3
|
+
export function assertManifestPublishable(manifest, { buildRecord } = {}) {
|
|
4
|
+
const outcome = manifest?.outcome;
|
|
5
|
+
const reviewFlagged = (manifest?.warnings ?? []).some((warning) =>
|
|
6
|
+
String(warning).startsWith('visual-review-required:'),
|
|
7
|
+
);
|
|
8
|
+
const durableClean = outcome === 'built-durable' && !reviewFlagged;
|
|
9
|
+
const inFlightClean =
|
|
10
|
+
outcome === 'incomplete' &&
|
|
11
|
+
!reviewFlagged &&
|
|
12
|
+
isCleanPublicationTransition(buildRecord);
|
|
13
|
+
|
|
14
|
+
if (!durableClean && !inFlightClean) {
|
|
15
|
+
throw publicationOutcomeError(
|
|
16
|
+
`Manifest outcome ${String(outcome ?? 'missing')} is not eligible for publication.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return manifest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isCleanPublicationTransition(buildRecord) {
|
|
23
|
+
if (
|
|
24
|
+
!buildRecord ||
|
|
25
|
+
buildRecord.outcome !== 'incomplete' ||
|
|
26
|
+
!Array.isArray(buildRecord.stages)
|
|
27
|
+
) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const publishIndex = buildRecord.stages.findIndex(
|
|
31
|
+
({ id }) => id === 'publish',
|
|
32
|
+
);
|
|
33
|
+
if (
|
|
34
|
+
publishIndex < 0 ||
|
|
35
|
+
buildRecord.stages[publishIndex]?.status !== 'running'
|
|
36
|
+
) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return buildRecord.stages
|
|
40
|
+
.slice(0, publishIndex)
|
|
41
|
+
.every(
|
|
42
|
+
({ status, warnings = [] }) =>
|
|
43
|
+
SUCCESSFUL_STAGE_STATUSES.has(status) &&
|
|
44
|
+
!warnings.some((warning) =>
|
|
45
|
+
String(warning).startsWith('visual-review-required:'),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function publicationOutcomeError(message) {
|
|
51
|
+
const error = new Error(message);
|
|
52
|
+
error.code = 'E_PUBLISH_OUTCOME';
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
@@ -62,7 +62,8 @@ const AUTHORING_TYPES = new Set(['markdown', 'html']);
|
|
|
62
62
|
const SAFE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
63
63
|
const RECIPE_FILES = [
|
|
64
64
|
'project-explainer.json',
|
|
65
|
-
'project-recap.json',
|
|
65
|
+
'project-recap.v1.json',
|
|
66
|
+
'project-recap.v2.json',
|
|
66
67
|
'engineer-tour.json',
|
|
67
68
|
'program-recap.json',
|
|
68
69
|
];
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
validateImmutablePackageEvidence,
|
|
16
16
|
} from './package-coverage.mjs';
|
|
17
17
|
import { resolveRootConfinedPath } from './safe-paths.mjs';
|
|
18
|
+
import {
|
|
19
|
+
assertTerminalEvidence,
|
|
20
|
+
createTerminalEvidence,
|
|
21
|
+
createVisualReviewEvidence,
|
|
22
|
+
} from './terminal-evidence.mjs';
|
|
18
23
|
|
|
19
24
|
export {
|
|
20
25
|
PACKAGE_COVERAGE_VERSION,
|
|
@@ -137,10 +142,10 @@ export async function updateBuildRecord(run, stage) {
|
|
|
137
142
|
...current,
|
|
138
143
|
status: stage.status,
|
|
139
144
|
outputPaths: stage.outputPaths ?? current.outputPaths,
|
|
140
|
-
warnings: stage.warnings ?? current.warnings,
|
|
145
|
+
warnings: structuredClone(stage.warnings ?? current.warnings),
|
|
141
146
|
};
|
|
142
147
|
if (stage.error !== undefined) {
|
|
143
|
-
next.error = stage.
|
|
148
|
+
next.error = localStageFailure(stage.id);
|
|
144
149
|
}
|
|
145
150
|
if (stage.status === 'running' && next.startedAt === undefined) {
|
|
146
151
|
next.startedAt = timestamp;
|
|
@@ -202,9 +207,50 @@ export async function writeVisualReviewAttempt(run, { attempt, review } = {}) {
|
|
|
202
207
|
'Visual review records must contain valid bound contracts.',
|
|
203
208
|
);
|
|
204
209
|
}
|
|
210
|
+
const retainedResult = createVisualReviewEvidence({
|
|
211
|
+
request: review.request,
|
|
212
|
+
attempt,
|
|
213
|
+
result: review.result,
|
|
214
|
+
});
|
|
215
|
+
return writeRetainedVisualReview(run, {
|
|
216
|
+
attempt,
|
|
217
|
+
request: review.request,
|
|
218
|
+
retainedResult,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function writeVisualReviewFailure(
|
|
223
|
+
run,
|
|
224
|
+
{ attempt, request, kind = 'provider-failure' } = {},
|
|
225
|
+
) {
|
|
226
|
+
assertRun(run);
|
|
227
|
+
if (
|
|
228
|
+
![1, 2].includes(attempt) ||
|
|
229
|
+
!isObject(request) ||
|
|
230
|
+
!['provider-failure', 'pipeline-failure'].includes(kind)
|
|
231
|
+
) {
|
|
232
|
+
throw new TypeError(
|
|
233
|
+
'Visual review failures require a bound request, attempt, and local failure kind.',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const retainedResult = createVisualReviewEvidence({
|
|
237
|
+
request,
|
|
238
|
+
attempt,
|
|
239
|
+
failureKind: kind,
|
|
240
|
+
});
|
|
241
|
+
return writeRetainedVisualReview(run, {
|
|
242
|
+
attempt,
|
|
243
|
+
request,
|
|
244
|
+
retainedResult,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
205
247
|
|
|
248
|
+
async function writeRetainedVisualReview(
|
|
249
|
+
run,
|
|
250
|
+
{ attempt, request, retainedResult },
|
|
251
|
+
) {
|
|
206
252
|
const directory = `qa/visual-review/attempt-${attempt}`;
|
|
207
|
-
const retainedRequest = structuredClone(
|
|
253
|
+
const retainedRequest = structuredClone(request);
|
|
208
254
|
const paths = [];
|
|
209
255
|
for (const artifact of retainedRequest.renderedArtifacts) {
|
|
210
256
|
for (const evidence of artifact.evidence) {
|
|
@@ -228,36 +274,99 @@ export async function writeVisualReviewAttempt(run, { attempt, review } = {}) {
|
|
|
228
274
|
const requestPath = `${directory}/request.json`;
|
|
229
275
|
const resultPath = `${directory}/result.json`;
|
|
230
276
|
await writeJsonAtomic(run.runRoot, requestPath, retainedRequest);
|
|
231
|
-
await writeJsonAtomic(run.runRoot, resultPath,
|
|
277
|
+
await writeJsonAtomic(run.runRoot, resultPath, retainedResult);
|
|
232
278
|
return [...paths, requestPath, resultPath];
|
|
233
279
|
}
|
|
234
280
|
|
|
235
|
-
export async function
|
|
281
|
+
export async function writeTerminalEvidence(run, input = {}) {
|
|
282
|
+
assertRun(run);
|
|
283
|
+
const allowed = new Set([
|
|
284
|
+
'outcome',
|
|
285
|
+
'manifest',
|
|
286
|
+
'reasons',
|
|
287
|
+
'evidenceDisposition',
|
|
288
|
+
]);
|
|
289
|
+
const unsupported = Object.keys(input).filter((key) => !allowed.has(key));
|
|
290
|
+
if (unsupported.length > 0) {
|
|
291
|
+
throw new TypeError(
|
|
292
|
+
`Terminal evidence rejects legacy or diagnostic fields: ${unsupported.join(', ')}.`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const { outcome, manifest, reasons, evidenceDisposition } = input;
|
|
296
|
+
if (
|
|
297
|
+
!['built-needs-review', 'failed'].includes(outcome) ||
|
|
298
|
+
!['retained', 'partial', 'superseded', 'unavailable'].includes(
|
|
299
|
+
evidenceDisposition,
|
|
300
|
+
) ||
|
|
301
|
+
!Array.isArray(reasons) ||
|
|
302
|
+
reasons.some((reason) => !isObject(reason))
|
|
303
|
+
) {
|
|
304
|
+
throw new TypeError('Terminal evidence has an invalid compact shape.');
|
|
305
|
+
}
|
|
306
|
+
if (
|
|
307
|
+
manifest !== undefined &&
|
|
308
|
+
(!isObject(manifest) ||
|
|
309
|
+
manifest.runId !== run.runId ||
|
|
310
|
+
manifest.slug !== run.slug)
|
|
311
|
+
) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
'Terminal evidence manifest identity does not match the run.',
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const path = 'terminal-evidence.json';
|
|
317
|
+
try {
|
|
318
|
+
await access(join(run.runRoot, path));
|
|
319
|
+
throw new Error('Terminal evidence is immutable once retained.');
|
|
320
|
+
} catch (caught) {
|
|
321
|
+
if (caught?.code !== 'ENOENT') throw caught;
|
|
322
|
+
}
|
|
323
|
+
const evidence = createTerminalEvidence({
|
|
324
|
+
runId: run.runId,
|
|
325
|
+
outcome,
|
|
326
|
+
manifest,
|
|
327
|
+
reasons,
|
|
328
|
+
evidenceDisposition,
|
|
329
|
+
});
|
|
330
|
+
await writeJsonAtomic(run.runRoot, path, evidence);
|
|
331
|
+
return path;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function supersedeTerminalEvidence(
|
|
236
335
|
run,
|
|
237
|
-
{
|
|
336
|
+
{ manifest, supersededBy } = {},
|
|
238
337
|
) {
|
|
239
338
|
assertRun(run);
|
|
240
339
|
if (
|
|
241
|
-
!
|
|
242
|
-
|
|
243
|
-
|
|
340
|
+
!isObject(manifest) ||
|
|
341
|
+
manifest.runId !== run.runId ||
|
|
342
|
+
manifest.slug !== run.slug
|
|
244
343
|
) {
|
|
245
|
-
throw new
|
|
246
|
-
'
|
|
344
|
+
throw new Error(
|
|
345
|
+
'Terminal evidence manifest identity does not match the run.',
|
|
247
346
|
);
|
|
248
347
|
}
|
|
249
|
-
const path =
|
|
250
|
-
await
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
348
|
+
const path = 'terminal-evidence.json';
|
|
349
|
+
const retained = JSON.parse(await readFile(join(run.runRoot, path), 'utf8'));
|
|
350
|
+
assertTerminalEvidence(retained, { manifest });
|
|
351
|
+
if (retained.evidenceDisposition === 'superseded') {
|
|
352
|
+
throw new Error('Terminal evidence is already superseded.');
|
|
353
|
+
}
|
|
354
|
+
const superseded = createTerminalEvidence({
|
|
355
|
+
runId: run.runId,
|
|
356
|
+
outcome: manifest.outcome,
|
|
357
|
+
manifest,
|
|
358
|
+
reasons: [
|
|
359
|
+
{
|
|
360
|
+
stage: 'finalization',
|
|
361
|
+
kind: 'superseded',
|
|
362
|
+
count: 1,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
evidenceDisposition: 'superseded',
|
|
366
|
+
supersededBy,
|
|
259
367
|
});
|
|
260
|
-
|
|
368
|
+
await writeJsonAtomic(run.runRoot, path, superseded);
|
|
369
|
+
return path;
|
|
261
370
|
}
|
|
262
371
|
|
|
263
372
|
export async function writeVisualRevision(run, { artifactIds, changes } = {}) {
|
|
@@ -764,6 +873,14 @@ function assertValidContract(kind, value, context) {
|
|
|
764
873
|
}
|
|
765
874
|
}
|
|
766
875
|
|
|
876
|
+
function localStageFailure(stageId) {
|
|
877
|
+
return {
|
|
878
|
+
code: `E_${stageId.toUpperCase().replaceAll('-', '_')}`,
|
|
879
|
+
message: `The ${stageId} stage failed.`,
|
|
880
|
+
recovery: [`Correct the ${stageId} stage and start a new run.`],
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
767
884
|
function assertRun(run) {
|
|
768
885
|
if (
|
|
769
886
|
!isObject(run) ||
|