@open-agent-toolkit/cli 0.2.72 → 0.2.74

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.
Files changed (165) hide show
  1. package/assets/agents/oat-reviewer.md +4 -3
  2. package/assets/agents/recon-worker.md +42 -8
  3. package/assets/bundle-metadata.json +1 -1
  4. package/assets/docs/cli-utilities/configuration.md +3 -56
  5. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  6. package/assets/docs/contributing/code.md +2 -2
  7. package/assets/docs/contributing/index.md +0 -1
  8. package/assets/docs/reference/cli-reference.md +1 -1
  9. package/assets/docs/reference/troubleshooting.md +17 -40
  10. package/assets/docs/workflows/projects/artifacts.md +16 -34
  11. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  12. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  13. package/assets/docs/workflows/skills/index.md +1 -2
  14. package/assets/docs/workflows/skills/recon.md +114 -23
  15. package/assets/public-package-versions.json +4 -4
  16. package/assets/skills/explainer-kit/SKILL.md +96 -171
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  20. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  21. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  22. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  23. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  24. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  25. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  27. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  29. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  32. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  33. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  34. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  35. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  36. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  37. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  38. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  39. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  40. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  41. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  42. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  43. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  44. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  45. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  46. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  47. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  48. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  49. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  50. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  51. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  52. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  53. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  54. package/assets/skills/oat-project-document/SKILL.md +1 -1
  55. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  57. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  58. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  59. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  60. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  62. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  63. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  64. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  65. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  66. package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
  67. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  68. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  69. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  70. package/assets/skills/recon/SKILL.md +103 -40
  71. package/assets/skills/recon/references/packet-contract.md +150 -67
  72. package/assets/skills/recon/references/profiles.md +38 -16
  73. package/assets/skills/recon/references/worker-contract.md +39 -8
  74. package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
  75. package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
  76. package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
  77. package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
  78. package/assets/skills/recon/scripts/render-packet.mjs +69 -1
  79. package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
  80. package/assets/skills/subagent-orchestration/SKILL.md +18 -9
  81. package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +9 -113
  84. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  85. package/dist/commands/project/archive/archive-utils.js +32 -184
  86. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  87. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  88. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  89. package/dist/config/oat-config.d.ts +0 -11
  90. package/dist/config/oat-config.d.ts.map +1 -1
  91. package/dist/config/oat-config.js +0 -34
  92. package/dist/config/resolve.d.ts.map +1 -1
  93. package/dist/config/resolve.js +0 -8
  94. package/dist/release/public-package-contract.d.ts.map +1 -1
  95. package/dist/release/public-package-contract.js +2 -10
  96. package/dist/validation/project-state.d.ts.map +1 -1
  97. package/dist/validation/project-state.js +42 -3
  98. package/package.json +2 -2
  99. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  100. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  101. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  102. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  103. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  104. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  105. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  106. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  107. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  108. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  109. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  110. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  111. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  112. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  113. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  114. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  115. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  116. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  117. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  118. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  119. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  120. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  121. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  122. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  123. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  124. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  125. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  126. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  127. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  128. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  129. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  130. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  131. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  132. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  133. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  134. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  135. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  136. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  137. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  138. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  139. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  140. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  141. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  142. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  143. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  144. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  145. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  146. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  147. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  148. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  149. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  150. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  151. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  152. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  153. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  154. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  155. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  156. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  157. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  158. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  159. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  160. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  161. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  162. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  163. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  164. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  165. package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
