@open-agent-toolkit/cli 0.2.10 → 0.2.12

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 (43) 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/assets/skills/oat-wave-execute/SKILL.md +126 -23
  31. package/assets/skills/oat-wave-program/SKILL.md +41 -7
  32. package/dist/commands/config/index.d.ts.map +1 -1
  33. package/dist/commands/config/index.js +34 -8
  34. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  35. package/dist/commands/project/archive/archive-utils.js +14 -5
  36. package/dist/config/oat-config.d.ts +2 -0
  37. package/dist/config/oat-config.d.ts.map +1 -1
  38. package/dist/config/oat-config.js +11 -0
  39. package/dist/config/resolve.d.ts.map +1 -1
  40. package/dist/config/resolve.js +3 -2
  41. package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
  42. package/dist/providers/codex/codec/config-merge.js +39 -1
  43. package/package.json +2 -2
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "clean-neutral",
3
+ "defaultMode": "light",
4
+ "modes": {
5
+ "light": {
6
+ "surface": {
7
+ "canvas": "#f4f6f9",
8
+ "panel": "#ffffff",
9
+ "elevated": "#eef1f5"
10
+ },
11
+ "ink": { "primary": "#16202e", "muted": "#4a5568", "inverse": "#ffffff" },
12
+ "accent": { "primary": "#2f56c9", "secondary": "#0e7180" },
13
+ "status": {
14
+ "success": "#157a54",
15
+ "warning": "#965108",
16
+ "danger": "#bd3a2c",
17
+ "info": "#2f56c9"
18
+ },
19
+ "diagramSeries": ["#2f56c9", "#965108", "#0e7180", "#6a4fc4"]
20
+ },
21
+ "dark": {
22
+ "surface": {
23
+ "canvas": "#111827",
24
+ "panel": "#1f2937",
25
+ "elevated": "#374151"
26
+ },
27
+ "ink": { "primary": "#f9fafb", "muted": "#d1d5db", "inverse": "#111827" },
28
+ "accent": { "primary": "#93c5fd", "secondary": "#5eead4" },
29
+ "status": {
30
+ "success": "#86efac",
31
+ "warning": "#fde047",
32
+ "danger": "#fca5a5",
33
+ "info": "#93c5fd"
34
+ },
35
+ "diagramSeries": ["#93c5fd", "#fde047", "#5eead4", "#c4b5fd"]
36
+ }
37
+ },
38
+ "typography": {
39
+ "sans": ["system-ui", "-apple-system", "BlinkMacSystemFont", "sans-serif"],
40
+ "serif": ["ui-serif", "Georgia", "serif"],
41
+ "mono": ["ui-monospace", "SFMono-Regular", "Consolas", "monospace"],
42
+ "scale": {
43
+ "caption": "0.875rem",
44
+ "body": "1rem",
45
+ "title": "1.5rem",
46
+ "display": "2.25rem"
47
+ },
48
+ "lineHeight": { "caption": 1.4, "body": 1.6, "title": 1.25, "display": 1.1 }
49
+ },
50
+ "spacing": {
51
+ "unit": 4,
52
+ "scale": { "xs": 4, "sm": 8, "md": 16, "lg": 24, "xl": 40 }
53
+ },
54
+ "geometry": { "radius": { "sm": 4, "md": 8, "lg": 12 }, "borderWidth": 1 },
55
+ "elevation": {
56
+ "shadows": {
57
+ "low": "0 1px 2px rgb(22 32 46 / 0.08)",
58
+ "high": "0 12px 28px rgb(22 32 46 / 0.16)"
59
+ }
60
+ },
61
+ "density": "comfortable",
62
+ "motion": {
63
+ "enabled": true,
64
+ "durationMs": { "fast": 120, "normal": 200, "slow": 320 },
65
+ "easing": {
66
+ "standard": "ease",
67
+ "emphasized": "cubic-bezier(0.2, 0, 0, 1)"
68
+ },
69
+ "reducedMotion": "disable-nonessential"
70
+ },
71
+ "diagrams": {
72
+ "lineWidth": 2,
73
+ "nodeGap": 32,
74
+ "arrowStyle": "straight",
75
+ "labelTreatment": "boxed"
76
+ }
77
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "dark-edgy",
3
+ "defaultMode": "dark",
4
+ "modes": {
5
+ "light": {
6
+ "surface": {
7
+ "canvas": "#f3f5f7",
8
+ "panel": "#ffffff",
9
+ "elevated": "#e6eaee"
10
+ },
11
+ "ink": { "primary": "#17202b", "muted": "#4f5c6d", "inverse": "#0d1117" },
12
+ "accent": { "primary": "#4eb4b9", "secondary": "#4f91ad" },
13
+ "status": {
14
+ "success": "#9fd68a",
15
+ "warning": "#f0b878",
16
+ "danger": "#ff9aa2",
17
+ "info": "#7ddce0"
18
+ },
19
+ "diagramSeries": ["#397a98", "#8d591d", "#4e872f", "#7654a8"]
20
+ },
21
+ "dark": {
22
+ "surface": {
23
+ "canvas": "#0d1117",
24
+ "panel": "#161b22",
25
+ "elevated": "#1c232c"
26
+ },
27
+ "ink": { "primary": "#d9e0e8", "muted": "#aeb8c7", "inverse": "#0d1117" },
28
+ "accent": { "primary": "#7ddce0", "secondary": "#6fb7d8" },
29
+ "status": {
30
+ "success": "#9fd68a",
31
+ "warning": "#f0b878",
32
+ "danger": "#ff9aa2",
33
+ "info": "#7ddce0"
34
+ },
35
+ "diagramSeries": ["#7ddce0", "#f0b878", "#6fb7d8", "#c4a4f0"]
36
+ }
37
+ },
38
+ "typography": {
39
+ "sans": ["system-ui", "-apple-system", "BlinkMacSystemFont", "sans-serif"],
40
+ "serif": ["ui-serif", "Georgia", "serif"],
41
+ "mono": ["ui-monospace", "SFMono-Regular", "Consolas", "monospace"],
42
+ "scale": {
43
+ "caption": "0.8125rem",
44
+ "body": "0.9375rem",
45
+ "title": "1.375rem",
46
+ "display": "2rem"
47
+ },
48
+ "lineHeight": { "caption": 1.35, "body": 1.5, "title": 1.2, "display": 1.1 }
49
+ },
50
+ "spacing": {
51
+ "unit": 3,
52
+ "scale": { "xs": 3, "sm": 6, "md": 12, "lg": 18, "xl": 30 }
53
+ },
54
+ "geometry": { "radius": { "sm": 2, "md": 4, "lg": 8 }, "borderWidth": 1 },
55
+ "elevation": {
56
+ "shadows": {
57
+ "low": "0 1px 2px rgb(0 0 0 / 0.22)",
58
+ "high": "0 12px 30px rgb(0 0 0 / 0.38)"
59
+ }
60
+ },
61
+ "density": "compact",
62
+ "motion": {
63
+ "enabled": true,
64
+ "durationMs": { "fast": 100, "normal": 180, "slow": 280 },
65
+ "easing": {
66
+ "standard": "ease",
67
+ "emphasized": "cubic-bezier(0.2, 0.8, 0.2, 1)"
68
+ },
69
+ "reducedMotion": "disable-nonessential"
70
+ },
71
+ "diagrams": {
72
+ "lineWidth": 2,
73
+ "nodeGap": 24,
74
+ "arrowStyle": "straight",
75
+ "labelTreatment": "boxed"
76
+ }
77
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "navy-ocean",
3
+ "defaultMode": "dark",
4
+ "modes": {
5
+ "light": {
6
+ "surface": {
7
+ "canvas": "#f4f7fb",
8
+ "panel": "#ffffff",
9
+ "elevated": "#e5ebf3"
10
+ },
11
+ "ink": { "primary": "#142033", "muted": "#46566d", "inverse": "#ffffff" },
12
+ "accent": { "primary": "#2867a8", "secondary": "#177583" },
13
+ "status": {
14
+ "success": "#187452",
15
+ "warning": "#92500b",
16
+ "danger": "#b83c32",
17
+ "info": "#2867a8"
18
+ },
19
+ "diagramSeries": ["#2867a8", "#92500b", "#177583", "#694eb5"]
20
+ },
21
+ "dark": {
22
+ "surface": {
23
+ "canvas": "#0e1420",
24
+ "panel": "#161d2b",
25
+ "elevated": "#1e2635"
26
+ },
27
+ "ink": { "primary": "#e7ecf3", "muted": "#aeb9c8", "inverse": "#0e1420" },
28
+ "accent": { "primary": "#77abec", "secondary": "#4cb8c4" },
29
+ "status": {
30
+ "success": "#5cc79a",
31
+ "warning": "#e3a54e",
32
+ "danger": "#e2857a",
33
+ "info": "#77abec"
34
+ },
35
+ "diagramSeries": ["#77abec", "#e3a54e", "#4cb8c4", "#a98fe0"]
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": {
49
+ "caption": 1.45,
50
+ "body": 1.65,
51
+ "title": 1.18,
52
+ "display": 1.05
53
+ }
54
+ },
55
+ "spacing": {
56
+ "unit": 5,
57
+ "scale": { "xs": 5, "sm": 10, "md": 20, "lg": 30, "xl": 48 }
58
+ },
59
+ "geometry": { "radius": { "sm": 4, "md": 8, "lg": 12 }, "borderWidth": 1 },
60
+ "elevation": {
61
+ "shadows": {
62
+ "low": "0 1px 3px rgb(0 0 0 / 0.18)",
63
+ "high": "0 16px 36px rgb(0 0 0 / 0.28)"
64
+ }
65
+ },
66
+ "density": "comfortable",
67
+ "motion": {
68
+ "enabled": true,
69
+ "durationMs": { "fast": 140, "normal": 240, "slow": 380 },
70
+ "easing": {
71
+ "standard": "ease-out",
72
+ "emphasized": "cubic-bezier(0.16, 1, 0.3, 1)"
73
+ },
74
+ "reducedMotion": "disable-nonessential"
75
+ },
76
+ "diagrams": {
77
+ "lineWidth": 2,
78
+ "nodeGap": 36,
79
+ "arrowStyle": "curved",
80
+ "labelTreatment": "boxed"
81
+ }
82
+ }
@@ -118,6 +118,20 @@
118
118
  outline: 2px solid var(--accent);
