@open-agent-toolkit/cli 0.2.30 → 0.2.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +41 -0
  3. package/assets/docs/contributing/code.md +19 -7
  4. package/assets/docs/contributing/explainer-kit-verification.md +9 -1
  5. package/assets/docs/contributing/skills.md +9 -0
  6. package/assets/docs/workflows/skills/explainer-kit-providers.md +16 -5
  7. package/assets/docs/workflows/skills/explainer-kit.md +98 -38
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/explainer-kit/SKILL.md +33 -8
  10. package/assets/skills/explainer-kit/briefs/project-recap.md +25 -7
  11. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +72 -0
  12. package/assets/skills/explainer-kit/references/contracts.md +49 -17
  13. package/assets/skills/explainer-kit/references/destination-contract.md +141 -25
  14. package/assets/skills/explainer-kit/references/extension-contract.md +19 -10
  15. package/assets/skills/explainer-kit/references/visual-authoring.md +24 -0
  16. package/assets/skills/explainer-kit/references/visual-review.md +19 -5
  17. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +241 -0
  18. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +215 -0
  19. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +34 -0
  20. package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -1
  21. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +81 -0
  22. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +66 -0
  23. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +109 -3
  24. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +550 -17
  25. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +90 -8
  26. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +5 -0
  27. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +538 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +129 -11
  29. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +54 -0
  30. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +2 -1
  31. package/assets/skills/explainer-kit/scripts/lib/records.mjs +139 -22
  32. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +353 -0
  33. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +237 -107
  34. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +1 -0
  35. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +157 -0
  36. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +26 -6
  37. package/assets/skills/explainer-kit/scripts/run.mjs +1006 -144
  38. package/assets/skills/oat-explainer-kit/SKILL.md +16 -3
  39. package/assets/skills/oat-explainer-kit/references/config-contract.md +13 -8
  40. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +50 -6
  41. package/assets/skills/oat-explainer-kit/references/migration.md +2 -1
  42. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +11 -0
  43. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +37 -15
  44. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +83 -0
  45. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +91 -0
  46. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +66 -10
  47. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +60 -21
  48. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +52 -8
  49. package/assets/skills/oat-explainer-kit/scripts/run.mjs +271 -36
  50. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
  51. package/assets/skills/oat-project-complete/SKILL.md +19 -3
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
  53. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +8 -0
  54. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
  55. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
  56. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
  57. package/dist/commands/config/index.d.ts.map +1 -1
  58. package/dist/commands/config/index.js +18 -0
  59. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  60. package/dist/commands/project/archive/archive-utils.js +57 -7
  61. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +29 -0
  62. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +1 -0
  63. package/dist/commands/project/archive/explainer-terminal-evidence.js +37 -0
  64. package/dist/config/oat-config.d.ts +2 -0
  65. package/dist/config/oat-config.d.ts.map +1 -1
  66. package/dist/config/oat-config.js +4 -0
  67. package/dist/config/resolve.d.ts.map +1 -1
  68. package/dist/config/resolve.js +1 -0
  69. package/package.json +2 -2
  70. /package/assets/skills/explainer-kit/recipes/{project-recap.json → project-recap.v1.json} +0 -0
  71. /package/assets/skills/explainer-kit/schemas/{publish-receipt.schema.json → publish-receipt.v1.schema.json} +0 -0
  72. /package/assets/skills/explainer-kit/schemas/{publish-request.schema.json → publish-request.v1.schema.json} +0 -0
