@open-agent-toolkit/cli 0.2.73 → 0.2.75
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 +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
|
@@ -1,26 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
basename,
|
|
5
|
+
dirname,
|
|
6
|
+
isAbsolute,
|
|
7
|
+
join,
|
|
8
|
+
relative,
|
|
9
|
+
resolve,
|
|
10
|
+
sep,
|
|
11
|
+
} from 'node:path';
|
|
2
12
|
import { pathToFileURL } from 'node:url';
|
|
3
13
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'failed',
|
|
9
|
-
]);
|
|
14
|
+
import { validateContract } from '../../explainer-kit/scripts/lib/contracts.mjs';
|
|
15
|
+
import { isFlowFailureStage } from '../../explainer-kit/scripts/lib/failure.mjs';
|
|
16
|
+
import { loadRecipe } from '../../explainer-kit/scripts/lib/recipes.mjs';
|
|
17
|
+
import { validateSatisfiedRunPackage } from '../../explainer-kit/scripts/lib/run-package.mjs';
|
|
10
18
|
|
|
11
19
|
/**
|
|
12
20
|
* A skip reason is the recorded `source` of the skip decision, so the guard
|
|
13
21
|
* accepts exactly the sources that may produce a `skip` recap record.
|
|
14
22
|
*/
|
|
15
|
-
const SKIP_REASONS = new Set([
|
|
23
|
+
const SKIP_REASONS = new Set([
|
|
24
|
+
'interactive',
|
|
25
|
+
'failed_attempt',
|
|
26
|
+
'capability_probe',
|
|
27
|
+
]);
|
|
16
28
|
|
|
17
|
-
export function checkTerminalOutcome({
|
|
29
|
+
export function checkTerminalOutcome({
|
|
30
|
+
intent,
|
|
31
|
+
reason,
|
|
32
|
+
manifest = null,
|
|
33
|
+
failure = null,
|
|
34
|
+
failureRootHash = null,
|
|
35
|
+
}) {
|
|
18
36
|
if (reason !== undefined && !SKIP_REASONS.has(reason)) {
|
|
19
37
|
throw recapOutcomeError(
|
|
20
38
|
`Recap skip reason must be one of ${[...SKIP_REASONS].join(', ')}.`,
|
|
21
39
|
);
|
|
22
40
|
}
|
|
23
41
|
if (intent === 'skip') {
|
|
42
|
+
if (
|
|
43
|
+
reason === 'failed_attempt' &&
|
|
44
|
+
!(isFailedManifest(manifest) || isFlowFailure(failure, failureRootHash))
|
|
45
|
+
) {
|
|
46
|
+
throw recapOutcomeError(
|
|
47
|
+
'failed_attempt requires a failed or incomplete manifest or failure.json.',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
24
50
|
return { ok: true, intent, outcome: null, reason: reason ?? null };
|
|
25
51
|
}
|
|
26
52
|
if (reason !== undefined) {
|
|
@@ -31,19 +57,106 @@ export function checkTerminalOutcome({ intent, outcome, reason }) {
|
|
|
31
57
|
if (intent !== 'generate') {
|
|
32
58
|
throw recapOutcomeError('Recap intent must be generate or skip.');
|
|
33
59
|
}
|
|
34
|
-
|
|
60
|
+
throw recapOutcomeError(
|
|
61
|
+
'A terminal recap outcome requires a complete satisfied project-recap package.',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main(argv) {
|
|
66
|
+
const { intent, manifestPath, failurePath, projectRoot, reason } =
|
|
67
|
+
parseArguments(argv);
|
|
68
|
+
if (manifestPath !== undefined && failurePath !== undefined) {
|
|
69
|
+
throw recapOutcomeError(
|
|
70
|
+
'Use either --manifest or --failure as failed-attempt evidence, not both.',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (intent === 'generate') {
|
|
74
|
+
if (
|
|
75
|
+
reason !== undefined ||
|
|
76
|
+
failurePath !== undefined ||
|
|
77
|
+
manifestPath === undefined ||
|
|
78
|
+
basename(manifestPath) !== 'manifest.json'
|
|
79
|
+
) {
|
|
80
|
+
throw recapOutcomeError(
|
|
81
|
+
'Generated project recaps require the canonical manifest.json for a complete satisfied package.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const manifest = await validateSatisfiedRunPackage(
|
|
86
|
+
dirname(manifestPath),
|
|
87
|
+
loadRecipe('project-recap', '2'),
|
|
88
|
+
);
|
|
89
|
+
return { ok: true, intent, outcome: manifest.outcome };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw recapOutcomeError(
|
|
92
|
+
`Generated project recap package assurance failed: ${error.message}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
(manifestPath !== undefined || failurePath !== undefined) &&
|
|
98
|
+
reason !== 'failed_attempt'
|
|
99
|
+
) {
|
|
35
100
|
throw recapOutcomeError(
|
|
36
|
-
'
|
|
101
|
+
'Failed-attempt evidence is accepted only for a skip/failed_attempt intent.',
|
|
37
102
|
);
|
|
38
103
|
}
|
|
39
|
-
|
|
104
|
+
if (projectRoot !== undefined && reason !== 'failed_attempt') {
|
|
105
|
+
throw recapOutcomeError(
|
|
106
|
+
'A failed-attempt project root applies only to skip/failed_attempt evidence.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (reason === 'failed_attempt') {
|
|
110
|
+
if (intent !== 'skip') {
|
|
111
|
+
throw recapOutcomeError(
|
|
112
|
+
'Failed-attempt evidence is accepted only for a skip/failed_attempt intent.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (projectRoot !== undefined) {
|
|
116
|
+
const suppliedPath = manifestPath ?? failurePath;
|
|
117
|
+
if (suppliedPath === undefined) {
|
|
118
|
+
return validateFailedAttemptEvidence({});
|
|
119
|
+
}
|
|
120
|
+
const canonicalProjectRoot = await realpath(projectRoot);
|
|
121
|
+
const locator = relative(canonicalProjectRoot, resolve(suppliedPath))
|
|
122
|
+
.split(sep)
|
|
123
|
+
.join('/');
|
|
124
|
+
const evidence = await resolveProjectFailedAttemptEvidence({
|
|
125
|
+
projectPath: canonicalProjectRoot,
|
|
126
|
+
locator,
|
|
127
|
+
});
|
|
128
|
+
if ((evidence.kind === 'manifest') !== (manifestPath !== undefined)) {
|
|
129
|
+
throw recapOutcomeError(
|
|
130
|
+
'Failed-attempt evidence kind does not match its guard flag.',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return validateFailedAttemptEvidence({
|
|
134
|
+
...(evidence.kind === 'manifest'
|
|
135
|
+
? { manifestPath: evidence.path }
|
|
136
|
+
: { failurePath: evidence.path }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return validateFailedAttemptEvidence({ manifestPath, failurePath });
|
|
140
|
+
}
|
|
141
|
+
return checkTerminalOutcome({
|
|
142
|
+
intent,
|
|
143
|
+
...(reason !== undefined && { reason }),
|
|
144
|
+
});
|
|
40
145
|
}
|
|
41
146
|
|
|
42
|
-
async function
|
|
43
|
-
|
|
44
|
-
|
|
147
|
+
export async function validateFailedAttemptEvidence({
|
|
148
|
+
manifestPath,
|
|
149
|
+
failurePath,
|
|
150
|
+
}) {
|
|
151
|
+
if (manifestPath !== undefined && failurePath !== undefined) {
|
|
152
|
+
throw recapOutcomeError(
|
|
153
|
+
'Use either --manifest or --failure as failed-attempt evidence, not both.',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
let manifest;
|
|
157
|
+
let failure;
|
|
158
|
+
let failureRootHash;
|
|
45
159
|
if (manifestPath !== undefined) {
|
|
46
|
-
let manifest;
|
|
47
160
|
try {
|
|
48
161
|
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
49
162
|
} catch (error) {
|
|
@@ -51,18 +164,105 @@ async function main(argv) {
|
|
|
51
164
|
`Recap manifest could not be read: ${error.message}`,
|
|
52
165
|
);
|
|
53
166
|
}
|
|
54
|
-
|
|
167
|
+
}
|
|
168
|
+
if (failurePath !== undefined) {
|
|
169
|
+
if (basename(failurePath) !== 'failure.json') {
|
|
170
|
+
throw recapOutcomeError(
|
|
171
|
+
'Failed-attempt evidence must name a failure.json file.',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
failure = JSON.parse(await readFile(failurePath, 'utf8'));
|
|
176
|
+
failureRootHash = hashText(await realpath(dirname(failurePath)));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw recapOutcomeError(
|
|
179
|
+
`Recap failure evidence could not be read: ${error.message}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
55
182
|
}
|
|
56
183
|
return checkTerminalOutcome({
|
|
57
|
-
intent,
|
|
58
|
-
|
|
59
|
-
|
|
184
|
+
intent: 'skip',
|
|
185
|
+
manifest,
|
|
186
|
+
failure,
|
|
187
|
+
failureRootHash,
|
|
188
|
+
reason: 'failed_attempt',
|
|
60
189
|
});
|
|
61
190
|
}
|
|
62
191
|
|
|
192
|
+
export async function resolveProjectFailedAttemptEvidence({
|
|
193
|
+
projectPath,
|
|
194
|
+
locator,
|
|
195
|
+
}) {
|
|
196
|
+
try {
|
|
197
|
+
if (typeof locator !== 'string') {
|
|
198
|
+
throw new Error('the persisted locator is not a string');
|
|
199
|
+
}
|
|
200
|
+
const parts = locator.split('/');
|
|
201
|
+
const runSlug = parts[1];
|
|
202
|
+
const fileName = parts[2];
|
|
203
|
+
if (
|
|
204
|
+
parts.length !== 3 ||
|
|
205
|
+
parts[0] !== 'explainers' ||
|
|
206
|
+
typeof runSlug !== 'string' ||
|
|
207
|
+
!/^[a-z0-9][a-z0-9._-]*$/.test(runSlug) ||
|
|
208
|
+
!['manifest.json', 'failure.json'].includes(fileName)
|
|
209
|
+
) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'the locator must name explainers/<run-slug>/manifest.json or failure.json',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const projectRoot = await realpath(projectPath);
|
|
216
|
+
const explainersRoot = await realpath(join(projectRoot, 'explainers'));
|
|
217
|
+
if (!isContained(projectRoot, explainersRoot)) {
|
|
218
|
+
throw new Error('the explainers root escapes the project root');
|
|
219
|
+
}
|
|
220
|
+
const runRoot = await realpath(join(explainersRoot, runSlug));
|
|
221
|
+
if (!isContained(explainersRoot, runRoot)) {
|
|
222
|
+
throw new Error('the evidence run escapes the explainers root');
|
|
223
|
+
}
|
|
224
|
+
const evidencePath = await realpath(join(projectRoot, locator));
|
|
225
|
+
if (
|
|
226
|
+
!isContained(runRoot, evidencePath) ||
|
|
227
|
+
basename(evidencePath) !== fileName
|
|
228
|
+
) {
|
|
229
|
+
throw new Error('the evidence path escapes its declared run root');
|
|
230
|
+
}
|
|
231
|
+
const evidenceInfo = await lstat(evidencePath);
|
|
232
|
+
if (!evidenceInfo.isFile()) {
|
|
233
|
+
throw new Error('the evidence path is not a regular file');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const kind = fileName === 'manifest.json' ? 'manifest' : 'failure';
|
|
237
|
+
await validateFailedAttemptEvidence({
|
|
238
|
+
...(kind === 'manifest'
|
|
239
|
+
? { manifestPath: evidencePath }
|
|
240
|
+
: { failurePath: evidencePath }),
|
|
241
|
+
});
|
|
242
|
+
return { kind, path: evidencePath };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error?.code === 'E_RECAP_OUTCOME') throw error;
|
|
245
|
+
throw recapOutcomeError(
|
|
246
|
+
`Failed-attempt evidence containment failed: ${error.message}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isContained(root, candidate) {
|
|
252
|
+
const relation = relative(root, candidate);
|
|
253
|
+
return (
|
|
254
|
+
relation !== '' &&
|
|
255
|
+
relation !== '..' &&
|
|
256
|
+
!relation.startsWith(`..${sep}`) &&
|
|
257
|
+
!isAbsolute(relation)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
63
261
|
function parseArguments(argv) {
|
|
64
262
|
let intent;
|
|
65
263
|
let manifestPath;
|
|
264
|
+
let failurePath;
|
|
265
|
+
let projectRoot;
|
|
66
266
|
let reason;
|
|
67
267
|
for (let index = 0; index < argv.length; index += 2) {
|
|
68
268
|
const flag = argv[index];
|
|
@@ -74,13 +274,48 @@ function parseArguments(argv) {
|
|
|
74
274
|
intent = value;
|
|
75
275
|
} else if (flag === '--manifest') {
|
|
76
276
|
manifestPath = value;
|
|
277
|
+
} else if (flag === '--failure') {
|
|
278
|
+
failurePath = value;
|
|
279
|
+
} else if (flag === '--project-root') {
|
|
280
|
+
projectRoot = value;
|
|
77
281
|
} else if (flag === '--skip-reason') {
|
|
78
282
|
reason = value;
|
|
79
283
|
} else {
|
|
80
284
|
throw recapOutcomeError(`Unsupported argument: ${flag}.`);
|
|
81
285
|
}
|
|
82
286
|
}
|
|
83
|
-
return { intent, manifestPath, reason };
|
|
287
|
+
return { intent, manifestPath, failurePath, projectRoot, reason };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isFailedManifest(value) {
|
|
291
|
+
if (!value || !validateContract('manifest', value).valid) return false;
|
|
292
|
+
return (
|
|
293
|
+
['failed', 'incomplete'].includes(value.outcome) &&
|
|
294
|
+
value.artifacts.length > 0 &&
|
|
295
|
+
value.artifacts.every(({ status }) => status === 'failed')
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isFlowFailure(value, expectedRootHash) {
|
|
300
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
301
|
+
const keys = Object.keys(value);
|
|
302
|
+
return (
|
|
303
|
+
keys.length === 5 &&
|
|
304
|
+
keys.every((key) =>
|
|
305
|
+
['schemaVersion', 'runRootHash', 'stage', 'cause', 'at'].includes(key),
|
|
306
|
+
) &&
|
|
307
|
+
value.schemaVersion === 'explainer-kit.failure/v1' &&
|
|
308
|
+
value.runRootHash === expectedRootHash &&
|
|
309
|
+
isFlowFailureStage(value.stage) &&
|
|
310
|
+
typeof value.cause === 'string' &&
|
|
311
|
+
value.cause.length > 0 &&
|
|
312
|
+
typeof value.at === 'string' &&
|
|
313
|
+
!Number.isNaN(Date.parse(value.at))
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function hashText(value) {
|
|
318
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
84
319
|
}
|
|
85
320
|
|
|
86
321
|
function recapOutcomeError(message) {
|
|
@@ -2,7 +2,10 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
validateIntentRecord,
|
|
7
|
+
validateWritableIntentRecord,
|
|
8
|
+
} from './resolve-intent.mjs';
|
|
6
9
|
|
|
7
10
|
const FRONTMATTER_KEYS = Object.freeze({
|
|
8
11
|
projectExplainer: 'oat_project_explainer',
|
|
@@ -17,7 +20,7 @@ export function hashStateContent(content) {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function updateStateFrontmatter(content, product, record) {
|
|
20
|
-
|
|
23
|
+
validateWritableIntentRecord(product, record);
|
|
21
24
|
if (typeof content !== 'string') {
|
|
22
25
|
throw new TypeError('State content must be a string.');
|
|
23
26
|
}
|
|
@@ -47,6 +50,9 @@ export function updateStateFrontmatter(content, product, record) {
|
|
|
47
50
|
` decision: ${record.decision}`,
|
|
48
51
|
` source: ${record.source}`,
|
|
49
52
|
` decided_at: '${record.decided_at}'`,
|
|
53
|
+
...(Object.hasOwn(record, 'failed_attempt_evidence')
|
|
54
|
+
? [` failed_attempt_evidence: '${record.failed_attempt_evidence}'`]
|
|
55
|
+
: []),
|
|
50
56
|
];
|
|
51
57
|
if (matches.length === 0) {
|
|
52
58
|
lines.splice(closingIndex, 0, ...replacement);
|
|
@@ -66,6 +72,69 @@ export function updateStateFrontmatter(content, product, record) {
|
|
|
66
72
|
return lines.join(newline);
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
export function readIntentFromStateContent(content, product) {
|
|
76
|
+
if (typeof content !== 'string') {
|
|
77
|
+
throw new TypeError('State content must be a string.');
|
|
78
|
+
}
|
|
79
|
+
const key = FRONTMATTER_KEYS[product];
|
|
80
|
+
if (!key) {
|
|
81
|
+
throw new Error(`Unsupported lifecycle product: ${String(product)}`);
|
|
82
|
+
}
|
|
83
|
+
const lines = content.split(/\r?\n/);
|
|
84
|
+
if (lines[0] !== '---') {
|
|
85
|
+
throw new Error('Project state must begin with YAML frontmatter.');
|
|
86
|
+
}
|
|
87
|
+
const closingIndex = lines.indexOf('---', 1);
|
|
88
|
+
if (closingIndex < 0) {
|
|
89
|
+
throw new Error('Project state has no closing frontmatter delimiter.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const keyPattern = new RegExp(`^${key}\\s*:\\s*(.*)$`);
|
|
93
|
+
const matches = [];
|
|
94
|
+
for (let index = 1; index < closingIndex; index += 1) {
|
|
95
|
+
const match = lines[index].match(keyPattern);
|
|
96
|
+
if (match) matches.push({ index, value: match[1].trim() });
|
|
97
|
+
}
|
|
98
|
+
if (matches.length > 1) {
|
|
99
|
+
throw new Error(`Project state contains duplicate ${key} fields.`);
|
|
100
|
+
}
|
|
101
|
+
if (matches.length === 0 || ['null', '~'].includes(matches[0].value)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (matches[0].value !== '') {
|
|
105
|
+
throw new Error(`${key} must use a lifecycle decision block.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const record = {};
|
|
109
|
+
for (
|
|
110
|
+
let index = matches[0].index + 1;
|
|
111
|
+
index < closingIndex && /^[ \t]/.test(lines[index]);
|
|
112
|
+
index += 1
|
|
113
|
+
) {
|
|
114
|
+
if (lines[index].trim() === '') continue;
|
|
115
|
+
const field = lines[index].match(
|
|
116
|
+
/^\s+(decision|source|decided_at|failed_attempt_evidence)\s*:\s*(.+?)\s*$/,
|
|
117
|
+
);
|
|
118
|
+
if (!field || Object.hasOwn(record, field[1])) {
|
|
119
|
+
throw new Error(`${key} contains an invalid lifecycle decision field.`);
|
|
120
|
+
}
|
|
121
|
+
record[field[1]] = parseStateScalar(field[2]);
|
|
122
|
+
}
|
|
123
|
+
validateIntentRecord(product, record);
|
|
124
|
+
return record;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function readPersistedIntent({ statePath, product }) {
|
|
128
|
+
if (typeof statePath !== 'string' || basename(statePath) !== 'state.md') {
|
|
129
|
+
throw new Error('Lifecycle intent may only be read from state.md.');
|
|
130
|
+
}
|
|
131
|
+
const fileInfo = await lstat(statePath);
|
|
132
|
+
if (fileInfo.isSymbolicLink() || !fileInfo.isFile()) {
|
|
133
|
+
throw new Error('Project state must be a regular file, not a symlink.');
|
|
134
|
+
}
|
|
135
|
+
return readIntentFromStateContent(await readFile(statePath, 'utf8'), product);
|
|
136
|
+
}
|
|
137
|
+
|
|
69
138
|
export async function persistIntent({
|
|
70
139
|
statePath,
|
|
71
140
|
product,
|
|
@@ -84,7 +153,7 @@ export async function persistIntent({
|
|
|
84
153
|
) {
|
|
85
154
|
throw new TypeError('expectedHash must be a SHA-256 state content hash.');
|
|
86
155
|
}
|
|
87
|
-
|
|
156
|
+
validateWritableIntentRecord(product, record);
|
|
88
157
|
|
|
89
158
|
const fileInfo = await lstat(statePath);
|
|
90
159
|
if (fileInfo.isSymbolicLink() || !fileInfo.isFile()) {
|
|
@@ -128,3 +197,13 @@ function assertExpectedHash(content, expectedHash) {
|
|
|
128
197
|
error.code = 'E_INTENT_STALE_WRITE';
|
|
129
198
|
throw error;
|
|
130
199
|
}
|
|
200
|
+
|
|
201
|
+
function parseStateScalar(value) {
|
|
202
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
203
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
204
|
+
}
|
|
205
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
206
|
+
return JSON.parse(value);
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { promisify } from 'node:util';
|
|
4
3
|
|
|
5
4
|
import { resolveSourceAwarePath } from './resolve-paths.mjs';
|
|
@@ -11,12 +10,6 @@ export const EXPLAINER_CONFIG_KEYS = Object.freeze([
|
|
|
11
10
|
'explainers.defaults.palette',
|
|
12
11
|
'explainers.defaults.visualProfile',
|
|
13
12
|
'explainers.defaults.themeBundlePath',
|
|
14
|
-
'explainers.publish.provider',
|
|
15
|
-
'explainers.publish.s3Uri',
|
|
16
|
-
'explainers.publish.publicBaseUrl',
|
|
17
|
-
'explainers.publish.awsRegion',
|
|
18
|
-
'explainers.publish.publicAccess',
|
|
19
|
-
'explainers.publish.awsProfile',
|
|
20
13
|
'workflow.explainers.projectExplainer',
|
|
21
14
|
'workflow.explainers.projectRecap',
|
|
22
15
|
]);
|
|
@@ -116,7 +109,6 @@ export async function resolveExplainerConfig({
|
|
|
116
109
|
}
|
|
117
110
|
}
|
|
118
111
|
|
|
119
|
-
const { publish, publishReport } = resolvePublish(values, sources);
|
|
120
112
|
const preferences = {
|
|
121
113
|
projectExplainer: resolvePreference(
|
|
122
114
|
'workflow.explainers.projectExplainer',
|
|
@@ -132,77 +124,11 @@ export async function resolveExplainerConfig({
|
|
|
132
124
|
values,
|
|
133
125
|
sources,
|
|
134
126
|
theme,
|
|
135
|
-
publish,
|
|
136
|
-
publishReport,
|
|
137
127
|
preferences,
|
|
138
128
|
warnings,
|
|
139
129
|
};
|
|
140
130
|
}
|
|
141
131
|
|
|
142
|
-
export function toExplainerRunRequest({
|
|
143
|
-
resolvedConfig,
|
|
144
|
-
recipe,
|
|
145
|
-
slug,
|
|
146
|
-
outputRoot,
|
|
147
|
-
factBase,
|
|
148
|
-
mode,
|
|
149
|
-
durabilityStrategy = 'none',
|
|
150
|
-
artDirection,
|
|
151
|
-
defaultMode,
|
|
152
|
-
renderStrategy,
|
|
153
|
-
retainRawArtDirection = false,
|
|
154
|
-
}) {
|
|
155
|
-
if (!resolvedConfig || !outputRoot || !factBase) {
|
|
156
|
-
throw new TypeError(
|
|
157
|
-
'resolvedConfig, outputRoot, and factBase are required to build a run request.',
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (retainRawArtDirection && !nullableString(artDirection)) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
'retainRawArtDirection requires a non-empty artDirection runtime input.',
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const theme = {
|
|
167
|
-
...resolvedConfig.theme,
|
|
168
|
-
...(nullableString(artDirection)
|
|
169
|
-
? { artDirection: artDirection.trim() }
|
|
170
|
-
: {}),
|
|
171
|
-
...(defaultMode ? { defaultMode } : {}),
|
|
172
|
-
...(renderStrategy ? { renderStrategy } : {}),
|
|
173
|
-
};
|
|
174
|
-
const durability = { strategy: durabilityStrategy };
|
|
175
|
-
if (durabilityStrategy === 'publish') {
|
|
176
|
-
if (!resolvedConfig.publish) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
'Publish durability requires a complete explainer publish configuration.',
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
durability.publish = {
|
|
182
|
-
schemaVersion: 'explainer-kit.publish-request/v2',
|
|
183
|
-
...resolvedConfig.publish,
|
|
184
|
-
publicAccess: resolvedConfig.publish.publicAccess ?? 'public',
|
|
185
|
-
siteRoot: join(outputRoot, slug, 'site'),
|
|
186
|
-
manifestPath: join(outputRoot, slug, 'manifest.json'),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
schemaVersion: 'explainer-kit.run-request/v1',
|
|
192
|
-
recipe: {
|
|
193
|
-
id: recipe,
|
|
194
|
-
version: recipe === 'project-recap' ? '2' : '1',
|
|
195
|
-
},
|
|
196
|
-
slug,
|
|
197
|
-
outputRoot,
|
|
198
|
-
factBase,
|
|
199
|
-
...(Object.keys(theme).length > 0 ? { theme } : {}),
|
|
200
|
-
durability,
|
|
201
|
-
privacy: { retainRawArtDirection },
|
|
202
|
-
mode,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
132
|
async function getConfigFromCli(repoRoot, key) {
|
|
207
133
|
let stdout;
|
|
208
134
|
try {
|
|
@@ -264,15 +190,6 @@ function normalizeRuntimeValue(key, value) {
|
|
|
264
190
|
);
|
|
265
191
|
}
|
|
266
192
|
const normalized = value.trim();
|
|
267
|
-
if (key === 'explainers.publish.provider' && normalized !== 's3-static') {
|
|
268
|
-
throw new Error(`${key} runtime override must be s3-static.`);
|
|
269
|
-
}
|
|
270
|
-
if (
|
|
271
|
-
key === 'explainers.publish.publicAccess' &&
|
|
272
|
-
!['public', 'protected'].includes(normalized)
|
|
273
|
-
) {
|
|
274
|
-
throw new Error(`${key} runtime override must be public or protected.`);
|
|
275
|
-
}
|
|
276
193
|
if (key === 'explainers.defaults.style' && !STYLES.has(normalized)) {
|
|
277
194
|
throw new Error(
|
|
278
195
|
`${key} runtime override must name a curated explainer style.`,
|
|
@@ -281,88 +198,9 @@ function normalizeRuntimeValue(key, value) {
|
|
|
281
198
|
if (key.startsWith('workflow.explainers.') && !PREFERENCES.has(normalized)) {
|
|
282
199
|
throw new Error(`${key} runtime override must be always, ask, or never.`);
|
|
283
200
|
}
|
|
284
|
-
if (
|
|
285
|
-
key === 'explainers.publish.s3Uri' &&
|
|
286
|
-
!/^s3:\/\/[^/\s]+(?:\/.*)?$/.test(normalized)
|
|
287
|
-
) {
|
|
288
|
-
throw new Error(`${key} runtime override must be an s3:// URI.`);
|
|
289
|
-
}
|
|
290
|
-
if (
|
|
291
|
-
key === 'explainers.publish.publicBaseUrl' &&
|
|
292
|
-
!/^https:\/\/[^\s]+$/.test(normalized)
|
|
293
|
-
) {
|
|
294
|
-
throw new Error(`${key} runtime override must be an https:// URL.`);
|
|
295
|
-
}
|
|
296
|
-
if (
|
|
297
|
-
key === 'explainers.publish.s3Uri' ||
|
|
298
|
-
key === 'explainers.publish.publicBaseUrl'
|
|
299
|
-
) {
|
|
300
|
-
return normalized.replace(/\/+$/, '');
|
|
301
|
-
}
|
|
302
201
|
return normalized;
|
|
303
202
|
}
|
|
304
203
|
|
|
305
|
-
function resolvePublish(values, sources) {
|
|
306
|
-
const fields = {
|
|
307
|
-
provider: nullableString(values['explainers.publish.provider']),
|
|
308
|
-
s3Uri: nullableString(values['explainers.publish.s3Uri']),
|
|
309
|
-
publicBaseUrl: nullableString(values['explainers.publish.publicBaseUrl']),
|
|
310
|
-
awsRegion: nullableString(values['explainers.publish.awsRegion']),
|
|
311
|
-
};
|
|
312
|
-
if (fields.provider && fields.provider !== 's3-static') {
|
|
313
|
-
throw new Error(
|
|
314
|
-
`Unsupported explainer publish provider: ${fields.provider}`,
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
const missing = Object.entries(fields)
|
|
318
|
-
.filter(([, value]) => !value)
|
|
319
|
-
.map(([key]) => key);
|
|
320
|
-
if (missing.length > 0) {
|
|
321
|
-
return {
|
|
322
|
-
publish: null,
|
|
323
|
-
publishReport: {
|
|
324
|
-
mode: 'build-only',
|
|
325
|
-
publishCapable: false,
|
|
326
|
-
missing,
|
|
327
|
-
},
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
if (!/^s3:\/\/[^/\s]+(?:\/.*)?$/.test(fields.s3Uri)) {
|
|
331
|
-
throw new Error('explainers.publish.s3Uri must be a valid s3:// URI.');
|
|
332
|
-
}
|
|
333
|
-
if (!/^https:\/\/[^\s]+$/.test(fields.publicBaseUrl)) {
|
|
334
|
-
throw new Error(
|
|
335
|
-
'explainers.publish.publicBaseUrl must be a valid https:// URL.',
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const awsProfile = nullableString(values['explainers.publish.awsProfile']);
|
|
340
|
-
const publicAccess =
|
|
341
|
-
nullableString(values['explainers.publish.publicAccess']) ?? 'public';
|
|
342
|
-
if (!['public', 'protected'].includes(publicAccess)) {
|
|
343
|
-
throw new Error(
|
|
344
|
-
'explainers.publish.publicAccess must be public or protected.',
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
return {
|
|
348
|
-
publish: {
|
|
349
|
-
provider: fields.provider,
|
|
350
|
-
s3Uri: fields.s3Uri.replace(/\/+$/, ''),
|
|
351
|
-
publicBaseUrl: fields.publicBaseUrl.replace(/\/+$/, ''),
|
|
352
|
-
awsRegion: fields.awsRegion,
|
|
353
|
-
...(sources['explainers.publish.publicAccess'] !== 'default'
|
|
354
|
-
? { publicAccess }
|
|
355
|
-
: {}),
|
|
356
|
-
...(awsProfile ? { awsProfile } : {}),
|
|
357
|
-
},
|
|
358
|
-
publishReport: {
|
|
359
|
-
mode: 'publish-capable',
|
|
360
|
-
publishCapable: true,
|
|
361
|
-
missing: [],
|
|
362
|
-
},
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
204
|
function resolvePreference(key, value) {
|
|
367
205
|
const preference = nullableString(value) ?? 'ask';
|
|
368
206
|
if (!PREFERENCES.has(preference)) {
|