@@ -1,954 +0,0 @@
1
- import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
2
- import { access, readFile, readdir, rm } from 'node:fs/promises';
3
- import { isAbsolute, join } from 'node:path';
4
-
5
- import { canonicalHash, validateContract } from './contracts.mjs';
6
- import {
7
- createConfinedRunRoot,
8
- writeFileAtomic,
9
- writeJsonAtomic,
10
- } from './fs-safe.mjs';
11
- import {
12
- SET_PLAN_RECORD_PATHS,
13
- VISUAL_REVISION_PATH,
14
- requiredImmutablePackagePaths,
15
- validateImmutablePackageEvidence,
16
- } from './package-coverage.mjs';
17
- import { resolveRootConfinedPath } from './safe-paths.mjs';
18
- import {
19
- assertTerminalEvidence,
20
- createTerminalEvidence,
21
- createVisualReviewEvidence,
22
- } from './terminal-evidence.mjs';
23
-
24
- export {
25
- PACKAGE_COVERAGE_VERSION,
26
- SET_PLAN_RECORD_PATHS,
27
- VISUAL_REVISION_PATH,
28
- requiredImmutablePackagePaths,
29
- validateImmutablePackageEvidence,
30
- } from './package-coverage.mjs';
31
-
32
- const STAGE_IDS = [
33
- 'validate',
34
- 'fact-base',
35
- 'content',
36
- 'theme',
37
- 'render',
38
- 'qa',
39
- 'durability',
40
- 'publish',
41
- ];
42
- const SUCCESSFUL_TERMINAL_STATUSES = new Set(['passed', 'warned', 'skipped']);
43
- const TERMINAL_STATUSES = new Set([...SUCCESSFUL_TERMINAL_STATUSES, 'failed']);
44
- const ALLOWED_TRANSITIONS = {
45
- pending: new Set(['running', 'failed', 'skipped']),
46
- running: new Set(['running', 'passed', 'warned', 'failed']),
47
- };
48
- const RESUME_TOKEN_V2_PREFIX = 'ekrt2:';
49
- const RESUME_TOKEN_V2_PATTERN = /^ekrt2:[a-f0-9]{64}$/;
50
-
51
- export function canonicalPersistedRunRequest(request, { outputRoot } = {}) {
52
- return privacySafeRequest(
53
- normalizeRunRequest(request, {
54
- ...(outputRoot !== undefined && { outputRoot }),
55
- }),
56
- );
57
- }
58
-
59
- export async function initializeRun(request) {
60
- const normalizedRequest = normalizeRunRequest(request);
61
- const paths = await createConfinedRunRoot(
62
- normalizedRequest.outputRoot,
63
- normalizedRequest.slug,
64
- );
65
- normalizedRequest.outputRoot = paths.outputRoot;
66
-
67
- const runId = `run-${randomUUID()}`;
68
- const startedAt = new Date().toISOString();
69
- const buildRecord = {
70
- schemaVersion: 'explainer-kit.build-record/v1',
71
- runId,
72
- renderStrategy: normalizedRequest.theme.renderStrategy,
73
- startedAt,
74
- stages: STAGE_IDS.map((id) => ({
75
- id,
76
- status: 'pending',
77
- outputPaths: [],
78
- warnings: [],
79
- })),
80
- outcome: 'incomplete',
81
- };
82
- assertValidContract('build-record', buildRecord);
83
-
84
- const run = {
85
- runId,
86
- slug: paths.slug,
87
- outputRoot: paths.outputRoot,
88
- runRoot: paths.runRoot,
89
- requestPath: join(paths.runRoot, 'run-request.json'),
90
- buildRecordPath: join(paths.runRoot, 'build-record.json'),
91
- manifestPath: join(paths.runRoot, 'manifest.json'),
92
- request: normalizedRequest,
93
- };
94
-
95
- await clearRunRoot(run);
96
- await writeJsonAtomic(run.runRoot, 'build-record.json', buildRecord);
97
- await writeJsonAtomic(
98
- run.runRoot,
99
- 'run-request.json',
100
- canonicalPersistedRunRequest(normalizedRequest, {
101
- outputRoot: paths.outputRoot,
102
- }),
103
- );
104
-
105
- return run;
106
- }
107
-
108
- export async function updateBuildRecord(run, stage) {
109
- assertRun(run);
110
- if (!isObject(stage) || !STAGE_IDS.includes(stage.id)) {
111
- throw new Error('Stage update must identify a supported stage.');
112
- }
113
-
114
- const record = JSON.parse(await readFile(run.buildRecordPath, 'utf8'));
115
- if (record.runId !== run.runId) {
116
- throw new Error('Build record does not belong to this run.');
117
- }
118
-
119
- const stageIndex = STAGE_IDS.indexOf(stage.id);
120
- const current = record.stages[stageIndex];
121
- const allowed = ALLOWED_TRANSITIONS[current.status];
122
- if (!allowed?.has(stage.status)) {
123
- throw new Error(
124
- `Stage transitions must be monotonic; ${stage.id} is terminal or cannot move from ${current.status} to ${stage.status}.`,
125
- );
126
- }
127
-
128
- if (
129
- current.status === 'pending' &&
130
- stageIndex > 0 &&
131
- !record.stages
132
- .slice(0, stageIndex)
133
- .every(({ status }) => SUCCESSFUL_TERMINAL_STATUSES.has(status))
134
- ) {
135
- throw new Error(
136
- `Stage ${stage.id} cannot start before every prior stage completes in order.`,
137
- );
138
- }
139
-
140
- const timestamp = new Date().toISOString();
141
- const next = {
142
- ...current,
143
- status: stage.status,
144
- outputPaths: stage.outputPaths ?? current.outputPaths,
145
- warnings: structuredClone(stage.warnings ?? current.warnings),
146
- };
147
- if (stage.error !== undefined) {
148
- next.error = localStageFailure(stage.id);
149
- }
150
- if (stage.status === 'running' && next.startedAt === undefined) {
151
- next.startedAt = timestamp;
152
- }
153
- if (TERMINAL_STATUSES.has(stage.status)) {
154
- next.startedAt ??= timestamp;
155
- next.completedAt = timestamp;
156
- }
157
- if (stage.status === 'failed' && !isObject(next.error)) {
158
- throw new Error('Failed stages require structured recovery details.');
159
- }
160
- if (stage.status !== 'failed' && 'error' in next) {
161
- throw new Error('Only failed stages may contain an error.');
162
- }
163
-
164
- record.stages[stageIndex] = next;
165
- if (stage.status === 'failed') {
166
- record.outcome = 'failed';
167
- record.completedAt = timestamp;
168
- } else if (
169
- record.stages.every(({ status }) => TERMINAL_STATUSES.has(status))
170
- ) {
171
- record.outcome = record.stages.some(({ status }) => status === 'failed')
172
- ? 'failed'
173
- : record.stages.some(({ warnings }) =>
174
- warnings.some((warning) =>
175
- warning.startsWith('visual-review-required:'),
176
- ),
177
- )
178
- ? 'built-needs-review'
179
- : 'built-not-durable';
180
- record.completedAt = timestamp;
181
- } else {
182
- record.outcome = 'incomplete';
183
- delete record.completedAt;
184
- }
185
-
186
- assertValidContract('build-record', record);
187
- await writeJsonAtomic(run.runRoot, 'build-record.json', record);
188
- return record;
189
- }
190
-
191
- export async function writeVisualReviewAttempt(run, { attempt, review } = {}) {
192
- assertRun(run);
193
- if (![1, 2].includes(attempt) || !isObject(review)) {
194
- throw new TypeError('Visual review records require attempt 1 or 2.');
195
- }
196
- const requestValidation = validateContract(
197
- 'visual-review-request',
198
- review.request,
199
- );
200
- const resultValidation = validateContract(
201
- 'visual-review-result',
202
- review.result,
203
- { visualReviewRequest: review.request },
204
- );
205
- if (!requestValidation.valid || !resultValidation.valid) {
206
- throw new Error(
207
- 'Visual review records must contain valid bound contracts.',
208
- );
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
- }
247
-
248
- async function writeRetainedVisualReview(
249
- run,
250
- { attempt, request, retainedResult },
251
- ) {
252
- const directory = `qa/visual-review/attempt-${attempt}`;
253
- const retainedRequest = structuredClone(request);
254
- const paths = [];
255
- for (const artifact of retainedRequest.renderedArtifacts) {
256
- for (const evidence of artifact.evidence) {
257
- const screenshotPath = `${directory}/evidence/${artifact.artifactId}/${evidence.viewport}.png`;
258
- const metricsPath = `${directory}/evidence/${artifact.artifactId}/${evidence.viewport}.json`;
259
- await copyConfinedEvidence(
260
- run.runRoot,
261
- evidence.screenshotPath,
262
- screenshotPath,
263
- evidence.screenshotHash,
264
- );
265
- await copyConfinedEvidence(
266
- run.runRoot,
267
- evidence.metricsPath,
268
- metricsPath,
269
- evidence.metricsHash,
270
- );
271
- paths.push(screenshotPath, metricsPath);
272
- }
273
- }
274
- const requestPath = `${directory}/request.json`;
275
- const resultPath = `${directory}/result.json`;
276
- await writeJsonAtomic(run.runRoot, requestPath, retainedRequest);
277
- await writeJsonAtomic(run.runRoot, resultPath, retainedResult);
278
- return [...paths, requestPath, resultPath];
279
- }
280
-
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(
335
- run,
336
- { manifest, supersededBy } = {},
337
- ) {
338
- assertRun(run);
339
- if (
340
- !isObject(manifest) ||
341
- manifest.runId !== run.runId ||
342
- manifest.slug !== run.slug
343
- ) {
344
- throw new Error(
345
- 'Terminal evidence manifest identity does not match the run.',
346
- );
347
- }
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,
367
- });
368
- await writeJsonAtomic(run.runRoot, path, superseded);
369
- return path;
370
- }
371
-
372
- export async function writeVisualRevision(run, { artifactIds, changes } = {}) {
373
- assertRun(run);
374
- if (
375
- !Array.isArray(artifactIds) ||
376
- artifactIds.length === 0 ||
377
- new Set(artifactIds).size !== artifactIds.length ||
378
- !Array.isArray(changes) ||
379
- changes.length !== artifactIds.length ||
380
- changes.some(
381
- (change) =>
382
- !isObject(change) ||
383
- !artifactIds.includes(change.artifactId) ||
384
- typeof change.contentPath !== 'string' ||
385
- typeof change.authorResultPath !== 'string' ||
386
- !/^sha256:[a-f0-9]{64}$/.test(change.previousHash) ||
387
- !/^sha256:[a-f0-9]{64}$/.test(change.revisedHash),
388
- )
389
- ) {
390
- throw new TypeError(
391
- 'One visual revision requires unique corrected artifacts and hash-bound changes.',
392
- );
393
- }
394
- try {
395
- await access(join(run.runRoot, VISUAL_REVISION_PATH));
396
- throw new Error('Only one visual revision may be retained per run.');
397
- } catch (error) {
398
- if (error?.code !== 'ENOENT') throw error;
399
- }
400
- await writeJsonAtomic(run.runRoot, VISUAL_REVISION_PATH, {
401
- schemaVersion: 'explainer-kit.visual-revision/v1',
402
- attempt: 1,
403
- artifactIds: [...artifactIds],
404
- changes: structuredClone(changes),
405
- });
406
- return [VISUAL_REVISION_PATH];
407
- }
408
-
409
- async function copyConfinedEvidence(
410
- runRoot,
411
- sourcePath,
412
- targetPath,
413
- expectedHash,
414
- ) {
415
- const confined = await resolveRootConfinedPath(runRoot, sourcePath);
416
- if (!confined.valid) {
417
- throw new Error(
418
- `Visual review evidence is not run-root confined: ${sourcePath}`,
419
- );
420
- }
421
- const bytes = await readFile(confined.absolutePath);
422
- const hash = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
423
- if (hash !== expectedHash) {
424
- throw new Error(
425
- `Visual review evidence hash changed before retention: ${sourcePath}`,
426
- );
427
- }
428
- await writeFileAtomic(runRoot, targetPath, bytes);
429
- }
430
-
431
- export async function reopenBuildStages(run, { ids, reason }) {
432
- assertRun(run);
433
- if (
434
- !Array.isArray(ids) ||
435
- ids.length === 0 ||
436
- ids.some((id) => !STAGE_IDS.includes(id)) ||
437
- typeof reason !== 'string' ||
438
- reason.length === 0
439
- ) {
440
- throw new Error('Stage reopen requires supported stage ids and a reason.');
441
- }
442
-
443
- const indexes = ids.map((id) => STAGE_IDS.indexOf(id));
444
- const firstIndex = Math.min(...indexes);
445
- const lastIndex = Math.max(...indexes);
446
- if (
447
- new Set(indexes).size !== indexes.length ||
448
- indexes.some((index, offset) => index !== firstIndex + offset) ||
449
- ids.some((id, index) => STAGE_IDS[firstIndex + index] !== id)
450
- ) {
451
- throw new Error('Reopened stages must be unique and contiguous in order.');
452
- }
453
-
454
- const record = JSON.parse(await readFile(run.buildRecordPath, 'utf8'));
455
- if (record.runId !== run.runId) {
456
- throw new Error('Build record does not belong to this run.');
457
- }
458
- if (
459
- !record.stages
460
- .slice(0, firstIndex)
461
- .every(({ status }) => SUCCESSFUL_TERMINAL_STATUSES.has(status)) ||
462
- !record.stages
463
- .slice(firstIndex, lastIndex + 1)
464
- .every(({ status }) => SUCCESSFUL_TERMINAL_STATUSES.has(status)) ||
465
- !record.stages
466
- .slice(lastIndex + 1)
467
- .every(({ status }) => status === 'pending')
468
- ) {
469
- throw new Error(
470
- 'Stages may reopen only after successful prerequisites and before downstream work.',
471
- );
472
- }
473
-
474
- const timestamp = new Date().toISOString();
475
- for (let index = firstIndex; index <= lastIndex; index += 1) {
476
- const previous = record.stages[index];
477
- record.stages[index] = {
478
- id: previous.id,
479
- status: 'pending',
480
- outputPaths: [],
481
- warnings: [...previous.warnings, `stage-reopened:${reason}:${timestamp}`],
482
- };
483
- }
484
- record.outcome = 'incomplete';
485
- delete record.completedAt;
486
-
487
- assertValidContract('build-record', record);
488
- await writeJsonAtomic(run.runRoot, 'build-record.json', record);
489
- return record;
490
- }
491
-
492
- export async function writeManifestAtomic(run, manifest) {
493
- assertRun(run);
494
- if (!isObject(manifest)) {
495
- throw new TypeError('Manifest must be an object.');
496
- }
497
- if (manifest.runId !== run.runId || manifest.slug !== run.slug) {
498
- throw new Error('Manifest identity does not match the initialized run.');
499
- }
500
-
501
- const buildRecord = JSON.parse(await readFile(run.buildRecordPath, 'utf8'));
502
- const runMode =
503
- manifest.recipe?.id === 'project-recap'
504
- ? await verifiedRunRequestMode(run, manifest)
505
- : undefined;
506
- assertImmutablePackageCoverage(manifest, { runMode });
507
- await validateImmutablePackageEvidence(manifest, {
508
- runMode,
509
- read: (path) => readFile(join(run.runRoot, path)),
510
- });
511
- assertValidContract('manifest', manifest, { buildRecord });
512
- await writeJsonAtomic(run.runRoot, 'manifest.json', manifest);
513
- return run.manifestPath;
514
- }
515
-
516
- function assertImmutablePackageCoverage(manifest, options) {
517
- const recorded = manifest.immutableHashes;
518
- if (!isObject(recorded)) {
519
- throw new Error('Manifest does not identify immutable package hashes.');
520
- }
521
- const missing = requiredImmutablePackagePaths(manifest, options).filter(
522
- (path) => typeof recorded[path] !== 'string',
523
- );
524
- if (missing.length > 0) {
525
- throw new Error(
526
- `Manifest immutable hashes do not cover the canonical package: ${missing.join(', ')}.`,
527
- );
528
- }
529
- }
530
-
531
- async function verifiedRunRequestMode(run, manifest) {
532
- const expectedHash = manifest.immutableHashes?.['run-request.json'];
533
- if (!/^sha256:[a-f0-9]{64}$/.test(expectedHash)) {
534
- throw new Error(
535
- 'Manifest must identify a valid immutable run-request.json hash before package coverage is evaluated.',
536
- );
537
- }
538
- const bytes = await readFile(run.requestPath);
539
- const actualHash = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
540
- if (actualHash !== expectedHash) {
541
- throw new Error(
542
- 'Immutable package hash mismatch for run-request.json before package coverage is evaluated.',
543
- );
544
- }
545
- let request;
546
- try {
547
- request = JSON.parse(bytes.toString('utf8'));
548
- } catch {
549
- throw new Error(
550
- 'Hash-verified run-request.json must contain valid JSON before package coverage is evaluated.',
551
- );
552
- }
553
- if (!['interactive', 'unattended'].includes(request?.mode)) {
554
- throw new Error(
555
- 'Hash-verified run-request.json must declare interactive or unattended mode.',
556
- );
557
- }
558
- return request.mode;
559
- }
560
-
561
- export async function writeSetPlanRecords(run, { request, plan }) {
562
- assertRun(run);
563
- if (
564
- !isObject(request) ||
565
- request.schemaVersion !== 'explainer-kit.set-plan-request/v1'
566
- ) {
567
- throw new TypeError('Set-plan request must use the v1 record contract.');
568
- }
569
- assertValidContract('set-plan', plan);
570
-
571
- const retainedRequest = {
572
- ...structuredClone(request),
573
- planHash: canonicalHash(plan),
574
- };
575
- const records = [
576
- ['source/set-plan/request.json', retainedRequest],
577
- ['source/set-plan/result.json', plan],
578
- [
579
- 'source/set-plan/ledger.json',
580
- {
581
- schemaVersion: 'explainer-kit.set-plan-ledger/v1',
582
- planId: plan.planId,
583
- ...plan.ledger,
584
- },
585
- ],
586
- [
587
- 'source/set-plan/portfolio.json',
588
- {
589
- schemaVersion: 'explainer-kit.set-plan-portfolio/v1',
590
- planId: plan.planId,
591
- artifacts: plan.portfolio,
592
- },
593
- ],
594
- [
595
- 'source/set-plan/drafts.json',
596
- {
597
- schemaVersion: 'explainer-kit.set-plan-drafts/v1',
598
- drafts: plan.portfolio.map(
599
- ({ artifactId, draft, visualIntent, justification }) => ({
600
- artifactId,
601
- draft,
602
- visualIntent,
603
- ...(justification && { justification }),
604
- }),
605
- ),
606
- },
607
- ],
608
- ];
609
- for (const [relativePath] of records) {
610
- try {
611
- await access(join(run.runRoot, relativePath));
612
- throw new Error(
613
- `Immutable set-plan record ${relativePath} already exists.`,
614
- );
615
- } catch (error) {
616
- if (error?.code !== 'ENOENT') throw error;
617
- }
618
- }
619
- for (const [relativePath, value] of records) {
620
- await writeJsonAtomic(run.runRoot, relativePath, value);
621
- }
622
- return records.map(([relativePath]) => relativePath);
623
- }
624
-
625
- export async function createSetPlanResumeToken(run) {
626
- assertRun(run);
627
- if (typeof run.outputRoot !== 'string' || !isAbsolute(run.outputRoot)) {
628
- throw new TypeError(
629
- 'Authenticated resume tokens require a canonical absolute output root.',
630
- );
631
- }
632
- const digest = await hashResumeToken(run, {
633
- domain: 'explainer-kit.set-plan-resume/v2\0',
634
- bindCanonicalRoot: true,
635
- bindRequest: true,
636
- });
637
- return `${RESUME_TOKEN_V2_PREFIX}${digest}`;
638
- }
639
-
640
- async function hashResumeToken(
641
- run,
642
- { domain, bindCanonicalRoot, bindRequest },
643
- ) {
644
- const tokenHash = createHash('sha256');
645
- tokenHash.update(domain);
646
- tokenHash.update(run.runId);
647
- tokenHash.update('\0');
648
- if (bindCanonicalRoot) {
649
- tokenHash.update(run.outputRoot);
650
- tokenHash.update('\0');
651
- }
652
- try {
653
- if (bindRequest) {
654
- const requestBytes = await readFile(
655
- run.requestPath ?? join(run.runRoot, 'run-request.json'),
656
- );
657
- updateResumeTokenFileHash(tokenHash, 'run-request.json', requestBytes);
658
- }
659
- for (const relativePath of SET_PLAN_RECORD_PATHS) {
660
- const bytes = await readFile(join(run.runRoot, relativePath));
661
- updateResumeTokenFileHash(tokenHash, relativePath, bytes);
662
- }
663
- } catch (error) {
664
- throw resumeRecordError(
665
- `Unable to hash the complete retained resume identity: ${error.message}`,
666
- );
667
- }
668
- return tokenHash.digest('hex');
669
- }
670
-
671
- function updateResumeTokenFileHash(tokenHash, relativePath, bytes) {
672
- const byteHash = createHash('sha256').update(bytes).digest();
673
- tokenHash.update(relativePath);
674
- tokenHash.update('\0');
675
- tokenHash.update(byteHash);
676
- }
677
-
678
- export async function verifySetPlanResumeToken(run, resumeToken) {
679
- assertRun(run);
680
- if (
681
- typeof resumeToken !== 'string' ||
682
- !RESUME_TOKEN_V2_PATTERN.test(resumeToken)
683
- ) {
684
- throw resumeRecordError(
685
- 'Interactive approval resume requires a valid external resume token.',
686
- );
687
- }
688
- const expectedToken = await createSetPlanResumeToken(run);
689
- const expected = Buffer.from(expectedToken, 'ascii');
690
- const supplied = Buffer.from(resumeToken, 'ascii');
691
- if (
692
- supplied.byteLength !== expected.byteLength ||
693
- !timingSafeEqual(supplied, expected)
694
- ) {
695
- throw resumeRecordError(
696
- 'Interactive approval resume token does not match the retained set plan.',
697
- );
698
- }
699
- }
700
-
701
- export async function readSetPlanRecords(run, { factBase, recipe }) {
702
- assertRun(run);
703
- if (
704
- !isObject(factBase) ||
705
- !Array.isArray(factBase.sources) ||
706
- !isObject(recipe) ||
707
- typeof recipe.id !== 'string' ||
708
- typeof recipe.version !== 'string'
709
- ) {
710
- throw resumeRecordError(
711
- 'Retained set-plan validation requires the fact base and active recipe.',
712
- );
713
- }
714
-
715
- let values;
716
- try {
717
- values = await Promise.all(
718
- SET_PLAN_RECORD_PATHS.map((relativePath) =>
719
- readJson(join(run.runRoot, relativePath)),
720
- ),
721
- );
722
- } catch (error) {
723
- throw resumeRecordError(
724
- `Unable to read the complete retained set plan: ${error.message}`,
725
- );
726
- }
727
- const [request, plan, ledger, portfolio, drafts] = values;
728
- const requestKeys = [
729
- 'discovery',
730
- 'factBaseHash',
731
- 'planHash',
732
- 'recipe',
733
- 'schemaVersion',
734
- 'sourceIds',
735
- ];
736
- if (
737
- !isObject(request) ||
738
- canonicalHash(Object.keys(request).sort()) !== canonicalHash(requestKeys) ||
739
- request.schemaVersion !== 'explainer-kit.set-plan-request/v1' ||
740
- canonicalHash(request.recipe) !==
741
- canonicalHash({ id: recipe.id, version: recipe.version }) ||
742
- request.factBaseHash !== canonicalHash(factBase) ||
743
- request.planHash !== canonicalHash(plan) ||
744
- !isObject(request.discovery) ||
745
- !Number.isInteger(request.discovery.rounds) ||
746
- request.discovery.rounds < 0 ||
747
- !Array.isArray(request.discovery.findings) ||
748
- !['not-requested', 'two-empty-rounds', 'hard-maximum'].includes(
749
- request.discovery.reason,
750
- )
751
- ) {
752
- throw resumeRecordError(
753
- 'Retained set-plan request does not match the fact base and active recipe.',
754
- );
755
- }
756
-
757
- const planValidation = validateContract('set-plan', plan);
758
- if (!planValidation.valid) {
759
- throw resumeRecordError('Retained set-plan result is invalid.');
760
- }
761
- const expectedSourceIds = factBase.sources
762
- .map(({ id }) => id)
763
- .filter((id) => !id.startsWith('critic:'));
764
- if (
765
- canonicalHash(plan.recipe) !==
766
- canonicalHash({ id: recipe.id, version: recipe.version }) ||
767
- canonicalHash(request.sourceIds) !== canonicalHash(expectedSourceIds) ||
768
- canonicalHash(plan.sourceIds) !== canonicalHash(request.sourceIds)
769
- ) {
770
- throw resumeRecordError(
771
- 'Retained set-plan request and result identities have drifted.',
772
- );
773
- }
774
-
775
- const expectedLedger = {
776
- schemaVersion: 'explainer-kit.set-plan-ledger/v1',
777
- planId: plan.planId,
778
- ...plan.ledger,
779
- };
780
- const expectedPortfolio = {
781
- schemaVersion: 'explainer-kit.set-plan-portfolio/v1',
782
- planId: plan.planId,
783
- artifacts: plan.portfolio,
784
- };
785
- const expectedDrafts = {
786
- schemaVersion: 'explainer-kit.set-plan-drafts/v1',
787
- drafts: plan.portfolio.map(
788
- ({ artifactId, draft, visualIntent, justification }) => ({
789
- artifactId,
790
- draft,
791
- visualIntent,
792
- ...(justification && { justification }),
793
- }),
794
- ),
795
- };
796
- for (const [label, actual, expected] of [
797
- ['ledger', ledger, expectedLedger],
798
- ['portfolio', portfolio, expectedPortfolio],
799
- ['drafts', drafts, expectedDrafts],
800
- ]) {
801
- if (canonicalHash(actual) !== canonicalHash(expected)) {
802
- throw resumeRecordError(
803
- `Retained set-plan ${label} projection does not match the canonical result.`,
804
- );
805
- }
806
- }
807
-
808
- return {
809
- request,
810
- plan,
811
- paths: [...SET_PLAN_RECORD_PATHS],
812
- };
813
- }
814
-
815
- function normalizeRequestSlug(slug) {
816
- if (
817
- typeof slug !== 'string' ||
818
- slug.includes('/') ||
819
- slug.includes('\\') ||
820
- slug.includes('\0') ||
821
- slug === '.' ||
822
- slug === '..'
823
- ) {
824
- throw new Error('Slug cannot contain path traversal or separators.');
825
- }
826
-
827
- const normalized = slug
828
- .normalize('NFKD')
829
- .replaceAll(/\p{Mark}/gu, '')
830
- .toLowerCase()
831
- .trim()
832
- .replaceAll(/[^a-z0-9]+/g, '-')
833
- .replaceAll(/^-+|-+$/g, '');
834
- if (!normalized) {
835
- throw new Error('Slug must contain at least one letter or number.');
836
- }
837
- return normalized;
838
- }
839
-
840
- function normalizeRunRequest(request, { outputRoot } = {}) {
841
- if (!isObject(request)) {
842
- throw new TypeError('Run request must be an object.');
843
- }
844
- const normalized = structuredClone(request);
845
- normalized.slug = normalizeRequestSlug(request.slug);
846
- normalized.theme = {
847
- ...(isObject(normalized.theme) ? normalized.theme : {}),
848
- renderStrategy: normalized.theme?.renderStrategy ?? 'default-only',
849
- };
850
- if (outputRoot !== undefined) {
851
- normalized.outputRoot = outputRoot;
852
- }
853
- assertValidContract('run-request', normalized);
854
- return normalized;
855
- }
856
-
857
- function privacySafeRequest(request) {
858
- const persisted = structuredClone(request);
859
- const retainRaw = persisted.privacy?.retainRawArtDirection === true;
860
- if (!retainRaw && isObject(persisted.theme)) {
861
- delete persisted.theme.artDirection;
862
- }
863
- return persisted;
864
- }
865
-
866
- function assertValidContract(kind, value, context) {
867
- const result = validateContract(kind, value, context);
868
- if (!result.valid) {
869
- const details = result.errors
870
- .map(({ path, code, message }) => `${path} [${code}]: ${message}`)
871
- .join('; ');
872
- throw new Error(`Invalid ${kind}: ${details}`);
873
- }
874
- }
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
-
884
- function assertRun(run) {
885
- if (
886
- !isObject(run) ||
887
- typeof run.runId !== 'string' ||
888
- typeof run.runRoot !== 'string'
889
- ) {
890
- throw new TypeError('Run must be returned by initializeRun().');
891
- }
892
- }
893
-
894
- function resumeRecordError(message) {
895
- const error = new Error(message);
896
- error.code = 'E_APPROVAL_RESUME';
897
- return error;
898
- }
899
-
900
- async function clearRunRoot(run) {
901
- const entries = await readdir(run.runRoot, { withFileTypes: true });
902
- if (entries.length === 0) return;
903
- await assertOwnedRunRoot(run);
904
- const removable = [];
905
- for (const entry of entries) {
906
- const path = join(run.runRoot, entry.name);
907
- if (!(await containsSymlink(path, entry))) {
908
- removable.push(path);
909
- }
910
- }
911
- await Promise.all(
912
- removable.map((path) => rm(path, { recursive: true, force: true })),
913
- );
914
- }
915
-
916
- async function assertOwnedRunRoot(run) {
917
- try {
918
- const [persistedRequest, persistedRecord] = await Promise.all([
919
- readJson(join(run.runRoot, 'run-request.json')),
920
- readJson(join(run.runRoot, 'build-record.json')),
921
- ]);
922
- assertValidContract('run-request', persistedRequest);
923
- assertValidContract('build-record', persistedRecord);
924
- if (
925
- persistedRequest.slug !== run.slug ||
926
- persistedRequest.outputRoot !== run.outputRoot
927
- ) {
928
- throw new Error('Prior run identity does not match this slug.');
929
- }
930
- } catch {
931
- throw new Error(
932
- 'Existing slug directory is not a prior Explainer Kit run; refusing to clear it.',
933
- );
934
- }
935
- }
936
-
937
- async function containsSymlink(path, entry) {
938
- if (entry.isSymbolicLink()) return true;
939
- if (!entry.isDirectory()) return false;
940
- for (const child of await readdir(path, { withFileTypes: true })) {
941
- if (await containsSymlink(join(path, child.name), child)) {
942
- return true;
943
- }
944
- }
945
- return false;
946
- }
947
-
948
- async function readJson(path) {
949
- return JSON.parse(await readFile(path, 'utf8'));
950
- }
951
-
952
- function isObject(value) {
953
- return typeof value === 'object' && value !== null && !Array.isArray(value);
954
- }