@@ -15,6 +15,7 @@ export const EXPLAINER_CONFIG_KEYS = Object.freeze([
15
15
  'explainers.publish.s3Uri',
16
16
  'explainers.publish.publicBaseUrl',
17
17
  'explainers.publish.awsRegion',
18
+ 'explainers.publish.publicAccess',
18
19
  'explainers.publish.awsProfile',
19
20
  'workflow.explainers.projectExplainer',
20
21
  'workflow.explainers.projectRecap',
@@ -115,7 +116,7 @@ export async function resolveExplainerConfig({
115
116
  }
116
117
  }
117
118
 
118
- const publish = resolvePublish(values);
119
+ const { publish, publishReport } = resolvePublish(values, sources);
119
120
  const preferences = {
120
121
  projectExplainer: resolvePreference(
121
122
  'workflow.explainers.projectExplainer',
@@ -127,7 +128,15 @@ export async function resolveExplainerConfig({
127
128
  ),
128
129
  };
129
130
 
130
- return { values, sources, theme, publish, preferences, warnings };
131
+ return {
132
+ values,
133
+ sources,
134
+ theme,
135
+ publish,
136
+ publishReport,
137
+ preferences,
138
+ warnings,
139
+ };
131
140
  }
132
141
 
133
142
  export function toExplainerRunRequest({
@@ -170,8 +179,9 @@ export function toExplainerRunRequest({
170
179
  );
171
180
  }
172
181
  durability.publish = {
173
- schemaVersion: 'explainer-kit.publish-request/v1',
182
+ schemaVersion: 'explainer-kit.publish-request/v2',
174
183
  ...resolvedConfig.publish,
184
+ publicAccess: resolvedConfig.publish.publicAccess ?? 'public',
175
185
  siteRoot: join(outputRoot, slug, 'site'),
176
186
  manifestPath: join(outputRoot, slug, 'manifest.json'),
177
187
  };
@@ -179,7 +189,10 @@ export function toExplainerRunRequest({
179
189
 
180
190
  return {
181
191
  schemaVersion: 'explainer-kit.run-request/v1',
182
- recipe: { id: recipe, version: '1' },
192
+ recipe: {
193
+ id: recipe,
194
+ version: recipe === 'project-recap' ? '2' : '1',
195
+ },
183
196
  slug,
184
197
  outputRoot,
185
198
  factBase,
@@ -254,6 +267,12 @@ function normalizeRuntimeValue(key, value) {
254
267
  if (key === 'explainers.publish.provider' && normalized !== 's3-static') {
255
268
  throw new Error(`${key} runtime override must be s3-static.`);
256
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
+ }
257
276
  if (key === 'explainers.defaults.style' && !STYLES.has(normalized)) {
258
277
  throw new Error(
259
278
  `${key} runtime override must name a curated explainer style.`,
@@ -283,27 +302,30 @@ function normalizeRuntimeValue(key, value) {
283
302
  return normalized;
284
303
  }
285
304
 
286
- function resolvePublish(values) {
287
- const provider = nullableString(values['explainers.publish.provider']);
288
- if (!provider) {
289
- return null;
290
- }
291
- if (provider !== 's3-static') {
292
- throw new Error(`Unsupported explainer publish provider: ${provider}`);
293
- }
294
-
305
+ function resolvePublish(values, sources) {
295
306
  const fields = {
307
+ provider: nullableString(values['explainers.publish.provider']),
296
308
  s3Uri: nullableString(values['explainers.publish.s3Uri']),
297
309
  publicBaseUrl: nullableString(values['explainers.publish.publicBaseUrl']),
298
310
  awsRegion: nullableString(values['explainers.publish.awsRegion']),
299
311
  };
312
+ if (fields.provider && fields.provider !== 's3-static') {
313
+ throw new Error(
314
+ `Unsupported explainer publish provider: ${fields.provider}`,
315
+ );
316
+ }
300
317
  const missing = Object.entries(fields)
301
318
  .filter(([, value]) => !value)
302
319
  .map(([key]) => key);
303
320
  if (missing.length > 0) {
304
- throw new Error(
305
- `Incomplete explainer publish configuration; missing ${missing.join(', ')}.`,
306
- );
321
+ return {
322
+ publish: null,
323
+ publishReport: {
324
+ mode: 'build-only',
325
+ publishCapable: false,
326
+ missing,
327
+ },
328
+ };
307
329
  }
308
330
  if (!/^s3:\/\/[^/\s]+(?:\/.*)?$/.test(fields.s3Uri)) {
309
331
  throw new Error('explainers.publish.s3Uri must be a valid s3:// URI.');
@@ -315,12 +337,29 @@ function resolvePublish(values) {
315
337
  }
316
338
 
317
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
+ }
318
347
  return {
319
- provider,
320
- s3Uri: fields.s3Uri.replace(/\/+$/, ''),
321
- publicBaseUrl: fields.publicBaseUrl.replace(/\/+$/, ''),
322
- awsRegion: fields.awsRegion,
323
- ...(awsProfile ? { awsProfile } : {}),
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
+ },
324
363
  };
325
364
  }
326
365
 
@@ -1,11 +1,12 @@
1
1
  import { lstat, realpath } from 'node:fs/promises';
2
- import { isAbsolute, join, relative, resolve, sep } from 'node:path';
2
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
3
 
4
4
  export async function resolveExplainerOutputRoot({
5
5
  repoRoot,
6
6
  invocation,
7
7
  activeProject,
8
8
  outputRoot,
9
+ slug,
9
10
  }) {
10
11
  if (!repoRoot) {
11
12
  throw new TypeError('repoRoot is required.');
@@ -17,19 +18,26 @@ export async function resolveExplainerOutputRoot({
17
18
  'Direct callers must provide an explicit outputRoot to the core.',
18
19
  );
19
20
  }
20
- return resolveSafePath(outputRoot, {
21
+ const resolvedRoot = await resolveSafePath(outputRoot, {
21
22
  baseRoot: repoRoot,
22
23
  field: 'outputRoot',
23
24
  });
25
+ assertParentOutputRoot(resolvedRoot, slug);
26
+ return resolvedRoot;
24
27
  }
25
28
 
26
29
  const canonicalRepoRoot = await realpath(resolve(repoRoot));
27
30
  if (invocation === 'repo') {
28
- return resolveSafePath('.oat/repo/reference/explainers', {
29
- baseRoot: canonicalRepoRoot,
30
- confinedRoot: canonicalRepoRoot,
31
- field: 'repo explainer output root',
32
- });
31
+ const resolvedRoot = await resolveSafePath(
32
+ '.oat/repo/reference/explainers',
33
+ {
34
+ baseRoot: canonicalRepoRoot,
35
+ confinedRoot: canonicalRepoRoot,
36
+ field: 'repo explainer output root',
37
+ },
38
+ );
39
+ assertParentOutputRoot(resolvedRoot, slug);
40
+ return resolvedRoot;
33
41
  }
34
42
 
35
43
  if (invocation !== 'project') {
@@ -49,11 +57,13 @@ export async function resolveExplainerOutputRoot({
49
57
  throw new Error('The active project cannot be the repository root.');
50
58
  }
51
59
 
52
- return resolveSafePath(join(projectRoot, 'explainers'), {
60
+ const resolvedRoot = await resolveSafePath(join(projectRoot, 'explainers'), {
53
61
  baseRoot: canonicalRepoRoot,
54
62
  confinedRoot: projectRoot,
55
63
  field: 'project explainer output root',
56
64
  });
65
+ assertParentOutputRoot(resolvedRoot, slug);
66
+ return resolvedRoot;
57
67
  }
58
68
 
59
69
  export async function resolveSourceAwarePath({
@@ -164,3 +174,37 @@ function isWithin(root, target) {
164
174
  function isMissing(error) {
165
175
  return error && typeof error === 'object' && error.code === 'ENOENT';
166
176
  }
177
+
178
+ function assertParentOutputRoot(outputRoot, slug) {
179
+ if (slug === undefined) return;
180
+ const normalizedSlug = normalizeRunSlug(slug);
181
+ if (basename(outputRoot) === normalizedSlug) {
182
+ throw new Error(
183
+ `Output root already ends in run slug "${normalizedSlug}"; supply the parent output root to avoid double-nesting.`,
184
+ );
185
+ }
186
+ }
187
+
188
+ function normalizeRunSlug(candidate) {
189
+ if (
190
+ typeof candidate !== 'string' ||
191
+ candidate.includes('/') ||
192
+ candidate.includes('\\') ||
193
+ candidate.includes('\0') ||
194
+ candidate === '.' ||
195
+ candidate === '..'
196
+ ) {
197
+ throw new Error('Run slug must be text, not a path or traversal value.');
198
+ }
199
+ const slug = candidate
200
+ .normalize('NFKD')
201
+ .replaceAll(/\p{Mark}/gu, '')
202
+ .toLowerCase()
203
+ .trim()
204
+ .replaceAll(/[^a-z0-9]+/g, '-')
205
+ .replaceAll(/^-+|-+$/g, '');
206
+ if (!slug) {
207
+ throw new Error('Run slug must contain at least one letter or number.');
208
+ }
209
+ return slug;
210
+ }
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFile } from 'node:fs/promises';
4
- import { dirname, join, resolve } from 'node:path';
4
+ import { basename, dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
 
7
- import { bindProjectSources } from './bind-project-sources.mjs';
7
+ import {
8
+ bindProjectSources,
9
+ bindRepositorySources,
10
+ } from './bind-project-sources.mjs';
8
11
  import { checkCoreCompatibility } from './check-core.mjs';
12
+ import { deriveExplainerDestination } from './derive-destination.mjs';
9
13
  import {
10
14
  resolveExplainerConfig,
11
15
  toExplainerRunRequest,
12
16
  } from './resolve-config.mjs';
13
17
  import { resolveExplainerOutputRoot } from './resolve-paths.mjs';
14
18
 
15
- export const MINIMUM_CORE_VERSION = '2.0.3';
19
+ export const MINIMUM_CORE_VERSION = '2.1.0';
16
20
  const ADAPTER_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
17
21
 
18
22
  export async function runOatExplainer({
@@ -80,21 +84,52 @@ export async function runOatExplainer({
80
84
  repoRoot,
81
85
  invocation,
82
86
  activeProject,
87
+ slug,
83
88
  });
84
- if (invocation !== 'project' || !activeProject) {
89
+ const destination = resolvedConfig.publish
90
+ ? deriveExplainerDestination({
91
+ invocation,
92
+ ...(invocation === 'project' && {
93
+ projectSlug: basename(activeProject),
94
+ }),
95
+ s3Uri: resolvedConfig.publish.s3Uri,
96
+ publicBaseUrl: resolvedConfig.publish.publicBaseUrl,
97
+ })
98
+ : null;
99
+ let bound;
100
+ if (invocation === 'project') {
101
+ if (!activeProject) {
102
+ throw new Error(
103
+ 'Project artifact binding requires an activeProject input.',
104
+ );
105
+ }
106
+ bound = await bindProjectSources({
107
+ projectRoot: resolve(repoRoot, activeProject),
108
+ repoRoot,
109
+ recipe,
110
+ suppliedFactBasePath,
111
+ });
112
+ } else if (invocation === 'repo') {
113
+ bound = await bindRepositorySources({
114
+ repoRoot,
115
+ suppliedFactBasePath,
116
+ });
117
+ } else {
85
118
  throw new Error(
86
- 'Project artifact binding requires a project invocation and activeProject.',
119
+ 'The OAT adapter accepts project or repository invocations; direct callers invoke the core with an explicit output root.',
87
120
  );
88
121
  }
89
- const projectRoot = resolve(repoRoot, activeProject);
90
- const bound = await bindProjectSources({
91
- projectRoot,
92
- repoRoot,
93
- recipe,
94
- suppliedFactBasePath,
95
- });
122
+ const requestConfig = destination
123
+ ? {
124
+ ...resolvedConfig,
125
+ publish: {
126
+ ...resolvedConfig.publish,
127
+ ...destination,
128
+ },
129
+ }
130
+ : resolvedConfig;
96
131
  const request = toExplainerRunRequest({
97
- resolvedConfig,
132
+ resolvedConfig: requestConfig,
98
133
  recipe,
99
134
  slug,
100
135
  outputRoot,
@@ -181,13 +216,6 @@ export async function runOatExplainer({
181
216
  }),
182
217
  reviewedSource: bound.reviewedSource,
183
218
  });
184
- const criticContractError = result?.errors?.find(
185
- ({ message }) =>
186
- typeof message === 'string' && message.includes('critic result contract'),
187
- );
188
- if (criticContractError) {
189
- throw new Error(criticContractError.message);
190
- }
191
219
  const manifest = await readManifest(result, request);
192
220
  return {
193
221
  compatibility,
@@ -195,7 +223,9 @@ export async function runOatExplainer({
195
223
  manifest,
196
224
  result,
197
225
  marking: result.marking ?? null,
226
+ publication: result.publication ?? null,
198
227
  outputRoot,
228
+ destination,
199
229
  };
200
230
  }
201
231
 
@@ -204,10 +234,11 @@ export function supportsAdaptiveSetPlanning(version) {
204
234
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-|$)/);
205
235
  if (!match) return false;
206
236
  const [major, minor, patch] = match.slice(1).map(Number);
237
+ const [minimumMajor, minimumMinor, minimumPatch] =
238
+ MINIMUM_CORE_VERSION.split('.').map(Number);
207
239
  return (
208
- major === 2 &&
209
- (minor > 0 ||
210
- (minor === 0 && patch >= Number(MINIMUM_CORE_VERSION.split('.')[2])))
240
+ major === minimumMajor &&
241
+ (minor > minimumMinor || (minor === minimumMinor && patch >= minimumPatch))
211
242
  );
212
243
  }
213
244
 
@@ -467,11 +498,8 @@ async function resolveLifecycleBrowserSession({
467
498
  return core.assertBrowserProbeSession(resolvedSession, {
468
499
  allowFixture: !required,
469
500
  });
470
- } catch (cause) {
471
- const error = new Error(
472
- `Browser session validation failed: ${cause?.message ?? String(cause)}`,
473
- { cause },
474
- );
501
+ } catch {
502
+ const error = new Error('Browser session validation failed.');
475
503
  error.code = 'E_BROWSER_PROBE_REQUIRED';
476
504
  throw error;
477
505
  }
@@ -633,24 +661,29 @@ async function readManifest(result, request) {
633
661
  return manifest;
634
662
  }
635
663
 
636
- async function runCli(argv = process.argv.slice(2), io = console) {
664
+ export async function runOatExplainerCli(
665
+ argv = process.argv.slice(2),
666
+ io = console,
667
+ run = runOatExplainer,
668
+ ) {
637
669
  try {
638
670
  if (argv.length !== 2 || argv[0] !== '--context') {
639
671
  throw new Error('Usage: run.mjs --context <adapter-context.json>');
640
672
  }
641
673
  const context = JSON.parse(await readFile(argv[1], 'utf8'));
642
- const result = await runOatExplainer(context);
643
- io.log(JSON.stringify(result, null, 2));
674
+ const result = await run(context);
675
+ io.log(JSON.stringify(projectAdapterCliResult(result), null, 2));
644
676
  return result.result.outcome === 'failed' ? 1 : 0;
645
- } catch (error) {
677
+ } catch {
646
678
  io.error(
647
679
  JSON.stringify(
648
680
  {
649
681
  outcome: 'failed',
650
- errors: [
682
+ reasons: [
651
683
  {
652
- code: error.code ?? 'E_ADAPTER',
653
- message: error instanceof Error ? error.message : String(error),
684
+ stage: 'finalization',
685
+ kind: 'pipeline-failure',
686
+ count: 1,
654
687
  },
655
688
  ],
656
689
  },
@@ -662,9 +695,211 @@ async function runCli(argv = process.argv.slice(2), io = console) {
662
695
  }
663
696
  }
664
697
 
698
+ function projectAdapterCliResult(value) {
699
+ if (!isObject(value) || !isObject(value.result)) {
700
+ return {
701
+ result: {
702
+ outcome: 'failed',
703
+ reasons: [
704
+ {
705
+ stage: 'finalization',
706
+ kind: 'pipeline-failure',
707
+ count: 1,
708
+ },
709
+ ],
710
+ },
711
+ };
712
+ }
713
+ const projected = {};
714
+ for (const key of ['compatibility', 'request', 'manifest', 'destination']) {
715
+ if (isObject(value[key])) projected[key] = structuredClone(value[key]);
716
+ }
717
+ for (const key of ['marking', 'outputRoot']) {
718
+ if (typeof value[key] === 'string' || value[key] === null) {
719
+ projected[key] = value[key];
720
+ }
721
+ }
722
+ projected.result = projectAdapterRunResult(value.result);
723
+ const publication = projectAdapterPublication(value.publication);
724
+ if (publication) projected.publication = publication;
725
+ return projected;
726
+ }
727
+
728
+ function projectAdapterRunResult(value) {
729
+ const projected = {};
730
+ for (const key of [
731
+ 'runId',
732
+ 'runRoot',
733
+ 'manifestPath',
734
+ 'buildRecordPath',
735
+ 'outcome',
736
+ 'marking',
737
+ ]) {
738
+ if (typeof value[key] === 'string') projected[key] = value[key];
739
+ }
740
+ if (Array.isArray(value.warnings)) {
741
+ projected.warnings = [
742
+ ...new Set(value.warnings.filter(isAdapterWarningCode)),
743
+ ];
744
+ }
745
+ for (const key of ['discovery', 'approval']) {
746
+ if (isObject(value[key])) projected[key] = structuredClone(value[key]);
747
+ }
748
+ const reasons = projectAdapterReasons(value.reasons);
749
+ if (reasons.length > 0) projected.reasons = reasons;
750
+ const visualReview = projectAdapterVisualReview(value.visualReview);
751
+ if (visualReview) projected.visualReview = visualReview;
752
+ const publication = projectAdapterPublication(value.publication);
753
+ if (publication) projected.publication = publication;
754
+ return projected;
755
+ }
756
+
757
+ function projectAdapterVisualReview(value) {
758
+ if (!isObject(value)) return null;
759
+ const reasons = projectAdapterReasons(value.reasons);
760
+ if (
761
+ value.schemaVersion !== 'explainer-kit.visual-review-evidence/v1' ||
762
+ typeof value.requestHash !== 'string' ||
763
+ ![1, 2].includes(value.attempt) ||
764
+ !['pass', 'correct', 'failed'].includes(value.disposition) ||
765
+ !Array.isArray(value.reasons) ||
766
+ reasons.length !== value.reasons.length
767
+ ) {
768
+ return null;
769
+ }
770
+ return {
771
+ schemaVersion: value.schemaVersion,
772
+ requestHash: value.requestHash,
773
+ attempt: value.attempt,
774
+ disposition: value.disposition,
775
+ reasons,
776
+ };
777
+ }
778
+
779
+ function projectAdapterPublication(value) {
780
+ if (!isObject(value)) return null;
781
+ if (
782
+ value.schemaVersion !== 'explainer-kit.publish-summary/v1' &&
783
+ value.schemaVersion !== 'explainer-kit.publish-summary/v2'
784
+ ) {
785
+ return null;
786
+ }
787
+ return {
788
+ schemaVersion: value.schemaVersion,
789
+ ...(typeof value.receiptSchemaVersion === 'string' && {
790
+ receiptSchemaVersion: value.receiptSchemaVersion,
791
+ }),
792
+ ...(typeof value.publicAccess === 'string' && {
793
+ publicAccess: value.publicAccess,
794
+ }),
795
+ ...(Array.isArray(value.artifacts) && {
796
+ artifacts: value.artifacts.map((artifact) =>
797
+ isObject(artifact)
798
+ ? {
799
+ ...pickAdapterStringFields(artifact, [
800
+ 'relativePath',
801
+ 'publicUrl',
802
+ 's3Uri',
803
+ 'hash',
804
+ 'contentType',
805
+ ]),
806
+ ...(isObject(artifact.source) && {
807
+ source: pickAdapterStringFields(artifact.source, [
808
+ 'kind',
809
+ 'artifactId',
810
+ 'name',
811
+ ]),
812
+ }),
813
+ ...(isObject(artifact.objectVerification) && {
814
+ objectVerification: projectAdapterVerification(
815
+ artifact.objectVerification,
816
+ ),
817
+ }),
818
+ ...(isObject(artifact.publicVerification) && {
819
+ publicVerification: projectAdapterVerification(
820
+ artifact.publicVerification,
821
+ ),
822
+ }),
823
+ }
824
+ : {},
825
+ ),
826
+ }),
827
+ };
828
+ }
829
+
830
+ function projectAdapterVerification(value) {
831
+ return {
832
+ ...pickAdapterStringFields(value, ['status', 'method', 'hash']),
833
+ ...(Number.isInteger(value.httpStatus) && {
834
+ httpStatus: value.httpStatus,
835
+ }),
836
+ };
837
+ }
838
+
839
+ function projectAdapterReasons(value) {
840
+ if (!Array.isArray(value)) return [];
841
+ return value.flatMap((reason) => {
842
+ if (
843
+ !isObject(reason) ||
844
+ ![
845
+ 'planning',
846
+ 'authoring',
847
+ 'rendering',
848
+ 'link-validation',
849
+ 'browser-review',
850
+ 'visual-review',
851
+ 'durability',
852
+ 'finalization',
853
+ ].includes(reason.stage) ||
854
+ ![
855
+ 'finding',
856
+ 'provider-failure',
857
+ 'pipeline-failure',
858
+ 'superseded',
859
+ ].includes(reason.kind) ||
860
+ !Number.isInteger(reason.count) ||
861
+ reason.count < 1 ||
862
+ reason.count > 50
863
+ ) {
864
+ return [];
865
+ }
866
+ return [
867
+ {
868
+ stage: reason.stage,
869
+ kind: reason.kind,
870
+ ...(typeof reason.artifactId === 'string' && {
871
+ artifactId: reason.artifactId,
872
+ }),
873
+ count: reason.count,
874
+ },
875
+ ];
876
+ });
877
+ }
878
+
879
+ function pickAdapterStringFields(value, keys) {
880
+ return Object.fromEntries(
881
+ keys.flatMap((key) =>
882
+ typeof value[key] === 'string' ? [[key, value[key]]] : [],
883
+ ),
884
+ );
885
+ }
886
+
887
+ function isAdapterWarningCode(value) {
888
+ return (
889
+ typeof value === 'string' &&
890
+ /^(?:fact-base-freshness-warning|theme-selection-normalized|durability-evidence-required|publish-receipt-evidence-required|(?:expansion|guideline|render|qa)-[a-z0-9-]+|visual-review-required:[a-z0-9-]+|stage-reopened:[a-z0-9-]+:[0-9TZ:.-]+|missing-(?:theme-token|required-anchor):[a-z0-9-]+)$/.test(
891
+ value,
892
+ )
893
+ );
894
+ }
895
+
896
+ function isObject(value) {
897
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
898
+ }
899
+
665
900
  if (
666
901
  process.argv[1] &&
667
902
  pathToFileURL(process.argv[1]).href === import.meta.url
668
903
  ) {
669
- process.exitCode = await runCli();
904
+ process.exitCode = await runOatExplainerCli();
670
905
  }
@@ -259,8 +259,8 @@ remains immutable baseline evidence for the original p01-t01 verification.
259
259
  | `oat-dispatch-subagents/references/record-schema.md` | `78f8069274a8 -> NG`; `1c9a84c4d1e0 -> NG` |
260
260
  | `oat-project-dispatch-subagents/SKILL.md` | `eca03ad1ca12 -> PDISPATCH-01`; `59fbcdc79cea -> PDISPATCH-01` |
261
261
  | `oat-project-plan/SKILL.md` | `f99e44b39932 -> PLAN-12`; `7e6a9854504b -> NG` |
262
- | `oat-project-implement/references/completion-and-closeout.md` | `6909a5a5a1fa -> IMPLEMENT-19`; `a7c02ef670fd -> IMPLEMENT-19` |
263
- | `oat-project-complete/SKILL.md` | `ffad9f909dba -> COMPLETE-01`; `ffe636f736e8 -> COMPLETE-01`; `9262624c148f -> COMPLETE-01`; `23ec8b6a5cee -> COMPLETE-01`; `9e6c4d7cc3a8 -> COMPLETE-01`; `47383d4ab654 -> COMPLETE-01`; `ba34449bd586 -> COMPLETE-01`; `2407ebed0921 -> NG`; `0177956707aa -> NG`; `8305b5bb65ef -> NG` |
262
+ | `oat-project-implement/references/completion-and-closeout.md` | `6909a5a5a1fa -> IMPLEMENT-19`; `a7c02ef670fd -> IMPLEMENT-19`; `9dde56d89a71 -> IMPLEMENT-16`; `13c41b2b2590 -> IMPLEMENT-16` |
263
+ | `oat-project-complete/SKILL.md` | `ffad9f909dba -> COMPLETE-01`; `ffe636f736e8 -> COMPLETE-01`; `9262624c148f -> COMPLETE-01`; `23ec8b6a5cee -> COMPLETE-01`; `9e6c4d7cc3a8 -> COMPLETE-01`; `47383d4ab654 -> COMPLETE-01`; `ba34449bd586 -> COMPLETE-01`; `0177956707aa -> NG`; `8305b5bb65ef -> NG`; `eb3e75663ac0 -> NG` |
264
264
 
265
265
  ## Prompt-scan comparison
266
266