@open-agent-toolkit/cli 0.2.20 → 0.2.21

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 (52) hide show
  1. package/assets/docs/workflows/projects/artifacts.md +1 -1
  2. package/assets/docs/workflows/skills/explainer-kit.md +188 -23
  3. package/assets/docs/workflows/skills/index.md +1 -1
  4. package/assets/public-package-versions.json +4 -4
  5. package/assets/skills/explainer-kit/SKILL.md +87 -25
  6. package/assets/skills/explainer-kit/briefs/deep-dive.md +35 -0
  7. package/assets/skills/explainer-kit/briefs/engineer-tour.md +45 -0
  8. package/assets/skills/explainer-kit/briefs/program-recap.md +41 -0
  9. package/assets/skills/explainer-kit/briefs/project-explainer.md +38 -0
  10. package/assets/skills/explainer-kit/briefs/project-page.md +38 -0
  11. package/assets/skills/explainer-kit/briefs/project-recap.md +47 -0
  12. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +28 -0
  13. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +35 -0
  14. package/assets/skills/explainer-kit/examples/project-recap/content.md +57 -14
  15. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +104 -0
  16. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +26 -4
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +27 -10
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +35 -11
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +27 -10
  20. package/assets/skills/explainer-kit/recipes/project-recap.json +43 -11
  21. package/assets/skills/explainer-kit/references/contracts.md +45 -19
  22. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +41 -0
  23. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +52 -0
  24. package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +442 -0
  25. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +223 -10
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +28 -43
  27. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +237 -0
  28. package/assets/skills/explainer-kit/scripts/lib/html-safety.mjs +680 -0
  29. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +414 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +309 -10
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +313 -41
  32. package/assets/skills/explainer-kit/scripts/lib/records.mjs +61 -0
  33. package/assets/skills/explainer-kit/scripts/lib/render.mjs +166 -12
  34. package/assets/skills/explainer-kit/scripts/render-qa.mjs +147 -2
  35. package/assets/skills/explainer-kit/scripts/run.mjs +796 -272
  36. package/assets/skills/explainer-kit/templates/deck-shell.html +25 -5
  37. package/assets/skills/explainer-kit/templates/diagram-shell.html +29 -7
  38. package/assets/skills/explainer-kit/templates/engineer-tour.html +133 -9
  39. package/assets/skills/explainer-kit/templates/house-style.html +82 -0
  40. package/assets/skills/oat-explainer-kit/SKILL.md +16 -9
  41. package/assets/skills/oat-explainer-kit/references/author-callback.md +51 -0
  42. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +10 -8
  43. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +14 -0
  44. package/assets/skills/oat-explainer-kit/scripts/run.mjs +7 -11
  45. package/assets/skills/oat-project-complete/SKILL.md +18 -2
  46. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  47. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +7 -1
  48. package/assets/skills/oat-wave-execute/SKILL.md +12 -19
  49. package/assets/skills/oat-wave-program/SKILL.md +12 -13
  50. package/package.json +2 -2
  51. package/assets/skills/explainer-kit/schemas/author-request.schema.json +0 -85
  52. package/assets/skills/explainer-kit/schemas/author-result.schema.json +0 -65
@@ -2,6 +2,8 @@ import { readFile } from 'node:fs/promises';
2
2
  import { posix } from 'node:path';
3
3
 
4
4
  import { canonicalHash, validateContract } from './contracts.mjs';
5
+ import { renderDiagram as renderDiagramBlock } from './diagram.mjs';
6
+ import { MarkdownSafetyError, parseMarkdownDocument } from './markdown.mjs';
5
7
 
6
8
  const RENDER_STRATEGIES = new Set(['default-only', 'user-switchable']);
