@open-agent-toolkit/cli 0.2.10 → 0.2.11

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 (41) hide show
  1. package/assets/docs/cli-utilities/configuration.md +28 -14
  2. package/assets/docs/reference/cli-reference.md +1 -1
  3. package/assets/docs/workflows/projects/artifacts.md +16 -4
  4. package/assets/docs/workflows/skills/explainer-kit.md +70 -27
  5. package/assets/public-package-versions.json +4 -4
  6. package/assets/skills/explainer-kit/SKILL.md +9 -3
  7. package/assets/skills/explainer-kit/references/contracts.md +22 -7
  8. package/assets/skills/explainer-kit/schemas/author-request.schema.json +85 -0
  9. package/assets/skills/explainer-kit/schemas/author-result.schema.json +65 -0
  10. package/assets/skills/explainer-kit/schemas/manifest.schema.json +7 -1
  11. package/assets/skills/explainer-kit/schemas/run-request.schema.json +8 -0
  12. package/assets/skills/explainer-kit/schemas/theme.schema.json +8 -0
  13. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +26 -1
  14. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +61 -0
  15. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +2 -14
  16. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +56 -0
  17. package/assets/skills/explainer-kit/scripts/lib/theme.mjs +102 -4
  18. package/assets/skills/explainer-kit/scripts/run.mjs +144 -11
  19. package/assets/skills/explainer-kit/styles/business-corporate.json +77 -0
  20. package/assets/skills/explainer-kit/styles/clean-neutral.json +77 -0
  21. package/assets/skills/explainer-kit/styles/dark-edgy.json +77 -0
  22. package/assets/skills/explainer-kit/styles/navy-ocean.json +82 -0
  23. package/assets/skills/explainer-kit/templates/deck-shell.html +57 -2
  24. package/assets/skills/oat-explainer-kit/SKILL.md +15 -9
  25. package/assets/skills/oat-explainer-kit/references/config-contract.md +11 -6
  26. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +13 -0
  27. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +10 -14
  28. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +53 -9
  29. package/assets/skills/oat-explainer-kit/scripts/run.mjs +68 -0
  30. package/dist/commands/config/index.d.ts.map +1 -1
  31. package/dist/commands/config/index.js +34 -8
  32. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  33. package/dist/commands/project/archive/archive-utils.js +14 -5
  34. package/dist/config/oat-config.d.ts +2 -0
  35. package/dist/config/oat-config.d.ts.map +1 -1
  36. package/dist/config/oat-config.js +11 -0
  37. package/dist/config/resolve.d.ts.map +1 -1
  38. package/dist/config/resolve.js +3 -2
  39. package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
  40. package/dist/providers/codex/codec/config-merge.js +39 -1
  41. package/package.json +2 -2
@@ -12,6 +12,8 @@ const SCHEMA_FILES = {
12
12
  'durability-evidence': 'durability-evidence.schema.json',
13
13
  'publish-request': 'publish-request.schema.json',
14
14
  'publish-receipt': 'publish-receipt.schema.json',
15
+ 'author-request': 'author-request.schema.json',
16
+ 'author-result': 'author-result.schema.json',
15
17
  };
16
18
 
17
19
  const SCHEMAS = Object.fromEntries(
@@ -450,6 +452,46 @@ function validateCrossRecord(kind, value, context, errors) {
450
452
  }
451
453
  }
452
454
 
