@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.
- package/README.md +12 -6
- package/agent-catalog/agentOS/SKILL.md +22 -0
- package/agent-catalog/agentOS/references/agent/decision-graph.json +530 -0
- package/agent-catalog/agentOS/references/agent/errors.json +497 -0
- package/agent-catalog/agentOS/references/agent/invariant-matrix.json +337 -0
- package/agent-catalog/agentOS/references/agent/primitives.json +989 -0
- package/agent-catalog/agentOS/references/agent/recipes.json +109 -0
- package/agent-catalog/agentOS/references/agent/start-here.md +25 -0
- package/agent-catalog/agentOS/references/package-map.md +73 -0
- package/agent-catalog/agentOS/references/provenance.json +251 -0
- package/agent-catalog/agentOS/references/public-api/cli.md +20 -0
- package/agent-catalog/agentOS/references/public-api/client.md +90 -0
- package/agent-catalog/agentOS/references/public-api/core.md +1907 -0
- package/agent-catalog/agentOS/references/public-api/runtime.md +843 -0
- package/dist/build/agent-authoring/config.d.ts +20 -5
- package/dist/build/agent-authoring/config.js +132 -32
- package/dist/build/agent-authoring/manifest-compiler.d.ts +131 -2
- package/dist/build/agent-authoring/manifest-compiler.js +630 -8
- package/dist/build/agent-authoring/shared.d.ts +2 -0
- package/dist/build/agent-authoring/shared.js +2 -0
- package/dist/build/agent-authoring/static-target.d.ts +6 -3
- package/dist/build/agent-authoring/static-target.js +1900 -281
- package/dist/build/agent-authoring.d.ts +3 -3
- package/dist/build/agent-authoring.js +1 -1
- package/dist/build/build-cli.d.ts +1 -1
- package/dist/build/build-cli.js +1629 -26
- package/dist/check/algorithmic/client-boundary-checks.mjs +3 -34
- package/dist/check/algorithmic/convergence-smoke-checks.mjs +652 -6
- package/dist/check/algorithmic/distribution-checks.mjs +8 -7
- package/dist/check/algorithmic/package-boundary-checks.mjs +3 -2
- package/dist/check/algorithmic/repo-surface-checks.mjs +55 -1
- package/dist/check/algorithmic/static-target-checks.mjs +83 -5
- package/dist/check/algorithmic-checks.mjs +10 -17
- package/dist/check/default-gate.mjs +3 -3
- package/dist/check/effect-scan-gate.mjs +121 -0
- package/dist/check/package-graph.mjs +2 -32
- package/dist/consumer-overlay.mjs +1281 -0
- package/dist/lib/public-api-model.mjs +19 -0
- package/dist/lib/repo-source-files.mjs +26 -0
- package/dist/lib/ts-module-loader.mjs +44 -0
- package/dist/lib/workspace-manifest.mjs +77 -0
- package/dist/main.mjs +171 -21
- package/dist/release-status.mjs +515 -0
- package/package.json +8 -4
- package/dist/check/check-coverage.mjs +0 -231
- package/dist/generate/generate-agent-docs.mjs +0 -435
- package/dist/generate/generate-carrier-reference.mjs +0 -514
- package/dist/generate/generate-docs.mjs +0 -345
- package/dist/generate/generate-effect-skill-manifests.mjs +0 -193
- package/dist/generate/project-docs-site.mjs +0 -190
- package/dist/lib/boundary-rules.mjs +0 -63
- package/dist/lib/capability-routes.mjs +0 -354
- 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
|
-
|
|
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(
|
|
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
|
};
|