@open-agent-toolkit/cli 0.2.25 → 0.2.27

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 (87) hide show
  1. package/assets/NOTICES.md +156 -0
  2. package/assets/docs/cli-utilities/configuration.md +42 -11
  3. package/assets/docs/contributing/explainer-kit-verification.md +125 -0
  4. package/assets/docs/contributing/index.md +1 -0
  5. package/assets/docs/reference/troubleshooting.md +47 -0
  6. package/assets/docs/workflows/projects/artifacts.md +24 -6
  7. package/assets/docs/workflows/projects/dispatch-ceiling.md +67 -16
  8. package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
  9. package/assets/docs/workflows/skills/explainer-kit.md +121 -69
  10. package/assets/docs/workflows/skills/index.md +1 -0
  11. package/assets/public-package-versions.json +4 -4
  12. package/assets/skills/explainer-kit/SKILL.md +18 -3
  13. package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
  14. package/assets/skills/explainer-kit/references/contracts.md +167 -20
  15. package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
  16. package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
  17. package/assets/skills/explainer-kit/references/visual-review.md +57 -0
  18. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
  19. package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
  20. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
  21. package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
  22. package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
  23. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
  24. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
  25. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
  26. package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
  27. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
  28. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
  29. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
  30. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
  31. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
  32. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
  33. package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
  34. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
  35. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
  36. package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
  37. package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
  38. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
  39. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
  40. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
  41. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
  42. package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
  43. package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
  44. package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
  45. package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
  46. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
  47. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
  48. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
  49. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
  50. package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
  51. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  53. package/assets/skills/oat-project-implement/SKILL.md +9 -11
  54. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +18 -9
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  56. package/assets/skills/oat-project-implement/references/phase-execution.md +13 -4
  57. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  58. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  59. package/dist/commands/config/index.d.ts.map +1 -1
  60. package/dist/commands/config/index.js +27 -3
  61. package/dist/commands/project/archive/archive-utils.d.ts +1 -0
  62. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  63. package/dist/commands/project/archive/archive-utils.js +109 -42
  64. package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
  65. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
  66. package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
  67. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
  68. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
  69. package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
  70. package/dist/commands/project/archive/push-runner.d.ts +2 -1
  71. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  72. package/dist/commands/project/archive/push-runner.js +5 -1
  73. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  74. package/dist/commands/project/dispatch-ceiling/index.js +90 -0
  75. package/dist/config/dispatch-notices.d.ts +8 -0
  76. package/dist/config/dispatch-notices.d.ts.map +1 -0
  77. package/dist/config/dispatch-notices.js +79 -0
  78. package/dist/config/dispatch-policy-options.d.ts +2 -0
  79. package/dist/config/dispatch-policy-options.d.ts.map +1 -1
  80. package/dist/config/dispatch-policy-options.js +14 -2
  81. package/dist/providers/identity/dispatch-report.d.ts +17 -0
  82. package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
  83. package/dist/providers/identity/dispatch-report.js +30 -0
  84. package/dist/release/public-package-contract.d.ts +6 -0
  85. package/dist/release/public-package-contract.d.ts.map +1 -1
  86. package/dist/release/public-package-contract.js +75 -0
  87. package/package.json +2 -2
@@ -1,6 +1,8 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
+ import { mkdir } from 'node:fs/promises';
3
4
  import { createServer } from 'node:http';
5
+ import { dirname } from 'node:path';
4
6
 
5
7
  // Locations a headless Chromium may already exist at on a developer or CI
6
8
  // machine. The core never installs a browser; it only uses one that is there.
