@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,26 +1,52 @@
1
- import { readFile } from 'node:fs/promises';
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
- const TERMINAL_OUTCOMES = new Set([
5
- 'built-durable',
6
- 'built-not-durable',
7
- 'built-needs-review',
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(['interactive', 'capability_probe']);
23
+ const SKIP_REASONS = new Set([
24
+ 'interactive',
25
+ 'failed_attempt',
26
+ 'capability_probe',
27
+ ]);
16
28
 
17
- export function checkTerminalOutcome({ intent, outcome, reason }) {
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
- if (!TERMINAL_OUTCOMES.has(outcome)) {
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
- 'Generated project recaps require a terminal recap outcome before approval.',
101
+ 'Failed-attempt evidence is accepted only for a skip/failed_attempt intent.',
37
102
  );
38
103
  }
39
- return { ok: true, intent, outcome };
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 main(argv) {
43
- const { intent, manifestPath, reason } = parseArguments(argv);
44
- let outcome;
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
- outcome = manifest?.outcome;
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
- outcome,
59
- ...(reason !== undefined && { reason }),
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 { validateIntentRecord } from './resolve-intent.mjs';
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
- validateIntentRecord(product, record);
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
- validateIntentRecord(product, record);
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)) {