7
9
  const TEMPLATE_BY_TYPE = new Map([
@@ -50,6 +52,7 @@ export async function renderArtifact({
50
52
  const sections = content.sections.map((section) => ({
51
53
  ...section,
52
54
  anchor: section.id,
55
+ ...prepareSectionMarkdown(section.content, theme),
53
56
  }));
54
57
  const links = content.artifactLinks ?? [];
55
58
  const values = templateValues({
@@ -82,6 +85,7 @@ export async function renderArtifact({
82
85
  : undefined,
83
86
  mediaType: 'text/html',
84
87
  html,
88
+ warnings: sections.flatMap((section) => section.warnings),
85
89
  };
86
90
  }
87
91
 
@@ -164,7 +168,7 @@ function templateValues({
164
168
  baseUrl,
165
169
  ),
166
170
  CONTENT: renderSections(sections, { tour: true }),
167
- DIAGRAM: renderDiagram(sections),
171
+ DIAGRAM: renderLegacyDiagram(sections),
168
172
  FOOTER: footer,
169
173
  };
170
174
  case 'deck-shell':
@@ -181,7 +185,7 @@ function templateValues({
181
185
  case 'diagram-shell':
182
186
  return {
183
187
  ...common,
184
- DIAGRAM: renderDiagram(sections),
188
+ DIAGRAM: renderLegacyDiagram(sections),
185
189
  LEGEND: [
186
190
  sections
187
191
  .map(
@@ -213,11 +217,134 @@ function renderSections(sections, { tour = false } = {}) {
213
217
  const tourAttributes = tour
214
218
  ? ` data-active-nodes="node-${index + 1}" data-active-edges=""`
215
219
  : '';
216
- return `<section id="${escapeAttribute(section.anchor)}"${tourAttributes}><div class="section-number">${index + 1}</div><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2><p>${escapeHtml(section.content)}</p></section>`;
220
+ return `<section id="${escapeAttribute(section.anchor)}"${tourAttributes}><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2>${renderMarkdownNodes(section.ast.children)}</section>`;
217
221
  })
218
222
  .join('');
219
223
  }
220
224
 
225
+ function prepareSectionMarkdown(content, theme) {
226
+ try {
227
+ const parsed = parseMarkdownDocument(content);
228
+ const diagramWarnings = [];
229
+ visitMarkdownNodes(parsed.ast.children, (node) => {
230
+ if (node.type !== 'diagram') return;
231
+ const rendered = renderDiagramBlock(node.source, { theme });
232
+ node.renderedHtml = rendered.html;
233
+ diagramWarnings.push(...rendered.warnings);
234
+ });
235
+ return {
236
+ ...parsed,
237
+ warnings: [...parsed.warnings, ...diagramWarnings],
238
+ };
239
+ } catch (error) {
240
+ if (
241
+ !(error instanceof MarkdownSafetyError) ||
242
+ !/raw html|node type: html/i.test(error.message)
243
+ ) {
244
+ throw error;
245
+ }
246
+ return {
247
+ ast: {
248
+ type: 'document',
249
+ children: [
250
+ {
251
+ type: 'paragraph',
252
+ children: [{ type: 'text', value: content }],
253
+ },
254
+ ],
255
+ },
256
+ warnings: [
257
+ {
258
+ code: 'legacy-raw-html-escaped',
259
+ message: 'Legacy section markup was rendered as escaped text.',
260
+ },
261
+ ],
262
+ };
263
+ }
264
+ }
265
+
266
+ function visitMarkdownNodes(nodes, callback) {
267
+ for (const node of nodes) {
268
+ callback(node);
269
+ if (Array.isArray(node.children)) {
270
+ visitMarkdownNodes(node.children, callback);
271
+ }
272
+ }
273
+ }
274
+
275
+ function renderMarkdownNodes(nodes) {
276
+ return nodes.map(renderMarkdownNode).join('');
277
+ }
278
+
279
+ function renderMarkdownNode(node) {
280
+ switch (node.type) {
281
+ case 'text':
282
+ return escapeHtml(node.value);
283
+ case 'paragraph':
284
+ if (node.children.length === 1 && node.children[0].type === 'figure') {
285
+ return renderMarkdownNode(node.children[0]);
286
+ }
287
+ return `<p>${renderMarkdownNodes(node.children)}</p>`;
288
+ case 'heading':
289
+ return `<h${node.depth}>${renderMarkdownNodes(node.children)}</h${node.depth}>`;
290
+ case 'strong':
291
+ return `<strong>${renderMarkdownNodes(node.children)}</strong>`;
292
+ case 'emphasis':
293
+ return `<em>${renderMarkdownNodes(node.children)}</em>`;
294
+ case 'delete':
295
+ return `<del>${renderMarkdownNodes(node.children)}</del>`;
296
+ case 'inlineCode':
297
+ return `<code>${escapeHtml(node.value)}</code>`;
298
+ case 'link':
299
+ return `<a href="${escapeAttribute(node.url)}"${node.title ? ` title="${escapeAttribute(node.title)}"` : ''}>${renderMarkdownNodes(node.children)}</a>`;
300
+ case 'figure':
301
+ return `<figure><a href="${escapeAttribute(node.url)}">${escapeHtml(node.alt || 'View figure')}</a>${node.title || node.alt ? `<figcaption>${escapeHtml(node.title ?? node.alt)}</figcaption>` : ''}</figure>`;
302
+ case 'blockquote':
303
+ return `<blockquote>${renderMarkdownNodes(node.children)}</blockquote>`;
304
+ case 'callout':
305
+ return `<aside class="callout callout--${escapeAttribute(node.kind)}" data-callout="${escapeAttribute(node.kind)}"><div class="callout__label">${escapeHtml(humanize(node.kind))}</div>${renderMarkdownNodes(node.children)}</aside>`;
306
+ case 'timeline':
307
+ return `<ol class="timeline">${node.entries
308
+ .map(
309
+ (entry) =>
310
+ `<li><time>${escapeHtml(entry.date)}</time><span>${escapeHtml(entry.label)}</span></li>`,
311
+ )
312
+ .join('')}</ol>`;
313
+ case 'code':
314
+ return `<pre><code${node.language ? ` class="language-${escapeAttribute(node.language)}"` : ''}>${escapeHtml(node.value)}</code></pre>`;
315
+ case 'diagram':
316
+ return `<div class="diagram-scroll">${node.renderedHtml}</div>`;
317
+ case 'list':
318
+ return renderMarkdownList(node);
319
+ case 'listItem':
320
+ return `<li>${node.checked === null ? '' : `<input type="checkbox" disabled${node.checked ? ' checked' : ''} aria-label="${node.checked ? 'Completed' : 'Incomplete'} task" />`}${renderMarkdownNodes(node.children)}</li>`;
321
+ case 'table':
322
+ return `<div class="table-scroll"><table><thead><tr>${node.header
323
+ .map((cell) => `<th>${renderMarkdownNodes(cell)}</th>`)
324
+ .join('')}</tr></thead><tbody>${node.rows
325
+ .map(
326
+ (row) =>
327
+ `<tr>${row
328
+ .map((cell) => `<td>${renderMarkdownNodes(cell)}</td>`)
329
+ .join('')}</tr>`,
330
+ )
331
+ .join('')}</tbody></table></div>`;
332
+ default:
333
+ throw new MarkdownSafetyError(
334
+ `Unsupported markdown render node: ${String(node.type)}.`,
335
+ );
336
+ }
337
+ }
338
+
339
+ function renderMarkdownList(node) {
340
+ const tag = node.ordered ? 'ol' : 'ul';
341
+ const attributes = [
342
+ node.task ? ' class="task-list"' : '',
343
+ node.ordered && node.start !== 1 ? ` start="${node.start}"` : '',
344
+ ].join('');
345
+ return `<${tag}${attributes}>${renderMarkdownNodes(node.children)}</${tag}>`;
346
+ }
347
+
221
348
  function renderSlides(sections, links, slug, renderedPath, baseUrl) {
222
349
  const slides = sections
223
350
  .map(
@@ -231,11 +358,18 @@ function renderSlides(sections, links, slug, renderedPath, baseUrl) {
231
358
  : slides;
232
359
  }
233
360
 
234
- function renderDiagram(sections) {
361
+ // Nodes must fit the shell's 360x540 viewBox, so the step shrinks as sections
362
+ // are added rather than overflowing the canvas.
363
+ function renderLegacyDiagram(sections) {
364
+ const margin = 16;
365
+ const span = 540 - margin * 2;
366
+ const step = Math.min(96, span / Math.max(sections.length, 1));
367
+ const height = Math.max(24, step - 16);
235
368
  return sections
236
369
  .map((section, index) => {
237
- const y = 80 + index * 120;
238
- return `<g data-node="node-${index + 1}" class="node"><rect x="80" y="${y}" width="360" height="72" rx="8"></rect><text x="104" y="${y + 43}">${escapeHtml(section.title ?? humanize(section.id))}</text></g>`;
370
+ const y = margin + index * step;
371
+ const label = section.title ?? humanize(section.id);
372
+ return `<g data-node="node-${index + 1}" class="node"><rect x="${margin}" y="${y}" width="${360 - margin * 2}" height="${height}" rx="8"></rect><text x="${margin + 14}" y="${y + height / 2 + 5}">${escapeHtml(label)}</text></g>`;
239
373
  })
240
374
  .join('');
241
375
  }
@@ -253,12 +387,18 @@ function renderRelatedLinks(links, slug, renderedPath, baseUrl) {
253
387
  .join(' ');
254
388
  }
255
389
 
256
- function artifactPath(artifact, slug) {
390
+ export function artifactPath(artifact, slug) {
257
391
  const directory = TYPE_DIRECTORIES.get(artifact.type);
258
392
  if (!directory)
259
393
  throw new Error(`Unsupported artifact type: ${artifact.type}.`);
260
394
  const parts = ['site', directory, slug];
261
- if (['diagram', 'deck'].includes(artifact.type)) parts.push(artifact.id);
395
+ const origin = artifact.origin ?? 'floor';
396
+ if (
397
+ origin === 'expansion' ||
398
+ (origin === 'floor' && ['diagram', 'deck'].includes(artifact.type))
399
+ ) {
400
+ parts.push(artifact.id);
401
+ }
262
402
  parts.push('index.html');
263
403
  return parts.join('/');
264
404
  }
@@ -337,11 +477,19 @@ function switchableThemeControl(theme) {
337
477
  }
338
478
 
339
479
  function assertRecipeArtifact(artifact) {
480
+ const hasLegacyKeys =
481
+ isObject(artifact) &&
482
+ hasExactKeys(artifact, ['id', 'type', 'template', 'required']);
483
+ const hasOriginKeys =
484
+ isObject(artifact) &&
485
+ hasExactKeys(artifact, ['id', 'type', 'template', 'required', 'origin']);
340
486
  if (
341
487
  !isObject(artifact) ||
342
- !hasExactKeys(artifact, ['id', 'type', 'template', 'required']) ||
488
+ (!hasLegacyKeys && !hasOriginKeys) ||
343
489
  !SLUG_PATTERN.test(artifact.id ?? '') ||
344
- typeof artifact.required !== 'boolean'
490
+ typeof artifact.required !== 'boolean' ||
491
+ (artifact.origin !== undefined &&
492
+ !['floor', 'expansion'].includes(artifact.origin))
345
493
  ) {
346
494
  throw new TypeError(
347
495
  'Recipe artifact is not a validated artifact descriptor.',
@@ -391,12 +539,18 @@ function assertContent(content, recipeArtifact) {
391
539
  throw new TypeError('Artifact links must be an array.');
392
540
  }
393
541
  for (const link of links) {
542
+ const hasLegacyKeys =
543
+ isObject(link) && hasExactKeys(link, ['id', 'type', 'label']);
544
+ const hasOriginKeys =
545
+ isObject(link) && hasExactKeys(link, ['id', 'type', 'label', 'origin']);
394
546
  if (
395
547
  !isObject(link) ||
396
- !hasExactKeys(link, ['id', 'type', 'label']) ||
548
+ (!hasLegacyKeys && !hasOriginKeys) ||
397
549
  !SLUG_PATTERN.test(link.id ?? '') ||
398
550
  !TYPE_DIRECTORIES.has(link.type) ||
399
- typeof link.label !== 'string'
551
+ typeof link.label !== 'string' ||
552
+ (link.origin !== undefined &&
553
+ !['floor', 'expansion'].includes(link.origin))
400
554
  ) {
401
555
  throw new TypeError(
402
556
  'Artifact cross-links must use typed artifact targets.',
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFile } from 'node:fs/promises';
4
- import { basename } from 'node:path';
4
+ import { createServer } from 'node:http';
5
+ import { basename, posix, resolve, sep } from 'node:path';
5
6
  import { pathToFileURL } from 'node:url';
6
7
 
7
- import { auditArtifactSet } from './lib/qa.mjs';
8
+ import {
9
+ RENDER_QA_WARNING_IDS,
10
+ auditArtifactSet,
11
+ renderQaWarningIds,
12
+ runBrowserProbes,
13
+ } from './lib/qa.mjs';
8
14
  import { renderArtifact } from './lib/render.mjs';
9
15
  import { resolveTheme } from './lib/theme.mjs';
10
16
 
@@ -49,6 +55,62 @@ export const RELEASE_ARTIFACTS = Object.freeze([
49
55
  }),
50
56
  ]);
51
57
 
58
+ export async function runRenderQaStage({
59
+ siteDir,
60
+ artifacts,
61
+ browserProbe,
62
+ widths,
63
+ } = {}) {
64
+ assertStageInput(siteDir, artifacts);
65
+ // Render QA is opt-in: the stage drives a probe the caller supplies and never
66
+ // launches a runtime of its own.
67
+ if (typeof browserProbe !== 'function') {
68
+ return {
69
+ valid: true,
70
+ skipped: true,
71
+ warnings: [RENDER_QA_WARNING_IDS.skippedNoProbe],
72
+ issues: [],
73
+ probes: 0,
74
+ };
75
+ }
76
+
77
+ return probeSiteArtifacts({ siteDir, artifacts, probe: browserProbe, widths });
78
+ }
79
+
80
+ async function probeSiteArtifacts({ siteDir, artifacts, probe, widths }) {
81
+ return withSiteServer(siteDir, async (origin) => {
82
+ const probeArtifacts = await Promise.all(
83
+ artifacts.map(async (artifact) => {
84
+ const relativePath = siteRelativePath(artifact.renderedPath);
85
+ return {
86
+ id: artifact.id,
87
+ type: artifact.type,
88
+ html: await readFile(
89
+ resolve(siteDir, ...relativePath.split('/')),
90
+ 'utf8',
91
+ ),
92
+ url: `${origin}/${relativePath
93
+ .split('/')
94
+ .map(encodeURIComponent)
95
+ .join('/')}`,
96
+ };
97
+ }),
98
+ );
99
+ const browser = await runBrowserProbes({
100
+ artifacts: probeArtifacts,
101
+ probe,
102
+ ...(widths && { widths }),
103
+ });
104
+ return {
105
+ valid: true,
106
+ skipped: false,
107
+ warnings: renderQaWarningIds(browser.issues),
108
+ issues: browser.issues,
109
+ probes: browser.probes,
110
+ };
111
+ });
112
+ }
113
+
52
114
  export function selectReleaseVisualMatrix() {
53
115
  const paletteModeCases = RELEASE_PALETTES.flatMap((palette, paletteIndex) =>
54
116
  RELEASE_MODES.map((mode, modeIndex) => ({
@@ -265,6 +327,89 @@ function releaseContent(artifactId) {
265
327
  };
266
328
  }
267
329
 
330
+ function assertStageInput(siteDir, artifacts) {
331
+ if (typeof siteDir !== 'string' || siteDir.length === 0) {
332
+ throw new TypeError('Render QA stage requires a built site directory.');
333
+ }
334
+ if (
335
+ !Array.isArray(artifacts) ||
336
+ artifacts.length === 0 ||
337
+ artifacts.some(
338
+ (artifact) =>
339
+ typeof artifact?.id !== 'string' ||
340
+ typeof artifact?.type !== 'string' ||
341
+ typeof artifact?.renderedPath !== 'string',
342
+ )
343
+ ) {
344
+ throw new TypeError(
345
+ 'Render QA stage requires artifacts with id, type, and renderedPath.',
346
+ );
347
+ }
348
+ for (const artifact of artifacts) siteRelativePath(artifact.renderedPath);
349
+ }
350
+
351
+ function siteRelativePath(renderedPath) {
352
+ const withoutPrefix = renderedPath.startsWith('site/')
353
+ ? renderedPath.slice('site/'.length)
354
+ : renderedPath;
355
+ const normalized = posix.normalize(withoutPrefix);
356
+ if (
357
+ normalized === '.' ||
358
+ normalized.startsWith('../') ||
359
+ posix.isAbsolute(normalized)
360
+ ) {
361
+ throw new TypeError(`Unsafe render QA artifact path: ${renderedPath}`);
362
+ }
363
+ return normalized;
364
+ }
365
+
366
+ async function withSiteServer(siteDir, callback) {
367
+ const root = resolve(siteDir);
368
+ const server = createServer(async (request, response) => {
369
+ try {
370
+ const pathname = decodeURIComponent(
371
+ new URL(request.url ?? '/', 'http://127.0.0.1').pathname,
372
+ );
373
+ const candidate = resolve(root, `.${pathname}`);
374
+ if (candidate !== root && !candidate.startsWith(`${root}${sep}`)) {
375
+ response.writeHead(403).end();
376
+ return;
377
+ }
378
+ const body = await readFile(candidate);
379
+ response.writeHead(200, {
380
+ 'content-type': candidate.endsWith('.html')
381
+ ? 'text/html; charset=utf-8'
382
+ : 'application/octet-stream',
383
+ 'cache-control': 'no-store',
384
+ });
385
+ response.end(body);
386
+ } catch {
387
+ response.writeHead(404).end();
388
+ }
389
+ });
390
+ await new Promise((resolveListen, rejectListen) => {
391
+ server.once('error', rejectListen);
392
+ server.listen(0, '127.0.0.1', resolveListen);
393
+ });
394
+ const address = server.address();
395
+ if (!address || typeof address === 'string') {
396
+ await closeServer(server);
397
+ throw new Error('Render QA server did not expose a TCP address.');
398
+ }
399
+
400
+ try {
401
+ return await callback(`http://127.0.0.1:${address.port}`);
402
+ } finally {
403
+ await closeServer(server);
404
+ }
405
+ }
406
+
407
+ function closeServer(server) {
408
+ return new Promise((resolveClose, rejectClose) => {
409
+ server.close((error) => (error ? rejectClose(error) : resolveClose()));
410
+ });
411
+ }
412
+
268
413
  if (
269
414
  process.argv[1] &&
270
415
  pathToFileURL(process.argv[1]).href === import.meta.url