@yansirplus/cli 0.5.17 → 0.5.19

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 (53) hide show
  1. package/README.md +12 -6
  2. package/agent-catalog/agentOS/SKILL.md +22 -0
  3. package/agent-catalog/agentOS/references/agent/decision-graph.json +530 -0
  4. package/agent-catalog/agentOS/references/agent/errors.json +497 -0
  5. package/agent-catalog/agentOS/references/agent/invariant-matrix.json +337 -0
  6. package/agent-catalog/agentOS/references/agent/primitives.json +989 -0
  7. package/agent-catalog/agentOS/references/agent/recipes.json +109 -0
  8. package/agent-catalog/agentOS/references/agent/start-here.md +25 -0
  9. package/agent-catalog/agentOS/references/package-map.md +73 -0
  10. package/agent-catalog/agentOS/references/provenance.json +251 -0
  11. package/agent-catalog/agentOS/references/public-api/cli.md +20 -0
  12. package/agent-catalog/agentOS/references/public-api/client.md +90 -0
  13. package/agent-catalog/agentOS/references/public-api/core.md +1907 -0
  14. package/agent-catalog/agentOS/references/public-api/runtime.md +843 -0
  15. package/dist/build/agent-authoring/config.d.ts +20 -5
  16. package/dist/build/agent-authoring/config.js +132 -32
  17. package/dist/build/agent-authoring/manifest-compiler.d.ts +131 -2
  18. package/dist/build/agent-authoring/manifest-compiler.js +630 -8
  19. package/dist/build/agent-authoring/shared.d.ts +2 -0
  20. package/dist/build/agent-authoring/shared.js +2 -0
  21. package/dist/build/agent-authoring/static-target.d.ts +6 -3
  22. package/dist/build/agent-authoring/static-target.js +1900 -281
  23. package/dist/build/agent-authoring.d.ts +3 -3
  24. package/dist/build/agent-authoring.js +1 -1
  25. package/dist/build/build-cli.d.ts +1 -1
  26. package/dist/build/build-cli.js +1629 -26
  27. package/dist/check/algorithmic/client-boundary-checks.mjs +3 -34
  28. package/dist/check/algorithmic/convergence-smoke-checks.mjs +652 -6
  29. package/dist/check/algorithmic/distribution-checks.mjs +8 -7
  30. package/dist/check/algorithmic/package-boundary-checks.mjs +3 -2
  31. package/dist/check/algorithmic/repo-surface-checks.mjs +55 -1
  32. package/dist/check/algorithmic/static-target-checks.mjs +83 -5
  33. package/dist/check/algorithmic-checks.mjs +10 -17
  34. package/dist/check/default-gate.mjs +3 -3
  35. package/dist/check/effect-scan-gate.mjs +121 -0
  36. package/dist/check/package-graph.mjs +2 -32
  37. package/dist/consumer-overlay.mjs +1281 -0
  38. package/dist/lib/public-api-model.mjs +19 -0
  39. package/dist/lib/repo-source-files.mjs +26 -0
  40. package/dist/lib/ts-module-loader.mjs +44 -0
  41. package/dist/lib/workspace-manifest.mjs +77 -0
  42. package/dist/main.mjs +171 -21
  43. package/dist/release-status.mjs +515 -0
  44. package/package.json +8 -4
  45. package/dist/check/check-coverage.mjs +0 -231
  46. package/dist/generate/generate-agent-docs.mjs +0 -435
  47. package/dist/generate/generate-carrier-reference.mjs +0 -514
  48. package/dist/generate/generate-docs.mjs +0 -345
  49. package/dist/generate/generate-effect-skill-manifests.mjs +0 -193
  50. package/dist/generate/project-docs-site.mjs +0 -190
  51. package/dist/lib/boundary-rules.mjs +0 -63
  52. package/dist/lib/capability-routes.mjs +0 -354
  53. package/dist/lib/projection-sink.mjs +0 -113