119
119
  outline-offset: 2px;
120
120
  }
121
+ .deck-progress {
122
+ position: fixed;
123
+ z-index: 4;
124
+ inset: auto 0 0;
125
+ height: 0.25rem;
126
+ background: var(--panel);
127
+ }
128
+ .deck-progress__bar {
129
+ display: block;
130
+ width: 0;
131
+ height: 100%;
132
+ background: var(--accent);
133
+ transition: width 180ms ease;
134
+ }
121
135
  html:not(.js) .deck {
122
136
  display: block;
123
137
  height: auto;
@@ -139,6 +153,17 @@
139
153
  .deck {
140
154
  scroll-behavior: auto;
141
155
  }
156
+ .deck-progress__bar {
157
+ transition: none;
158
+ }
159
+ }
160
+ @media (max-height: 42rem) {
161
+ .slide {
162
+ padding-block: 1.5rem 4rem;
163
+ }
164
+ .slide__content {
165
+ overflow-y: auto;
166
+ }
142
167
  }
143
168
  @media print {
144
169
  .deck {
@@ -166,6 +191,9 @@
166
191
  .deck-controls {
167
192
  display: none;
168
193
  }
194
+ .deck-progress {
195
+ display: none;
196
+ }
169
197
  }
170
198
  </style>
171
199
  </head>
@@ -183,21 +211,43 @@
183
211
 
184
212
  </button>
185
213
  </nav>
214
+ <div
215
+ class="deck-progress"
216
+ id="deck-progress"
217
+ role="progressbar"
218
+ aria-label="Slide progress"
219
+ aria-valuemin="1"
220
+ >
221
+ <span class="deck-progress__bar"></span>
222
+ </div>
186
223
  <script>
187
224
  (() => {
188
225
  const deck = document.querySelector('.deck');
189
226
  const slides = [...document.querySelectorAll('.slide')];
190
227
  const counter = document.querySelector('#deck-counter');
228
+ const progress = document.querySelector('#deck-progress');
229
+ const progressBar = progress.querySelector('.deck-progress__bar');
230
+ const previous = document.querySelector('[data-direction="-1"]');
231
+ const next = document.querySelector('[data-direction="1"]');
232
+ const reducedMotion = matchMedia(
233
+ '(prefers-reduced-motion: reduce)',
234
+ ).matches;
191
235
  let current = 0;
192
236
 
193
237
  const update = (index) => {
194
238
  current = Math.max(0, Math.min(slides.length - 1, index));
195
239
  counter.textContent = `${current + 1} / ${slides.length}`;
240
+ progress.setAttribute('aria-valuemax', String(slides.length));
241
+ progress.setAttribute('aria-valuenow', String(current + 1));
242
+ progressBar.style.width = `${((current + 1) / slides.length) * 100}%`;
243
+ previous.disabled = current === 0;
244
+ next.disabled = current === slides.length - 1;
245
+ history.replaceState(null, '', `#slide-${current + 1}`);
196
246
  };
197
247
  const go = (index) => {
198
248
  update(index);
199
249
  slides[current]?.scrollIntoView({
200
- behavior: 'smooth',
250
+ behavior: reducedMotion ? 'auto' : 'smooth',
201
251
  block: 'nearest',
202
252
  inline: 'start',
203
253
  });
@@ -226,7 +276,12 @@
226
276
  { root: deck, threshold: 0.6 },
227
277
  );
228
278
  slides.forEach((slide) => observer.observe(slide));
229
- update(0);
279
+ const requested = location.hash.match(/^#slide-(\d+)$/);
280
+ go(requested ? Number(requested[1]) - 1 : 0);
281
+ addEventListener('hashchange', () => {
282
+ const match = location.hash.match(/^#slide-(\d+)$/);
283
+ if (match) go(Number(match[1]) - 1);
284
+ });
230
285
  })();
231
286
  </script>
232
287
  </body>
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: oat-explainer-kit
3
- version: 1.0.0
3
+ version: 1.0.1
4
4
  description: Use when building project explainers or recaps from OAT configuration, state, and lifecycle artifacts.
5
5
  disable-model-invocation: false
6
6
  user-invocable: true
@@ -69,14 +69,20 @@ artifact is required. An explicit supplied fact-base path bypasses artifact
69
69
  federation and is passed through as `factBase.mode: supplied`.
70
70
 
71
71
  Unattended project runs pass `approved-oat-artifacts` provenance to the core's
72
- content-approval seam and never prompt. Federated runs still require an
73
- explicit provider-neutral critic callback. In-process callers may supply
74
- `critic` (or `coreOptions.critic` for compatibility); JSON/CLI callers supply
75
- `criticModulePath` naming a module whose `critic` export implements the same
76
- provider-neutral request/result contract. Supply exactly one critic seam.
77
- Approval provenance does not bypass fact reconciliation. Do not read private
78
- presets, vault files, provider configuration, or ambient destination
79
- configuration.
72
+ content-approval seam and never prompt. Every unattended run also requires
73
+ exactly one provider-neutral author seam: in-process callers supply `author`,
74
+ while JSON/CLI callers supply `authorModulePath` naming a module with an
75
+ `author` function export. The adapter validates and resolves that executable
76
+ input before passing it to `core.runExplainer`; callbacks and module paths never
77
+ enter the persisted run request.
78
+
79
+ Federated runs still require an explicit provider-neutral critic callback.
80
+ In-process callers may supply `critic` (or `coreOptions.critic` for
81
+ compatibility); JSON/CLI callers supply `criticModulePath` naming a module whose
82
+ `critic` export implements the same provider-neutral request/result contract.
83
+ Supply exactly one critic seam. Approval provenance does not bypass fact
84
+ reconciliation. Do not read private presets, vault files, provider
85
+ configuration, or ambient destination configuration.
80
86
 
81
87
  ## Progress Indicators (User-Facing)
82
88
 
@@ -9,8 +9,9 @@ constructing an `explainer-kit.run-request/v1`.
9
9
 
10
10
  | Key | Stored scopes | Built-in default |
11
11
  | -------------------------------------- | ------------------- | ---------------- |
12
- | `explainers.defaults.palette` | local, shared, user | `neutral` |
13
- | `explainers.defaults.visualProfile` | local, shared, user | `clean` |
12
+ | `explainers.defaults.style` | local, shared, user | `clean-neutral` |
13
+ | `explainers.defaults.palette` | local, shared, user | unset |
14
+ | `explainers.defaults.visualProfile` | local, shared, user | unset |
14
15
  | `explainers.defaults.themeBundlePath` | local, shared | unset |
15
16
  | `explainers.publish.provider` | shared | unset |
16
17
  | `explainers.publish.s3Uri` | shared | unset |
@@ -20,7 +21,7 @@ constructing an `explainer-kit.run-request/v1`.
20
21
  | `workflow.explainers.projectExplainer` | local, shared, user | `ask` |
21
22
  | `workflow.explainers.projectRecap` | local, shared, user | `ask` |
22
23
 
23
- Explicit runtime inputs may override these ten keys for one invocation. They
24
+ Explicit runtime inputs may override these eleven keys for one invocation. They
24
25
  do not write config. Recipe, slug, fact-base path, output root, per-run art
25
26
  direction, and private wrapper lanes are invocation inputs rather than config
26
27
  keys and are rejected from the runtime config-override map.
@@ -33,9 +34,13 @@ path follows the same repository confinement; a local absolute path may point
33
34
  outside the repository. Runtime paths follow the local rule. User config cannot
34
35
  set a theme bundle path.
35
36
 
36
- When a theme bundle path is present it replaces named palette and visual-profile
37
- selection and produces a warning. The adapter passes the resolved canonical
38
- path as `theme.suppliedBundlePath`.
37
+ The four curated styles are `clean-neutral`, `business-corporate`,
38
+ `navy-ocean`, and `dark-edgy`. A theme bundle path has highest precedence,
39
+ followed by an explicit style. Legacy palette and visual-profile fields remain
40
+ accepted as an advanced compatibility path and produce deprecation warnings.
41
+ When no source explicitly configures any selection, the adapter leaves the
42
+ request theme empty so the core records its visible `clean-neutral` fallback
43
+ warning. The adapter passes a resolved bundle as `theme.suppliedBundlePath`.
39
44
 
40
45
  ## Publish block
41
46
 
@@ -71,6 +71,19 @@ On a stale-write conflict, the caller must re-read state, resolve precedence
71
71
  again, and decide whether a write is still required. It must not retry the old
72
72
  record blindly.
73
73
 
74
+ ## Unattended author execution
75
+
76
+ Every unattended adapter run must provide exactly one provider-neutral author
77
+ seam. In-process callers pass `author`; JSON-only and official CLI callers put
78
+ `authorModulePath` in the adapter context, naming a module whose `author` export
79
+ is a function. Missing module files, invalid exports, and direct-plus-module
80
+ conflicts fail at the adapter boundary.
81
+
82
+ The resolved callback is passed only as the `author` option to
83
+ `core.runExplainer`. It is never copied into `ExplainerRunRequestV1`,
84
+ `run-request.json`, or another retained data contract. Interactive runs may
85
+ omit an author and retain their existing reviewed-source path.
86
+
74
87
  ## Tracked-run finalization
75
88
 
76
89
  `planTrackedRunFinalization(request, context)` is the shared command planner for
@@ -246,22 +246,18 @@ function commitCommands(paths, subject) {
246
246
  }
247
247
 
248
248
  function immutablePackagePaths(manifest) {
249
- const paths = [
250
- manifest.source?.factBasePath,
251
- 'source/fact-base.md',
252
- ...(manifest.artifacts ?? []).map(({ contentPath }) => contentPath),
253
- manifest.theme?.path,
254
- ...(manifest.artifacts ?? [])
255
- .filter(
256
- ({ status, renderedPath }) =>
257
- status === 'built' && typeof renderedPath === 'string',
258
- )
259
- .map(({ renderedPath }) => renderedPath),
260
- ];
261
- if (paths.some((path) => typeof path !== 'string' || path.length === 0)) {
249
+ if (
250
+ !manifest.immutableHashes ||
251
+ typeof manifest.immutableHashes !== 'object' ||
252
+ Array.isArray(manifest.immutableHashes)
253
+ ) {
254
+ throw new Error('Manifest does not identify immutable package hashes.');
255
+ }
256
+ const paths = Object.keys(manifest.immutableHashes);
257
+ if (paths.length === 0) {
262
258
  throw new Error('Manifest does not identify a complete immutable package.');
263
259
  }
264
- return [...new Set(paths)];
260
+ return paths;
265
261
  }
266
262
 
267
263
  function resolveRunPath(runRoot, path) {
@@ -7,6 +7,7 @@ import { resolveSourceAwarePath } from './resolve-paths.mjs';
7
7
  const execFileAsync = promisify(execFile);
8
8
 
9
9
  export const EXPLAINER_CONFIG_KEYS = Object.freeze([
10
+ 'explainers.defaults.style',
10
11
  'explainers.defaults.palette',
11
12
  'explainers.defaults.visualProfile',
12
13
  'explainers.defaults.themeBundlePath',
@@ -22,6 +23,12 @@ export const EXPLAINER_CONFIG_KEYS = Object.freeze([
22
23
  const CONFIG_KEY_SET = new Set(EXPLAINER_CONFIG_KEYS);
23
24
  const SOURCES = new Set(['local', 'shared', 'user', 'env', 'default']);
24
25
  const PREFERENCES = new Set(['always', 'ask', 'never']);
26
+ const STYLES = new Set([
27
+ 'clean-neutral',
28
+ 'business-corporate',
29
+ 'navy-ocean',
30
+ 'dark-edgy',
31
+ ]);
25
32
 
26
33
  export async function resolveExplainerConfig({
27
34
  repoRoot,
@@ -54,6 +61,22 @@ export async function resolveExplainerConfig({
54
61
  const bundlePath = nullableString(
55
62
  values['explainers.defaults.themeBundlePath'],
56
63
  );
64
+ const style = nullableString(values['explainers.defaults.style']);
65
+ if (style && !STYLES.has(style)) {
66
+ throw new Error(
67
+ 'explainers.defaults.style must name a curated explainer style.',
68
+ );
69
+ }
70
+ const palette = nullableString(values['explainers.defaults.palette']);
71
+ const visualProfile = nullableString(
72
+ values['explainers.defaults.visualProfile'],
73
+ );
74
+ const hasLegacySelection = palette !== null || visualProfile !== null;
75
+ if (hasLegacySelection) {
76
+ warnings.push(
77
+ 'Palette and visual profile configuration is deprecated; use explainers.defaults.style.',
78
+ );
79
+ }
57
80
  if (bundlePath) {
58
81
  theme.suppliedBundlePath = await resolveSourceAwarePath({
59
82
  repoRoot,
@@ -61,19 +84,35 @@ export async function resolveExplainerConfig({
61
84
  source: sources['explainers.defaults.themeBundlePath'],
62
85
  field: 'explainers.defaults.themeBundlePath',
63
86
  });
64
- if (
65
- nullableString(values['explainers.defaults.palette']) ||
66
- nullableString(values['explainers.defaults.visualProfile'])
67
- ) {
87
+ if (style || palette || visualProfile) {
68
88
  warnings.push(
69
- 'explainers.defaults.themeBundlePath overrides configured palette and visual profile.',
89
+ 'explainers.defaults.themeBundlePath overrides configured style, palette, and visual profile.',
70
90
  );
71
91
  }
72
92
  } else {
73
- theme.palette =
74
- nullableString(values['explainers.defaults.palette']) ?? 'neutral';
75
- theme.visualProfile =
76
- nullableString(values['explainers.defaults.visualProfile']) ?? 'clean';
93
+ const explicitStyle =
94
+ style && sources['explainers.defaults.style'] !== 'default'
95
+ ? style
96
+ : null;
97
+ if (explicitStyle) {
98
+ theme.style = explicitStyle;
99
+ }
100
+ if (palette) {
101
+ theme.palette = palette;
102
+ }
103
+ if (visualProfile) {
104
+ theme.visualProfile = visualProfile;
105
+ }
106
+ if (explicitStyle && hasLegacySelection) {
107
+ warnings.push(
108
+ 'explainers.defaults.style wins over deprecated palette and visual profile configuration.',
109
+ );
110
+ }
111
+ if (!explicitStyle && !hasLegacySelection) {
112
+ warnings.push(
113
+ 'No explicit explainer style is configured; the core will default to clean-neutral.',
114
+ );
115
+ }
77
116
  }
78
117
 
79
118
  const publish = resolvePublish(values);
@@ -215,6 +254,11 @@ function normalizeRuntimeValue(key, value) {
215
254
  if (key === 'explainers.publish.provider' && normalized !== 's3-static') {
216
255
  throw new Error(`${key} runtime override must be s3-static.`);
217
256
  }
257
+ if (key === 'explainers.defaults.style' && !STYLES.has(normalized)) {
258
+ throw new Error(
259
+ `${key} runtime override must name a curated explainer style.`,
260
+ );
261
+ }
218
262
  if (key.startsWith('workflow.explainers.') && !PREFERENCES.has(normalized)) {
219
263
  throw new Error(`${key} runtime override must be always, ask, or never.`);
220
264
  }
@@ -32,6 +32,8 @@ export async function runOatExplainer({
32
32
  defaultMode,
33
33
  renderStrategy,
34
34
  retainRawArtDirection = false,
35
+ author,
36
+ authorModulePath,
35
37
  critic,
36
38
  criticModulePath,
37
39
  coreOptions = {},
@@ -108,6 +110,12 @@ export async function runOatExplainer({
108
110
  throw error;
109
111
  }
110
112
 
113
+ const lifecycleAuthor = await resolveLifecycleAuthor({
114
+ author,
115
+ authorModulePath,
116
+ coreOptions,
117
+ mode: request.mode,
118
+ });
111
119
  const lifecycleCritic = await resolveLifecycleCritic({
112
120
  critic,
113
121
  criticModulePath,
@@ -115,6 +123,7 @@ export async function runOatExplainer({
115
123
  });
116
124
  const result = await core.runExplainer(request, {
117
125
  ...coreOptions,
126
+ ...(lifecycleAuthor && { author: lifecycleAuthor }),
118
127
  ...(lifecycleCritic && { critic: lifecycleCritic }),
119
128
  ...(bound.sourceLoader && { sourceLoader: bound.sourceLoader }),
120
129
  reviewedSource: bound.reviewedSource,
@@ -136,6 +145,65 @@ export async function runOatExplainer({
136
145
  };
137
146
  }
138
147
 
148
+ async function resolveLifecycleAuthor({
149
+ author,
150
+ authorModulePath,
151
+ coreOptions,
152
+ mode,
153
+ }) {
154
+ if (coreOptions?.author !== undefined) {
155
+ throw new TypeError(
156
+ 'coreOptions.author is not supported at the OAT adapter boundary; supply author directly.',
157
+ );
158
+ }
159
+ if (author !== undefined && typeof author !== 'function') {
160
+ throw new TypeError('author must be a function when supplied.');
161
+ }
162
+ if (author !== undefined && authorModulePath !== undefined) {
163
+ throw new Error(
164
+ 'Supply only one provider-neutral author callback or author module entry point.',
165
+ );
166
+ }
167
+
168
+ if (author === undefined && authorModulePath === undefined) {
169
+ if (mode === 'unattended') {
170
+ const error = new Error(
171
+ 'Unattended OAT explainer runs require exactly one provider-neutral author callback or author module entry point.',
172
+ );
173
+ error.code = 'E_AUTHOR_REQUIRED';
174
+ throw error;
175
+ }
176
+ return null;
177
+ }
178
+ if (authorModulePath === undefined) {
179
+ return author;
180
+ }
181
+ if (
182
+ typeof authorModulePath !== 'string' ||
183
+ authorModulePath.trim().length === 0
184
+ ) {
185
+ throw new TypeError('authorModulePath must be a non-empty path.');
186
+ }
187
+
188
+ let authorModule;
189
+ try {
190
+ authorModule = await import(
191
+ pathToFileURL(resolve(authorModulePath.trim())).href
192
+ );
193
+ } catch (cause) {
194
+ throw new Error(
195
+ `Unable to load provider-neutral author module at ${authorModulePath}.`,
196
+ { cause },
197
+ );
198
+ }
199
+ if (typeof authorModule.author !== 'function') {
200
+ throw new TypeError(
201
+ 'Provider-neutral author module must export an author function.',
202
+ );
203
+ }
204
+ return authorModule.author;
205
+ }
206
+
139
207
  async function resolveLifecycleCritic({
140
208
  critic,
141
209
  criticModulePath,