@@ -23,6 +25,7 @@ const PROBE_REQUEST_FIELDS = new Set([
23
25
  'media',
24
26
  'reducedMotion',
25
27
  'scenario',
28
+ 'screenshotPath',
26
29
  'themeToggle',
27
30
  'viewport',
28
31
  'wideContent',
@@ -34,6 +37,13 @@ const DISABLE_ANIMATIONS_CSS = `*, *::before, *::after {
34
37
  transition-duration: 0s !important;
35
38
  scroll-behavior: auto !important;
36
39
  }`;
40
+ const BROWSER_SESSION_BRANDS = new WeakSet();
41
+ const CAPTURE_SETTINGS = deepFreeze({
42
+ format: 'png',
43
+ fullPage: false,
44
+ reducedMotion: 'reduce',
45
+ animationsDisabled: true,
46
+ });
37
47
 
38
48
  export const RUNTIME_UNAVAILABLE_REASONS = Object.freeze({
39
49
  driverMissing: 'browser-driver-not-installed',
@@ -134,6 +144,13 @@ export async function createBrowserProbeSession(options = {}) {
134
144
  }
135
145
 
136
146
  const browser = await runtime.launch();
147
+ let browserRuntime;
148
+ try {
149
+ browserRuntime = launchedBrowserRuntime(browser);
150
+ } catch (cause) {
151
+ await browser.close();
152
+ throw cause;
153
+ }
137
154
  const pages = new Map();
138
155
  const server = createServer((request, response) => {
139
156
  const html = pages.get(request.url);
@@ -156,9 +173,10 @@ export async function createBrowserProbeSession(options = {}) {
156
173
  throw cause;
157
174
  }
158
175
 
159
- return {
176
+ return brandBrowserProbeSession({
160
177
  available: true,
161
- runtime: { name: runtime.name, version: browser.version() },
178
+ runtime: browserRuntime,
179
+ capture: CAPTURE_SETTINGS,
162
180
  probe: async (probeRequest) => {
163
181
  const route = `/${randomBytes(12).toString('hex')}.html`;
164
182
  pages.set(route, probeRequest.artifact.html);
@@ -175,7 +193,49 @@ export async function createBrowserProbeSession(options = {}) {
175
193
  close: async () => {
176
194
  await Promise.allSettled([browser.close(), closeServer(server)]);
177
195
  },
178
- };
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Deterministic probes are useful for bounded unit and integration tests, but
201
+ * they are never trusted production browser evidence.
202
+ */
203
+ export function createFixtureBrowserProbeSession({ probe } = {}) {
204
+ if (typeof probe !== 'function') {
205
+ throw new TypeError('Fixture browser sessions require a probe callback.');
206
+ }
207
+ return brandBrowserProbeSession({
208
+ available: true,
209
+ runtime: {
210
+ kind: 'fixture',
211
+ name: 'chromium',
212
+ version: 'deterministic-fixture',
213
+ },
214
+ capture: CAPTURE_SETTINGS,
215
+ probe,
216
+ close: async () => {},
217
+ });
218
+ }
219
+
220
+ export function assertBrowserProbeSession(
221
+ session,
222
+ { allowFixture = false } = {},
223
+ ) {
224
+ if (
225
+ !session ||
226
+ typeof session !== 'object' ||
227
+ !BROWSER_SESSION_BRANDS.has(session)
228
+ ) {
229
+ throw new TypeError(
230
+ 'Browser evidence requires a trusted browser session brand created by createBrowserProbeSession().',
231
+ );
232
+ }
233
+ if (session.runtime.kind === 'fixture' && !allowFixture) {
234
+ throw new TypeError(
235
+ 'Deterministic fixture browser sessions are not allowed for production evidence.',
236
+ );
237
+ }
238
+ return session;
179
239
  }
180
240
 
181
241
  export async function probeRenderedPage(browser, url, request) {
@@ -222,6 +282,13 @@ export async function probeRenderedPage(browser, url, request) {
222
282
  const themeToggle = request.themeToggle
223
283
  ? await probeThemeToggle(page, request.themeToggle)
224
284
  : layout.themeToggle;
285
+ if (request.screenshotPath) {
286
+ await mkdir(dirname(request.screenshotPath), { recursive: true });
287
+ await page.screenshot({
288
+ path: request.screenshotPath,
289
+ fullPage: false,
290
+ });
291
+ }
225
292
  return {
226
293
  ...layout,
227
294
  keyboard,
@@ -256,6 +323,13 @@ function assertProbeRequestFields(request) {
256
323
  ) {
257
324
  throw new TypeError('Browser probe injectedCss must be a string.');
258
325
  }
326
+ if (
327
+ request.screenshotPath !== undefined &&
328
+ (typeof request.screenshotPath !== 'string' ||
329
+ request.screenshotPath.length === 0)
330
+ ) {
331
+ throw new TypeError('Browser probe screenshotPath must be a path string.');
332
+ }
259
333
  }
260
334
 
261
335
  function probeStylesheet(request) {
@@ -440,3 +514,73 @@ function listen(server) {
440
514
  function closeServer(server) {
441
515
  return new Promise((resolveClose) => server.close(resolveClose));
442
516
  }
517
+
518
+ function launchedBrowserRuntime(browser) {
519
+ const name = browser?.browserType?.().name?.();
520
+ const version = browser?.version?.();
521
+ if (
522
+ name !== 'chromium' ||
523
+ typeof version !== 'string' ||
524
+ version.trim().length === 0
525
+ ) {
526
+ throw new TypeError(
527
+ 'Launched Chromium did not expose a trusted browser type and version.',
528
+ );
529
+ }
530
+ return deepFreeze({
531
+ kind: 'launched',
532
+ name,
533
+ version: version.trim(),
534
+ });
535
+ }
536
+
537
+ function brandBrowserProbeSession({
538
+ available,
539
+ runtime,
540
+ capture,
541
+ probe,
542
+ close,
543
+ }) {
544
+ const trustedRuntime = deepFreeze(structuredClone(runtime));
545
+ const trustedCapture = deepFreeze(structuredClone(capture));
546
+ const session = Object.freeze({
547
+ available,
548
+ runtime: trustedRuntime,
549
+ capture: trustedCapture,
550
+ captureIdentity: browserCaptureIdentity(trustedRuntime, trustedCapture),
551
+ probe,
552
+ close,
553
+ });
554
+ BROWSER_SESSION_BRANDS.add(session);
555
+ return session;
556
+ }
557
+
558
+ export function browserCaptureIdentity(runtime, capture) {
559
+ return `sha256:${createHash('sha256')
560
+ .update(canonicalStringify({ runtime, capture }))
561
+ .digest('hex')}`;
562
+ }
563
+
564
+ function canonicalStringify(value) {
565
+ return JSON.stringify(canonicalize(value));
566
+ }
567
+
568
+ function canonicalize(value) {
569
+ if (Array.isArray(value)) return value.map(canonicalize);
570
+ if (value && typeof value === 'object') {
571
+ return Object.fromEntries(
572
+ Object.keys(value)
573
+ .sort()
574
+ .map((key) => [key, canonicalize(value[key])]),
575
+ );
576
+ }
577
+ return value;
578
+ }
579
+
580
+ function deepFreeze(value) {
581
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
582
+ return value;
583
+ }
584
+ for (const nested of Object.values(value)) deepFreeze(nested);
585
+ return Object.freeze(value);
586
+ }
@@ -0,0 +1,243 @@
1
+ import { canonicalStringify, validateContract } from './contracts.mjs';
2
+
3
+ const CATALOG_SCHEMA_VERSION = 'explainer-kit.initiative-catalog/v1';
4
+
5
+ export function initiativeCatalogPath(slug) {
6
+ if (typeof slug !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
7
+ throw new TypeError('Initiative catalog requires a safe initiative slug.');
8
+ }
9
+ return `site/initiatives/${slug}/catalog.json`;
10
+ }
11
+
12
+ export function catalogFromManifest(manifest, publicBaseUrl) {
13
+ const validation = validateContract('manifest', manifest);
14
+ if (!validation.valid) {
15
+ throw new TypeError(
16
+ `Initiative catalog requires a valid finalized manifest: ${validation.errors[0].message}`,
17
+ );
18
+ }
19
+ const baseUrl = normalizePublicBaseUrl(publicBaseUrl);
20
+ const artifacts = manifest.artifacts.map((artifact) => {
21
+ if (
22
+ artifact.status !== 'built' ||
23
+ typeof artifact.renderedPath !== 'string' ||
24
+ !artifact.renderedPath.startsWith('site/') ||
25
+ typeof artifact.hash !== 'string'
26
+ ) {
27
+ throw new TypeError(
28
+ `Initiative catalog cannot publish non-built artifact ${artifact.id}.`,
29
+ );
30
+ }
31
+ return {
32
+ id: artifact.id,
33
+ type: artifact.type,
34
+ status: artifact.status,
35
+ renderedPath: artifact.renderedPath,
36
+ ...(artifact.mediaType && { mediaType: artifact.mediaType }),
37
+ hash: artifact.hash,
38
+ url: absoluteArtifactUrl(baseUrl, artifact.renderedPath),
39
+ };
40
+ });
41
+
42
+ return {
43
+ schemaVersion: CATALOG_SCHEMA_VERSION,
44
+ runId: manifest.runId,
45
+ slug: manifest.slug,
46
+ recipe: structuredClone(manifest.recipe),
47
+ createdAt: manifest.createdAt,
48
+ artifacts,
49
+ sourceBacklinks: structuredClone(manifest.source.backlinks ?? []),
50
+ };
51
+ }
52
+
53
+ export function validateInitiativeCatalog(catalog, manifest, publicBaseUrl) {
54
+ const errors = [];
55
+ let normalizedPublicBaseUrl;
56
+ try {
57
+ normalizedPublicBaseUrl = normalizePublicBaseUrl(publicBaseUrl);
58
+ } catch (error) {
59
+ add(
60
+ errors,
61
+ '$.artifacts',
62
+ 'catalog-public-root',
63
+ error instanceof Error
64
+ ? error.message
65
+ : 'Initiative catalog requires an HTTPS public base URL.',
66
+ );
67
+ }
68
+ if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) {
69
+ return {
70
+ valid: false,
71
+ errors: [
72
+ {
73
+ path: '$',
74
+ code: 'catalog-shape',
75
+ message: 'Initiative catalog must be an object.',
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ const expectedRootKeys = new Set([
81
+ 'schemaVersion',
82
+ 'runId',
83
+ 'slug',
84
+ 'recipe',
85
+ 'createdAt',
86
+ 'artifacts',
87
+ 'sourceBacklinks',
88
+ ]);
89
+ if (Object.keys(catalog).some((key) => !expectedRootKeys.has(key))) {
90
+ add(
91
+ errors,
92
+ '$',
93
+ 'catalog-shape',
94
+ 'Initiative catalog contains an unknown property.',
95
+ );
96
+ }
97
+
98
+ for (const [field, expected] of [
99
+ ['schemaVersion', CATALOG_SCHEMA_VERSION],
100
+ ['runId', manifest?.runId],
101
+ ['slug', manifest?.slug],
102
+ ['createdAt', manifest?.createdAt],
103
+ ]) {
104
+ if (catalog[field] !== expected) {
105
+ add(
106
+ errors,
107
+ `$.${field}`,
108
+ 'catalog-manifest-mismatch',
109
+ `Catalog ${field} does not match the finalized manifest.`,
110
+ );
111
+ }
112
+ }
113
+ if (
114
+ canonicalStringify(catalog.recipe) !== canonicalStringify(manifest?.recipe)
115
+ ) {
116
+ add(
117
+ errors,
118
+ '$.recipe',
119
+ 'catalog-manifest-mismatch',
120
+ 'Catalog recipe does not match the finalized manifest.',
121
+ );
122
+ }
123
+
124
+ const catalogArtifacts = Array.isArray(catalog.artifacts)
125
+ ? catalog.artifacts
126
+ : [];
127
+ const manifestArtifacts = Array.isArray(manifest?.artifacts)
128
+ ? manifest.artifacts
129
+ : [];
130
+ if (
131
+ !Array.isArray(catalog.artifacts) ||
132
+ catalogArtifacts.length !== manifestArtifacts.length
133
+ ) {
134
+ add(
135
+ errors,
136
+ '$.artifacts',
137
+ 'catalog-artifact-parity',
138
+ 'Catalog artifacts must exactly cover the finalized manifest artifacts.',
139
+ );
140
+ }
141
+ for (const [index, artifact] of manifestArtifacts.entries()) {
142
+ const entry = catalogArtifacts[index];
143
+ const expected = {
144
+ id: artifact.id,
145
+ type: artifact.type,
146
+ status: artifact.status,
147
+ renderedPath: artifact.renderedPath,
148
+ ...(artifact.mediaType && { mediaType: artifact.mediaType }),
149
+ hash: artifact.hash,
150
+ url:
151
+ normalizedPublicBaseUrl === undefined
152
+ ? undefined
153
+ : absoluteArtifactUrl(normalizedPublicBaseUrl, artifact.renderedPath),
154
+ };
155
+ const actual = entry && typeof entry === 'object' ? entry : undefined;
156
+ if (canonicalStringify(actual) !== canonicalStringify(expected)) {
157
+ add(
158
+ errors,
159
+ `$.artifacts[${index}]`,
160
+ 'catalog-artifact-mismatch',
161
+ `Catalog artifact ${artifact.id} is stale or does not match the finalized manifest.`,
162
+ );
163
+ }
164
+ }
165
+
166
+ if (
167
+ canonicalStringify(catalog.sourceBacklinks) !==
168
+ canonicalStringify(manifest?.source?.backlinks ?? [])
169
+ ) {
170
+ add(
171
+ errors,
172
+ '$.sourceBacklinks',
173
+ 'catalog-source-mismatch',
174
+ 'Catalog source backlinks do not match the finalized manifest.',
175
+ );
176
+ }
177
+ for (const [index, backlink] of (Array.isArray(catalog.sourceBacklinks)
178
+ ? catalog.sourceBacklinks
179
+ : []
180
+ ).entries()) {
181
+ if (
182
+ typeof backlink?.sourceId !== 'string' ||
183
+ !isImmutableGithubUrl(backlink?.url)
184
+ ) {
185
+ add(
186
+ errors,
187
+ `$.sourceBacklinks[${index}]`,
188
+ 'catalog-source-url',
189
+ 'Catalog source backlinks must be immutable absolute GitHub URLs.',
190
+ );
191
+ }
192
+ }
193
+
194
+ return { valid: errors.length === 0, errors };
195
+ }
196
+
197
+ export function serializeInitiativeCatalog(catalog) {
198
+ return `${JSON.stringify(catalog, null, 2)}\n`;
199
+ }
200
+
201
+ function normalizePublicBaseUrl(value) {
202
+ let url;
203
+ try {
204
+ url = new URL(value);
205
+ } catch {
206
+ throw new TypeError(
207
+ 'Initiative catalog requires an HTTPS public base URL.',
208
+ );
209
+ }
210
+ if (
211
+ url.protocol !== 'https:' ||
212
+ url.username ||
213
+ url.password ||
214
+ url.search ||
215
+ url.hash
216
+ ) {
217
+ throw new TypeError(
218
+ 'Initiative catalog requires credential-free HTTPS without query or fragment.',
219
+ );
220
+ }
221
+ return url.href.replace(/\/+$/, '');
222
+ }
223
+
224
+ function absoluteArtifactUrl(baseUrl, renderedPath) {
225
+ return `${baseUrl}/${renderedPath
226
+ .slice('site/'.length)
227
+ .split('/')
228
+ .map((part) => encodeURIComponent(part))
229
+ .join('/')}`;
230
+ }
231
+
232
+ function isImmutableGithubUrl(value) {
233
+ return (
234
+ typeof value === 'string' &&
235
+ /^https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[a-f0-9]{40}\/.+#L[1-9][0-9]*(?:-L[1-9][0-9]*)?$/.test(
236
+ value,
237
+ )
238
+ );
239
+ }
240
+
241
+ function add(errors, path, code, message) {
242
+ errors.push({ path, code, message });
243
+ }