@yansirplus/cli 0.5.17 → 0.5.18

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/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 +72 -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 +88 -0
  13. package/agent-catalog/agentOS/references/public-api/core.md +1817 -0
  14. package/agent-catalog/agentOS/references/public-api/runtime.md +794 -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 +1807 -286
  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 +1614 -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 +802 -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 +151 -21
  43. package/package.json +8 -4
  44. package/dist/check/check-coverage.mjs +0 -231
  45. package/dist/generate/generate-agent-docs.mjs +0 -435
  46. package/dist/generate/generate-carrier-reference.mjs +0 -514
  47. package/dist/generate/generate-docs.mjs +0 -345
  48. package/dist/generate/generate-effect-skill-manifests.mjs +0 -193
  49. package/dist/generate/project-docs-site.mjs +0 -190
  50. package/dist/lib/boundary-rules.mjs +0 -63
  51. package/dist/lib/capability-routes.mjs +0 -354
  52. package/dist/lib/projection-sink.mjs +0 -113
@@ -1,3 +1,5 @@
1
+ import { workspaceCatalog } from "../../lib/workspace-manifest.mjs";
2
+
1
3
  export const createDistributionChecks = ({
2
4
  fs,
3
5
  path,
@@ -29,6 +31,8 @@ export const createDistributionChecks = ({
29
31
  /\b(?:node-gyp|prebuild(?:ify|-install)?|cmake-js|node-pre-gyp)\b/u;
30
32
  const distributionNativeFilePattern = /(?:^|\/)(?:binding\.gyp|CMakeLists\.txt)$|\.node$/u;
31
33
  const distributionSourcePattern = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs|d\.ts)$/u;
34
+ const sourceEffectPeerSpecifier = () =>
35
+ workspaceCatalog(repoRoot).effect === undefined ? undefined : "catalog:";
32
36
 
33
37
  const distributionFinding = ({
34
38
  kind,
@@ -620,7 +624,7 @@ export const createDistributionChecks = ({
620
624
  distributionUnitFinding({
621
625
  kind: "package-unit-effect-peer-invariant",
622
626
  unit,
623
- message: `effect peer range must match root catalog single source ${expectedEffectRange}`,
627
+ message: `effect peer range must use catalog single source ${expectedEffectRange}`,
624
628
  specifier: "effect",
625
629
  target: peer.range,
626
630
  }),
@@ -667,7 +671,7 @@ export const createDistributionChecks = ({
667
671
  ? distributionRoots.targetProfiles.map((profile) => profile.id)
668
672
  : [],
669
673
  );
670
- const expectedEffectRange = readJson("package.json").catalog?.effect;
674
+ const expectedEffectRange = sourceEffectPeerSpecifier();
671
675
  return [
672
676
  ...packageUnitsRegistryFindings({
673
677
  registry: packageUnits,
@@ -856,10 +860,7 @@ export const createDistributionChecks = ({
856
860
  edges: graph.edges,
857
861
  });
858
862
  }),
859
- ...distributionEffectPeerFindings(
860
- recordsWithManifests,
861
- readJson("package.json").catalog?.effect,
862
- ),
863
+ ...distributionEffectPeerFindings(recordsWithManifests, sourceEffectPeerSpecifier()),
863
864
  ].sort(
864
865
  (left, right) =>
865
866
  compare(left.severity, right.severity) ||
@@ -890,7 +891,7 @@ export const createDistributionChecks = ({
890
891
  });
891
892
  const effectPeerFindings = distributionEffectPeerFindings(
892
893
  recordsWithManifests,
893
- readJson("package.json").catalog?.effect,
894
+ sourceEffectPeerSpecifier(),
894
895
  );
895
896
  const failures = [
896
897
  ...distributionArchitectureFailures(),
@@ -825,7 +825,8 @@ export const createPackageBoundaryChecks = ({
825
825
  );
826
826
  fs.rmSync(outDir, { recursive: true, force: true });
827
827
  const args = [
828
- "build",
828
+ "exec",
829
+ "esbuild",
829
830
  entryPath,
830
831
  "--outdir",
831
832
  outDir,
@@ -835,7 +836,7 @@ export const createPackageBoundaryChecks = ({
835
836
  if (profile.ambient === "cloudflare-worker") args.push("--external", "cloudflare:*");
836
837
  if (profile.ambient !== "node") args.push("--external", "node:*");
837
838
  try {
838
- execFileSync("bun", args, {
839
+ execFileSync("pnpm", args, {
839
840
  cwd: repoRoot,
840
841
  encoding: "utf8",
841
842
  stdio: ["ignore", "pipe", "pipe"],
@@ -59,6 +59,27 @@ export const createRepoSurfaceChecks = ({
59
59
 
60
60
  const checkPublicApi = () => {
61
61
  const failures = [];
62
+ const runtimeRoot = "packages/runtime/src/index.ts";
63
+ const runtimeRootSource = read(runtimeRoot);
64
+ const runtimeRootAst = ts.createSourceFile(
65
+ path.join(repoRoot, runtimeRoot),
66
+ runtimeRootSource,
67
+ ts.ScriptTarget.Latest,
68
+ true,
69
+ );
70
+ for (const statement of runtimeRootAst.statements) {
71
+ if (
72
+ ts.isExportDeclaration(statement) &&
73
+ (statement.exportClause === undefined || ts.isNamespaceExport(statement.exportClause))
74
+ ) {
75
+ const { line, character } = runtimeRootAst.getLineAndCharacterOfPosition(
76
+ statement.getStart(runtimeRootAst),
77
+ );
78
+ failures.push(
79
+ `${runtimeRoot}:${line + 1}:${character + 1}: runtime root barrel must use explicit named exports; export-star syntax is forbidden`,
80
+ );
81
+ }
82
+ }
62
83
  for (const target of targetPackages()) {
63
84
  if (target.apiSource === undefined) {
64
85
  failures.push(
@@ -78,7 +99,7 @@ export const createRepoSurfaceChecks = ({
78
99
  failures.push(...validateSourceTsdocRecords(target, records));
79
100
  const expected = `${sourceTsdocApiMarkdown(target, records).replace(/\s+$/u, "")}\n`;
80
101
  if (fs.readFileSync(manifest, "utf8") !== expected) {
81
- failures.push(`${target.apiSource} is stale; run bun run docs:generate`);
102
+ failures.push(`${target.apiSource} is stale; run pnpm run docs:generate`);
82
103
  }
83
104
  } else if (mode !== "manual") {
84
105
  failures.push(`${target.name}: unsupported apiSourceMode ${mode}`);
@@ -254,6 +275,38 @@ export const createRepoSurfaceChecks = ({
254
275
  failIfAny("repo tooling surface", failures);
255
276
  };
256
277
 
278
+ const checkEvalResultArtifactBoundary = () => {
279
+ const failures = [];
280
+ if (!read(".gitignore").split(/\r?\n/u).includes(".agentos/eval-results/")) {
281
+ failures.push(".gitignore: missing .agentos/eval-results/ ignore entry");
282
+ }
283
+
284
+ const writerPath = "packages/cli/src/build/build-cli.ts";
285
+ const writer = read(writerPath);
286
+ if (!writer.includes('path.join(cwd, ".agentos", "eval-results")')) {
287
+ failures.push(`${writerPath}: eval runner must own the eval-results output path`);
288
+ }
289
+ if (!writer.includes("writeEvalReportArtifact")) {
290
+ failures.push(`${writerPath}: eval runner must write a derived eval report artifact`);
291
+ }
292
+
293
+ const forbiddenReaders = [
294
+ "packages/cli/src/build/agent-authoring",
295
+ "packages/cli/src/generate",
296
+ "packages/runtime/src",
297
+ "packages/core/src",
298
+ "packages/evals/src",
299
+ "agent-catalog/agentOS",
300
+ ];
301
+ for (const file of forbiddenReaders.flatMap((root) => walk(root))) {
302
+ if (!/\.(?:mjs|js|ts|tsx|md|json)$/u.test(file)) continue;
303
+ if (read(file).includes("eval-results")) {
304
+ failures.push(`${file}: eval-results must remain write-only derived output`);
305
+ }
306
+ }
307
+ failIfAny("eval result artifact boundary", failures);
308
+ };
309
+
257
310
  return {
258
311
  manifestEntries,
259
312
  manifestNames,
@@ -262,6 +315,7 @@ export const createRepoSurfaceChecks = ({
262
315
  ruleConstraints,
263
316
  checkPublicApi,
264
317
  checkEventNamespaces,
318
+ checkEvalResultArtifactBoundary,
265
319
  checkRepoToolingSurface,
266
320
  };
267
321
  };
@@ -1,4 +1,4 @@
1
- export const createStaticTargetChecks = ({ read, failIfAny }) => {
1
+ export const createStaticTargetChecks = ({ read, readJson, failIfAny }) => {
2
2
  const sliceBetweenMarkers = (source, startMarker, endMarker) => {
3
3
  const start = source.indexOf(startMarker);
4
4
  if (start === -1) return "";
@@ -12,6 +12,7 @@ export const createStaticTargetChecks = ({ read, failIfAny }) => {
12
12
  const workspaceAgentSourcePath = "packages/core/src/workspace-agent.ts";
13
13
  const source = read(sourcePath);
14
14
  const workspaceAgentSource = read(workspaceAgentSourcePath);
15
+ const surface = readJson("docs/surface.json");
15
16
  const renderWorkspaceStaticTargetSource = sliceBetweenMarkers(
16
17
  source,
17
18
  "const renderWorkspaceStaticTarget =",
@@ -260,15 +261,17 @@ export const createStaticTargetChecks = ({ read, failIfAny }) => {
260
261
  'import semanticDeclarations from "./manifest.json";',
261
262
  'import deploymentProvenance from "./deployment.json";',
262
263
  "createAgentDurableObject",
263
- "installCloudflareWorkspaceOperationProvider",
264
+ "resolveRuntimeInstallGraph",
265
+ "workspaceOperations",
264
266
  "OpenAiCompatibleLlmTransportLive",
267
+ "preflightOpenAiCompatibleProviderMaterial",
265
268
  "defineWorkspaceAgentMount",
266
- "bindWorkspaceToolsForRuntime",
267
269
  "makeCloudflareWorkspaceEnv",
268
270
  "getSandbox",
269
271
  "generatedCustomTools",
270
272
  "llmTransport: () => OpenAiCompatibleLlmTransportLive",
271
- "extensions: (env) => workspaceOperationInstallFor(env).extensions",
273
+ "generatedCapabilityInstallGraphFor",
274
+ "extensions: (env) => generatedCapabilityInstallGraphFor(env).extensions",
272
275
  "override submit(spec: AgentSubmitSpec): Promise<SubmitResult>",
273
276
  "submitRunInput(input: SubmitRunInput): Promise<SubmitResult>",
274
277
  "readWorkspaceFile(",
@@ -281,16 +284,57 @@ export const createStaticTargetChecks = ({ read, failIfAny }) => {
281
284
  }
282
285
  }
283
286
 
287
+ const requiredSkillSupportMarkers = [
288
+ "GENERATED_LOAD_SKILL_TOOL_NAME",
289
+ "const renderSkillSupport =",
290
+ "const generatedLoadSkillTool = defineProductTool",
291
+ "const generatedSkillsSystemAdvert =",
292
+ "const generatedFrameworkToolsFor =",
293
+ "const renderSubmitSpecFromRunInput =",
294
+ "generatedSystemPrompt(input.system, dynamicCapabilityProjection)",
295
+ ];
296
+ for (const marker of requiredSkillSupportMarkers) {
297
+ if (!source.includes(marker)) {
298
+ failures.push(
299
+ `${sourcePath}: generated-static-target-linking: generated skills support missing ${marker}`,
300
+ );
301
+ }
302
+ }
303
+ const assertProfileSkillProjection = ({ targetSource, profile }) => {
304
+ for (const marker of [
305
+ "const hasSkills = normalized.skills.length > 0;",
306
+ '...(hasSkills ? ["defineProductTool"] : [])',
307
+ 'renderNamedImport(hasSkills ? ["Effect", "Schema"] : ["Effect"], modules.effect)',
308
+ "${renderSkillSupport(normalized.skills)}",
309
+ "${renderSubmitSpecFromRunInput(hasSkills)}",
310
+ "...generatedFrameworkTools",
311
+ ]) {
312
+ if (!targetSource.includes(marker)) {
313
+ failures.push(
314
+ `${sourcePath}: generated-static-target-linking: ${profile} missing generated skills projection marker ${marker}`,
315
+ );
316
+ }
317
+ }
318
+ };
319
+ assertProfileSkillProjection({
320
+ targetSource: renderWorkspaceStaticTargetSource,
321
+ profile: "workspace@1",
322
+ });
323
+ assertProfileSkillProjection({
324
+ targetSource: renderChatStaticTargetSource,
325
+ profile: "chat@1",
326
+ });
327
+
284
328
  const requiredModuleKinds = [
285
329
  '"semantic-json"',
286
330
  '"target-runtime"',
287
331
  '"target-scope-helper"',
288
332
  '"target-worker"',
289
333
  '"target-config"',
334
+ '"capability-runtime"',
290
335
  '"provider-runtime"',
291
336
  '"workspace-host"',
292
337
  '"authored-tool"',
293
- '"workspace-binding"',
294
338
  '"execution-domain-runtime"',
295
339
  '"platform-runtime"',
296
340
  '"client-core"',
@@ -305,6 +349,40 @@ export const createStaticTargetChecks = ({ read, failIfAny }) => {
305
349
  }
306
350
  }
307
351
 
352
+ const packagesBySlug = new Map((surface.packages ?? []).map((pkg) => [pkg.slug, pkg]));
353
+ const generatedImportAudience = new Set(["default-direct", "generated-only"]);
354
+ const generatedPackageSpecifiers = [
355
+ ...source.matchAll(/publicPackageSpecifier\(scope,\s*"([^"]+)"\)/gu),
356
+ ].map((match) => match[1]);
357
+ if (generatedPackageSpecifiers.length === 0) {
358
+ failures.push(
359
+ `${sourcePath}: generated-static-target-linking: generated package specifier map is empty`,
360
+ );
361
+ }
362
+ for (const specifier of generatedPackageSpecifiers) {
363
+ const [slug, ...subpathParts] = specifier.split("/");
364
+ const pkg = packagesBySlug.get(slug);
365
+ const subpath = subpathParts.length === 0 ? "." : `./${subpathParts.join("/")}`;
366
+ if (pkg === undefined) {
367
+ failures.push(
368
+ `${sourcePath}: generated-static-target-linking: ${specifier} has no docs/surface.json package`,
369
+ );
370
+ continue;
371
+ }
372
+ const entrypoint = (pkg.entrypoints ?? []).find((entry) => entry.subpath === subpath);
373
+ if (entrypoint === undefined) {
374
+ failures.push(
375
+ `${sourcePath}: generated-static-target-linking: ${pkg.name}${subpath === "." ? "" : subpath.slice(1)} is missing docs/surface.json entrypoint metadata`,
376
+ );
377
+ continue;
378
+ }
379
+ if (!entrypoint.audiences.some((audience) => generatedImportAudience.has(audience))) {
380
+ failures.push(
381
+ `${sourcePath}: generated-static-target-linking: generated target must not import ${pkg.name}${subpath === "." ? "" : subpath.slice(1)} with audiences ${entrypoint.audiences.join(", ")}`,
382
+ );
383
+ }
384
+ }
385
+
308
386
  const durableObjectConfigSections = [
309
387
  renderWorkspaceStaticTargetSource,
310
388
  renderChatStaticTargetSource,
@@ -16,6 +16,8 @@ import {
16
16
  tsconfigReferenceEdges,
17
17
  workspacePackageRecords as graphWorkspacePackageRecords,
18
18
  } from "./package-graph.mjs";
19
+ import { workspacePackagePatterns } from "../lib/workspace-manifest.mjs";
20
+ import { walkRepoSourceFiles } from "../lib/repo-source-files.mjs";
19
21
  import { collectAgentDocsModel } from "../lib/agent-docs-model.mjs";
20
22
  import { createOwnerChecks } from "./algorithmic/owner-checks.mjs";
21
23
  import { createArchitectureChecks } from "./algorithmic/architecture-checks.mjs";
@@ -47,21 +49,7 @@ const read = (relativePath) => fs.readFileSync(path.join(repoRoot, relativePath)
47
49
  const readJson = (relativePath) => JSON.parse(read(relativePath));
48
50
  const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
49
51
 
50
- const walk = (relativePath, options = {}) => {
51
- const absolutePath = path.join(repoRoot, relativePath);
52
- if (!fs.existsSync(absolutePath)) return [];
53
- const stat = fs.statSync(absolutePath);
54
- if (stat.isFile()) return [relativePath];
55
- const ignored = options.ignored ?? new Set(["node_modules", "dist", ".wrangler", ".turbo"]);
56
- const files = [];
57
- for (const entry of fs.readdirSync(absolutePath, { withFileTypes: true })) {
58
- if (entry.isDirectory() && ignored.has(entry.name)) continue;
59
- const child = path.join(relativePath, entry.name);
60
- if (entry.isDirectory()) files.push(...walk(child, options));
61
- if (entry.isFile()) files.push(child.split(path.sep).join("/"));
62
- }
63
- return files.sort(compare);
64
- };
52
+ const walk = (relativePath, options = {}) => walkRepoSourceFiles(repoRoot, relativePath, options);
65
53
 
66
54
  const failIfAny = (label, failures) => {
67
55
  if (failures.length === 0) {
@@ -94,6 +82,7 @@ const manifestNames = repoSurfaceChecks.manifestNames;
94
82
  const ruleConstraints = repoSurfaceChecks.ruleConstraints;
95
83
  const checkPublicApi = repoSurfaceChecks.checkPublicApi;
96
84
  const checkEventNamespaces = repoSurfaceChecks.checkEventNamespaces;
85
+ const checkEvalResultArtifactBoundary = repoSurfaceChecks.checkEvalResultArtifactBoundary;
97
86
  const checkRepoToolingSurface = repoSurfaceChecks.checkRepoToolingSurface;
98
87
 
99
88
  const clientBoundaryChecks = createClientBoundaryChecks({
@@ -340,7 +329,7 @@ const checkModuleGraphOracle = packageBoundaryChecks.checkModuleGraphOracle;
340
329
  const checkSubpathNoLeak = packageBoundaryChecks.checkSubpathNoLeak;
341
330
  const checkProfileVerification = packageBoundaryChecks.checkProfileVerification;
342
331
 
343
- const staticTargetChecks = createStaticTargetChecks({ read, failIfAny });
332
+ const staticTargetChecks = createStaticTargetChecks({ read, readJson, failIfAny });
344
333
  const checkGeneratedStaticTargetLinking = staticTargetChecks.checkGeneratedStaticTargetLinking;
345
334
 
346
335
  const projectionBoundaryChecks = createProjectionBoundaryChecks({
@@ -379,6 +368,7 @@ const convergenceSmokeChecks = createConvergenceSmokeChecks({
379
368
  checkGeneratedStaticTargetLinking,
380
369
  checkSpikeHygiene,
381
370
  moduleBucketRegistry,
371
+ workspacePackagePatterns: () => workspacePackagePatterns(repoRoot),
382
372
  workspacePackageRecords,
383
373
  consumerFacingSpecifierFailures,
384
374
  packageUnitPublicSpecifiers,
@@ -400,12 +390,14 @@ const checkDocsSiteBuild = convergenceSmokeChecks.checkDocsSiteBuild;
400
390
  const checkCliSurface = convergenceSmokeChecks.checkCliSurface;
401
391
  const checkConsumerImports = convergenceSmokeChecks.checkConsumerImports;
402
392
  const checkDogfoodSmoke = convergenceSmokeChecks.checkDogfoodSmoke;
393
+ const checkBlueprintRecipes = convergenceSmokeChecks.checkBlueprintRecipes;
403
394
 
404
- const checkBoundaryProjection = () => runCommand("vp check", { cwd: repoRoot });
395
+ const checkBoundaryProjection = () => runCommand("vp fmt --check", { cwd: repoRoot });
405
396
 
406
397
  const checkerById = new Map([
407
398
  ["architecture-sources", checkArchitectureSources],
408
399
  ["backend-neutrality", checkBackendNeutrality],
400
+ ["blueprint-recipes", checkBlueprintRecipes],
409
401
  ["boundaries", checkBoundaryProjection],
410
402
  ["cli-surface", checkCliSurface],
411
403
  ["client-boundaries", checkClientBoundaries],
@@ -419,6 +411,7 @@ const checkerById = new Map([
419
411
  ["docs-link-integrity", checkDocsLinkIntegrity],
420
412
  ["docs-site-build", checkDocsSiteBuild],
421
413
  ["event-namespaces", checkEventNamespaces],
414
+ ["eval-result-artifact-boundary", checkEvalResultArtifactBoundary],
422
415
  ["limit-registry", checkLimitRegistry],
423
416
  ["generated-static-target-linking", checkGeneratedStaticTargetLinking],
424
417
  ["gate-tier-governance", checkGateTierGovernance],
@@ -32,9 +32,9 @@ const runStage = (label, command, args) =>
32
32
  export const runDefaultGate = async () => {
33
33
  const startedAt = Date.now();
34
34
  const stages = await Promise.all([
35
- runStage("structural", "bun", ["run", "check:structural"]),
36
- runStage("typecheck", "bun", ["run", "typecheck"]),
37
- runStage("test", "bun", ["run", "test"]),
35
+ runStage("structural", "pnpm", ["run", "check:structural"]),
36
+ runStage("typecheck", "pnpm", ["run", "typecheck"]),
37
+ runStage("test", "pnpm", ["run", "test"]),
38
38
  ]);
39
39
  for (const stage of stages) {
40
40
  console.log(`${stage.label} duration: ${stage.durationMs}ms`);
@@ -0,0 +1,121 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
7
+
8
+ const parseArgs = (rawArgs, defaultRepoRoot) => {
9
+ const args = {
10
+ repo: defaultRepoRoot,
11
+ evidence: path.join(os.tmpdir(), "agentos-effect-scan"),
12
+ scanner: "effect-skill-scan",
13
+ };
14
+ for (let index = 0; index < rawArgs.length; index += 1) {
15
+ const arg = rawArgs[index];
16
+ switch (arg) {
17
+ case "--repo":
18
+ if (rawArgs[index + 1] === undefined) throw new Error("--repo requires a path");
19
+ args.repo = rawArgs[index + 1];
20
+ index += 1;
21
+ break;
22
+ case "--evidence":
23
+ if (rawArgs[index + 1] === undefined) throw new Error("--evidence requires a path");
24
+ args.evidence = rawArgs[index + 1];
25
+ index += 1;
26
+ break;
27
+ case "--scanner":
28
+ if (rawArgs[index + 1] === undefined) throw new Error("--scanner requires a command");
29
+ args.scanner = rawArgs[index + 1];
30
+ index += 1;
31
+ break;
32
+ default:
33
+ throw new Error(`unexpected argument ${arg}`);
34
+ }
35
+ }
36
+ return {
37
+ repo: path.resolve(args.repo),
38
+ evidence: path.resolve(args.evidence),
39
+ scanner: args.scanner,
40
+ };
41
+ };
42
+
43
+ export const validateEffectScanGateJson = (value) => {
44
+ const failures = [];
45
+ if (!isRecord(value)) return ["effect scan gate-json must be an object"];
46
+ if (value.schemaVersion !== 1) failures.push("effect scan gate-json schemaVersion must be 1");
47
+ if (typeof value.ok !== "boolean") failures.push("effect scan gate-json ok must be boolean");
48
+ if (!isRecord(value.tiers)) {
49
+ failures.push("effect scan gate-json tiers object is required");
50
+ return failures;
51
+ }
52
+ if (!Array.isArray(value.tiers.block)) {
53
+ failures.push("effect scan gate-json tiers.block array is required");
54
+ }
55
+ if (!Array.isArray(value.tiers.report)) {
56
+ failures.push("effect scan gate-json tiers.report array is required");
57
+ }
58
+ if (!isRecord(value.tiers.review)) {
59
+ failures.push("effect scan gate-json tiers.review object is required");
60
+ }
61
+ return failures;
62
+ };
63
+
64
+ const effectScanBlockCount = (value) =>
65
+ isRecord(value) && isRecord(value.tiers) && Array.isArray(value.tiers.block)
66
+ ? value.tiers.block.length
67
+ : 0;
68
+
69
+ export const effectScanGateFailures = (value, processExitCode) => {
70
+ const validationFailures = validateEffectScanGateJson(value);
71
+ if (validationFailures.length > 0) return validationFailures;
72
+ const failures = [];
73
+ const blockCount = effectScanBlockCount(value);
74
+ if (value.ok !== true) {
75
+ failures.push("effect scan gate-json ok is false");
76
+ }
77
+ if (blockCount > 0) {
78
+ failures.push(`effect scan gate-json contains ${blockCount} scanner-owned block finding(s)`);
79
+ }
80
+ if (processExitCode !== 0 && value.ok === true && blockCount === 0) {
81
+ failures.push(
82
+ `effect scan process exited ${processExitCode} while gate-json reported no block findings`,
83
+ );
84
+ }
85
+ return failures;
86
+ };
87
+
88
+ const parseGateJson = (stdout) => {
89
+ try {
90
+ return JSON.parse(stdout);
91
+ } catch (error) {
92
+ throw new Error(
93
+ `effect scan did not emit parseable gate-json: ${error instanceof Error ? error.message : String(error)}`,
94
+ );
95
+ }
96
+ };
97
+
98
+ export const runEffectScanGate = (rawArgs, options) => {
99
+ const args = parseArgs(rawArgs, options.defaultRepoRoot);
100
+ fs.rmSync(args.evidence, { recursive: true, force: true });
101
+ fs.mkdirSync(args.evidence, { recursive: true });
102
+ const result = spawnSync(
103
+ args.scanner,
104
+ [args.repo, "--strict", "--output", "gate-json", "--evidence", args.evidence],
105
+ {
106
+ cwd: args.repo,
107
+ encoding: "utf8",
108
+ maxBuffer: 64 * 1024 * 1024,
109
+ },
110
+ );
111
+ if (result.error !== undefined) {
112
+ throw new Error(`effect scan failed to start: ${result.error.message}`);
113
+ }
114
+ const projection = parseGateJson(result.stdout);
115
+ process.stdout.write(`${JSON.stringify(projection, null, 2)}\n`);
116
+ if (result.stderr.length > 0) process.stderr.write(result.stderr);
117
+ const failures = effectScanGateFailures(projection, result.status ?? 1);
118
+ if (failures.length > 0) {
119
+ throw new Error(`agentos check effect-scan: ${failures.join("; ")}`);
120
+ }
121
+ };
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import ts from "typescript";
4
+ import { workspacePackageRecords as workspaceManifestPackageRecords } from "../lib/workspace-manifest.mjs";
4
5
 
5
6
  const compare = (left, right) => left.localeCompare(right);
6
7
  const readJsonFile = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
@@ -23,38 +24,7 @@ export const walkFiles = (repoRoot, relativePath, options = {}) => {
23
24
  return files.sort(compare);
24
25
  };
25
26
 
26
- export const workspacePackageRecords = (repoRoot) => {
27
- const rootPackage = readJsonFile(path.join(repoRoot, "package.json"));
28
- const workspaces = Array.isArray(rootPackage.workspaces)
29
- ? rootPackage.workspaces
30
- : Array.isArray(rootPackage.workspaces?.packages)
31
- ? rootPackage.workspaces.packages
32
- : [];
33
- const records = [];
34
-
35
- for (const workspace of workspaces) {
36
- if (typeof workspace !== "string") continue;
37
- if (workspace.endsWith("/*")) {
38
- const base = workspace.slice(0, -2);
39
- const baseDir = path.join(repoRoot, base);
40
- if (!fs.existsSync(baseDir)) continue;
41
- for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
42
- if (!entry.isDirectory()) continue;
43
- const packagePath = `${base}/${entry.name}`;
44
- const packageJsonPath = path.join(repoRoot, packagePath, "package.json");
45
- if (!fs.existsSync(packageJsonPath)) continue;
46
- records.push({ name: readJsonFile(packageJsonPath).name, path: packagePath });
47
- }
48
- continue;
49
- }
50
-
51
- const packageJsonPath = path.join(repoRoot, workspace, "package.json");
52
- if (!fs.existsSync(packageJsonPath)) continue;
53
- records.push({ name: readJsonFile(packageJsonPath).name, path: workspace });
54
- }
55
-
56
- return records.sort((left, right) => left.path.localeCompare(right.path));
57
- };
27
+ export const workspacePackageRecords = (repoRoot) => workspaceManifestPackageRecords(repoRoot);
58
28
 
59
29
  const scriptKindForFile = (fileName) => {
60
30
  if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;