455
+ if (kind === 'author-request') {
456
+ const requiredNarrative = Array.isArray(value.recipe?.requiredNarrative)
457
+ ? value.recipe.requiredNarrative
458
+ : [];
459
+ const outlineIds = Array.isArray(value.narrativeOutline)
460
+ ? value.narrativeOutline.map((section) => section?.id)
461
+ : [];
462
+ if (
463
+ requiredNarrative.length !== outlineIds.length ||
464
+ requiredNarrative.some((id, index) => outlineIds[index] !== id)
465
+ ) {
466
+ add(
467
+ errors,
468
+ '$.narrativeOutline',
469
+ 'narrative-outline-mismatch',
470
+ 'Author request narrative outline must exactly match recipe requiredNarrative order.',
471
+ );
472
+ }
473
+ }
474
+
475
+ if (kind === 'author-result') {
476
+ for (const [index, section] of (Array.isArray(value.content?.sections)
477
+ ? value.content.sections
478
+ : []
479
+ ).entries()) {
480
+ if (
481
+ isObject(section) &&
482
+ typeof section.prose === 'string' &&
483
+ section.prose.trim().length === 0
484
+ ) {
485
+ add(
486
+ errors,
487
+ `$.content.sections[${index}].prose`,
488
+ 'empty-prose',
489
+ 'Authored section prose must contain non-whitespace text.',
490
+ );
491
+ }
492
+ }
493
+ }
494
+
453
495
  if (kind === 'manifest') {
454
496
  const paths = [];
455
497
  for (const artifact of Array.isArray(value.artifacts)
@@ -502,9 +544,17 @@ function validateCrossRecord(kind, value, context, errors) {
502
544
  );
503
545
  }
504
546
 
547
+ const requiredProvenance = [
548
+ 'run-request.json',
549
+ 'source/content-approval.json',
550
+ ];
505
551
  const expectedImmutable = new Set([
552
+ ...requiredProvenance,
506
553
  value.source?.factBasePath,
507
554
  'source/fact-base.md',
555
+ ...(Array.isArray(value.source?.authorResultPaths)
556
+ ? value.source.authorResultPaths
557
+ : []),
508
558
  value.theme?.path,
509
559
  ...(Array.isArray(value.artifacts)
510
560
  ? value.artifacts.flatMap((artifact) => [
@@ -520,6 +570,17 @@ function validateCrossRecord(kind, value, context, errors) {
520
570
  const recordedImmutable = isObject(value.immutableHashes)
521
571
  ? new Set(Object.keys(value.immutableHashes))
522
572
  : new Set();
573
+ const missingLegacyPaths = requiredProvenance.filter(
574
+ (path) => !recordedImmutable.has(path),
575
+ );
576
+ if (missingLegacyPaths.length > 0) {
577
+ add(
578
+ errors,
579
+ '$.immutableHashes',
580
+ 'legacy-manifest-incomplete',
581
+ `Legacy manifest is missing immutable coverage for ${missingLegacyPaths.join(', ')}; regenerate the recap package before archival.`,
582
+ );
583
+ }
523
584
  if (
524
585
  expectedImmutable.size !== recordedImmutable.size ||
525
586
  [...expectedImmutable].some((path) => !recordedImmutable.has(path))
@@ -457,21 +457,9 @@ function requiredArtifacts(manifest) {
457
457
  }
458
458
 
459
459
  function immutablePackage(manifest) {
460
- const expectedPaths = [
461
- manifest.source.factBasePath,
462
- 'source/fact-base.md',
463
- ...manifest.artifacts.map(({ contentPath }) => contentPath),
464
- manifest.theme.path,
465
- ...manifest.artifacts
466
- .filter(
467
- ({ status, renderedPath }) =>
468
- status === 'built' && typeof renderedPath === 'string',
469
- )
470
- .map(({ renderedPath }) => renderedPath),
471
- ];
472
- return [...new Set(expectedPaths)].map((path) => ({
460
+ return Object.entries(manifest.immutableHashes).map(([path, hash]) => ({
473
461
  path,
474
- hash: manifest.immutableHashes[path],
462
+ hash,
475
463
  }));
476
464
  }
477
465
 
@@ -23,6 +23,62 @@ const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
23
23
 
24
24
  export const REPRESENTATIVE_WIDTHS = Object.freeze([320, 768, 1440]);
25
25
 
26
+ export function checkSourceDumping({
27
+ authoredText,
28
+ authoredSections,
29
+ sourceTexts,
30
+ shingleSize = 8,
31
+ maxOverlapRatio = 0.6,
32
+ minMatchedShingles = 3,
33
+ }) {
34
+ const sourceShingles = new Set(
35
+ (Array.isArray(sourceTexts) ? sourceTexts : []).flatMap((text) =>
36
+ shingles(text, shingleSize),
37
+ ),
38
+ );
39
+ const sections = Array.isArray(authoredSections)
40
+ ? authoredSections
41
+ : [{ text: authoredText }];
42
+ const issues = sections.flatMap(({ id, text }) => {
43
+ const authoredShingles = shingles(text, shingleSize);
44
+ const matchedShingles = authoredShingles.filter((value) =>
45
+ sourceShingles.has(value),
46
+ ).length;
47
+ const overlapRatio =
48
+ authoredShingles.length === 0
49
+ ? 0
50
+ : matchedShingles / authoredShingles.length;
51
+ return matchedShingles >= minMatchedShingles &&
52
+ overlapRatio > maxOverlapRatio
53
+ ? [
54
+ {
55
+ code: 'source-dump',
56
+ message:
57
+ 'Authored narrative contains too much verbatim source text; rewrite it as audience-ready prose.',
58
+ details: {
59
+ ...(typeof id === 'string' && { sectionId: id }),
60
+ matchedShingles,
61
+ authoredShingles: authoredShingles.length,
62
+ overlapRatio,
63
+ },
64
+ },
65
+ ]
66
+ : [];
67
+ });
68
+ return { valid: issues.length === 0, issues };
69
+ }
70
+
71
+ function shingles(value, size) {
72
+ const words =
73
+ typeof value === 'string'
74
+ ? (value.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [])
75
+ : [];
76
+ if (words.length < size) return [];
77
+ return Array.from({ length: words.length - size + 1 }, (_, index) =>
78
+ words.slice(index, index + size).join(' '),
79
+ );
80
+ }
81
+
26
82
  export const BROWSER_PROBE_EVALUATE = `(() => {
27
83
  const root = document.documentElement;
28
84
  const clippedX = [...document.querySelectorAll('body *')]
@@ -10,6 +10,12 @@ const PALETTE_NAMES = new Set([
10
10
  'violet',
11
11
  ]);
12
12
  const PROFILE_NAMES = new Set(['clean', 'editorial', 'technical']);
13
+ const STYLE_NAMES = new Set([
14
+ 'clean-neutral',
15
+ 'business-corporate',
16
+ 'navy-ocean',
17
+ 'dark-edgy',
18
+ ]);
13
19
  const RENDER_STRATEGIES = new Set(['default-only', 'user-switchable']);
14
20
  const DEFAULT_MODES = new Set(['light', 'dark']);
15
21
  const MODE_ROLES = ['surface', 'ink', 'accent', 'status', 'diagramSeries'];
@@ -20,20 +26,28 @@ export async function resolveTheme(selection = {}) {
20
26
  const renderStrategy = selection.renderStrategy ?? 'default-only';
21
27
  const warnings = [];
22
28
  let theme;
29
+ const hasLegacySelection = ['palette', 'visualProfile'].some(
30
+ (key) => selection[key] !== undefined,
31
+ );
32
+ if (hasLegacySelection) {
33
+ warnings.push(
34
+ 'Palette and visual profile selection is deprecated; use a curated style.',
35
+ );
36
+ }
23
37
 
24
38
  if (selection.suppliedBundlePath !== undefined) {
25
39
  theme = await resolveSuppliedBundle(selection.suppliedBundlePath);
26
40
  if (
27
- ['palette', 'visualProfile', 'artDirection', 'defaultMode'].some(
41
+ ['style', 'palette', 'visualProfile', 'artDirection', 'defaultMode'].some(
28
42
  (key) => selection[key] !== undefined,
29
43
  )
30
44
  ) {
31
45
  warnings.push(
32
- 'Supplied bundle wins over palette, visual profile, art direction, and default mode selections.',
46
+ 'Supplied bundle wins over style, palette, visual profile, art direction, and default mode selections.',
33
47
  );
34
48
  }
35
49
  } else {
36
- theme = await resolveNamedTheme(selection);
50
+ theme = await resolveNamedTheme(selection, warnings);
37
51
  }
38
52
 
39
53
  assertResolvedTheme(theme);
@@ -48,7 +62,49 @@ export async function resolveTheme(selection = {}) {
48
62
  };
49
63
  }
50
64
 
51
- async function resolveNamedTheme(selection) {
65
+ async function resolveNamedTheme(selection, warnings) {
66
+ if (
67
+ selection.style !== undefined ||
68
+ (selection.palette === undefined && selection.visualProfile === undefined)
69
+ ) {
70
+ const styleName = selection.style ?? 'clean-neutral';
71
+ assertNamedSelection(styleName, STYLE_NAMES, 'style');
72
+ if (selection.style === undefined) {
73
+ warnings.push(
74
+ 'No explicit style was selected; defaulted to clean-neutral.',
75
+ );
76
+ }
77
+ if (
78
+ selection.style !== undefined &&
79
+ (selection.palette !== undefined || selection.visualProfile !== undefined)
80
+ ) {
81
+ warnings.push(
82
+ 'Curated style wins over deprecated palette and visual profile selections.',
83
+ );
84
+ }
85
+ const style = await loadBundledJson('styles', styleName);
86
+ assertStyle(style, styleName);
87
+ const theme = {
88
+ schemaVersion: 'explainer-kit.theme/v1',
89
+ name: style.name,
90
+ defaultMode: selection.defaultMode ?? style.defaultMode,
91
+ modes: structuredClone(style.modes),
92
+ provenance: { style: styleName, derived: false },
93
+ typography: structuredClone(style.typography),
94
+ spacing: structuredClone(style.spacing),
95
+ geometry: structuredClone(style.geometry),
96
+ elevation: structuredClone(style.elevation),
97
+ density: style.density,
98
+ motion: structuredClone(style.motion),
99
+ diagrams: structuredClone(style.diagrams),
100
+ };
101
+ if (selection.artDirection !== undefined) {
102
+ applyArtDirection(theme, selection.artDirection);
103
+ }
104
+ theme.bundleHash = canonicalHash(theme);
105
+ return theme;
106
+ }
107
+
52
108
  const paletteName = selection.palette ?? 'neutral';
53
109
  const profileName = selection.visualProfile ?? 'clean';
54
110
  assertNamedSelection(paletteName, PALETTE_NAMES, 'palette');
@@ -135,6 +191,7 @@ function assertSelection(selection) {
135
191
  throw new TypeError('Theme selection must be an object.');
136
192
  }
137
193
  const allowed = new Set([
194
+ 'style',
138
195
  'palette',
139
196
  'visualProfile',
140
197
  'suppliedBundlePath',
@@ -160,6 +217,47 @@ function assertSelection(selection) {
160
217
  }
161
218
  }
162
219
 
220
+ function assertStyle(style, expectedName) {
221
+ const expectedKeys = [
222
+ 'name',
223
+ 'defaultMode',
224
+ 'modes',
225
+ 'typography',
226
+ 'spacing',
227
+ 'geometry',
228
+ 'elevation',
229
+ 'density',
230
+ 'motion',
231
+ 'diagrams',
232
+ ];
233
+ if (
234
+ !isObject(style) ||
235
+ style.name !== expectedName ||
236
+ !hasExactKeys(style, expectedKeys) ||
237
+ !DEFAULT_MODES.has(style.defaultMode) ||
238
+ !isObject(style.modes) ||
239
+ !hasExactKeys(style.modes, ['light', 'dark'])
240
+ ) {
241
+ throw new Error(`Bundled style ${expectedName} has an invalid shape.`);
242
+ }
243
+ for (const mode of Object.values(style.modes)) {
244
+ assertMode(mode);
245
+ }
246
+ assertProfile(
247
+ {
248
+ name: expectedName,
249
+ typography: style.typography,
250
+ spacing: style.spacing,
251
+ geometry: style.geometry,
252
+ elevation: style.elevation,
253
+ density: style.density,
254
+ motion: style.motion,
255
+ diagrams: style.diagrams,
256
+ },
257
+ expectedName,
258
+ );
259
+ }
260
+
163
261
  function assertNamedSelection(value, allowed, label) {
164
262
  if (typeof value !== 'string' || !allowed.has(value)) {
165
263
  throw new Error(`Unknown ${label}: ${String(value)}.`);
@@ -9,7 +9,7 @@ import { resolveContentApproval } from './lib/content-approval.mjs';
9
9
  import { canonicalHash, validateContract } from './lib/contracts.mjs';
10
10
  import { processFactBase } from './lib/fact-base.mjs';
11
11
  import { writeJsonAtomic, writeTextAtomic } from './lib/fs-safe.mjs';
12
- import { auditArtifactSet } from './lib/qa.mjs';
12
+ import { auditArtifactSet, checkSourceDumping } from './lib/qa.mjs';
13
13
  import {
14
14
  loadRecipe,
15
15
  shouldStopDiscovery,
@@ -38,6 +38,7 @@ export async function runExplainer(request, options = {}) {
38
38
  inputHashes: {},
39
39
  contentModels: [],
40
40
  contentPaths: new Map(),
41
+ authorResultPaths: [],
41
42
  theme: null,
42
43
  renderStrategy: run.request.theme.renderStrategy,
43
44
  rendered: [],
@@ -83,9 +84,15 @@ export async function runExplainer(request, options = {}) {
83
84
  });
84
85
  await executeStage(run, 'content', options, async () => {
85
86
  state.discovery = await runDiscovery(recipe, state.factBase, options);
86
- state.contentModels = recipe.artifacts.map((artifact) =>
87
- createContentModel(recipe, artifact, run.slug, state.factBase),
88
- );
87
+ if (run.request.mode === 'unattended') {
88
+ const authored = await createAuthoredContent(state, options.author);
89
+ state.contentModels = authored.models;
90
+ state.authorResultPaths = authored.resultPaths;
91
+ } else {
92
+ state.contentModels = recipe.artifacts.map((artifact) =>
93
+ createContentModel(recipe, artifact, run.slug, state.factBase),
94
+ );
95
+ }
89
96
  for (const model of state.contentModels) {
90
97
  const validation = validateContentModel(recipe, model);
91
98
  if (!validation.valid) {
@@ -106,6 +113,7 @@ export async function runExplainer(request, options = {}) {
106
113
  run,
107
114
  run.request.mode,
108
115
  options.reviewedSource,
116
+ state.authorResultPaths,
109
117
  );
110
118
  if (!state.approval.canResume) {
111
119
  return resultFor(state);
@@ -251,9 +259,14 @@ async function loadResumableRun(request) {
251
259
  }
252
260
 
253
261
  async function hydrateResumableState(state) {
254
- state.factBase = await readJson(
255
- join(state.run.runRoot, 'source/fact-base.json'),
256
- );
262
+ const [factBase, approval] = await Promise.all([
263
+ readJson(join(state.run.runRoot, 'source/fact-base.json')),
264
+ readJson(join(state.run.runRoot, 'source/content-approval.json')),
265
+ ]);
266
+ state.factBase = factBase;
267
+ state.authorResultPaths = Array.isArray(approval.authorResultPaths)
268
+ ? [...approval.authorResultPaths]
269
+ : [];
257
270
  state.inputHashes = inputHashes(state.factBase);
258
271
  state.factBaseHash = canonicalHash(state.factBase);
259
272
  state.contentModels = [];
@@ -534,6 +547,9 @@ function manifestFor(state, buildRecord, createdAt, immutableHashes) {
534
547
  factBasePath: 'source/fact-base.json',
535
548
  factBaseHash: state.factBaseHash,
536
549
  inputHashes: state.inputHashes,
550
+ ...(state.authorResultPaths.length > 0 && {
551
+ authorResultPaths: state.authorResultPaths,
552
+ }),
537
553
  },
538
554
  theme: {
539
555
  path: 'theme.resolved.json',
@@ -551,6 +567,117 @@ function manifestFor(state, buildRecord, createdAt, immutableHashes) {
551
567
  };
552
568
  }
553
569
 
570
+ async function createAuthoredContent(state, author) {
571
+ if (typeof author !== 'function') {
572
+ throw codedError(
573
+ 'E_AUTHOR_REQUIRED',
574
+ 'Unattended runs require an explicit author callback.',
575
+ );
576
+ }
577
+
578
+ const authored = [];
579
+ for (const artifact of state.recipe.artifacts) {
580
+ const resultPath = `source/author/${artifact.id}.json`;
581
+ const authorRequest = {
582
+ schemaVersion: 'explainer-kit.author-request/v1',
583
+ run: { runId: state.run.runId, slug: state.run.slug },
584
+ recipe: {
585
+ id: state.recipe.id,
586
+ version: state.recipe.version,
587
+ requiredNarrative: [...state.recipe.requiredNarrative],
588
+ },
589
+ artifact: {
590
+ id: artifact.id,
591
+ type: artifact.type,
592
+ },
593
+ narrativeOutline: state.recipe.requiredNarrative.map((id) => ({
594
+ id,
595
+ title: humanize(id),
596
+ })),
597
+ factBase: structuredClone(state.factBase),
598
+ discovery: structuredClone(state.discovery),
599
+ };
600
+ const requestValidation = validateContract('author-request', authorRequest);
601
+ if (!requestValidation.valid) {
602
+ throw codedError(
603
+ 'E_AUTHOR_REQUEST',
604
+ contractErrorMessage('author request', requestValidation.errors),
605
+ );
606
+ }
607
+
608
+ const result = await author(structuredClone(authorRequest));
609
+ const resultValidation = validateContract('author-result', result);
610
+ if (!resultValidation.valid) {
611
+ throw codedError(
612
+ 'E_AUTHOR_RESULT',
613
+ contractErrorMessage('author result', resultValidation.errors),
614
+ );
615
+ }
616
+ const sectionIds = result.content.sections.map(({ id }) => id);
617
+ if (
618
+ result.artifactId !== artifact.id ||
619
+ sectionIds.length !== state.recipe.requiredNarrative.length ||
620
+ state.recipe.requiredNarrative.some(
621
+ (id, index) => sectionIds[index] !== id,
622
+ )
623
+ ) {
624
+ throw codedError(
625
+ 'E_AUTHOR_RESULT',
626
+ `Author result for ${artifact.id} must contain the exact required section IDs in order.`,
627
+ );
628
+ }
629
+ const dumpCheck = checkSourceDumping({
630
+ authoredSections: result.content.sections.map(({ id, prose }) => ({
631
+ id,
632
+ text: prose,
633
+ })),
634
+ sourceTexts: [
635
+ ...state.factBase.claims,
636
+ ...state.factBase.unresolvedClaims,
637
+ ].map(({ text }) => text),
638
+ });
639
+ if (!dumpCheck.valid) {
640
+ throw codedError(
641
+ 'E_SOURCE_DUMP',
642
+ dumpCheck.issues.map(({ message }) => message).join('; '),
643
+ );
644
+ }
645
+
646
+ authored.push({
647
+ result: structuredClone(result),
648
+ resultPath,
649
+ model: {
650
+ artifactId: artifact.id,
651
+ slug: state.run.slug,
652
+ title: result.content.title,
653
+ description: result.content.description,
654
+ ...(result.content.eyebrow && { eyebrow: result.content.eyebrow }),
655
+ ...(result.content.footer && { footer: result.content.footer }),
656
+ sections: result.content.sections.map(({ id, title, prose }) => ({
657
+ id,
658
+ title,
659
+ content: prose,
660
+ })),
661
+ artifactLinks: result.content.artifactLinks ?? [],
662
+ },
663
+ });
664
+ }
665
+
666
+ for (const item of authored) {
667
+ await writeJsonAtomic(state.run.runRoot, item.resultPath, item.result);
668
+ }
669
+ return {
670
+ models: authored.map(({ model }) => model),
671
+ resultPaths: authored.map(({ resultPath }) => resultPath),
672
+ };
673
+ }
674
+
675
+ function contractErrorMessage(label, errors) {
676
+ return `Invalid ${label}: ${errors
677
+ .map(({ path, message }) => `${path}: ${message}`)
678
+ .join('; ')}`;
679
+ }
680
+
554
681
  function createContentModel(recipe, artifact, slug, factBase) {
555
682
  const facts = [
556
683
  ...factBase.claims.map(({ text, sections }) => ({ text, sections })),
@@ -614,8 +741,11 @@ function validateRecipeSources(recipe, binding) {
614
741
 
615
742
  async function immutableHashesFor(state) {
616
743
  const paths = [
744
+ 'run-request.json',
617
745
  'source/fact-base.json',
618
746
  'source/fact-base.md',
747
+ 'source/content-approval.json',
748
+ ...state.authorResultPaths,
619
749
  ...state.contentPaths.values(),
620
750
  ...(state.theme ? ['theme.resolved.json'] : []),
621
751
  ...state.artifacts
@@ -727,9 +857,12 @@ async function parseCli(argv) {
727
857
  if (!path) throw new Error('--reviewed-source requires a JSON path.');
728
858
  options.reviewedSource = JSON.parse(await readFile(path, 'utf8'));
729
859
  } else if (
730
- ['--critic-module', '--publish-module', '--durability-module'].includes(
731
- value,
732
- )
860
+ [
861
+ '--author-module',
862
+ '--critic-module',
863
+ '--publish-module',
864
+ '--durability-module',
865
+ ].includes(value)
733
866
  ) {
734
867
  const path = argv[++index];
735
868
  if (!path) throw new Error(`${value} requires a module path.`);
@@ -745,7 +878,7 @@ async function parseCli(argv) {
745
878
  }
746
879
  if (!requestPath) {
747
880
  throw new Error(
748
- 'Usage: run.mjs --request <json> [--reviewed-source <json>] [--critic-module <mjs>] [--publish-module <mjs>] [--durability-module <mjs>]',
881
+ 'Usage: run.mjs --request <json> [--reviewed-source <json>] [--author-module <mjs>] [--critic-module <mjs>] [--publish-module <mjs>] [--durability-module <mjs>]',
749
882
  );
750
883
  }
751
884
  return { requestPath, options };
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "business-corporate",
3
+ "defaultMode": "light",
4
+ "modes": {
5
+ "light": {
6
+ "surface": {
7
+ "canvas": "#ffffff",
8
+ "panel": "#f7f8fa",
9
+ "elevated": "#eef0f4"
10
+ },
11
+ "ink": { "primary": "#1a1f29", "muted": "#4a5568", "inverse": "#ffffff" },
12
+ "accent": { "primary": "#1b4f8a", "secondary": "#2f6fce" },
13
+ "status": {
14
+ "success": "#197a56",
15
+ "warning": "#965108",
16
+ "danger": "#b83226",
17
+ "info": "#1b4f8a"
18
+ },
19
+ "diagramSeries": ["#2f6fce", "#965108", "#0f7180", "#6b4fbf"]
20
+ },
21
+ "dark": {
22
+ "surface": {
23
+ "canvas": "#121a26",
24
+ "panel": "#1b2533",
25
+ "elevated": "#293649"
26
+ },
27
+ "ink": { "primary": "#f4f7fb", "muted": "#c5cfdb", "inverse": "#121a26" },
28
+ "accent": { "primary": "#8fc2ff", "secondary": "#75d6df" },
29
+ "status": {
30
+ "success": "#8ce0b9",
31
+ "warning": "#ffd17a",
32
+ "danger": "#ffaaa3",
33
+ "info": "#8fc2ff"
34
+ },
35
+ "diagramSeries": ["#8fc2ff", "#ffd17a", "#75d6df", "#c5a9ff"]
36
+ }
37
+ },
38
+ "typography": {
39
+ "sans": ["system-ui", "-apple-system", "BlinkMacSystemFont", "sans-serif"],
40
+ "serif": ["Charter", "Bitstream Charter", "Georgia", "serif"],
41
+ "mono": ["ui-monospace", "SFMono-Regular", "Consolas", "monospace"],
42
+ "scale": {
43
+ "caption": "0.875rem",
44
+ "body": "1.0625rem",
45
+ "title": "1.75rem",
46
+ "display": "2.75rem"
47
+ },
48
+ "lineHeight": { "caption": 1.5, "body": 1.7, "title": 1.2, "display": 1.05 }
49
+ },
50
+ "spacing": {
51
+ "unit": 5,
52
+ "scale": { "xs": 5, "sm": 10, "md": 20, "lg": 30, "xl": 50 }
53
+ },
54
+ "geometry": { "radius": { "sm": 4, "md": 8, "lg": 12 }, "borderWidth": 1 },
55
+ "elevation": {
56
+ "shadows": {
57
+ "low": "0 1px 2px rgb(26 31 41 / 0.08)",
58
+ "high": "0 10px 28px rgb(26 31 41 / 0.18)"
59
+ }
60
+ },
61
+ "density": "spacious",
62
+ "motion": {
63
+ "enabled": true,
64
+ "durationMs": { "fast": 140, "normal": 220, "slow": 360 },
65
+ "easing": {
66
+ "standard": "ease-out",
67
+ "emphasized": "cubic-bezier(0.16, 1, 0.3, 1)"
68
+ },
69
+ "reducedMotion": "disable-nonessential"
70
+ },
71
+ "diagrams": {
72
+ "lineWidth": 1.5,
73
+ "nodeGap": 40,
74
+ "arrowStyle": "curved",
75
+ "labelTreatment": "inline"
76
+ }
77
+ }