@@ -1,3 +1,606 @@
1
+ import ts from "typescript";
2
+
3
+ const runtimeSurfaceClassValues = [
4
+ "stable-contract",
5
+ "first-party-host-substrate",
6
+ "generated-target-wiring",
7
+ "app-owned-integration-recipe",
8
+ ];
9
+ const runtimeSurfaceClasses = new Set(runtimeSurfaceClassValues);
10
+ const runtimeSubstrateClasses = new Set(["stable-contract", "first-party-host-substrate"]);
11
+ const runtimeFirstPartyHostPrefixes = ["./cloudflare", "./in-memory", "./local", "./node"];
12
+ const runtimeExtensionCategoryTokens = {
13
+ channel: ["channel", "slack", "discord", "teams", "telegram", "email", "webhook", "sms"],
14
+ sandbox: ["sandbox", "e2b", "container", "browser", "vm"],
15
+ database: [
16
+ "database",
17
+ "db",
18
+ "postgres",
19
+ "postgresql",
20
+ "mysql",
21
+ "sqlite",
22
+ "d1",
23
+ "sql",
24
+ "neon",
25
+ "prisma",
26
+ "drizzle",
27
+ ],
28
+ provider: ["provider", "llm", "ai", "openai", "anthropic", "gemini", "mistral", "cohere"],
29
+ observability: [
30
+ "observability",
31
+ "telemetry",
32
+ "otlp",
33
+ "trace",
34
+ "tracing",
35
+ "metrics",
36
+ "sentry",
37
+ "langfuse",
38
+ "log",
39
+ "logging",
40
+ ],
41
+ };
42
+
43
+ const isPlainRecord = (value) =>
44
+ value !== null && typeof value === "object" && !Array.isArray(value);
45
+
46
+ const runtimeSurfaceLabel = (subpath) =>
47
+ `@agent-os/runtime${subpath === "." ? "" : subpath.slice(1)}`;
48
+
49
+ const runtimeExportSubpaths = (runtimePackageJson) =>
50
+ isPlainRecord(runtimePackageJson?.exports)
51
+ ? Object.keys(runtimePackageJson.exports)
52
+ .filter((subpath) => subpath !== "./package.json")
53
+ .sort((left, right) => left.localeCompare(right))
54
+ : [];
55
+
56
+ const publicApiSections = ["Public exports", "Experimental exports", "Deprecated exports"];
57
+
58
+ const markdownSectionBody = (source, heading) => {
59
+ const start = source.indexOf(`## ${heading}`);
60
+ if (start === -1) return "";
61
+ const bodyStart = start + heading.length + 3;
62
+ const rest = source.slice(bodyStart);
63
+ const next = rest.search(/^## /mu);
64
+ return next === -1 ? rest : rest.slice(0, next);
65
+ };
66
+
67
+ const runtimeApiSymbolsForSubpath = (source, subpath) =>
68
+ new Set(
69
+ publicApiSections
70
+ .flatMap((section) => [
71
+ ...markdownSectionBody(source, section).matchAll(/`([^`:]+):([^`]+)`/gu),
72
+ ])
73
+ .filter((match) => match[1] === subpath)
74
+ .map((match) => match[2]),
75
+ );
76
+
77
+ const exportedDeclarationNames = (statement) => {
78
+ if (ts.isExportAssignment(statement)) {
79
+ return [statement.isExportEquals === true ? "export=" : "default"];
80
+ }
81
+ if (
82
+ statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) !== true
83
+ ) {
84
+ return [];
85
+ }
86
+ if (statement.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
87
+ return ["default"];
88
+ }
89
+ if (
90
+ ts.isInterfaceDeclaration(statement) ||
91
+ ts.isTypeAliasDeclaration(statement) ||
92
+ ts.isClassDeclaration(statement) ||
93
+ ts.isFunctionDeclaration(statement) ||
94
+ ts.isEnumDeclaration(statement)
95
+ ) {
96
+ return statement.name === undefined ? [] : [statement.name.text];
97
+ }
98
+ if (ts.isVariableStatement(statement)) {
99
+ return statement.declarationList.declarations
100
+ .map((declaration) => (ts.isIdentifier(declaration.name) ? declaration.name.text : undefined))
101
+ .filter((name) => name !== undefined);
102
+ }
103
+ return [];
104
+ };
105
+
106
+ const explicitPublicBarrelSymbols = ({ source, file }) => {
107
+ const findings = [];
108
+ const names = new Set();
109
+ const sourceFile = ts.createSourceFile(
110
+ file,
111
+ source,
112
+ ts.ScriptTarget.Latest,
113
+ true,
114
+ ts.ScriptKind.TS,
115
+ );
116
+
117
+ for (const statement of sourceFile.statements) {
118
+ if (ts.isExportDeclaration(statement)) {
119
+ if (statement.exportClause === undefined || ts.isNamespaceExport(statement.exportClause)) {
120
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
121
+ statement.getStart(sourceFile),
122
+ );
123
+ findings.push(
124
+ `${file}:${line + 1}:${character + 1}: runtime cloudflare public barrel must use explicit named exports; export-star syntax is forbidden`,
125
+ );
126
+ continue;
127
+ }
128
+
129
+ for (const element of statement.exportClause.elements) {
130
+ names.add(element.name.text);
131
+ }
132
+ continue;
133
+ }
134
+
135
+ for (const name of exportedDeclarationNames(statement)) names.add(name);
136
+ }
137
+
138
+ return { findings, names };
139
+ };
140
+
141
+ const cloudflarePublicBarrelFindings = ({
142
+ runtimeApiMarkdown,
143
+ cloudflarePublicBarrelSource,
144
+ cloudflarePublicBarrelPath,
145
+ }) => {
146
+ if (typeof runtimeApiMarkdown !== "string" || typeof cloudflarePublicBarrelSource !== "string") {
147
+ return [];
148
+ }
149
+
150
+ const docsSymbols = runtimeApiSymbolsForSubpath(runtimeApiMarkdown, "./cloudflare");
151
+ const barrel = explicitPublicBarrelSymbols({
152
+ source: cloudflarePublicBarrelSource,
153
+ file: cloudflarePublicBarrelPath,
154
+ });
155
+ const findings = [...barrel.findings];
156
+
157
+ for (const symbol of [...barrel.names]
158
+ .map(String)
159
+ .sort((left, right) => left.localeCompare(right))) {
160
+ if (!docsSymbols.has(symbol)) {
161
+ findings.push(
162
+ `${cloudflarePublicBarrelPath}: exports ./cloudflare:${symbol}, but docs/api/runtime.md does not declare it`,
163
+ );
164
+ }
165
+ }
166
+ for (const symbol of [...docsSymbols].sort((left, right) => left.localeCompare(right))) {
167
+ if (!barrel.names.has(symbol)) {
168
+ findings.push(
169
+ `docs/api/runtime.md: declares ./cloudflare:${symbol}, but ${cloudflarePublicBarrelPath} does not export it`,
170
+ );
171
+ }
172
+ }
173
+
174
+ return findings;
175
+ };
176
+
177
+ const isFirstPartyRuntimeHostSubpath = (subpath) =>
178
+ runtimeFirstPartyHostPrefixes.some(
179
+ (prefix) => subpath === prefix || subpath.startsWith(`${prefix}/`),
180
+ );
181
+
182
+ const runtimeExtensionCategoriesForSubpath = (subpath) => {
183
+ if (subpath === ".") return [];
184
+ const tokens = new Set(
185
+ subpath
186
+ .toLowerCase()
187
+ .split(/[^a-z0-9]+/u)
188
+ .filter(Boolean),
189
+ );
190
+ return Object.entries(runtimeExtensionCategoryTokens)
191
+ .filter(([, categoryTokens]) => categoryTokens.some((token) => tokens.has(token)))
192
+ .map(([category]) => category);
193
+ };
194
+
195
+ export const runtimePublicSurfaceFindings = ({
196
+ surfacePackage,
197
+ runtimePackageJson,
198
+ runtimeApiMarkdown,
199
+ cloudflarePublicBarrelSource,
200
+ cloudflarePublicBarrelPath = "packages/runtime/src/cloudflare/index.ts",
201
+ }) => {
202
+ const findings = [];
203
+ if (!isPlainRecord(surfacePackage)) {
204
+ return ["docs/surface.json: @agent-os/runtime package is missing"];
205
+ }
206
+ if (!isPlainRecord(runtimePackageJson?.exports)) {
207
+ return ["packages/runtime/package.json: exports must be an object"];
208
+ }
209
+
210
+ const exportSubpaths = runtimeExportSubpaths(runtimePackageJson);
211
+ const exportSubpathSet = new Set(exportSubpaths);
212
+ const entrypoints = Array.isArray(surfacePackage.entrypoints) ? surfacePackage.entrypoints : [];
213
+ const entrypointsBySubpath = new Map();
214
+ for (const entrypoint of entrypoints) {
215
+ if (!isPlainRecord(entrypoint) || typeof entrypoint.subpath !== "string") continue;
216
+ entrypointsBySubpath.set(entrypoint.subpath, entrypoint);
217
+ }
218
+
219
+ for (const subpath of exportSubpaths) {
220
+ if (!entrypointsBySubpath.has(subpath)) {
221
+ findings.push(
222
+ `${runtimeSurfaceLabel(subpath)}: runtime package export is missing docs/surface.json entrypoint`,
223
+ );
224
+ }
225
+ }
226
+
227
+ for (const entrypoint of entrypoints) {
228
+ if (!isPlainRecord(entrypoint)) {
229
+ findings.push("@agent-os/runtime: docs/surface.json runtime entrypoint must be an object");
230
+ continue;
231
+ }
232
+ const subpath = entrypoint.subpath;
233
+ if (typeof subpath !== "string") {
234
+ findings.push("@agent-os/runtime: docs/surface.json runtime entrypoint missing subpath");
235
+ continue;
236
+ }
237
+ const label = runtimeSurfaceLabel(subpath);
238
+ if (!exportSubpathSet.has(subpath)) {
239
+ findings.push(`${label}: docs/surface.json entrypoint has no package.json export`);
240
+ }
241
+
242
+ const surfaceClass = entrypoint.surfaceClass;
243
+ if (typeof surfaceClass !== "string" || !runtimeSurfaceClasses.has(surfaceClass)) {
244
+ findings.push(
245
+ `${label}: runtime surfaceClass must be one of ${runtimeSurfaceClassValues.join(", ")}`,
246
+ );
247
+ continue;
248
+ }
249
+ if (surfaceClass === "app-owned-integration-recipe") {
250
+ findings.push(
251
+ `${label}: runtime public export cannot be classified app-owned-integration-recipe; keep app-owned integrations in blueprint recipes`,
252
+ );
253
+ }
254
+ if (surfaceClass === "first-party-host-substrate" && !isFirstPartyRuntimeHostSubpath(subpath)) {
255
+ findings.push(
256
+ `${label}: first-party-host-substrate is only valid for ${runtimeFirstPartyHostPrefixes.join(", ")} runtime subpaths`,
257
+ );
258
+ }
259
+ if (
260
+ surfaceClass === "generated-target-wiring" &&
261
+ !entrypoint.audiences?.includes("generated-only")
262
+ ) {
263
+ findings.push(`${label}: generated-target-wiring requires generated-only audience`);
264
+ }
265
+
266
+ const extensionCategories = runtimeExtensionCategoriesForSubpath(subpath);
267
+ if (extensionCategories.length > 0 && !runtimeSubstrateClasses.has(surfaceClass)) {
268
+ findings.push(
269
+ `${label}: ${extensionCategories.join("/")} integration-shaped runtime export must be classified as stable substrate, not ${surfaceClass}`,
270
+ );
271
+ }
272
+ }
273
+
274
+ findings.push(
275
+ ...cloudflarePublicBarrelFindings({
276
+ runtimeApiMarkdown,
277
+ cloudflarePublicBarrelSource,
278
+ cloudflarePublicBarrelPath,
279
+ }),
280
+ );
281
+
282
+ return findings.sort((left, right) => left.localeCompare(right));
283
+ };
284
+
285
+ const blueprintRecipeSchemaVersion = 1;
286
+ const blueprintRecipeKinds = [
287
+ "channel",
288
+ "schedule",
289
+ "sandbox",
290
+ "database",
291
+ "provider",
292
+ "observability",
293
+ ];
294
+ const blueprintRecipeKindSet = new Set(blueprintRecipeKinds);
295
+ const blueprintRecipeActions = ["agentos add", "agentos update"];
296
+ const blueprintLifecycleOwnershipKinds = new Set(["provider", "sandbox"]);
297
+ const blueprintLifecycleOwnership = {
298
+ create: "app-or-generated-target",
299
+ reuse: "app-or-generated-target",
300
+ delete: "app-or-generated-target",
301
+ credentials: "app-owned-material",
302
+ network: "app-or-generated-target",
303
+ };
304
+ const blueprintLifecycleOwnershipAxes = Object.keys(blueprintLifecycleOwnership);
305
+ const blueprintRecipePrimaryFiles = new Set([
306
+ "agent/channels/<name>.ts",
307
+ "agent/schedules/<id>.ts",
308
+ "agentos.config.jsonc",
309
+ "agent/agent.json",
310
+ "package.json",
311
+ ]);
312
+ const blueprintRecipeGuidePath = "blueprints/UPGRADE.md";
313
+ const blueprintRecipeIdPattern =
314
+ /^(channel|schedule|sandbox|database|provider|observability)\.[a-z0-9]+(?:-[a-z0-9]+)*$/u;
315
+ const blueprintPrimaryFileMarkerPattern = /<!--\s*agentos:primary-file\s+path="([^"]+)"\s*-->/gu;
316
+ const blueprintUpgradeMarkerPattern = /<!--\s*agentos:blueprint-upgrade\s+id="([^"]+)"\s*-->/gu;
317
+ const blueprintForbiddenTokens = [
318
+ "target--node",
319
+ "createLocalWorkspaceEnv",
320
+ "createLocalAgentRuntime",
321
+ "packages/runtime",
322
+ "@agent-os/runtime/",
323
+ "@yansirplus/runtime/",
324
+ ".dev.vars",
325
+ ];
326
+ const blueprintForbiddenPatterns = [
327
+ {
328
+ label: "source import statements",
329
+ pattern: /(?:^|\n)\s*import\s+[\s\S]*?\s+from\s+["'][^"']+["']/u,
330
+ },
331
+ {
332
+ label: "Cloudflare workspace lifecycle helper wiring",
333
+ pattern:
334
+ /\b(?:create|install)Cloudflare(?:Sandbox)?Workspace(?:EnvResolver|OperationProvider|JobProfile)\b/u,
335
+ },
336
+ ];
337
+ const blueprintChannelBoundary = {
338
+ identity: "agent/channels/<name>.ts",
339
+ inboundRequest: "provider-native-raw-request",
340
+ authority: "verifier-derived-principal",
341
+ outboundSdk: "app-owned",
342
+ deduplication: "app-owned",
343
+ secretHandling: "redacted-before-submit-or-dispatch",
344
+ };
345
+ const blueprintChannelBoundaryAxes = Object.keys(blueprintChannelBoundary);
346
+ const blueprintScheduleBoundary = {
347
+ identity: "agent/schedules/<id>.ts",
348
+ timeAuthority: "provider-scheduled-metadata",
349
+ fireIdentity: "stable-app-principal-schedule-id-utc-minute",
350
+ productIngress: "sessions-or-workflows",
351
+ externalSideEffects: "app-owned",
352
+ historyProjection: "schedule-fire-events-plus-linked-product-projections",
353
+ };
354
+ const blueprintScheduleBoundaryAxes = Object.keys(blueprintScheduleBoundary);
355
+
356
+ const blueprintRecipeLabel = (source) => source.file ?? "blueprint recipe";
357
+
358
+ const parseBlueprintRecipe = (source, findings) => {
359
+ const label = blueprintRecipeLabel(source);
360
+ const match = /^---json\n([\s\S]*?)\n---\n?([\s\S]*)$/u.exec(source.content);
361
+ if (match === null) {
362
+ findings.push(
363
+ `${label}: blueprint recipe must start with JSON frontmatter delimited by ---json and ---`,
364
+ );
365
+ return undefined;
366
+ }
367
+ let frontmatter;
368
+ try {
369
+ frontmatter = JSON.parse(match[1]);
370
+ } catch (error) {
371
+ findings.push(`${label}: JSON frontmatter is invalid: ${error.message}`);
372
+ return undefined;
373
+ }
374
+ if (!isPlainRecord(frontmatter)) {
375
+ findings.push(`${label}: JSON frontmatter must be an object`);
376
+ return undefined;
377
+ }
378
+ return { file: source.file, frontmatter, body: match[2] };
379
+ };
380
+
381
+ const collectMarkerValues = (content, pattern) => {
382
+ const values = [];
383
+ for (const match of content.matchAll(pattern)) values.push(match[1]);
384
+ return values;
385
+ };
386
+
387
+ const requiredBlueprintActionSet = new Set(blueprintRecipeActions);
388
+
389
+ const validateBlueprintLifecycleOwnership = ({ label, kind, frontmatter, body, findings }) => {
390
+ if (!blueprintLifecycleOwnershipKinds.has(kind)) return;
391
+ const ownership = frontmatter.lifecycleOwnership;
392
+ if (!isPlainRecord(ownership)) {
393
+ findings.push(`${label}: ${kind} recipe requires lifecycleOwnership object`);
394
+ return;
395
+ }
396
+
397
+ const actualAxes = Object.keys(ownership).sort((left, right) => left.localeCompare(right));
398
+ const expectedAxes = [...blueprintLifecycleOwnershipAxes].sort((left, right) =>
399
+ left.localeCompare(right),
400
+ );
401
+ if (actualAxes.join(",") !== expectedAxes.join(",")) {
402
+ findings.push(`${label}: lifecycleOwnership axes must be exactly ${expectedAxes.join(", ")}`);
403
+ }
404
+
405
+ for (const axis of blueprintLifecycleOwnershipAxes) {
406
+ const expectedOwner = blueprintLifecycleOwnership[axis];
407
+ if (ownership[axis] !== expectedOwner) {
408
+ findings.push(`${label}: lifecycleOwnership.${axis} must be ${expectedOwner}`);
409
+ }
410
+ }
411
+
412
+ if (!body.includes("## Lifecycle Ownership")) {
413
+ findings.push(`${label}: ${kind} recipe body must contain ## Lifecycle Ownership`);
414
+ }
415
+ };
416
+
417
+ const validateBlueprintChannelBoundary = ({ label, kind, frontmatter, body, findings }) => {
418
+ if (kind !== "channel") return;
419
+ const boundary = frontmatter.channelBoundary;
420
+ if (!isPlainRecord(boundary)) {
421
+ findings.push(`${label}: channel recipe requires channelBoundary object`);
422
+ return;
423
+ }
424
+
425
+ const actualAxes = Object.keys(boundary).sort((left, right) => left.localeCompare(right));
426
+ const expectedAxes = [...blueprintChannelBoundaryAxes].sort((left, right) =>
427
+ left.localeCompare(right),
428
+ );
429
+ if (actualAxes.join(",") !== expectedAxes.join(",")) {
430
+ findings.push(`${label}: channelBoundary axes must be exactly ${expectedAxes.join(", ")}`);
431
+ }
432
+
433
+ for (const axis of blueprintChannelBoundaryAxes) {
434
+ const expectedValue = blueprintChannelBoundary[axis];
435
+ if (boundary[axis] !== expectedValue) {
436
+ findings.push(`${label}: channelBoundary.${axis} must be ${expectedValue}`);
437
+ }
438
+ }
439
+
440
+ if (!body.includes("## Channel Boundary")) {
441
+ findings.push(`${label}: channel recipe body must contain ## Channel Boundary`);
442
+ }
443
+ };
444
+
445
+ const validateBlueprintScheduleBoundary = ({ label, kind, frontmatter, body, findings }) => {
446
+ if (kind !== "schedule") return;
447
+ const boundary = frontmatter.scheduleBoundary;
448
+ if (!isPlainRecord(boundary)) {
449
+ findings.push(`${label}: schedule recipe requires scheduleBoundary object`);
450
+ return;
451
+ }
452
+
453
+ const actualAxes = Object.keys(boundary).sort((left, right) => left.localeCompare(right));
454
+ const expectedAxes = [...blueprintScheduleBoundaryAxes].sort((left, right) =>
455
+ left.localeCompare(right),
456
+ );
457
+ if (actualAxes.join(",") !== expectedAxes.join(",")) {
458
+ findings.push(`${label}: scheduleBoundary axes must be exactly ${expectedAxes.join(", ")}`);
459
+ }
460
+
461
+ for (const axis of blueprintScheduleBoundaryAxes) {
462
+ const expectedValue = blueprintScheduleBoundary[axis];
463
+ if (boundary[axis] !== expectedValue) {
464
+ findings.push(`${label}: scheduleBoundary.${axis} must be ${expectedValue}`);
465
+ }
466
+ }
467
+
468
+ if (!body.includes("## Schedule Boundary")) {
469
+ findings.push(`${label}: schedule recipe body must contain ## Schedule Boundary`);
470
+ }
471
+ };
472
+
473
+ const validateBlueprintRecipe = (recipe, findings) => {
474
+ const { file, frontmatter, body } = recipe;
475
+ const id = frontmatter.id;
476
+ const kind = frontmatter.kind;
477
+ const title = frontmatter.title;
478
+ const summary = frontmatter.summary;
479
+ const primaryFile = frontmatter.primaryFile;
480
+ const appliesTo = frontmatter.appliesTo;
481
+ const upgradeGuide = frontmatter.upgradeGuide;
482
+ const label = file;
483
+
484
+ if (frontmatter.schemaVersion !== blueprintRecipeSchemaVersion) {
485
+ findings.push(`${label}: schemaVersion must be ${blueprintRecipeSchemaVersion}`);
486
+ }
487
+ if (typeof id !== "string" || !blueprintRecipeIdPattern.test(id)) {
488
+ findings.push(`${label}: id must be <kind>.<slug> for ${blueprintRecipeKinds.join(", ")}`);
489
+ }
490
+ if (typeof kind !== "string" || !blueprintRecipeKindSet.has(kind)) {
491
+ findings.push(`${label}: kind must be one of ${blueprintRecipeKinds.join(", ")}`);
492
+ }
493
+ if (typeof id === "string" && typeof kind === "string" && id.split(".")[0] !== kind) {
494
+ findings.push(`${label}: id prefix must match kind`);
495
+ }
496
+ if (typeof title !== "string" || title.trim().length === 0) {
497
+ findings.push(`${label}: title must be a non-empty string`);
498
+ }
499
+ if (typeof summary !== "string" || summary.trim().length === 0) {
500
+ findings.push(`${label}: summary must be a non-empty string`);
501
+ }
502
+ if (typeof primaryFile !== "string" || !blueprintRecipePrimaryFiles.has(primaryFile)) {
503
+ findings.push(
504
+ `${label}: primaryFile must be one of ${[...blueprintRecipePrimaryFiles].join(", ")}`,
505
+ );
506
+ }
507
+ if (
508
+ !Array.isArray(appliesTo) ||
509
+ appliesTo.length !== requiredBlueprintActionSet.size ||
510
+ new Set(appliesTo).size !== requiredBlueprintActionSet.size ||
511
+ appliesTo.some((action) => !requiredBlueprintActionSet.has(action))
512
+ ) {
513
+ findings.push(`${label}: appliesTo must be exactly ${blueprintRecipeActions.join(", ")}`);
514
+ }
515
+ if (upgradeGuide !== blueprintRecipeGuidePath) {
516
+ findings.push(`${label}: upgradeGuide must be ${blueprintRecipeGuidePath}`);
517
+ }
518
+ if (typeof id === "string" && blueprintRecipeIdPattern.test(id)) {
519
+ const [idKind, slug] = id.split(".");
520
+ const expectedPath = `blueprints/recipes/${idKind}/${slug}.md`;
521
+ if (file !== expectedPath) findings.push(`${label}: recipe path must be ${expectedPath}`);
522
+ }
523
+ validateBlueprintLifecycleOwnership({ label, kind, frontmatter, body, findings });
524
+ validateBlueprintChannelBoundary({ label, kind, frontmatter, body, findings });
525
+ validateBlueprintScheduleBoundary({ label, kind, frontmatter, body, findings });
526
+
527
+ if (typeof title === "string" && !body.includes(`# ${title}`)) {
528
+ findings.push(`${label}: body must contain # ${title}`);
529
+ }
530
+ for (const heading of ["## Boundary", "## Steps", "## Upgrade Guide"]) {
531
+ if (!body.includes(heading)) findings.push(`${label}: body must contain ${heading}`);
532
+ }
533
+
534
+ const primaryMarkers = collectMarkerValues(body, blueprintPrimaryFileMarkerPattern);
535
+ if (primaryMarkers.length !== 1) {
536
+ findings.push(`${label}: body must contain exactly one agentos:primary-file marker`);
537
+ } else if (primaryMarkers[0] !== primaryFile) {
538
+ findings.push(`${label}: primary-file marker must match frontmatter.primaryFile`);
539
+ }
540
+
541
+ for (const token of blueprintForbiddenTokens) {
542
+ if (body.includes(token))
543
+ findings.push(`${label}: blueprint recipe must not reference ${token}`);
544
+ }
545
+ for (const { label: patternLabel, pattern } of blueprintForbiddenPatterns) {
546
+ if (pattern.test(body)) {
547
+ findings.push(`${label}: blueprint recipe must not contain ${patternLabel}`);
548
+ }
549
+ }
550
+ };
551
+
552
+ export const blueprintRecipeFindingsForSources = ({ recipeSources, upgradeGuideContent }) => {
553
+ const findings = [];
554
+ if (!Array.isArray(recipeSources) || recipeSources.length === 0) {
555
+ findings.push("blueprints/recipes: at least one blueprint recipe is required");
556
+ return findings;
557
+ }
558
+ if (typeof upgradeGuideContent !== "string") {
559
+ findings.push(`${blueprintRecipeGuidePath}: upgrade guide is missing`);
560
+ return findings;
561
+ }
562
+
563
+ const recipes = [];
564
+ for (const source of recipeSources) {
565
+ if (
566
+ !isPlainRecord(source) ||
567
+ typeof source.file !== "string" ||
568
+ typeof source.content !== "string"
569
+ ) {
570
+ findings.push("blueprint recipe source must include file and content");
571
+ continue;
572
+ }
573
+ const recipe = parseBlueprintRecipe(source, findings);
574
+ if (recipe !== undefined) recipes.push(recipe);
575
+ }
576
+
577
+ /** @type {Set<string>} */
578
+ const recipeIds = new Set();
579
+ for (const recipe of recipes) {
580
+ validateBlueprintRecipe(recipe, findings);
581
+ const id = recipe.frontmatter.id;
582
+ if (typeof id !== "string") continue;
583
+ if (recipeIds.has(id)) findings.push(`${recipe.file}: duplicate blueprint recipe id ${id}`);
584
+ recipeIds.add(id);
585
+ }
586
+
587
+ const guideIds = collectMarkerValues(upgradeGuideContent, blueprintUpgradeMarkerPattern);
588
+ const guideIdCounts = new Map();
589
+ for (const id of guideIds) guideIdCounts.set(id, (guideIdCounts.get(id) ?? 0) + 1);
590
+ for (const [id, count] of guideIdCounts) {
591
+ if (count !== 1) findings.push(`${blueprintRecipeGuidePath}: duplicate upgrade marker ${id}`);
592
+ if (!recipeIds.has(id))
593
+ findings.push(`${blueprintRecipeGuidePath}: unknown upgrade marker ${id}`);
594
+ }
595
+ for (const id of recipeIds) {
596
+ if (!guideIdCounts.has(id)) {
597
+ findings.push(`${blueprintRecipeGuidePath}: missing upgrade marker ${id}`);
598
+ }
599
+ }
600
+
601
+ return findings.sort((left, right) => left.localeCompare(right));
602
+ };
603
+
1
604
  export const createConvergenceSmokeChecks = ({
2
605
  fs,
3
606
  os,
@@ -16,6 +619,7 @@ export const createConvergenceSmokeChecks = ({
16
619
  checkGeneratedStaticTargetLinking,
17
620
  checkSpikeHygiene,
18
621
  moduleBucketRegistry,
622
+ workspacePackagePatterns,
19
623
  workspacePackageRecords,
20
624
  consumerFacingSpecifierFailures,
21
625
  packageUnitPublicSpecifiers,
@@ -34,10 +638,25 @@ export const createConvergenceSmokeChecks = ({
34
638
  const checkConvergenceBoundary = () => {
35
639
  checkClientBoundaries();
36
640
  checkGeneratedStaticTargetLinking();
641
+ checkBlueprintRecipes();
37
642
  checkSpikeHygiene();
38
643
  console.log("convergence boundary passed");
39
644
  };
40
645
 
646
+ const checkBlueprintRecipes = () => {
647
+ const recipeRoot = "blueprints/recipes";
648
+ const recipeFiles = walk(recipeRoot).filter((file) => file.endsWith(".md"));
649
+ const upgradeGuidePath = path.join(repoRoot, blueprintRecipeGuidePath);
650
+ const failures = blueprintRecipeFindingsForSources({
651
+ recipeSources: recipeFiles.map((file) => ({ file, content: read(file) })),
652
+ upgradeGuideContent: fs.existsSync(upgradeGuidePath)
653
+ ? read(blueprintRecipeGuidePath)
654
+ : undefined,
655
+ });
656
+ failIfAny("blueprint recipes", failures);
657
+ console.log(`blueprint recipes covered ${recipeFiles.length} recipe(s)`);
658
+ };
659
+
41
660
  const publicExportNames = (apiSource) =>
42
661
  new Set([
43
662
  ...manifestNames(path.join(repoRoot, apiSource), "Public exports"),
@@ -71,6 +690,14 @@ export const createConvergenceSmokeChecks = ({
71
690
 
72
691
  const surfacePackages = readJson("docs/surface.json").packages ?? [];
73
692
  const surfaceByName = new Map(surfacePackages.map((pkg) => [pkg.name, pkg]));
693
+ failures.push(
694
+ ...runtimePublicSurfaceFindings({
695
+ surfacePackage: surfaceByName.get("@agent-os/runtime"),
696
+ runtimePackageJson: readJson("packages/runtime/package.json"),
697
+ runtimeApiMarkdown: read("docs/api/runtime.md"),
698
+ cloudflarePublicBarrelSource: read("packages/runtime/src/cloudflare/index.ts"),
699
+ }),
700
+ );
74
701
  const retiredPackages = new Set(manifest.retiredPackages ?? []);
75
702
  for (const record of workspacePackageRecords()) {
76
703
  if (retiredPackages.has(record.name)) {
@@ -159,6 +786,7 @@ export const createConvergenceSmokeChecks = ({
159
786
  const checkCliSurface = () => {
160
787
  const failures = [];
161
788
  const rootPackage = readJson("package.json");
789
+ const workspacePatterns = workspacePackagePatterns();
162
790
  const records = workspacePackageRecords();
163
791
  const packageNames = new Set(records.map((record) => record.name));
164
792
  const surfacePackages = readJson("docs/surface.json").packages ?? [];
@@ -171,11 +799,8 @@ export const createConvergenceSmokeChecks = ({
171
799
  "tooling/ops-htmx/package.json",
172
800
  ];
173
801
 
174
- if (
175
- !Array.isArray(rootPackage.workspaces) ||
176
- !rootPackage.workspaces.includes("packages/cli")
177
- ) {
178
- failures.push("package.json: workspaces must include packages/cli");
802
+ if (!workspacePatterns.includes("packages/cli")) {
803
+ failures.push("pnpm-workspace.yaml: packages must include packages/cli");
179
804
  }
180
805
  if (rootPackage.scripts?.agentos !== "node packages/cli/src/main.mjs") {
181
806
  failures.push("package.json: scripts.agentos must execute packages/cli/src/main.mjs");
@@ -581,8 +1206,28 @@ export const createConvergenceSmokeChecks = ({
581
1206
  "const bindings = defineAgentBindings({ handlers: {} });",
582
1207
  "if (!ABORT || !bindings || !LLM_WIRE_DESCRIPTOR_VERSION || !TRACE_CONTEXT_VERSION || !DISPATCH_INBOUND_ACCEPTED) throw new Error('missing core import');",
583
1208
  ].join("\n");
1209
+ const entryPath = path.join(dir, "entry.ts");
1210
+ const outFile = path.join(dir, "entry.mjs");
1211
+ fs.writeFileSync(entryPath, code);
584
1212
  try {
585
- execFileSync("bun", ["--eval", code], {
1213
+ execFileSync(
1214
+ "pnpm",
1215
+ [
1216
+ "exec",
1217
+ "esbuild",
1218
+ entryPath,
1219
+ "--bundle",
1220
+ "--platform=node",
1221
+ "--format=esm",
1222
+ `--outfile=${outFile}`,
1223
+ ],
1224
+ {
1225
+ cwd: repoRoot,
1226
+ encoding: "utf8",
1227
+ stdio: ["ignore", "pipe", "pipe"],
1228
+ },
1229
+ );
1230
+ execFileSync(process.execPath, [outFile], {
586
1231
  cwd: dir,
587
1232
  encoding: "utf8",
588
1233
  stdio: ["ignore", "pipe", "pipe"],
@@ -604,5 +1249,6 @@ export const createConvergenceSmokeChecks = ({
604
1249
  checkCliSurface,
605
1250
  checkConsumerImports,
606
1251
  checkDogfoodSmoke,
1252
+ checkBlueprintRecipes,
607
1253
  };
608
1254
  };