@vibe-agent-toolkit/cli 0.2.0-rc.6 → 0.2.0-rc.8
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 +4 -5
- package/dist/bin/vat +34 -19
- package/dist/bin/vat.js +34 -19
- package/dist/bin/vat.js.map +1 -1
- package/dist/bin.js +44 -158
- package/dist/bin.js.map +1 -1
- package/dist/command-loaders.d.ts +2 -2
- package/dist/command-loaders.d.ts.map +1 -1
- package/dist/command-loaders.js +4 -2
- package/dist/command-loaders.js.map +1 -1
- package/dist/commands/agent/build.d.ts.map +1 -1
- package/dist/commands/agent/build.js +2 -1
- package/dist/commands/agent/build.js.map +1 -1
- package/dist/commands/agent/import.d.ts.map +1 -1
- package/dist/commands/agent/import.js +3 -2
- package/dist/commands/agent/import.js.map +1 -1
- package/dist/commands/agent/install-path.d.ts +36 -0
- package/dist/commands/agent/install-path.d.ts.map +1 -0
- package/dist/commands/agent/install-path.js +44 -0
- package/dist/commands/agent/install-path.js.map +1 -0
- package/dist/commands/agent/install.d.ts.map +1 -1
- package/dist/commands/agent/install.js +22 -10
- package/dist/commands/agent/install.js.map +1 -1
- package/dist/commands/agent/installed.d.ts.map +1 -1
- package/dist/commands/agent/installed.js +22 -12
- package/dist/commands/agent/installed.js.map +1 -1
- package/dist/commands/agent/list.d.ts.map +1 -1
- package/dist/commands/agent/list.js +2 -1
- package/dist/commands/agent/list.js.map +1 -1
- package/dist/commands/agent/run.d.ts.map +1 -1
- package/dist/commands/agent/run.js +2 -1
- package/dist/commands/agent/run.js.map +1 -1
- package/dist/commands/agent/uninstall.d.ts.map +1 -1
- package/dist/commands/agent/uninstall.js +13 -6
- package/dist/commands/agent/uninstall.js.map +1 -1
- package/dist/commands/agent/validate.d.ts.map +1 -1
- package/dist/commands/agent/validate.js +3 -2
- package/dist/commands/agent/validate.js.map +1 -1
- package/dist/commands/ard/emit.d.ts +153 -0
- package/dist/commands/ard/emit.d.ts.map +1 -0
- package/dist/commands/ard/emit.js +276 -0
- package/dist/commands/ard/emit.js.map +1 -0
- package/dist/commands/ard/index.d.ts +12 -0
- package/dist/commands/ard/index.d.ts.map +1 -0
- package/dist/commands/ard/index.js +80 -0
- package/dist/commands/ard/index.js.map +1 -0
- package/dist/commands/ard/surfaces.d.ts +56 -0
- package/dist/commands/ard/surfaces.d.ts.map +1 -0
- package/dist/commands/ard/surfaces.js +150 -0
- package/dist/commands/ard/surfaces.js.map +1 -0
- package/dist/commands/audit/distributed-tree.d.ts.map +1 -1
- package/dist/commands/audit/distributed-tree.js +9 -6
- package/dist/commands/audit/distributed-tree.js.map +1 -1
- package/dist/commands/audit/scan-population.d.ts +158 -0
- package/dist/commands/audit/scan-population.d.ts.map +1 -0
- package/dist/commands/audit/scan-population.js +292 -0
- package/dist/commands/audit/scan-population.js.map +1 -0
- package/dist/commands/audit-settings.d.ts.map +1 -1
- package/dist/commands/audit-settings.js +6 -5
- package/dist/commands/audit-settings.js.map +1 -1
- package/dist/commands/audit.d.ts +78 -9
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +853 -473
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +24 -24
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/cache/clear.d.ts.map +1 -1
- package/dist/commands/cache/clear.js +17 -10
- package/dist/commands/cache/clear.js.map +1 -1
- package/dist/commands/claude/budget.d.ts +136 -24
- package/dist/commands/claude/budget.d.ts.map +1 -1
- package/dist/commands/claude/budget.js +152 -57
- package/dist/commands/claude/budget.js.map +1 -1
- package/dist/commands/claude/context.d.ts.map +1 -1
- package/dist/commands/claude/context.js +4 -3
- package/dist/commands/claude/context.js.map +1 -1
- package/dist/commands/claude/marketplace/changelog-utils.d.ts +1 -1
- package/dist/commands/claude/marketplace/changelog-utils.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/changelog-utils.js +1 -2
- package/dist/commands/claude/marketplace/changelog-utils.js.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.js +3 -2
- package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
- package/dist/commands/claude/marketplace/license-utils.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/license-utils.js +0 -1
- package/dist/commands/claude/marketplace/license-utils.js.map +1 -1
- package/dist/commands/claude/marketplace/publish-tree.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/publish-tree.js +0 -6
- package/dist/commands/claude/marketplace/publish-tree.js.map +1 -1
- package/dist/commands/claude/marketplace/publish.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/publish.js +2 -1
- package/dist/commands/claude/marketplace/publish.js.map +1 -1
- package/dist/commands/claude/marketplace/validate.d.ts +55 -22
- package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/validate.js +392 -77
- package/dist/commands/claude/marketplace/validate.js.map +1 -1
- package/dist/commands/claude/org/api-keys.d.ts +0 -3
- package/dist/commands/claude/org/api-keys.d.ts.map +1 -1
- package/dist/commands/claude/org/api-keys.js +2 -1
- package/dist/commands/claude/org/api-keys.js.map +1 -1
- package/dist/commands/claude/org/helpers.d.ts +17 -22
- package/dist/commands/claude/org/helpers.d.ts.map +1 -1
- package/dist/commands/claude/org/helpers.js +6 -22
- package/dist/commands/claude/org/helpers.js.map +1 -1
- package/dist/commands/claude/org/invites.d.ts +0 -3
- package/dist/commands/claude/org/invites.d.ts.map +1 -1
- package/dist/commands/claude/org/invites.js +3 -2
- package/dist/commands/claude/org/invites.js.map +1 -1
- package/dist/commands/claude/org/skills.d.ts +103 -4
- package/dist/commands/claude/org/skills.d.ts.map +1 -1
- package/dist/commands/claude/org/skills.js +331 -123
- package/dist/commands/claude/org/skills.js.map +1 -1
- package/dist/commands/claude/org/users.d.ts +0 -3
- package/dist/commands/claude/org/users.d.ts.map +1 -1
- package/dist/commands/claude/org/users.js +3 -2
- package/dist/commands/claude/org/users.js.map +1 -1
- package/dist/commands/claude/org/workspaces.d.ts +0 -3
- package/dist/commands/claude/org/workspaces.d.ts.map +1 -1
- package/dist/commands/claude/org/workspaces.js +6 -5
- package/dist/commands/claude/org/workspaces.js.map +1 -1
- package/dist/commands/claude/plugin/build.d.ts +8 -0
- package/dist/commands/claude/plugin/build.d.ts.map +1 -1
- package/dist/commands/claude/plugin/build.js +28 -33
- package/dist/commands/claude/plugin/build.js.map +1 -1
- package/dist/commands/claude/plugin/helpers.d.ts.map +1 -1
- package/dist/commands/claude/plugin/helpers.js +0 -6
- package/dist/commands/claude/plugin/helpers.js.map +1 -1
- package/dist/commands/claude/plugin/install.d.ts +12 -0
- package/dist/commands/claude/plugin/install.d.ts.map +1 -1
- package/dist/commands/claude/plugin/install.js +71 -60
- package/dist/commands/claude/plugin/install.js.map +1 -1
- package/dist/commands/claude/plugin/list.d.ts.map +1 -1
- package/dist/commands/claude/plugin/list.js +3 -2
- package/dist/commands/claude/plugin/list.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-changelog.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-changelog.js +0 -2
- package/dist/commands/claude/plugin/plugin-changelog.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-files.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-files.js +0 -3
- package/dist/commands/claude/plugin/plugin-files.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-validators.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-validators.js +3 -6
- package/dist/commands/claude/plugin/plugin-validators.js.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.d.ts +73 -0
- package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.js +221 -15
- package/dist/commands/claude/plugin/tree-copy.js.map +1 -1
- package/dist/commands/claude/plugin/uninstall.d.ts.map +1 -1
- package/dist/commands/claude/plugin/uninstall.js +4 -4
- package/dist/commands/claude/plugin/uninstall.js.map +1 -1
- package/dist/commands/consistency-check.d.ts +9 -13
- package/dist/commands/consistency-check.d.ts.map +1 -1
- package/dist/commands/consistency-check.js +91 -81
- package/dist/commands/consistency-check.js.map +1 -1
- package/dist/commands/corpus/index.d.ts.map +1 -1
- package/dist/commands/corpus/index.js +3 -0
- package/dist/commands/corpus/index.js.map +1 -1
- package/dist/commands/corpus/report.d.ts +0 -1
- package/dist/commands/corpus/report.d.ts.map +1 -1
- package/dist/commands/corpus/report.js +1 -3
- package/dist/commands/corpus/report.js.map +1 -1
- package/dist/commands/corpus/runner.d.ts +58 -1
- package/dist/commands/corpus/runner.d.ts.map +1 -1
- package/dist/commands/corpus/runner.js +92 -21
- package/dist/commands/corpus/runner.js.map +1 -1
- package/dist/commands/corpus/scan.d.ts.map +1 -1
- package/dist/commands/corpus/scan.js +2 -4
- package/dist/commands/corpus/scan.js.map +1 -1
- package/dist/commands/corpus/seed.d.ts +4 -4
- package/dist/commands/corpus/seed.d.ts.map +1 -1
- package/dist/commands/corpus/seed.js +1 -3
- package/dist/commands/corpus/seed.js.map +1 -1
- package/dist/commands/doctor.d.ts +14 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +147 -51
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/inventory.d.ts.map +1 -1
- package/dist/commands/inventory.js +6 -7
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/mcp/list-collections.d.ts.map +1 -1
- package/dist/commands/mcp/list-collections.js +2 -1
- package/dist/commands/mcp/list-collections.js.map +1 -1
- package/dist/commands/mcp/serve.d.ts.map +1 -1
- package/dist/commands/mcp/serve.js +3 -2
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/okf/index.d.ts +11 -0
- package/dist/commands/okf/index.d.ts.map +1 -0
- package/dist/commands/okf/index.js +114 -0
- package/dist/commands/okf/index.js.map +1 -0
- package/dist/commands/okf/validate.d.ts +191 -0
- package/dist/commands/okf/validate.d.ts.map +1 -0
- package/dist/commands/okf/validate.js +190 -0
- package/dist/commands/okf/validate.js.map +1 -0
- package/dist/commands/phase-utils.d.ts +30 -13
- package/dist/commands/phase-utils.d.ts.map +1 -1
- package/dist/commands/phase-utils.js +56 -19
- package/dist/commands/phase-utils.js.map +1 -1
- package/dist/commands/positional-args.d.ts.map +1 -1
- package/dist/commands/positional-args.js +2 -1
- package/dist/commands/positional-args.js.map +1 -1
- package/dist/commands/rag/clear-command.d.ts.map +1 -1
- package/dist/commands/rag/clear-command.js +2 -1
- package/dist/commands/rag/clear-command.js.map +1 -1
- package/dist/commands/rag/index-command.d.ts +27 -2
- package/dist/commands/rag/index-command.d.ts.map +1 -1
- package/dist/commands/rag/index-command.js +48 -8
- package/dist/commands/rag/index-command.js.map +1 -1
- package/dist/commands/rag/index.d.ts.map +1 -1
- package/dist/commands/rag/index.js +2 -0
- package/dist/commands/rag/index.js.map +1 -1
- package/dist/commands/rag/query-command.d.ts.map +1 -1
- package/dist/commands/rag/query-command.js +2 -1
- package/dist/commands/rag/query-command.js.map +1 -1
- package/dist/commands/rag/stats-command.d.ts.map +1 -1
- package/dist/commands/rag/stats-command.js +2 -1
- package/dist/commands/rag/stats-command.js.map +1 -1
- package/dist/commands/resources/check-progress.d.ts +3 -0
- package/dist/commands/resources/check-progress.d.ts.map +1 -1
- package/dist/commands/resources/check-progress.js +11 -2
- package/dist/commands/resources/check-progress.js.map +1 -1
- package/dist/commands/resources/check-supervisor.d.ts.map +1 -1
- package/dist/commands/resources/check-supervisor.js +14 -7
- package/dist/commands/resources/check-supervisor.js.map +1 -1
- package/dist/commands/resources/check.d.ts +168 -8
- package/dist/commands/resources/check.d.ts.map +1 -1
- package/dist/commands/resources/check.js +222 -118
- package/dist/commands/resources/check.js.map +1 -1
- package/dist/commands/resources/index.d.ts.map +1 -1
- package/dist/commands/resources/index.js +64 -29
- package/dist/commands/resources/index.js.map +1 -1
- package/dist/commands/resources/query.d.ts +18 -1
- package/dist/commands/resources/query.d.ts.map +1 -1
- package/dist/commands/resources/query.js +33 -12
- package/dist/commands/resources/query.js.map +1 -1
- package/dist/commands/resources/scan.d.ts.map +1 -1
- package/dist/commands/resources/scan.js +4 -8
- package/dist/commands/resources/scan.js.map +1 -1
- package/dist/commands/resources/validate.d.ts +30 -1
- package/dist/commands/resources/validate.d.ts.map +1 -1
- package/dist/commands/resources/validate.js +116 -14
- package/dist/commands/resources/validate.js.map +1 -1
- package/dist/commands/skill/review-checklist.d.ts +3 -2
- package/dist/commands/skill/review-checklist.d.ts.map +1 -1
- package/dist/commands/skill/review-checklist.js +4 -2
- package/dist/commands/skill/review-checklist.js.map +1 -1
- package/dist/commands/skill/review.d.ts +247 -0
- package/dist/commands/skill/review.d.ts.map +1 -1
- package/dist/commands/skill/review.js +98 -61
- package/dist/commands/skill/review.js.map +1 -1
- package/dist/commands/skill/test/configure.d.ts.map +1 -1
- package/dist/commands/skill/test/configure.js +0 -1
- package/dist/commands/skill/test/configure.js.map +1 -1
- package/dist/commands/skill/test/run.d.ts +12 -6
- package/dist/commands/skill/test/run.d.ts.map +1 -1
- package/dist/commands/skill/test/run.js +137 -67
- package/dist/commands/skill/test/run.js.map +1 -1
- package/dist/commands/skills/build.d.ts +69 -2
- package/dist/commands/skills/build.d.ts.map +1 -1
- package/dist/commands/skills/build.js +128 -42
- package/dist/commands/skills/build.js.map +1 -1
- package/dist/commands/skills/install.d.ts +2 -1
- package/dist/commands/skills/install.d.ts.map +1 -1
- package/dist/commands/skills/install.js +20 -30
- package/dist/commands/skills/install.js.map +1 -1
- package/dist/commands/skills/list.d.ts +2 -1
- package/dist/commands/skills/list.d.ts.map +1 -1
- package/dist/commands/skills/list.js +44 -26
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/package.d.ts +1 -1
- package/dist/commands/skills/package.d.ts.map +1 -1
- package/dist/commands/skills/package.js +6 -8
- package/dist/commands/skills/package.js.map +1 -1
- package/dist/commands/skills/scope-guard.d.ts.map +1 -1
- package/dist/commands/skills/scope-guard.js +2 -4
- package/dist/commands/skills/scope-guard.js.map +1 -1
- package/dist/commands/skills/shared.d.ts.map +1 -1
- package/dist/commands/skills/shared.js +2 -4
- package/dist/commands/skills/shared.js.map +1 -1
- package/dist/commands/skills/skill-discovery.d.ts +40 -1
- package/dist/commands/skills/skill-discovery.d.ts.map +1 -1
- package/dist/commands/skills/skill-discovery.js +32 -4
- package/dist/commands/skills/skill-discovery.js.map +1 -1
- package/dist/commands/skills/source-resolvers.d.ts +15 -0
- package/dist/commands/skills/source-resolvers.d.ts.map +1 -1
- package/dist/commands/skills/source-resolvers.js +25 -4
- package/dist/commands/skills/source-resolvers.js.map +1 -1
- package/dist/commands/skills/validate-command.d.ts.map +1 -1
- package/dist/commands/skills/validate-command.js +11 -3
- package/dist/commands/skills/validate-command.js.map +1 -1
- package/dist/commands/skills/validate.d.ts +66 -7
- package/dist/commands/skills/validate.d.ts.map +1 -1
- package/dist/commands/skills/validate.js +133 -29
- package/dist/commands/skills/validate.js.map +1 -1
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +8 -6
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.d.ts +102 -5
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +246 -69
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/report-schemas.d.ts +45 -0
- package/dist/report-schemas.d.ts.map +1 -0
- package/dist/report-schemas.js +102 -0
- package/dist/report-schemas.js.map +1 -0
- package/dist/skill-resolution/packaging-config.d.ts +49 -5
- package/dist/skill-resolution/packaging-config.d.ts.map +1 -1
- package/dist/skill-resolution/packaging-config.js +59 -19
- package/dist/skill-resolution/packaging-config.js.map +1 -1
- package/dist/skill-resolution/resolve-skill-reference.d.ts.map +1 -1
- package/dist/skill-resolution/resolve-skill-reference.js +8 -6
- package/dist/skill-resolution/resolve-skill-reference.js.map +1 -1
- package/dist/utils/agent-discovery.d.ts.map +1 -1
- package/dist/utils/agent-discovery.js +22 -11
- package/dist/utils/agent-discovery.js.map +1 -1
- package/dist/utils/agent-runner.d.ts.map +1 -1
- package/dist/utils/agent-runner.js +0 -1
- package/dist/utils/agent-runner.js.map +1 -1
- package/dist/utils/command-error.d.ts +87 -55
- package/dist/utils/command-error.d.ts.map +1 -1
- package/dist/utils/command-error.js +113 -64
- package/dist/utils/command-error.js.map +1 -1
- package/dist/utils/config-loader.d.ts +2 -1
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/context-budget-issues.d.ts +1 -1
- package/dist/utils/context-budget-issues.js +1 -1
- package/dist/utils/corpus-target.d.ts +5 -27
- package/dist/utils/corpus-target.d.ts.map +1 -1
- package/dist/utils/corpus-target.js +7 -33
- package/dist/utils/corpus-target.js.map +1 -1
- package/dist/utils/edge-lens-evaluation.d.ts +83 -0
- package/dist/utils/edge-lens-evaluation.d.ts.map +1 -0
- package/dist/utils/edge-lens-evaluation.js +110 -0
- package/dist/utils/edge-lens-evaluation.js.map +1 -0
- package/dist/utils/help-loader.d.ts.map +1 -1
- package/dist/utils/help-loader.js +0 -1
- package/dist/utils/help-loader.js.map +1 -1
- package/dist/utils/issue-rendering.d.ts +2 -2
- package/dist/utils/issue-rendering.js +2 -2
- package/dist/utils/optional-backend.d.ts +5 -4
- package/dist/utils/optional-backend.d.ts.map +1 -1
- package/dist/utils/optional-backend.js +7 -5
- package/dist/utils/optional-backend.js.map +1 -1
- package/dist/utils/output.d.ts +9 -0
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +16 -0
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/package-json.d.ts +24 -0
- package/dist/utils/package-json.d.ts.map +1 -0
- package/dist/utils/package-json.js +49 -0
- package/dist/utils/package-json.js.map +1 -0
- package/dist/utils/project-root-policy.d.ts +20 -0
- package/dist/utils/project-root-policy.d.ts.map +1 -1
- package/dist/utils/project-root-policy.js +30 -0
- package/dist/utils/project-root-policy.js.map +1 -1
- package/dist/utils/projection-query.d.ts +41 -4
- package/dist/utils/projection-query.d.ts.map +1 -1
- package/dist/utils/projection-query.js +44 -10
- package/dist/utils/projection-query.js.map +1 -1
- package/dist/utils/projection-store.d.ts +41 -15
- package/dist/utils/projection-store.d.ts.map +1 -1
- package/dist/utils/projection-store.js +65 -18
- package/dist/utils/projection-store.js.map +1 -1
- package/dist/utils/repeatable-option.d.ts +37 -0
- package/dist/utils/repeatable-option.d.ts.map +1 -0
- package/dist/utils/repeatable-option.js +39 -0
- package/dist/utils/repeatable-option.js.map +1 -0
- package/dist/utils/resource-loader.d.ts +4 -3
- package/dist/utils/resource-loader.d.ts.map +1 -1
- package/dist/utils/resource-loader.js +63 -32
- package/dist/utils/resource-loader.js.map +1 -1
- package/dist/utils/root-argv.d.ts +53 -0
- package/dist/utils/root-argv.d.ts.map +1 -0
- package/dist/utils/root-argv.js +215 -0
- package/dist/utils/root-argv.js.map +1 -0
- package/dist/utils/run-integrity.d.ts +95 -0
- package/dist/utils/run-integrity.d.ts.map +1 -0
- package/dist/utils/run-integrity.js +102 -0
- package/dist/utils/run-integrity.js.map +1 -0
- package/dist/utils/skill-packaging-config.d.ts +27 -0
- package/dist/utils/skill-packaging-config.d.ts.map +1 -1
- package/dist/utils/skill-packaging-config.js +30 -0
- package/dist/utils/skill-packaging-config.js.map +1 -1
- package/dist/utils/sqlite-experimental-warning.d.ts +69 -0
- package/dist/utils/sqlite-experimental-warning.d.ts.map +1 -0
- package/dist/utils/sqlite-experimental-warning.js +144 -0
- package/dist/utils/sqlite-experimental-warning.js.map +1 -0
- package/dist/utils/url-redact.d.ts.map +1 -1
- package/dist/utils/url-redact.js +4 -6
- package/dist/utils/url-redact.js.map +1 -1
- package/dist/utils/user-context-scanner.d.ts +8 -0
- package/dist/utils/user-context-scanner.d.ts.map +1 -1
- package/dist/utils/user-context-scanner.js +4 -2
- package/dist/utils/user-context-scanner.js.map +1 -1
- package/dist/utils/validate-help-files.d.ts.map +1 -1
- package/dist/utils/validate-help-files.js +12 -6
- package/dist/utils/validate-help-files.js.map +1 -1
- package/dist/utils/vat-bin-path.d.ts.map +1 -1
- package/dist/utils/vat-bin-path.js +0 -1
- package/dist/utils/vat-bin-path.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -3
- package/dist/version.js.map +1 -1
- package/docs/audit.md +141 -37
- package/docs/doctor.md +7 -6
- package/docs/index.md +19 -11
- package/docs/rag.md +3 -2
- package/docs/resources.md +5 -2
- package/docs/skill-test.md +10 -9
- package/docs/skills.md +53 -69
- package/package.json +24 -29
- package/schemas/ard-emit.json +149 -0
- package/schemas/claude-budget.json +197 -0
- package/schemas/okf-validate.json +170 -0
- package/schemas/resources-check.json +186 -0
- package/schemas/skill-review.json +230 -0
package/dist/commands/audit.js
CHANGED
|
@@ -5,22 +5,24 @@
|
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
6
|
import { existsSync as fsExistsSync } from 'node:fs';
|
|
7
7
|
import { basename } from 'node:path';
|
|
8
|
-
import { crawlAndResolveRegistry, detectDeclaredButMissing, detectMarketplacePluginSourceMissing, resetPackagingRegistryCache, detectPresentButUndeclared, detectReferenceTargetMissing, detectResourceFormat, enumerateSurfaces, materializeIssue, validate, validateMarketplace, validateSkill, validateSkillForPackaging, } from '@vibe-agent-toolkit/agent-skills';
|
|
8
|
+
import { conventionalSuiteProbe, crawlAndResolveRegistry, detectDeclaredButMissing, detectMarketplacePluginSourceMissing, resetPackagingRegistryCache, detectPresentButUndeclared, detectReferenceTargetMissing, detectResourceFormat, enumerateSurfaces, materializeIssue, validate, validateMarketplace, validateSkill, validateSkillForPackaging, } from '@vibe-agent-toolkit/agent-skills';
|
|
9
9
|
import { analyzeCompatibility, checkSettingsCompatibility, detectSkillClaudePluginNameMismatch, crawlSkillLinkRegistry, extractClaudeMarketplaceInventory, extractClaudePluginInventory, getClaudeUserPaths, readEffectiveSettings, validatePlugin, } from '@vibe-agent-toolkit/claude-marketplace';
|
|
10
10
|
import { detectFormat } from '@vibe-agent-toolkit/discovery';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { listingRefusalRemedy } from '@vibe-agent-toolkit/resources';
|
|
12
|
+
import { calculateValidationStatus, countBySeverity, ExitCode, } from '@vibe-agent-toolkit/schema';
|
|
13
|
+
import { findProjectRoot, isFilesystemAccessError, isPathAbsentError, issueLocation, relativeEscapesRoot, resetProjectRootCaches, safePath, toForwardSlash, } from '@vibe-agent-toolkit/utils';
|
|
14
|
+
import { DirectoryListingRefusedError } from '@vibe-agent-toolkit/utils/crawl';
|
|
13
15
|
import { gitFindRoot, isGitUrl, parseGitUrl, } from '@vibe-agent-toolkit/utils/git';
|
|
14
16
|
import { Command } from 'commander';
|
|
15
|
-
import picomatch from 'picomatch';
|
|
16
17
|
import { resetSkillDiscoveryCache, resolveProjectDeclaredEvalSuites, resolveSkillPackagingConfig, stripValidationAllowForDisplay, } from '../skill-resolution/packaging-config.js';
|
|
17
|
-
import { handleCommandError } from '../utils/command-error.js';
|
|
18
|
+
import { handleCommandError, handleExpectedFailure } from '../utils/command-error.js';
|
|
18
19
|
import { ConfigLoadError, loadConfig, resetLoadedConfigCache, } from '../utils/config-loader.js';
|
|
19
|
-
import { countCollapsedFindings, formatCollapsedFindingsHint, formatIssueLines, formatSeverityBreakdown, issuesToRenderAtVerbosity, } from '../utils/issue-rendering.js';
|
|
20
|
+
import { countCollapsedFindings, formatCollapsedFindingsHint, formatIssueLines, formatSeverityBreakdown, issuesToRenderAtVerbosity, sumSeverityCounts, } from '../utils/issue-rendering.js';
|
|
20
21
|
import { resolveIssueSeverity } from '../utils/issue-severity.js';
|
|
21
22
|
import { createLogger } from '../utils/logger.js';
|
|
22
23
|
import { writeYamlOutput } from '../utils/output.js';
|
|
23
24
|
import { relativizePathEntries } from '../utils/relativize-paths.js';
|
|
25
|
+
import { nothingCheckedFinding } from '../utils/run-integrity.js';
|
|
24
26
|
import { mergeSkillPackagingConfig } from '../utils/skill-packaging-config.js';
|
|
25
27
|
import { renderSkillQualityFooter } from '../utils/skill-quality-footer.js';
|
|
26
28
|
import { computeConfigVerdicts } from '../utils/verdict-helpers.js';
|
|
@@ -28,13 +30,13 @@ import { crawlOwnsSubtree, distributedTreeFindings, getOrCreateGitTracker, gitTr
|
|
|
28
30
|
import { withClonedRepo } from './audit/git-url-clone.js';
|
|
29
31
|
import { buildHierarchicalOutput } from './audit/hierarchical-output.js';
|
|
30
32
|
import { renderProvenanceHeader, rewritePathsInResults, } from './audit/provenance.js';
|
|
33
|
+
import { enumerateAuditPopulation, resolveProjectExcludes, VAT_CONFIG_FILENAME, } from './audit/scan-population.js';
|
|
31
34
|
import { createAuditSettingsCommand } from './audit-settings.js';
|
|
32
|
-
import { discoverSkillsFromConfig } from './skills/skill-discovery.js';
|
|
35
|
+
import { discoverSkillsFromConfig, SKILLS_INCLUDE_REMEDY, } from './skills/skill-discovery.js';
|
|
33
36
|
/** Resource type constant for agent skills, avoiding duplicate string literals. */
|
|
34
37
|
const RESOURCE_TYPE_AGENT_SKILL = 'agent-skill';
|
|
35
38
|
/** Resource type constant for Claude plugins, avoiding duplicate string literals. */
|
|
36
39
|
const RESOURCE_TYPE_CLAUDE_PLUGIN = 'claude-plugin';
|
|
37
|
-
const VAT_CONFIG_FILENAME = 'vibe-agent-toolkit.config.yaml';
|
|
38
40
|
/**
|
|
39
41
|
* Build config-aware context for a single VAT project at `scanRoot`.
|
|
40
42
|
*
|
|
@@ -51,9 +53,8 @@ const VAT_CONFIG_FILENAME = 'vibe-agent-toolkit.config.yaml';
|
|
|
51
53
|
*
|
|
52
54
|
* Returns `null` if `scanRoot` has no config or the config has no skills.
|
|
53
55
|
*/
|
|
54
|
-
async function buildVATProjectContext(scanRoot, logger) {
|
|
56
|
+
async function buildVATProjectContext(scanRoot, logger, locationRoot) {
|
|
55
57
|
const configPath = safePath.join(scanRoot, VAT_CONFIG_FILENAME);
|
|
56
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- scanRoot is a controlled parameter
|
|
57
58
|
if (!fs.existsSync(configPath)) {
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
@@ -69,7 +70,7 @@ async function buildVATProjectContext(scanRoot, logger) {
|
|
|
69
70
|
return null;
|
|
70
71
|
const skillConfigs = new Map();
|
|
71
72
|
try {
|
|
72
|
-
const discovered = await discoverSkillsFromConfig(config.skills, scanRoot);
|
|
73
|
+
const discovered = await discoverSkillsFromConfig(config.skills, scanRoot, degradingDiscovery(scanRoot, logger, locationRoot));
|
|
73
74
|
const { defaults, config: perSkillConfig } = config.skills;
|
|
74
75
|
for (const skill of discovered) {
|
|
75
76
|
const absPath = safePath.resolve(skill.sourcePath);
|
|
@@ -79,7 +80,16 @@ async function buildVATProjectContext(scanRoot, logger) {
|
|
|
79
80
|
logger.debug(`Config-aware audit: found ${discovered.length} skill(s) in ${scanRoot}`);
|
|
80
81
|
}
|
|
81
82
|
catch (error) {
|
|
82
|
-
|
|
83
|
+
// Discovery was handed the degrade policy above, so a refused listing never
|
|
84
|
+
// reaches here; one that does (a caller that switched it to `'refuse'`) is
|
|
85
|
+
// still degraded past through the same ledger rather than swallowed. It used
|
|
86
|
+
// to be `logger.debug` for ANY error, which is how a `chmod 000` directory
|
|
87
|
+
// under `skills.include` downgraded every skill to config-free validation with
|
|
88
|
+
// the directory named nowhere an operator without `--debug` could see it. A
|
|
89
|
+
// defect in discovery is not environmental and still fails loudly.
|
|
90
|
+
if (!(error instanceof DirectoryListingRefusedError))
|
|
91
|
+
throw error;
|
|
92
|
+
recordRefusedDiscovery(error, scanRoot, logger, locationRoot);
|
|
83
93
|
}
|
|
84
94
|
if (skillConfigs.size === 0) {
|
|
85
95
|
return null;
|
|
@@ -207,22 +217,38 @@ function packagingResultToValidationResult(skillPath, result, configTargets, loc
|
|
|
207
217
|
* no governing config OR when the one found could not be loaded.
|
|
208
218
|
*/
|
|
209
219
|
async function resolveGoverningConfig(skillPath, logger, locationRoot) {
|
|
220
|
+
// The walk-up the resolver is about to do (cached), so the degrade handler can
|
|
221
|
+
// name the config whose include reached the directory. `null` means no config
|
|
222
|
+
// governs this skill; the resolver answers the same and nothing can be refused.
|
|
223
|
+
const configRoot = findProjectRoot(safePath.resolve(skillPath, '..'));
|
|
224
|
+
if (configRoot === null)
|
|
225
|
+
return null;
|
|
210
226
|
let fullConfig;
|
|
211
227
|
try {
|
|
212
|
-
fullConfig = await resolveSkillPackagingConfig(skillPath);
|
|
228
|
+
fullConfig = await resolveSkillPackagingConfig(skillPath, degradingDiscovery(configRoot, logger, locationRoot));
|
|
213
229
|
}
|
|
214
230
|
catch (err) {
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
231
|
+
if (err instanceof ConfigLoadError) {
|
|
232
|
+
recordUnloadableConfig(err, logger, locationRoot);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
// Unreachable while the resolver is handed `degradingDiscovery` above — the
|
|
236
|
+
// crawl then enumerates around a refused directory instead of throwing — but
|
|
237
|
+
// kept as the fallback for the default policy: same two channels, the skill
|
|
238
|
+
// goes on config-free rather than the run aborting.
|
|
239
|
+
if (err instanceof DirectoryListingRefusedError) {
|
|
240
|
+
recordRefusedDiscovery(err, configRoot, logger, locationRoot);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
throw err;
|
|
219
244
|
}
|
|
220
245
|
return fullConfig === null ? null : stripValidationAllowForDisplay(fullConfig);
|
|
221
246
|
}
|
|
222
247
|
/**
|
|
223
|
-
* Configs this run has already
|
|
224
|
-
*
|
|
225
|
-
* the
|
|
248
|
+
* Configs this run has already degraded past — unloadable, or loaded but with a
|
|
249
|
+
* `skills.include` the crawl could not fully expand — keyed by the PATH that was
|
|
250
|
+
* refused (the config file, or the directory the crawl could not list), each
|
|
251
|
+
* holding the synthetic result that carries its finding into the report.
|
|
226
252
|
*
|
|
227
253
|
* 🚨 MODULE-scoped, and cleared by {@link resetAuditCaches}, exactly like
|
|
228
254
|
* `config-loader.ts`'s `warnedConfigPaths`. It used to be a `Set` threaded as a
|
|
@@ -235,15 +261,45 @@ async function resolveGoverningConfig(skillPath, logger, locationRoot) {
|
|
|
235
261
|
* while the lane that actually repeated was never exercised. A run-scoped ledger
|
|
236
262
|
* cannot be re-created per call by mistake; a parameter can.
|
|
237
263
|
*
|
|
238
|
-
* ⚠️ Keyed on `ConfigLoadError.projectRoot
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
264
|
+
* ⚠️ Keyed on the refused PATH (derived from `ConfigLoadError.projectRoot` or
|
|
265
|
+
* `DirectoryListingRefusedError.refusal.directory`), NOT on `err.message`. The
|
|
266
|
+
* old key was the message with a comment claiming it "names the offending config
|
|
267
|
+
* path" — true only for a Zod failure, which is what the fixture happened to be.
|
|
268
|
+
* A YAML SYNTAX error is thrown out of `yaml.parse` and wrapped as `Failed to
|
|
269
|
+
* load config: ${message}`, and `YAMLParseError.message` carries no filename at
|
|
270
|
+
* all, so two nested projects with the same copy-pasted broken config collapsed
|
|
271
|
+
* to ONE warning naming NEITHER.
|
|
245
272
|
*/
|
|
246
273
|
const unloadableConfigResults = new Map();
|
|
274
|
+
/**
|
|
275
|
+
* The RUN's conventional-suite probe — the memo behind "does
|
|
276
|
+
* `<skill-root>/evals/evals.json` exist?".
|
|
277
|
+
*
|
|
278
|
+
* Resolving one skill's test input asks that question of the SUBJECT and of
|
|
279
|
+
* every entry in `projectSkills`, so a probe minted per skill costs S questions
|
|
280
|
+
* per skill and S² per run over the same S paths. Measured with the lab on a
|
|
281
|
+
* 103-skill adopter: 10,815 probes over 103 distinct paths, half of the
|
|
282
|
+
* command's entire filesystem traffic.
|
|
283
|
+
*
|
|
284
|
+
* 🚨 MODULE-scoped and cleared by {@link resetAuditCaches}, for the same reason
|
|
285
|
+
* `unloadableConfigResults` above is: this command has TWO lanes that build a
|
|
286
|
+
* shared context — {@link validateSingleSkill} and {@link handleFileEntry} — and
|
|
287
|
+
* both build it PER SKILL, inside their own loops. A probe threaded as a
|
|
288
|
+
* parameter would be re-minted by whichever lane forgot to thread it, silently,
|
|
289
|
+
* and that is exactly how this stayed quadratic: the field existed and no caller
|
|
290
|
+
* set it.
|
|
291
|
+
*
|
|
292
|
+
* ⚠️ Its lifetime is the RUN, never longer. The answer is a filesystem snapshot,
|
|
293
|
+
* and a memo outliving the invocation would keep answering for a tree that has
|
|
294
|
+
* since changed — in the CLI entrypoint AND in an in-process test sharing a
|
|
295
|
+
* vitest worker.
|
|
296
|
+
*/
|
|
297
|
+
let runSuiteProbe;
|
|
298
|
+
/** The run's probe, minted on first use and dropped by {@link resetAuditCaches}. */
|
|
299
|
+
function suiteProbeForRun() {
|
|
300
|
+
runSuiteProbe ??= conventionalSuiteProbe();
|
|
301
|
+
return runSuiteProbe;
|
|
302
|
+
}
|
|
247
303
|
/**
|
|
248
304
|
* Warn about — and file a finding for — a governing config this run could not
|
|
249
305
|
* load, once per config however many skills it governs.
|
|
@@ -253,10 +309,10 @@ const unloadableConfigResults = new Map();
|
|
|
253
309
|
* @param locationRoot - The run's anchor base for the emitted location
|
|
254
310
|
*/
|
|
255
311
|
function recordUnloadableConfig(err, logger, locationRoot) {
|
|
256
|
-
if (unloadableConfigResults.has(err.projectRoot))
|
|
257
|
-
return;
|
|
258
312
|
const configPath = safePath.join(err.projectRoot, VAT_CONFIG_FILENAME);
|
|
259
|
-
unloadableConfigResults.
|
|
313
|
+
if (unloadableConfigResults.has(configPath))
|
|
314
|
+
return;
|
|
315
|
+
unloadableConfigResults.set(configPath, unreadablePathResult(configPath, err, locationRoot));
|
|
260
316
|
// The filesystem answer changes the NOUN, because the two conditions need
|
|
261
317
|
// different actions from the operator: `chmod`/ownership on one side, an edit
|
|
262
318
|
// to the file on the other. Collapsing them into one sentence was the whole
|
|
@@ -265,6 +321,102 @@ function recordUnloadableConfig(err, logger, locationRoot) {
|
|
|
265
321
|
logger.warn(`Ignoring ${why} config at ${configPath}; skills it governs are validated`
|
|
266
322
|
+ ` config-free: ${err.message}`);
|
|
267
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Audit's discovery policy: enumerate AROUND a directory the crawl cannot list,
|
|
326
|
+
* report the gap, and keep every skill the crawl could see under its config.
|
|
327
|
+
*
|
|
328
|
+
* 🔑 This is what keeps ONE `chmod 000` sibling from costing a readable,
|
|
329
|
+
* declared skill every config-aware check. Discovery's default is to refuse —
|
|
330
|
+
* right for a build that must not ship a shorter bundle — but audit already
|
|
331
|
+
* validates "a skill beside the unreadable path exactly as if it were not
|
|
332
|
+
* there", and that promise is only kept if discovery degrades the same way the
|
|
333
|
+
* walk does. The refusal is recorded through the same ledger as an unloadable
|
|
334
|
+
* config, so it is warned about once and filed once however many skills ask.
|
|
335
|
+
*
|
|
336
|
+
* @param configRoot - The project root whose `skills.include` is being expanded
|
|
337
|
+
* @param logger - Warnings go to stderr; the finding goes to the report
|
|
338
|
+
* @param locationRoot - The run's anchor base for the emitted location
|
|
339
|
+
*/
|
|
340
|
+
function degradingDiscovery(configRoot, logger, locationRoot) {
|
|
341
|
+
return {
|
|
342
|
+
degrade: (refusal) => {
|
|
343
|
+
recordRefusedDiscovery(new DirectoryListingRefusedError(refusal, { root: configRoot, remedy: SKILLS_INCLUDE_REMEDY }), configRoot, logger, locationRoot);
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Audit's ruling for the registry crawl — the link graph `crawlAndResolveRegistry`
|
|
349
|
+
* builds over the whole project root for the config-aware lane: enumerate
|
|
350
|
+
* AROUND a directory it cannot list, file the gap, keep every readable document.
|
|
351
|
+
*
|
|
352
|
+
* The other half of {@link degradingDiscovery}, on the other crawl the
|
|
353
|
+
* config-aware lane makes. Every other verb hands this crawl `'refuse'` (a build
|
|
354
|
+
* must not ship a shorter bundle), and while the registry carried that ruling
|
|
355
|
+
* INSIDE itself `vat audit` on a tree with one `chmod 000` sibling exited 2
|
|
356
|
+
* with `status: error` and ZERO findings — the unreadable-directory defect's
|
|
357
|
+
* exact shape, from the crawl its first fix never looked at. Audit's contract is the opposite:
|
|
358
|
+
* status describes what was found, exit describes whether the run completed.
|
|
359
|
+
*
|
|
360
|
+
* Recorded through the same per-directory ledger as a refused discovery, so a
|
|
361
|
+
* directory both crawls meet is warned about once and filed once; a directory
|
|
362
|
+
* only THIS crawl meets (the link graph covers the whole root, discovery only
|
|
363
|
+
* `skills.include`'s bases) gets its own sentence, naming the crawl that
|
|
364
|
+
* skipped it. The finding carries the projection's remedy — the same sentence
|
|
365
|
+
* the refuse ruling throws — because `--exclude` governs audit's own walk, not
|
|
366
|
+
* this crawl, and an ignore rule is the one knob that does.
|
|
367
|
+
*
|
|
368
|
+
* @param projectRoot - The root the link graph is crawled from
|
|
369
|
+
* @param logger - Warnings go to stderr; the finding goes to the report
|
|
370
|
+
* @param locationRoot - The run's anchor base for the emitted location
|
|
371
|
+
*/
|
|
372
|
+
function degradingRegistry(projectRoot, logger, locationRoot) {
|
|
373
|
+
return {
|
|
374
|
+
degrade: (refusal) => {
|
|
375
|
+
const err = new DirectoryListingRefusedError(refusal, { root: projectRoot, remedy: listingRefusalRemedy(projectRoot) });
|
|
376
|
+
recordRefusedListing(err, logger, locationRoot, () => `The link graph under ${projectRoot} skipped an unreadable directory; every document beneath it`
|
|
377
|
+
+ ` is missing from this audit's link and reference checks: ${err.message}`);
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Warn about — and file a finding for — a directory that skill discovery under
|
|
383
|
+
* `configRoot` could not list, once per directory however many skills ask.
|
|
384
|
+
*
|
|
385
|
+
* The finding is anchored on the DIRECTORY, not the config: the config loaded
|
|
386
|
+
* fine, and the directory is the path the filesystem refused. In the directory
|
|
387
|
+
* lane the walk's own per-entry guard files the same code on the same location;
|
|
388
|
+
* {@link dedupeUnreadablePathResults} keeps one. The warning carries the crawl's
|
|
389
|
+
* own sentence, which already names the directory against the project root and
|
|
390
|
+
* the `skills.include` remedy.
|
|
391
|
+
*
|
|
392
|
+
* @param err - The refusal, naming the directory the crawl could not list
|
|
393
|
+
* @param configRoot - The project root whose `skills.include` reached it
|
|
394
|
+
* @param logger - Warnings go to stderr; the finding goes to the report
|
|
395
|
+
* @param locationRoot - The run's anchor base for the emitted location
|
|
396
|
+
*/
|
|
397
|
+
function recordRefusedDiscovery(err, configRoot, logger, locationRoot) {
|
|
398
|
+
const configPath = safePath.join(configRoot, VAT_CONFIG_FILENAME);
|
|
399
|
+
recordRefusedListing(err, logger, locationRoot, () => `Skill discovery under ${configPath} skipped an unreadable directory; any skill beneath it`
|
|
400
|
+
+ ` is missing from this audit: ${err.message}`);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* The one ledger write for a refused listing, whichever crawl met it: file
|
|
404
|
+
* `SCAN_PATH_UNREADABLE` on the directory and warn once on stderr. Keyed on the
|
|
405
|
+
* refused directory, so the second crawl to meet it — discovery and the link
|
|
406
|
+
* graph both walk `skills/locked` in the directory lane — says nothing more.
|
|
407
|
+
*
|
|
408
|
+
* @param err - The refusal, naming the directory the crawl could not list
|
|
409
|
+
* @param logger - Warnings go to stderr; the finding goes to the report
|
|
410
|
+
* @param locationRoot - The run's anchor base for the emitted location
|
|
411
|
+
* @param warning - The stderr sentence, built only when this is the first record
|
|
412
|
+
*/
|
|
413
|
+
function recordRefusedListing(err, logger, locationRoot, warning) {
|
|
414
|
+
const directory = err.refusal.directory;
|
|
415
|
+
if (unloadableConfigResults.has(directory))
|
|
416
|
+
return;
|
|
417
|
+
unloadableConfigResults.set(directory, unreadablePathResult(directory, err, locationRoot));
|
|
418
|
+
logger.warn(warning());
|
|
419
|
+
}
|
|
268
420
|
/**
|
|
269
421
|
* The synthetic results for every config this run degraded past, so the report
|
|
270
422
|
* carries what the stderr warning says.
|
|
@@ -275,6 +427,23 @@ function recordUnloadableConfig(err, logger, locationRoot) {
|
|
|
275
427
|
function unloadableConfigFindings() {
|
|
276
428
|
return [...unloadableConfigResults.values()];
|
|
277
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* A run's results plus the findings for every config it degraded past, with each
|
|
432
|
+
* refused path reported once.
|
|
433
|
+
*
|
|
434
|
+
* Appended, not merged into a neighbour: a config the scan could not load, or a
|
|
435
|
+
* directory its discovery could not list, has no host result to hang off, and
|
|
436
|
+
* attributing it to a skill would name a file that was read fine. Without the
|
|
437
|
+
* append the ONLY record was a stderr warning, so a run that downgraded every
|
|
438
|
+
* skill under that config to the weaker config-free validator exited 0 with a
|
|
439
|
+
* report that said nothing about it. Deduped AFTER the append, because the
|
|
440
|
+
* directory lane's walk files its own `SCAN_PATH_UNREADABLE` on the very
|
|
441
|
+
* directory a refused discovery names — one fact, one finding, whichever lane
|
|
442
|
+
* met it first.
|
|
443
|
+
*/
|
|
444
|
+
function withDegradedConfigFindings(results) {
|
|
445
|
+
return dedupeUnreadablePathResults([...results, ...unloadableConfigFindings()]);
|
|
446
|
+
}
|
|
278
447
|
async function validateSingleSkill(skillPath, options, logger, locationRoot, crawlTree, isVATGenerated) {
|
|
279
448
|
// Config-aware validation: walk UP to the skill's nearest-ancestor
|
|
280
449
|
// vibe-agent-toolkit.config.yaml and apply the skill's packaging block. A
|
|
@@ -295,10 +464,18 @@ async function validateSingleSkill(skillPath, options, logger, locationRoot, cra
|
|
|
295
464
|
// The project's declared eval suites, memoized per config root, so this lane
|
|
296
465
|
// predicts the same bundle the packager produces: no skill's report counts a
|
|
297
466
|
// SIBLING skill's eval suite as an ordinary bundled file.
|
|
467
|
+
// Audit's ruling for the link-graph crawl — see {@link degradingRegistry}.
|
|
468
|
+
// Stated on the context too, for the fallback crawl `validateSkillForPackaging`
|
|
469
|
+
// makes when this registry does not cover the skill: one ruling, both crawls.
|
|
470
|
+
const unreadable = degradingRegistry(projectRoot, logger, locationRoot);
|
|
298
471
|
const sharedCtx = {
|
|
299
|
-
registry: await crawlAndResolveRegistry(projectRoot),
|
|
472
|
+
registry: await crawlAndResolveRegistry(projectRoot, { unreadable }),
|
|
473
|
+
unreadable,
|
|
300
474
|
locationRoot,
|
|
301
|
-
projectSkills: await resolveProjectDeclaredEvalSuites(skillPath),
|
|
475
|
+
projectSkills: await resolveProjectDeclaredEvalSuites(skillPath, degradingDiscovery(projectRoot, logger, locationRoot)),
|
|
476
|
+
// The RUN's probe, not this call's — see {@link runSuiteProbe}. This lane
|
|
477
|
+
// is called in a loop over every skill in a plugin.
|
|
478
|
+
suiteProbe: suiteProbeForRun(),
|
|
302
479
|
};
|
|
303
480
|
if (gitTracker !== null) {
|
|
304
481
|
sharedCtx.gitTracker = gitTracker;
|
|
@@ -308,8 +485,8 @@ async function validateSingleSkill(skillPath, options, logger, locationRoot, cra
|
|
|
308
485
|
await appendDistributedTreeFindings(configAware, skillPath, locationRoot, crawlTree);
|
|
309
486
|
return configAware;
|
|
310
487
|
}
|
|
311
|
-
// Fallback: basic validation
|
|
312
|
-
const validateOptions = { skillPath, locationRoot };
|
|
488
|
+
// Fallback: basic validation. Severity is applied by `applySeverityFilter`.
|
|
489
|
+
const validateOptions = { skillPath, locationRoot, validation: {} };
|
|
313
490
|
if (options.warnUnreferencedFiles) {
|
|
314
491
|
validateOptions.checkUnreferencedFiles = true;
|
|
315
492
|
}
|
|
@@ -355,6 +532,14 @@ function collect(value, previous) {
|
|
|
355
532
|
/**
|
|
356
533
|
* Create audit command
|
|
357
534
|
* Top-level command: vat audit [path]
|
|
535
|
+
*
|
|
536
|
+
* The exit code follows the document's `status`, under the one exit-code
|
|
537
|
+
* contract every verb shares: `OK` when nothing is at error severity,
|
|
538
|
+
* `FINDINGS` when something is (or the run audited zero files), `ERROR` when
|
|
539
|
+
* the audit could not run at all. This command used to be the one exception —
|
|
540
|
+
* "advisory", exit 0 over `status: error` — which a CI author reading the
|
|
541
|
+
* shared contract got wrong on exactly this verb. Warnings never move the
|
|
542
|
+
* exit code; `validation.severity` is the dial that decides what counts.
|
|
358
543
|
*/
|
|
359
544
|
export function createAuditCommand() {
|
|
360
545
|
const audit = new Command('audit');
|
|
@@ -421,11 +606,20 @@ Description:
|
|
|
421
606
|
done
|
|
422
607
|
(See packages/cli/docs/audit.md for details.)
|
|
423
608
|
|
|
609
|
+
Output:
|
|
610
|
+
The YAML 'status' describes the findings, and the exit code follows it, as
|
|
611
|
+
in every other command of this CLI.
|
|
612
|
+
|
|
613
|
+
A run that audited ZERO files is refused, not passed: 'status: error' with one
|
|
614
|
+
non-overridable RESOURCE_CHECK_BROKEN under a top-level 'issues:' (the claim is
|
|
615
|
+
about the run, so it is not a files[] row), and exit 1.
|
|
616
|
+
|
|
424
617
|
Validation Behavior:
|
|
425
|
-
|
|
618
|
+
Audit surfaces all validation issues for inspection.
|
|
426
619
|
Unlike 'vat skills validate', audit:
|
|
427
620
|
- NEVER applies validation.allow (allowed codes are always shown)
|
|
428
|
-
- Respects validation.severity:
|
|
621
|
+
- Respects validation.severity: a code set to 'ignore' is hidden; warnings
|
|
622
|
+
and errors are both reported, each at its configured severity
|
|
429
623
|
- ALWAYS exits 0 for validation results (never gates on errors)
|
|
430
624
|
|
|
431
625
|
For gated validation (CI/CD), use: vat skills validate
|
|
@@ -445,11 +639,16 @@ Validation Checks:
|
|
|
445
639
|
|
|
446
640
|
Warnings (should fix):
|
|
447
641
|
- Skill exceeds recommended length (>5000 lines)
|
|
448
|
-
-
|
|
449
|
-
(
|
|
450
|
-
COMPAT_REQUIRES_EXTERNAL_CLI). See docs/validation-codes.md.
|
|
642
|
+
- With --compat: a declared target cannot or may not run the skill
|
|
643
|
+
(COMPAT_TARGET_INCOMPATIBLE, COMPAT_TARGET_NEEDS_REVIEW)
|
|
451
644
|
- Unreferenced files detected (with --warn-unreferenced-files)
|
|
452
645
|
|
|
646
|
+
Info (observations):
|
|
647
|
+
- Capability observations — browser auth, local shell, or external CLI
|
|
648
|
+
(CAPABILITY_BROWSER_AUTH, CAPABILITY_LOCAL_SHELL, CAPABILITY_EXTERNAL_CLI);
|
|
649
|
+
with --compat, a skill declaring no targets (COMPAT_TARGET_UNDECLARED).
|
|
650
|
+
See docs/validation-codes.md.
|
|
651
|
+
|
|
453
652
|
Gitignore-Aware Scanning:
|
|
454
653
|
When scanning inside a git repository, paths matched by .gitignore are
|
|
455
654
|
skipped by default. This excludes build artifacts (dist/), dependencies
|
|
@@ -459,8 +658,9 @@ Gitignore-Aware Scanning:
|
|
|
459
658
|
Use --include-artifacts to scan gitignored paths (e.g., to audit a
|
|
460
659
|
bundled marketplace plugin in dist/).
|
|
461
660
|
|
|
462
|
-
User-supplied --exclude patterns are always applied on top.
|
|
463
|
-
|
|
661
|
+
User-supplied --exclude patterns are always applied on top. The
|
|
662
|
+
directories no VAT crawl enters (node_modules/, .git/, coverage/,
|
|
663
|
+
.turbo/, git worktrees) stay out under every flag, in or out of git.
|
|
464
664
|
|
|
465
665
|
Config-Aware Validation:
|
|
466
666
|
The governing vibe-agent-toolkit.config.yaml is found by walking UP from
|
|
@@ -470,14 +670,29 @@ Config-Aware Validation:
|
|
|
470
670
|
which prevents false warnings for links the build pipeline resolves.
|
|
471
671
|
|
|
472
672
|
Config-aware mode never applies validation.allow — audit always shows
|
|
473
|
-
all issues. validation.severity IS applied
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
673
|
+
all issues. validation.severity IS applied, from three scopes, least
|
|
674
|
+
specific first:
|
|
675
|
+
resources.validation.severity project-wide (the same dial
|
|
676
|
+
'vat resources validate' reads)
|
|
677
|
+
skills.defaults.validation.severity project-wide (skills, plugins and
|
|
678
|
+
marketplaces alike)
|
|
679
|
+
skills.config.<name>.validation... that one skill
|
|
477
680
|
|
|
478
681
|
Exit Codes:
|
|
479
|
-
0 -
|
|
480
|
-
|
|
682
|
+
0 - The audit completed with nothing at error severity (warnings and
|
|
683
|
+
informational findings are in the report, not the exit code).
|
|
684
|
+
1 - The audit completed and reports 'status: error': at least one
|
|
685
|
+
error-severity finding, or zero files audited. A path inside the tree
|
|
686
|
+
the scan could not read, or a governing config that cannot be loaded
|
|
687
|
+
or whose skills.include reaches an unreadable directory, is
|
|
688
|
+
SCAN_PATH_UNREADABLE (warning): the run is degraded, not failed —
|
|
689
|
+
readable siblings are still validated, the config's skills
|
|
690
|
+
config-free, and the refused path is not counted in filesScanned (so
|
|
691
|
+
a root with nothing readable is a zero-files refusal).
|
|
692
|
+
2 - The audit could not run at all: the path does not exist or is a file
|
|
693
|
+
no audit lane recognises, --user with no Claude config dir, a git URL
|
|
694
|
+
that would not clone, an unknown flag, an internal failure. There is
|
|
695
|
+
no report to read.
|
|
481
696
|
|
|
482
697
|
Examples:
|
|
483
698
|
$ vat audit ./plugins/ # Audit recursively (default)
|
|
@@ -531,11 +746,8 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
531
746
|
// --user scans three sibling directories; they share ONE enclosing Claude
|
|
532
747
|
// config dir, so that is the run's single stated root.
|
|
533
748
|
const scanRoot = safePath.resolve(claudeDir);
|
|
534
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- safe: path constructed from os.homedir()
|
|
535
749
|
const pluginsDirExists = fs.existsSync(pluginsDir);
|
|
536
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- safe: path constructed from os.homedir()
|
|
537
750
|
const skillsDirExists = fs.existsSync(skillsDir);
|
|
538
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- safe: path constructed from os.homedir()
|
|
539
751
|
const marketplacesDirExists = fs.existsSync(marketplacesDir);
|
|
540
752
|
if (!pluginsDirExists && !skillsDirExists && !marketplacesDirExists) {
|
|
541
753
|
logger.error(`No user-level Claude directories found:`);
|
|
@@ -543,9 +755,9 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
543
755
|
logger.error(` Skills: ${skillsDir}`);
|
|
544
756
|
logger.error(` Marketplaces: ${marketplacesDir}`);
|
|
545
757
|
logger.error('Claude plugins/skills/marketplaces have not been installed yet.');
|
|
546
|
-
process.exit(
|
|
758
|
+
process.exit(ExitCode.ERROR);
|
|
547
759
|
}
|
|
548
|
-
const
|
|
760
|
+
const scanned = [];
|
|
549
761
|
// The roots that exist, minus any one another already covers — see
|
|
550
762
|
// {@link userScanTargets}. `marketplaces/` lives inside `plugins/`.
|
|
551
763
|
const present = [
|
|
@@ -555,12 +767,12 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
555
767
|
];
|
|
556
768
|
for (const target of userScanTargets(present, recursive)) {
|
|
557
769
|
logger.debug(`Auditing user-level resources at: ${target}`);
|
|
558
|
-
|
|
770
|
+
scanned.push(...await getValidationResults(target, recursive, options, logger, scanRoot));
|
|
559
771
|
}
|
|
560
772
|
// The `--user` report is assembled here rather than by `buildAuditReport`, so
|
|
561
773
|
// it needs its own append or the config findings would reach stderr and not the
|
|
562
774
|
// document — see the same call in `buildAuditReport`.
|
|
563
|
-
results
|
|
775
|
+
const results = withDegradedConfigFindings(scanned);
|
|
564
776
|
// Run compatibility analysis if --compat flag is set
|
|
565
777
|
const compatMap = options.compat
|
|
566
778
|
? await runCompatAnalysis(results, logger, scanRoot) // --settings not supported in --user mode
|
|
@@ -573,7 +785,7 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
573
785
|
if (verbose) {
|
|
574
786
|
renderVerboseEvidence(results, scanRoot, logger);
|
|
575
787
|
}
|
|
576
|
-
logHierarchicalSummary(results, logger);
|
|
788
|
+
logHierarchicalSummary(results, summary.issues, logger);
|
|
577
789
|
}
|
|
578
790
|
/** Skill resource types that can have per-skill validation config. */
|
|
579
791
|
const SKILL_RESULT_TYPES = new Set([RESOURCE_TYPE_AGENT_SKILL, 'vat-agent']);
|
|
@@ -597,7 +809,6 @@ const SKILL_RESULT_TYPES = new Set([RESOURCE_TYPE_AGENT_SKILL, 'vat-agent']);
|
|
|
597
809
|
*/
|
|
598
810
|
export function deriveScanRoot(targetPath) {
|
|
599
811
|
const resolved = safePath.resolve(targetPath);
|
|
600
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- resolved is the operator's own audit target
|
|
601
812
|
const isDirectory = fs.existsSync(resolved) && fs.statSync(resolved).isDirectory();
|
|
602
813
|
return isDirectory ? resolved : safePath.resolve(resolved, '..');
|
|
603
814
|
}
|
|
@@ -645,7 +856,7 @@ function buildFilteredResult(result, filteredIssues) {
|
|
|
645
856
|
/**
|
|
646
857
|
* Apply severity resolution to validation results.
|
|
647
858
|
*
|
|
648
|
-
* Audit
|
|
859
|
+
* Audit shows everything: it applies `validation.severity` to decide what to
|
|
649
860
|
* show and at what severity, but deliberately ignores `validation.allow`.
|
|
650
861
|
*
|
|
651
862
|
* **Both directions, via the one shared resolver.** This used to compute the
|
|
@@ -657,13 +868,21 @@ function buildFilteredResult(result, filteredIssues) {
|
|
|
657
868
|
* same config promoted correctly. Two lanes, one config key, opposite answers.
|
|
658
869
|
*
|
|
659
870
|
* Status and counts are re-derived from the resolved severities by
|
|
660
|
-
* {@link buildFilteredResult}, so a promotion moves the reported
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
871
|
+
* {@link buildFilteredResult}, so a promotion moves the reported STATUS — which
|
|
872
|
+
* describes the findings — and never the exit code, which describes whether the
|
|
873
|
+
* run completed. The two are different questions and audit publishes both; see
|
|
874
|
+
* {@link handleAuditResults} and the `Output` section of this command's help.
|
|
875
|
+
* `vat verify` derives its exit code from severity because it is a gate; this
|
|
876
|
+
* command is a report.
|
|
877
|
+
*
|
|
878
|
+
* THREE scopes, least specific first:
|
|
879
|
+
* - `resources.validation.severity` — the PROJECT-WIDE map `vat resources
|
|
880
|
+
* validate` already honours. Audit read only the two `skills.*` scopes below,
|
|
881
|
+
* so one dial in one config file produced opposite answers from the two
|
|
882
|
+
* commands: measured on a two-code fixture, `LINK_MISSING_TARGET: ignore` and
|
|
883
|
+
* `LINK_TO_NAVIGATION_FILE: error` written here moved neither finding in
|
|
884
|
+
* audit. It is applied FIRST — a `skills` scope naming the same code is the
|
|
885
|
+
* more specific statement and wins.
|
|
667
886
|
* - `skills.defaults.validation.severity` — the PROJECT-WIDE map, applied to
|
|
668
887
|
* every result this run produced, including `claude-plugin` and `marketplace`
|
|
669
888
|
* ones. It used to be applied only to skill-typed results, which left the
|
|
@@ -698,11 +917,13 @@ function buildFilteredResult(result, filteredIssues) {
|
|
|
698
917
|
* @returns New results array with ignored codes removed from issues
|
|
699
918
|
*/
|
|
700
919
|
function applySeverityFilter(results, config) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const
|
|
920
|
+
const skillsConfig = config?.skills;
|
|
921
|
+
// NOT gated on `config.skills` existing. It was, and that made the resources
|
|
922
|
+
// dial below inert for exactly the projects that have only one — a repo of
|
|
923
|
+
// plugins declares no skills at all.
|
|
924
|
+
const resourceSeverity = config?.resources?.validation?.severity ?? {};
|
|
925
|
+
const defaultSeverity = skillsConfig?.defaults?.validation?.severity ?? {};
|
|
926
|
+
const projectSeverity = { ...resourceSeverity, ...defaultSeverity };
|
|
706
927
|
// `.filter` after `.map`, not `.map` alone: a synthetic unreadable-path result
|
|
707
928
|
// exists ONLY to carry its finding, so once the adopter has set
|
|
708
929
|
// `severity.SCAN_PATH_UNREADABLE: ignore` the result has nothing left to say.
|
|
@@ -714,9 +935,9 @@ function applySeverityFilter(results, config) {
|
|
|
714
935
|
const skillName = SKILL_RESULT_TYPES.has(result.type) ? result.metadata?.name : undefined;
|
|
715
936
|
const perSkillSeverity = skillName === undefined
|
|
716
937
|
? {}
|
|
717
|
-
: (skillsConfig
|
|
718
|
-
// Merge:
|
|
719
|
-
const effectiveSeverity = { ...
|
|
938
|
+
: (skillsConfig?.config?.[skillName]?.validation?.severity ?? {});
|
|
939
|
+
// Merge, least specific first: resources → skills.defaults → skills.config.<name>
|
|
940
|
+
const effectiveSeverity = { ...projectSeverity, ...perSkillSeverity };
|
|
720
941
|
if (Object.keys(effectiveSeverity).length === 0) {
|
|
721
942
|
return result;
|
|
722
943
|
}
|
|
@@ -779,15 +1000,18 @@ export async function buildAuditReport(scanPath, options, startTime, logger) {
|
|
|
779
1000
|
const recursive = options.recursive !== false;
|
|
780
1001
|
logger.debug(`Auditing resources at: ${scanPath}`);
|
|
781
1002
|
const scanRoot = deriveScanRoot(scanPath);
|
|
782
|
-
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1003
|
+
const scanned = await getValidationResults(scanPath, recursive, options, logger, scanRoot);
|
|
1004
|
+
// Build config-aware context scoped to the single derived config root —
|
|
1005
|
+
// used by compat analysis to resolve the plugin-level `targets` union.
|
|
1006
|
+
// VAT's design: one config per project, no composition.
|
|
1007
|
+
//
|
|
1008
|
+
// BEFORE the report is assembled, not after: this is the lane that expands
|
|
1009
|
+
// `skills.include` even when the scan reached no SKILL.md under the config,
|
|
1010
|
+
// and a refusal it meets is recorded in the same ledger the append below
|
|
1011
|
+
// reads. Built afterwards, that finding reached stderr and not the document.
|
|
1012
|
+
const vatContextForCompat = await buildVATProjectContext(deriveConfigRoot(scanPath), logger, scanRoot);
|
|
1013
|
+
// See {@link withDegradedConfigFindings} for why these are appended, not merged.
|
|
1014
|
+
const rawResults = withDegradedConfigFindings(scanned);
|
|
791
1015
|
// Load config for severity filtering (audit ignores allow; only severity matters).
|
|
792
1016
|
//
|
|
793
1017
|
// Guarded, and the ordering is why: the scan above has ALREADY run and collected
|
|
@@ -814,13 +1038,9 @@ export async function buildAuditReport(scanPath, options, startTime, logger) {
|
|
|
814
1038
|
logger.warn(`Config could not be read; severity overrides are not applied: ${String(err)}`);
|
|
815
1039
|
}
|
|
816
1040
|
// Apply severity filtering: hide codes whose effective severity is 'ignore'.
|
|
817
|
-
// Allow is deliberately NOT applied — audit
|
|
1041
|
+
// Allow is deliberately NOT applied — audit shows every finding.
|
|
818
1042
|
const results = applySeverityFilter(rawResults, config);
|
|
819
1043
|
const effectiveSettings = await resolveEffectiveSettings(options, scanPath, logger);
|
|
820
|
-
// Build config-aware context scoped to the single derived config root —
|
|
821
|
-
// used by compat analysis to resolve the plugin-level `targets` union.
|
|
822
|
-
// VAT's design: one config per project, no composition.
|
|
823
|
-
const vatContextForCompat = await buildVATProjectContext(deriveConfigRoot(scanPath), logger);
|
|
824
1044
|
// Run compatibility analysis if --compat flag is set
|
|
825
1045
|
const compatMap = options.compat
|
|
826
1046
|
? await runCompatAnalysis(results, logger, scanRoot, effectiveSettings, vatContextForCompat)
|
|
@@ -881,7 +1101,6 @@ export async function auditCommand(targetPath, options) {
|
|
|
881
1101
|
// cloned https://github.com/<that>.git instead, reaching out to the
|
|
882
1102
|
// network with a name derived from the caller's own directory layout.
|
|
883
1103
|
// Shorthand is a fallback for arguments that name nothing on disk.
|
|
884
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- targetPath is the user's own audit target; existence is the question being asked
|
|
885
1104
|
if (targetPath !== undefined && !fsExistsSync(targetPath) && isGitUrl(targetPath)) {
|
|
886
1105
|
await runUrlAudit(targetPath, options);
|
|
887
1106
|
return;
|
|
@@ -893,6 +1112,12 @@ export async function auditCommand(targetPath, options) {
|
|
|
893
1112
|
return;
|
|
894
1113
|
}
|
|
895
1114
|
const scanPath = targetPath ? safePath.resolve(targetPath) : process.cwd();
|
|
1115
|
+
const unusable = await unusableRootReason(scanPath);
|
|
1116
|
+
if (unusable !== undefined) {
|
|
1117
|
+
// `return` the call though its type is `never`: under test `process.exit`
|
|
1118
|
+
// is a spy that returns, and a bare call would fall through into the scan.
|
|
1119
|
+
return handleExpectedFailure(unusable, ExitCode.ERROR, startTime);
|
|
1120
|
+
}
|
|
896
1121
|
await runAuditAtPath(scanPath, options);
|
|
897
1122
|
}
|
|
898
1123
|
catch (error) {
|
|
@@ -902,6 +1127,41 @@ export async function auditCommand(targetPath, options) {
|
|
|
902
1127
|
handleCommandError(error, logger, startTime, 'Audit');
|
|
903
1128
|
}
|
|
904
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Why the ROOT argument cannot be audited at all, or `undefined` when the scan
|
|
1132
|
+
* may start.
|
|
1133
|
+
*
|
|
1134
|
+
* The exit-code contract every verb shares: a path that does not exist, or a
|
|
1135
|
+
* file no audit lane recognises, is the INVOCATION's mistake — `ExitCode.ERROR`
|
|
1136
|
+
* (2) before any scan, the same ending `vat resources validate` and `vat skill
|
|
1137
|
+
* review` give the same argument. Until this check the audit ran anyway and
|
|
1138
|
+
* published `UNKNOWN_FORMAT` as a finding at exit 1 with `filesScanned: 1`: a
|
|
1139
|
+
* typo in a CI step read as "the tree failed its gate", and the denominator
|
|
1140
|
+
* counted a file that was never there.
|
|
1141
|
+
*
|
|
1142
|
+
* ONLY the root, and only these two conditions. A root the OS refuses to stat
|
|
1143
|
+
* is not answered here — the scan degrades it to `SCAN_PATH_UNREADABLE`, which
|
|
1144
|
+
* is not counted as a scanned file, so a tree with nothing readable ends on the
|
|
1145
|
+
* zero-files refusal (exit 1) like any other tree that yielded nothing. A
|
|
1146
|
+
* sub-path inside a readable tree is never the invocation's fault and stays a
|
|
1147
|
+
* finding. Pinned by the `audit → error` rows of `exit-codes.system.test.ts`.
|
|
1148
|
+
*/
|
|
1149
|
+
async function unusableRootReason(scanPath) {
|
|
1150
|
+
let isDirectory;
|
|
1151
|
+
try {
|
|
1152
|
+
isDirectory = fs.lstatSync(scanPath).isDirectory();
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
if (isPathAbsentError(error))
|
|
1156
|
+
return `Path does not exist: ${scanPath}`;
|
|
1157
|
+
return undefined;
|
|
1158
|
+
}
|
|
1159
|
+
if (isDirectory || detectFormat(scanPath) === RESOURCE_TYPE_AGENT_SKILL)
|
|
1160
|
+
return undefined;
|
|
1161
|
+
if ((await detectResourceFormat(scanPath)).type !== 'unknown')
|
|
1162
|
+
return undefined;
|
|
1163
|
+
return `Path is not a resource this command can audit (a SKILL.md, a plugin or marketplace directory, or a Claude registry file): ${scanPath}`;
|
|
1164
|
+
}
|
|
905
1165
|
/**
|
|
906
1166
|
* Dispatch a single surface to the matching validator. Keeps the multi-surface
|
|
907
1167
|
* branch of {@link getValidationResults} focused so cognitive complexity stays
|
|
@@ -1004,22 +1264,50 @@ async function appendPluginInventoryToSurfaceResults(surfaceResults, scanPath, l
|
|
|
1004
1264
|
}
|
|
1005
1265
|
/**
|
|
1006
1266
|
* Surface `inventory.parseErrors[]` as `PLUGIN_INVALID_JSON` findings on
|
|
1007
|
-
* `result`, skipping the
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1267
|
+
* `result`, skipping the entries another lane already files under their own
|
|
1268
|
+
* code:
|
|
1269
|
+
*
|
|
1270
|
+
* - the plugin.json manifest entry, covered by `validatePlugin`;
|
|
1271
|
+
* - every `SKILL.md` entry. The skill extractor records a SKILL.md it could
|
|
1272
|
+
* not open, could not parse, or could not link-walk on the plugin's
|
|
1273
|
+
* `parseErrors[]` — and each of those is the SKILL's finding, filed on the
|
|
1274
|
+
* skill by its own validation (`SCAN_PATH_UNREADABLE`,
|
|
1275
|
+
* `SKILL_MISSING_FRONTMATTER`) in both the plugin-target lane
|
|
1276
|
+
* (`validatePluginSkillsViaInventory`, over the same inventory) and the
|
|
1277
|
+
* directory lane (the walk reaches every SKILL.md beneath the plugin).
|
|
1278
|
+
* Re-filing it here was a second copy under a code that names a file it is
|
|
1279
|
+
* not (plugin.json) and a format it is not (JSON), at error severity beside
|
|
1280
|
+
* the skill lane's warning, and carrying the build host's absolute path.
|
|
1281
|
+
*
|
|
1282
|
+
* What remains — hooks/hooks.json, .mcp.json, a directory the crawl could not
|
|
1283
|
+
* list — is appended with error severity when the SUBJECT is defective, and
|
|
1284
|
+
* as `SCAN_PATH_UNREADABLE` (warning) when the row is marked `unreadable`
|
|
1285
|
+
* (the OS refused the path); the message is spelled scan-root-relative
|
|
1286
|
+
* wherever it names the file.
|
|
1010
1287
|
*/
|
|
1011
1288
|
function appendInventoryParseErrors(result, inv, locationRoot) {
|
|
1012
1289
|
const pluginJsonSuffix = safePath.join('.claude-plugin', 'plugin.json');
|
|
1290
|
+
// One refused path, one refusal: the distributed-tree detector crawls the
|
|
1291
|
+
// same directories and files the ones it cannot list on this same result.
|
|
1292
|
+
const refusalsFiled = new Set(result.issues.filter((issue) => issue.code === 'SCAN_PATH_UNREADABLE').map((issue) => issue.location));
|
|
1013
1293
|
const parseIssues = [];
|
|
1014
1294
|
for (const err of inv.parseErrors) {
|
|
1015
|
-
if (err.path.endsWith(pluginJsonSuffix))
|
|
1295
|
+
if (err.path.endsWith(pluginJsonSuffix) || basename(err.path) === 'SKILL.md')
|
|
1016
1296
|
continue;
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1297
|
+
const message = withRelativePath(err.message, err.path, locationRoot);
|
|
1298
|
+
const location = issueLocation(err.path, locationRoot);
|
|
1299
|
+
if (err.unreadable !== true) {
|
|
1300
|
+
parseIssues.push({ severity: 'error', code: 'PLUGIN_INVALID_JSON', message, location });
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
// A path the OS refused was not examined; that is the run degrading, not
|
|
1304
|
+
// a defect of the plugin, and it is filed at the severity every other lane
|
|
1305
|
+
// gives a refusal. Under `PLUGIN_INVALID_JSON` (error) an `EACCES` on one
|
|
1306
|
+
// skill directory failed the whole audit over a mode bit.
|
|
1307
|
+
if (refusalsFiled.has(location))
|
|
1308
|
+
continue;
|
|
1309
|
+
refusalsFiled.add(location);
|
|
1310
|
+
parseIssues.push(materializeIssue('SCAN_PATH_UNREADABLE', { location, detail: `${location}: ${message}` }));
|
|
1023
1311
|
}
|
|
1024
1312
|
// Status was hand-set to 'error' here while `issueCounts` and `summary` kept
|
|
1025
1313
|
// describing the pre-append list. One appender derives all three.
|
|
@@ -1043,7 +1331,8 @@ function appendInventoryParseErrors(result, inv, locationRoot) {
|
|
|
1043
1331
|
* way into a validator unprotected: a `SKILL.md` named directly on the command
|
|
1044
1332
|
* line, a plugin directory, a marketplace, each surface of a multi-surface tree.
|
|
1045
1333
|
* All of them still ended the whole run with `status: error`, exit 2 and zero
|
|
1046
|
-
* findings — the exact symptom of
|
|
1334
|
+
* findings — the exact symptom of the unreadable-directory defect, in the lanes
|
|
1335
|
+
* nobody had checked.
|
|
1047
1336
|
*
|
|
1048
1337
|
* Guarding the seven dispatch sites individually would have been the same mistake
|
|
1049
1338
|
* a seventh time. This function is what every one of them is reached through, so
|
|
@@ -1058,7 +1347,7 @@ function appendInventoryParseErrors(result, inv, locationRoot) {
|
|
|
1058
1347
|
*/
|
|
1059
1348
|
export async function getValidationResults(scanPath, recursive, options, logger, locationRoot) {
|
|
1060
1349
|
try {
|
|
1061
|
-
return await validateAuditSubject(scanPath, recursive, options, logger, locationRoot);
|
|
1350
|
+
return dedupeUnreadablePathResults(await validateAuditSubject(scanPath, recursive, options, logger, locationRoot));
|
|
1062
1351
|
}
|
|
1063
1352
|
catch (error) {
|
|
1064
1353
|
// Only the filesystem refusing the subject degrades; a defect in a validator
|
|
@@ -1069,6 +1358,83 @@ export async function getValidationResults(scanPath, recursive, options, logger,
|
|
|
1069
1358
|
return [unreadablePathResult(scanPath, error, locationRoot)];
|
|
1070
1359
|
}
|
|
1071
1360
|
}
|
|
1361
|
+
/**
|
|
1362
|
+
* One refused path, one `SCAN_PATH_UNREADABLE` — however many lanes met it.
|
|
1363
|
+
*
|
|
1364
|
+
* Two lanes can reach the same unreadable directory in one run: the directory
|
|
1365
|
+
* walk (`scanDirectory`, which files the synthetic {@link unreadablePathResult})
|
|
1366
|
+
* and the distributed-tree detector, which crawls a skill's own directory at any
|
|
1367
|
+
* depth and reports what it could not list as an issue ON the skill's result.
|
|
1368
|
+
* Their populations overlap, but neither contains the other: the walk skips
|
|
1369
|
+
* what `--exclude`, the project's `resources.exclude` and gitignore skip, and
|
|
1370
|
+
* does not descend at all under `--no-recursive` or when the SKILL.md is named
|
|
1371
|
+
* directly — the detector enters all of those. So "the walk already reports
|
|
1372
|
+
* everything under the scan root" is false in both directions, and a rule that
|
|
1373
|
+
* silenced one lane by geography would drop the refusal in exactly the cases
|
|
1374
|
+
* where it is the only report. The rule is instead keyed on the FACT: the
|
|
1375
|
+
* code and the location, which both lanes derive from the same directory and
|
|
1376
|
+
* the same anchor root.
|
|
1377
|
+
*
|
|
1378
|
+
* The hosted issue wins and the synthetic result is dropped, not the reverse:
|
|
1379
|
+
* the detector's copy says which SKILL's crawl was cut short, which the
|
|
1380
|
+
* synthetic result cannot, and it is the only copy the direct-target lane ever
|
|
1381
|
+
* produces — so keeping it makes the two lanes' reports the same shape. A
|
|
1382
|
+
* synthetic result that carried nothing but the duplicate is removed whole; one
|
|
1383
|
+
* that somehow carried more keeps its other issues (it cannot today, but the
|
|
1384
|
+
* filter says what it means rather than assuming).
|
|
1385
|
+
*
|
|
1386
|
+
* A THIRD lane files synthetic results too: skill discovery under a governing
|
|
1387
|
+
* config, which refuses a directory its `skills.include` cannot list and is
|
|
1388
|
+
* recorded through {@link recordRefusedDiscovery}. In the directory lane the
|
|
1389
|
+
* walk has already filed its own synthetic result on that same directory, so
|
|
1390
|
+
* two synthetic results can carry one fact. Among synthetics the FIRST wins and
|
|
1391
|
+
* later copies of the same location are dropped — the walk's copy carries the
|
|
1392
|
+
* raw errno, the discovery's copy the crawl's remedy sentence, and the stderr
|
|
1393
|
+
* warning carries the latter either way.
|
|
1394
|
+
*/
|
|
1395
|
+
function dedupeUnreadablePathResults(results) {
|
|
1396
|
+
const reported = new Set();
|
|
1397
|
+
for (const result of results) {
|
|
1398
|
+
if (result.type === 'unknown')
|
|
1399
|
+
continue;
|
|
1400
|
+
for (const issue of result.issues) {
|
|
1401
|
+
if (issue.code === 'SCAN_PATH_UNREADABLE')
|
|
1402
|
+
reported.add(issue.location ?? '');
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return results.flatMap((result) => {
|
|
1406
|
+
if (result.type !== 'unknown')
|
|
1407
|
+
return [result];
|
|
1408
|
+
const kept = withoutHostedRefusals(result, reported);
|
|
1409
|
+
for (const survivor of kept) {
|
|
1410
|
+
for (const issue of survivor.issues) {
|
|
1411
|
+
if (issue.code === 'SCAN_PATH_UNREADABLE')
|
|
1412
|
+
reported.add(issue.location ?? '');
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return kept;
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* A synthetic result minus the refusals some hosted result already carries —
|
|
1420
|
+
* as an empty array when nothing is left, so {@link dedupeUnreadablePathResults}
|
|
1421
|
+
* can `flatMap` it away.
|
|
1422
|
+
*/
|
|
1423
|
+
function withoutHostedRefusals(result, hosted) {
|
|
1424
|
+
const issues = result.issues.filter((issue) => !(issue.code === 'SCAN_PATH_UNREADABLE' && hosted.has(issue.location ?? '')));
|
|
1425
|
+
if (issues.length === 0)
|
|
1426
|
+
return [];
|
|
1427
|
+
if (issues.length === result.issues.length)
|
|
1428
|
+
return [result];
|
|
1429
|
+
const issueCounts = countBySeverity(issues);
|
|
1430
|
+
return [{
|
|
1431
|
+
...result,
|
|
1432
|
+
status: calculateValidationStatus(issues),
|
|
1433
|
+
summary: formatCountsSummary(issueCounts),
|
|
1434
|
+
issues,
|
|
1435
|
+
issueCounts,
|
|
1436
|
+
}];
|
|
1437
|
+
}
|
|
1072
1438
|
async function validateAuditSubject(scanPath, recursive, options, logger, locationRoot) {
|
|
1073
1439
|
const format = detectFormat(scanPath);
|
|
1074
1440
|
// Special handling for direct SKILL.md file
|
|
@@ -1133,26 +1499,45 @@ async function validateAuditSubject(scanPath, recursive, options, logger, locati
|
|
|
1133
1499
|
return [result];
|
|
1134
1500
|
}
|
|
1135
1501
|
// If unknown format, check if it's a directory we can scan
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
// merged into `options.exclude`, whose patterns are relative to the
|
|
1144
|
-
// directory the operator named. See resolveProjectExcludes.
|
|
1145
|
-
return scanDirectory(scanPath, recursive, options, logger, await resolveScanContext(scanPath, locationRoot, logger));
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
catch {
|
|
1149
|
-
// Path doesn't exist or not accessible, let validate() handle it
|
|
1502
|
+
if (await isScannableDirectory(scanPath)) {
|
|
1503
|
+
logger.debug('Scanning directory for resources');
|
|
1504
|
+
// The project's `resources.exclude` is applied by the scan context,
|
|
1505
|
+
// on the project-root basis its globs were written against — NOT
|
|
1506
|
+
// merged into `options.exclude`, whose patterns are relative to the
|
|
1507
|
+
// directory the operator named. See resolveProjectExcludes.
|
|
1508
|
+
return scanDirectory(scanPath, recursive, options, logger, await resolveScanContext(scanPath, locationRoot, logger));
|
|
1150
1509
|
}
|
|
1151
1510
|
// Unknown resource type - use unified validator which will return appropriate error
|
|
1152
1511
|
logger.debug(`Unknown resource type at: ${scanPath}`);
|
|
1153
1512
|
const result = await validate(scanPath, { locationRoot });
|
|
1154
1513
|
return [result];
|
|
1155
1514
|
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Whether `scanPath` is a directory the walk can enter.
|
|
1517
|
+
*
|
|
1518
|
+
* `false` for a file and for NOTHING — `validate()` then names the path as
|
|
1519
|
+
* unknown or absent, which is the answer the operator can act on. A refusal on
|
|
1520
|
+
* the stat is neither: it propagates to `getValidationResults`, whose own catch
|
|
1521
|
+
* files it as `SCAN_PATH_UNREADABLE`, so the audit still degrades (the run
|
|
1522
|
+
* completes, the refusal a finding) rather than refusing the run.
|
|
1523
|
+
*
|
|
1524
|
+
* 🪤 The stat used to share ONE `try` with `scanDirectory` itself, and the
|
|
1525
|
+
* catch was bare. Every throw from the scan — a crawl refusal, a `TypeError`
|
|
1526
|
+
* two frames down — was absorbed into "let validate() handle it", and
|
|
1527
|
+
* `validate()` then reported the directory as an unknown resource format. A
|
|
1528
|
+
* scan that examined nothing was reported as a directory that was nothing.
|
|
1529
|
+
*/
|
|
1530
|
+
async function isScannableDirectory(scanPath) {
|
|
1531
|
+
const fsp = await import('node:fs/promises');
|
|
1532
|
+
try {
|
|
1533
|
+
return (await fsp.stat(scanPath)).isDirectory();
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
if (isPathAbsentError(error))
|
|
1537
|
+
return false;
|
|
1538
|
+
throw error;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1156
1541
|
/**
|
|
1157
1542
|
* Inventory-driven plugin skill walker. Builds the plugin inventory via
|
|
1158
1543
|
* `extractClaudePluginInventory` and dispatches `validateSingleSkill` for each
|
|
@@ -1218,9 +1603,9 @@ function resolveConfigTargetsForPlugin(pluginDir, vatContext) {
|
|
|
1218
1603
|
const union = new Set();
|
|
1219
1604
|
for (const [skillPathAbs, packagingConfig] of vatContext.skillConfigs) {
|
|
1220
1605
|
const rel = safePath.relative(pluginDirAbs, skillPathAbs);
|
|
1221
|
-
// Skill lives inside the plugin directory iff relative path
|
|
1222
|
-
//
|
|
1223
|
-
if (rel === '' ||
|
|
1606
|
+
// Skill lives inside the plugin directory iff the relative path neither
|
|
1607
|
+
// climbs out nor (on Windows, cross-drive) comes back absolute.
|
|
1608
|
+
if (rel === '' || relativeEscapesRoot(rel))
|
|
1224
1609
|
continue;
|
|
1225
1610
|
const skillTargets = packagingConfig.targets;
|
|
1226
1611
|
if (skillTargets === undefined)
|
|
@@ -1232,7 +1617,15 @@ function resolveConfigTargetsForPlugin(pluginDir, vatContext) {
|
|
|
1232
1617
|
return union.size === 0 ? undefined : [...union];
|
|
1233
1618
|
}
|
|
1234
1619
|
/**
|
|
1235
|
-
*
|
|
1620
|
+
* `message`, with `absolutePath` spelled root-relative wherever it appears.
|
|
1621
|
+
* An OS refusal names the path it refused, absolutely; every other path in
|
|
1622
|
+
* the document is relative to `root`, and this one leaks the build host.
|
|
1623
|
+
*/
|
|
1624
|
+
function withRelativePath(message, absolutePath, locationRoot) {
|
|
1625
|
+
return message.replaceAll(absolutePath, issueLocation(absolutePath, locationRoot) || '.');
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Run compatibility analysis on plugin results and return a map of path -> PluginCompatEntry.
|
|
1236
1629
|
* Non-plugin results are skipped silently.
|
|
1237
1630
|
* When effectiveSettings is provided, also runs settings conflict detection.
|
|
1238
1631
|
*
|
|
@@ -1253,67 +1646,99 @@ export async function runCompatAnalysis(results, logger, locationRoot, effective
|
|
|
1253
1646
|
for (const result of results) {
|
|
1254
1647
|
if (result.type !== RESOURCE_TYPE_CLAUDE_PLUGIN)
|
|
1255
1648
|
continue;
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
: { configTargets };
|
|
1262
|
-
const compat = await analyzeCompatibility(result.path, locationRoot, analyzeOptions);
|
|
1263
|
-
// Settings conflict detection (when --settings flag is used)
|
|
1264
|
-
if (effectiveSettings === undefined) {
|
|
1265
|
-
compatMap.set(result.path, compat);
|
|
1266
|
-
}
|
|
1267
|
-
else {
|
|
1268
|
-
try {
|
|
1269
|
-
const settingsConflicts = await checkSettingsCompatibility(result.path, effectiveSettings);
|
|
1270
|
-
compatMap.set(result.path, { ...compat, settingsConflicts });
|
|
1271
|
-
}
|
|
1272
|
-
catch (settingsErr) {
|
|
1273
|
-
logger.debug(`Settings compat check skipped for ${result.path}: ${String(settingsErr)}`);
|
|
1274
|
-
compatMap.set(result.path, compat);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
catch (err) {
|
|
1279
|
-
// Log but do not fail the audit — compat analysis is best-effort
|
|
1280
|
-
logger.debug(`Compatibility analysis skipped for ${result.path}: ${String(err)}`);
|
|
1649
|
+
const entry = {
|
|
1650
|
+
compat: await analyzeOrExplain(result.path, locationRoot, vatContext, logger),
|
|
1651
|
+
};
|
|
1652
|
+
if (effectiveSettings !== undefined) {
|
|
1653
|
+
entry.settings = await settingsVerdict(result.path, effectiveSettings, locationRoot, logger);
|
|
1281
1654
|
}
|
|
1655
|
+
compatMap.set(result.path, entry);
|
|
1282
1656
|
}
|
|
1283
1657
|
return compatMap;
|
|
1284
1658
|
}
|
|
1659
|
+
/**
|
|
1660
|
+
* The analyzer's result for one plugin, or — when it threw — the reason, said
|
|
1661
|
+
* on stderr and carried into the document. Compat analysis is best-effort:
|
|
1662
|
+
* it never fails the audit, but a plugin it could not analyze is reported as
|
|
1663
|
+
* exactly that, never as a plugin with no `compatibility:` block.
|
|
1664
|
+
*/
|
|
1665
|
+
async function analyzeOrExplain(pluginDir, locationRoot, vatContext, logger) {
|
|
1666
|
+
try {
|
|
1667
|
+
logger.debug(`Running compatibility analysis for: ${pluginDir}`);
|
|
1668
|
+
const configTargets = resolveConfigTargetsForPlugin(pluginDir, vatContext);
|
|
1669
|
+
const analyzeOptions = configTargets === undefined ? undefined : { configTargets };
|
|
1670
|
+
return await analyzeCompatibility(pluginDir, locationRoot, analyzeOptions);
|
|
1671
|
+
}
|
|
1672
|
+
catch (err) {
|
|
1673
|
+
const refused = typeof err.path === 'string' ? err.path : undefined;
|
|
1674
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
1675
|
+
const reason = refused === undefined ? raw : withRelativePath(raw, refused, locationRoot);
|
|
1676
|
+
logger.warn(`Compatibility analysis could not run for ${issueLocation(pluginDir, locationRoot) || '.'}: ${reason}`);
|
|
1677
|
+
return { analyzed: false, reason };
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* One plugin's settings verdict: the checker's conflicts and the paths it
|
|
1682
|
+
* never saw — and, when the checker itself fails, the plugin as the one thing
|
|
1683
|
+
* unchecked, rather than no `settings:` block at all.
|
|
1684
|
+
*
|
|
1685
|
+
* The checker failing used to be `logger.debug` and a result with no settings
|
|
1686
|
+
* block, so a plugin the check could not run on was indistinguishable from one
|
|
1687
|
+
* the operator never asked about. A run that asked for `--settings` and got no
|
|
1688
|
+
* answer for a plugin is told so on stderr and in the document.
|
|
1689
|
+
*/
|
|
1690
|
+
async function settingsVerdict(pluginDir, effectiveSettings, locationRoot, logger) {
|
|
1691
|
+
try {
|
|
1692
|
+
return await checkSettingsCompatibility(pluginDir, effectiveSettings);
|
|
1693
|
+
}
|
|
1694
|
+
catch (settingsErr) {
|
|
1695
|
+
const reason = settingsErr instanceof Error ? settingsErr.message : String(settingsErr);
|
|
1696
|
+
logger.warn(`Settings compatibility could not be checked for ${issueLocation(pluginDir, locationRoot) || '.'}: ${reason}`);
|
|
1697
|
+
return { conflicts: [], unchecked: [{ path: pluginDir, reason }] };
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* The `settings:` block for one plugin, every path anchored at `locationRoot`.
|
|
1702
|
+
* `compatible` is derived from BOTH halves: zero conflicts over a skill the
|
|
1703
|
+
* check never read is not compatibility, it is silence.
|
|
1704
|
+
*/
|
|
1705
|
+
function renderSettingsBlock(check, locationRoot) {
|
|
1706
|
+
const settings = {
|
|
1707
|
+
compatible: check.conflicts.length === 0 && check.unchecked.length === 0,
|
|
1708
|
+
conflicts: check.conflicts,
|
|
1709
|
+
};
|
|
1710
|
+
if (check.unchecked.length > 0) {
|
|
1711
|
+
settings.unchecked = check.unchecked.map(({ path, reason }) => ({
|
|
1712
|
+
path: issueLocation(path, locationRoot) || '.',
|
|
1713
|
+
reason: withRelativePath(reason, path, locationRoot),
|
|
1714
|
+
}));
|
|
1715
|
+
}
|
|
1716
|
+
return settings;
|
|
1717
|
+
}
|
|
1285
1718
|
/**
|
|
1286
1719
|
* Merge compatibility analysis results into validation result output objects.
|
|
1287
1720
|
* Returns an array of plain objects ready for YAML serialization.
|
|
1288
|
-
*
|
|
1721
|
+
* A plugin the run asked `--settings` about gets a `settings:` block, whatever
|
|
1722
|
+
* the analyzer did.
|
|
1289
1723
|
*/
|
|
1290
|
-
function mergeCompatIntoResults(results, compatMap) {
|
|
1724
|
+
function mergeCompatIntoResults(results, compatMap, locationRoot) {
|
|
1291
1725
|
return results.map(r => {
|
|
1292
|
-
const
|
|
1293
|
-
if (
|
|
1726
|
+
const entry = compatMap.get(r.path);
|
|
1727
|
+
if (entry === undefined)
|
|
1294
1728
|
return r;
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
...r,
|
|
1300
|
-
compatibility: compatWithoutSettings,
|
|
1301
|
-
settings: {
|
|
1302
|
-
compatible: settingsConflicts.length === 0,
|
|
1303
|
-
conflicts: settingsConflicts,
|
|
1304
|
-
},
|
|
1305
|
-
};
|
|
1306
|
-
}
|
|
1307
|
-
return { ...r, compatibility: compat };
|
|
1729
|
+
const merged = { ...r, compatibility: entry.compat };
|
|
1730
|
+
if (entry.settings !== undefined)
|
|
1731
|
+
merged.settings = renderSettingsBlock(entry.settings, locationRoot);
|
|
1732
|
+
return merged;
|
|
1308
1733
|
});
|
|
1309
1734
|
}
|
|
1310
1735
|
/**
|
|
1311
1736
|
* Apply compatibility data to results if a compatMap is provided and non-empty.
|
|
1312
1737
|
* Returns the original results array when no compat data is available.
|
|
1313
1738
|
*/
|
|
1314
|
-
function applyCompatMap(results, compatMap) {
|
|
1739
|
+
function applyCompatMap(results, compatMap, locationRoot) {
|
|
1315
1740
|
if (compatMap !== undefined && compatMap.size > 0) {
|
|
1316
|
-
return mergeCompatIntoResults(results, compatMap);
|
|
1741
|
+
return mergeCompatIntoResults(results, compatMap, locationRoot);
|
|
1317
1742
|
}
|
|
1318
1743
|
return results;
|
|
1319
1744
|
}
|
|
@@ -1350,7 +1775,7 @@ function applyVerboseFilter(results, verbose) {
|
|
|
1350
1775
|
const NESTED_PATH_CARRIERS = ['linkedFiles'];
|
|
1351
1776
|
function calculateSummary(results, startTime, compatMap, verbose, root) {
|
|
1352
1777
|
const { entries, ...base } = buildBaseSummary(results, startTime);
|
|
1353
|
-
const withCompat = applyCompatMap(entries, compatMap);
|
|
1778
|
+
const withCompat = applyCompatMap(entries, compatMap, root);
|
|
1354
1779
|
return {
|
|
1355
1780
|
// Stated once, first, and the only absolute path in the document.
|
|
1356
1781
|
root,
|
|
@@ -1430,16 +1855,6 @@ function findingSubject(result, root) {
|
|
|
1430
1855
|
const directoryName = basename(result.path);
|
|
1431
1856
|
return directoryName === '' ? '.' : directoryName;
|
|
1432
1857
|
}
|
|
1433
|
-
/**
|
|
1434
|
-
* The clause every "audit found errors" line ends with.
|
|
1435
|
-
*
|
|
1436
|
-
* One string because the two summary lanes — per-file and per-skill — both make
|
|
1437
|
-
* the same claim and used to word it differently, which is how one of them ended
|
|
1438
|
-
* up saying "Audit failed" over an exit code of 0. Naming the command that DOES
|
|
1439
|
-
* gate is the actionable half: an adopter reading "advisory" still has to be
|
|
1440
|
-
* told what to reach for instead.
|
|
1441
|
-
*/
|
|
1442
|
-
const ADVISORY_EXIT_NOTE = '— advisory, exit 0; use `vat validate` to gate on this';
|
|
1443
1858
|
/**
|
|
1444
1859
|
* Render supporting evidence beneath each CAPABILITY_* issue when the
|
|
1445
1860
|
* audit was invoked with --verbose. Evidence comes from the validation
|
|
@@ -1463,24 +1878,75 @@ function renderVerboseEvidence(results, root, logger) {
|
|
|
1463
1878
|
}
|
|
1464
1879
|
}
|
|
1465
1880
|
}
|
|
1881
|
+
/**
|
|
1882
|
+
* The stderr half of a run-level refusal, so the human channel says what the
|
|
1883
|
+
* document says. The document is derived in `buildBaseSummary`; this only
|
|
1884
|
+
* RENDERS it. Both stderr lanes call it first, because "Audit successful: 0
|
|
1885
|
+
* file(s) passed" over `status: error` is the exact disagreement the refusal
|
|
1886
|
+
* exists to end (invariant 6 of `run-integrity.ts`). The run ends on
|
|
1887
|
+
* `FINDINGS`, like every other `status: error` this command publishes.
|
|
1888
|
+
*
|
|
1889
|
+
* @returns Whether a refusal was rendered, so the caller skips its verdict line
|
|
1890
|
+
*/
|
|
1891
|
+
function logRunIntegrity(runIssues, logger) {
|
|
1892
|
+
if (runIssues === undefined || runIssues.length === 0)
|
|
1893
|
+
return false;
|
|
1894
|
+
logger.error('Audit is not a verdict: it audited 0 files');
|
|
1895
|
+
for (const issue of runIssues) {
|
|
1896
|
+
for (const line of formatIssueLines(issue, ' '))
|
|
1897
|
+
logger.error(line);
|
|
1898
|
+
}
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* The stderr line for the settings lane: how many conflicts it found and how
|
|
1903
|
+
* many paths it never compared, across every plugin in the document.
|
|
1904
|
+
*
|
|
1905
|
+
* 🚩 This read `f.compatibility?.settingsConflicts` — a field the renderer had
|
|
1906
|
+
* already moved out into the `settings:` block — so the count was 0 on every
|
|
1907
|
+
* run and the line never printed. It reads the rendered block now, the same
|
|
1908
|
+
* one the YAML carries, and it counts the unchecked half too: a run that
|
|
1909
|
+
* skipped a skill says so without `--debug`.
|
|
1910
|
+
*/
|
|
1911
|
+
function logSettingsTotals(files, logger) {
|
|
1912
|
+
let conflicts = 0;
|
|
1913
|
+
let unchecked = 0;
|
|
1914
|
+
// The compat lane's own unchecked paths — a plugin the analyzer could only
|
|
1915
|
+
// partly read is in the document under `compatibility.unchecked`, and this is
|
|
1916
|
+
// the one line that keeps it from being silent on the human channel while the
|
|
1917
|
+
// settings lane says its half. `analyzed: false` has no list: it is the
|
|
1918
|
+
// plugin-wide failure, and the `reason` line already says so.
|
|
1919
|
+
let compatUnchecked = 0;
|
|
1920
|
+
for (const { compatibility, settings } of files) {
|
|
1921
|
+
conflicts += settings?.conflicts.length ?? 0;
|
|
1922
|
+
unchecked += settings?.unchecked?.length ?? 0;
|
|
1923
|
+
if (compatibility !== undefined && 'unchecked' in compatibility)
|
|
1924
|
+
compatUnchecked += compatibility.unchecked.length;
|
|
1925
|
+
}
|
|
1926
|
+
if (conflicts > 0) {
|
|
1927
|
+
logger.error(`⚠ ${conflicts} settings conflict(s) found — see 'settings' section in YAML output`);
|
|
1928
|
+
}
|
|
1929
|
+
if (unchecked > 0) {
|
|
1930
|
+
logger.error(`⚠ ${unchecked} path(s) the settings check could not compare — see 'settings.unchecked' in YAML output`);
|
|
1931
|
+
}
|
|
1932
|
+
if (compatUnchecked > 0) {
|
|
1933
|
+
logger.error(`⚠ ${compatUnchecked} path(s) the compatibility analysis could not read — see 'compatibility.unchecked' in YAML output`);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1466
1936
|
function handleAuditResults(results, summary, logger, verbose) {
|
|
1467
1937
|
const { filesWithErrors: errorCount, filesWithWarnings: warningCount, filesPassed: successCount, } = summary.summary;
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
// happened; the gate is `vat validate`, which is named here rather than
|
|
1481
|
-
// implied.
|
|
1482
|
-
if (errorCount > 0) {
|
|
1483
|
-
logger.error(`Audit found ${errorCount} file(s) with errors ${ADVISORY_EXIT_NOTE}`);
|
|
1938
|
+
logSettingsTotals(summary.files ?? [], logger);
|
|
1939
|
+
// The exit code follows the document's `status`, as it does in every other
|
|
1940
|
+
// command of this CLI: `status: error` — an error-severity finding, or a run
|
|
1941
|
+
// that audited zero files — ends on `FINDINGS`. This used to be the one
|
|
1942
|
+
// command that exited 0 over `status: error` ("advisory"), and an adopter
|
|
1943
|
+
// wiring it into CI read "Audit failed" on stderr beside a green step.
|
|
1944
|
+
const refused = logRunIntegrity(summary.issues, logger);
|
|
1945
|
+
if (refused) {
|
|
1946
|
+
// Rendered above; there is no file verdict to print over zero files.
|
|
1947
|
+
}
|
|
1948
|
+
else if (errorCount > 0) {
|
|
1949
|
+
logger.error(`Audit found ${errorCount} file(s) with errors`);
|
|
1484
1950
|
logFindingsForStatus(results, 'error', summary.root, logger.error.bind(logger), verbose);
|
|
1485
1951
|
}
|
|
1486
1952
|
else if (warningCount > 0) {
|
|
@@ -1496,7 +1962,7 @@ function handleAuditResults(results, summary, logger, verbose) {
|
|
|
1496
1962
|
: `Audit successful: ${successCount} file(s) passed`);
|
|
1497
1963
|
}
|
|
1498
1964
|
renderAuditFooter(results, logger);
|
|
1499
|
-
process.exit(0);
|
|
1965
|
+
process.exit(refused || errorCount > 0 ? ExitCode.FINDINGS : ExitCode.OK);
|
|
1500
1966
|
}
|
|
1501
1967
|
/**
|
|
1502
1968
|
* Render the skill-quality checklist footer when audit results contain
|
|
@@ -1591,17 +2057,6 @@ function logFindingsForStatus(results, status, root, logFn, verbose) {
|
|
|
1591
2057
|
logFn(line);
|
|
1592
2058
|
}
|
|
1593
2059
|
}
|
|
1594
|
-
/**
|
|
1595
|
-
* Check whether a path should be excluded during directory scanning.
|
|
1596
|
-
* For directories, checks both bare path and path with trailing slash
|
|
1597
|
-
* so patterns like "dist/**" prune the directory itself.
|
|
1598
|
-
*/
|
|
1599
|
-
function isExcludedPath(isMatch, relativePath, isDirectory) {
|
|
1600
|
-
if (isDirectory) {
|
|
1601
|
-
return isMatch(relativePath) || isMatch(relativePath + '/');
|
|
1602
|
-
}
|
|
1603
|
-
return isMatch(relativePath);
|
|
1604
|
-
}
|
|
1605
2060
|
/**
|
|
1606
2061
|
* Does the SKILL.md at `skillMdPath` own its own presence-side crawl, or has an
|
|
1607
2062
|
* ancestor crawl already covered it?
|
|
@@ -1615,39 +2070,27 @@ function ownsOwnCrawl(scanCtx, skillMdPath) {
|
|
|
1615
2070
|
return scanCtx.crawledRoot === null || !isWithin(scanCtx.crawledRoot, safePath.resolve(skillMdPath));
|
|
1616
2071
|
}
|
|
1617
2072
|
/**
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
|
|
1621
|
-
function claimSubtree(scanCtx, owner) {
|
|
1622
|
-
if (owner === null || scanCtx.crawledRoot !== null)
|
|
1623
|
-
return scanCtx;
|
|
1624
|
-
return { ...scanCtx, crawledRoot: safePath.resolve(owner) };
|
|
1625
|
-
}
|
|
1626
|
-
/**
|
|
1627
|
-
* The context this directory's SUBDIRECTORIES are walked with.
|
|
1628
|
-
*
|
|
1629
|
-
* A `SKILL.md` here means this directory's own crawl reaches every descendant at
|
|
1630
|
-
* any depth, so it — not each nested skill in turn — owns the subtree below.
|
|
2073
|
+
* The directories whose presence-side crawl already owns everything beneath
|
|
2074
|
+
* them — see {@link ScanContext.crawledRoot} for why there are two owner kinds
|
|
2075
|
+
* and why the first owner wins.
|
|
1631
2076
|
*
|
|
1632
|
-
*
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1637
|
-
* Conditional on the crawl actually RUNNING ({@link crawlOwnsSubtree}), not on
|
|
1638
|
-
* the SKILL.md merely existing: a repo-source skill's crawl is skipped
|
|
1639
|
-
* entirely, so claiming its subtree would trade a double count for a missed
|
|
1640
|
-
* finding — and provenance is not monotone down a tree.
|
|
2077
|
+
* Kept as a list of roots rather than threaded down a recursion: the
|
|
2078
|
+
* population arrives flat and top-down (an ancestor's subjects before a
|
|
2079
|
+
* descendant's), so "who owns this path" is the first claimed root that
|
|
2080
|
+
* contains it, and a claim is only ever made where no ancestor holds one.
|
|
1641
2081
|
*/
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
return
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
2082
|
+
class SubtreeClaims {
|
|
2083
|
+
roots = [];
|
|
2084
|
+
/** The claimed root containing `absolutePath`, or `null` while nothing does. */
|
|
2085
|
+
owner(absolutePath) {
|
|
2086
|
+
const resolved = safePath.resolve(absolutePath);
|
|
2087
|
+
return this.roots.find((root) => isWithin(root, resolved)) ?? null;
|
|
2088
|
+
}
|
|
2089
|
+
/** Claim everything below `dir`, unless an ancestor already did. First owner wins. */
|
|
2090
|
+
claim(dir) {
|
|
2091
|
+
if (this.owner(dir) === null)
|
|
2092
|
+
this.roots.push(safePath.resolve(dir));
|
|
2093
|
+
}
|
|
1651
2094
|
}
|
|
1652
2095
|
/**
|
|
1653
2096
|
* Handle file entry during directory scan
|
|
@@ -1678,17 +2121,28 @@ async function handleFileEntry(entry, fullPath, options, logger, scanCtx) {
|
|
|
1678
2121
|
// checks in the link-graph walk stay O(1).
|
|
1679
2122
|
// `projectSkills`: same project-wide test-input rule as the lane above,
|
|
1680
2123
|
// resolved through the same per-config-root memo.
|
|
1681
|
-
const projectSkills = await resolveProjectDeclaredEvalSuites(fullPath);
|
|
2124
|
+
const projectSkills = await resolveProjectDeclaredEvalSuites(fullPath, degradingDiscovery(findProjectRoot(safePath.resolve(fullPath, '..')) ?? locationRoot, logger, locationRoot));
|
|
2125
|
+
// `suiteProbe`: the RUN's, not this entry's — the directory walk reaches
|
|
2126
|
+
// this function once per SKILL.md it finds. See {@link runSuiteProbe}.
|
|
2127
|
+
const suiteProbe = suiteProbeForRun();
|
|
2128
|
+
// No registry is supplied on this lane, so `validateSkillForPackaging`
|
|
2129
|
+
// crawls one per project root through `crawlAndResolveRegistry` — under
|
|
2130
|
+
// audit's ruling, which is the ONLY reason one `chmod 000` sibling no
|
|
2131
|
+
// longer exits this whole run at 2 with zero findings. The root is the
|
|
2132
|
+
// one the validator itself derives (`findProjectRoot(dirname) ?? dirname`),
|
|
2133
|
+
// so the refuse sentence and the finding name the same crawl.
|
|
2134
|
+
const skillDir = safePath.resolve(fullPath, '..');
|
|
2135
|
+
const unreadable = degradingRegistry(findProjectRoot(skillDir) ?? skillDir, logger, locationRoot);
|
|
1682
2136
|
const sharedCtx = scanCtx.gitTracker === null
|
|
1683
|
-
? { locationRoot, projectSkills }
|
|
1684
|
-
: { gitTracker: scanCtx.gitTracker, locationRoot, projectSkills };
|
|
2137
|
+
? { locationRoot, projectSkills, suiteProbe, unreadable }
|
|
2138
|
+
: { gitTracker: scanCtx.gitTracker, locationRoot, projectSkills, suiteProbe, unreadable };
|
|
1685
2139
|
const packagingResult = await validateSkillForPackaging(fullPath, skillConfig, 'source', sharedCtx);
|
|
1686
2140
|
const configAware = packagingResultToValidationResult(fullPath, packagingResult, skillConfig.targets, locationRoot);
|
|
1687
2141
|
await appendDistributedTreeFindings(configAware, fullPath, locationRoot, ownsOwnCrawl(scanCtx, fullPath));
|
|
1688
2142
|
return configAware;
|
|
1689
2143
|
}
|
|
1690
|
-
// Wild mode: basic validation
|
|
1691
|
-
const validateOptions = { skillPath: fullPath, locationRoot };
|
|
2144
|
+
// Wild mode: basic validation. Severity is applied by `applySeverityFilter`.
|
|
2145
|
+
const validateOptions = { skillPath: fullPath, locationRoot, validation: {} };
|
|
1692
2146
|
if (options.warnUnreferencedFiles) {
|
|
1693
2147
|
validateOptions.checkUnreferencedFiles = true;
|
|
1694
2148
|
}
|
|
@@ -1702,89 +2156,15 @@ async function handleFileEntry(entry, fullPath, options, logger, scanCtx) {
|
|
|
1702
2156
|
return null;
|
|
1703
2157
|
}
|
|
1704
2158
|
/**
|
|
1705
|
-
*
|
|
2159
|
+
* Validate a directory the population found holding `.claude-plugin/` — a
|
|
2160
|
+
* plugin or a marketplace, whichever the unified validator decides.
|
|
1706
2161
|
*/
|
|
1707
|
-
async function
|
|
1708
|
-
|
|
1709
|
-
const { locationRoot }
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
const hasClaudePlugin = await fs.access(claudePluginDir).then(() => true).catch(() => false);
|
|
1714
|
-
if (hasClaudePlugin) {
|
|
1715
|
-
logger.debug(`Validating resource directory: ${fullPath}`);
|
|
1716
|
-
const result = await validate(fullPath, { validatePlugin, locationRoot });
|
|
1717
|
-
const inv = await pluginInventoryAt(fullPath);
|
|
1718
|
-
appendInventoryParseErrors(result, inv, locationRoot);
|
|
1719
|
-
results.push(result);
|
|
1720
|
-
}
|
|
1721
|
-
// Recurse into subdirectories (both plugin/marketplace dirs and regular dirs).
|
|
1722
|
-
// Below a plugin, `validatePlugin` has already crawled the whole subtree for
|
|
1723
|
-
// agent-instruction files, so every crawl below must stand down or one file
|
|
1724
|
-
// is reported twice. See {@link ScanContext.crawledRoot} — first owner wins,
|
|
1725
|
-
// and it never clears on the way down.
|
|
1726
|
-
const childCtx = claimSubtree(scanCtx, hasClaudePlugin ? fullPath : null);
|
|
1727
|
-
if (recursive) {
|
|
1728
|
-
const subResults = await scanDirectory(fullPath, recursive, options, logger, childCtx, baseDir, nestedConfigLog);
|
|
1729
|
-
results.push(...subResults);
|
|
1730
|
-
}
|
|
1731
|
-
return results;
|
|
1732
|
-
}
|
|
1733
|
-
/**
|
|
1734
|
-
* Compile the governing project's `resources.exclude` for a scan.
|
|
1735
|
-
*
|
|
1736
|
-
* The config is found by walking UP from the scan directory ({@link findProjectRoot}),
|
|
1737
|
-
* not by looking in it. Looking only in the scan directory is how a path argument
|
|
1738
|
-
* silently voided every exclude the project had declared: `vat audit
|
|
1739
|
-
* packages/x/resources/skills/` found no config there, so a package that excludes
|
|
1740
|
-
* its deliberately-broken eval fixtures had them audited as production skills the
|
|
1741
|
-
* moment anyone named a subdirectory. Commit 8a466b0c fixed the same defect for
|
|
1742
|
-
* `vat resources validate|scan` and `vat rag index`; this is the lane it missed.
|
|
1743
|
-
* The rule is the same one that commit established: a path argument says WHICH
|
|
1744
|
-
* tree to audit, and `exclude` applies either way.
|
|
1745
|
-
*
|
|
1746
|
-
* With one deliberate exception, mirroring the gitignore rule in
|
|
1747
|
-
* {@link resolveScanContext}: when the operator points the scan AT an excluded
|
|
1748
|
-
* tree, their explicit intent wins and the excludes are dropped for that run.
|
|
1749
|
-
* Otherwise naming an excluded directory would report `filesScanned: 0,
|
|
1750
|
-
* status: success` — a green run that scanned nothing.
|
|
1751
|
-
*/
|
|
1752
|
-
function resolveProjectExcludes(scanDir, logger) {
|
|
1753
|
-
const projectRoot = findProjectRoot(scanDir);
|
|
1754
|
-
if (projectRoot === null)
|
|
1755
|
-
return null;
|
|
1756
|
-
let patterns;
|
|
1757
|
-
try {
|
|
1758
|
-
patterns = loadConfig(projectRoot)?.resources?.exclude ?? [];
|
|
1759
|
-
}
|
|
1760
|
-
catch (err) {
|
|
1761
|
-
// Audit is a bulk linter over trees it does not own; a broken governing
|
|
1762
|
-
// config must not abort the scan. Say so rather than dropping it silently.
|
|
1763
|
-
//
|
|
1764
|
-
// 🚨 `warn`, not `debug`. This comment already said "say so" while logging at
|
|
1765
|
-
// a level nobody sees without `--debug`, so what actually happened was the
|
|
1766
|
-
// silent drop it forbids — and the consequence is the one this function's own
|
|
1767
|
-
// docstring spells out: EVERY `resources.exclude` the project declared is
|
|
1768
|
-
// void, so a package that excludes its deliberately-broken eval fixtures has
|
|
1769
|
-
// them audited as production skills. That is findings APPEARING, not findings
|
|
1770
|
-
// disappearing, which is exactly the direction an operator will read as VAT
|
|
1771
|
-
// being wrong rather than as their config being broken.
|
|
1772
|
-
logger.warn(`Config at ${projectRoot} could not be read; every resources.exclude it declares is`
|
|
1773
|
-
+ ` dropped for this run, so excluded trees are audited as ordinary source: ${String(err)}`);
|
|
1774
|
-
return null;
|
|
1775
|
-
}
|
|
1776
|
-
if (patterns.length === 0)
|
|
1777
|
-
return null;
|
|
1778
|
-
// dot:true so excludes like `**/.cache/*` match through dotfile dirs;
|
|
1779
|
-
// without it the exclude silently never fires.
|
|
1780
|
-
const isMatch = picomatch([...patterns], { dot: true });
|
|
1781
|
-
const scanRelToProject = safePath.relative(projectRoot, safePath.resolve(scanDir)).replaceAll('\\', '/');
|
|
1782
|
-
if (scanRelToProject !== '' && isExcludedPath(isMatch, scanRelToProject, true)) {
|
|
1783
|
-
logger.debug(`Scan root ${scanRelToProject} is excluded by the config at ${projectRoot}; ` +
|
|
1784
|
-
'auditing it anyway because it was named explicitly');
|
|
1785
|
-
return null;
|
|
1786
|
-
}
|
|
1787
|
-
return { isMatch, base: projectRoot, label: 'excluded by config' };
|
|
2162
|
+
async function validatePluginDirectory(dir, logger, locationRoot) {
|
|
2163
|
+
logger.debug(`Validating resource directory: ${dir}`);
|
|
2164
|
+
const result = await validate(dir, { validatePlugin, locationRoot });
|
|
2165
|
+
const inv = await pluginInventoryAt(dir);
|
|
2166
|
+
appendInventoryParseErrors(result, inv, locationRoot);
|
|
2167
|
+
return result;
|
|
1788
2168
|
}
|
|
1789
2169
|
/**
|
|
1790
2170
|
* Cache of (projectRoot → registry for the *inventory* link walk).
|
|
@@ -1855,6 +2235,7 @@ export function resetAuditCaches() {
|
|
|
1855
2235
|
resetPackagingRegistryCache();
|
|
1856
2236
|
inventoryRegistryCache.clear();
|
|
1857
2237
|
unloadableConfigResults.clear();
|
|
2238
|
+
runSuiteProbe = undefined;
|
|
1858
2239
|
resetProjectRootCaches();
|
|
1859
2240
|
resetLoadedConfigCache();
|
|
1860
2241
|
resetSkillDiscoveryCache();
|
|
@@ -1883,43 +2264,6 @@ async function resolveScanContext(dirPath, locationRoot, logger) {
|
|
|
1883
2264
|
}
|
|
1884
2265
|
return { gitRoot: detectedRoot, gitTracker: tracker, locationRoot, projectExcludes, crawledRoot: null };
|
|
1885
2266
|
}
|
|
1886
|
-
/**
|
|
1887
|
-
* Build the gitignore exclusion map for a set of entry paths.
|
|
1888
|
-
* Returns null when gitignore filtering is not applicable (no git root,
|
|
1889
|
-
* --include-artifacts, or outside a git repo).
|
|
1890
|
-
*/
|
|
1891
|
-
function buildGitIgnoreMap(entryPaths, gitTracker, includeArtifacts) {
|
|
1892
|
-
if (gitTracker === null || includeArtifacts) {
|
|
1893
|
-
return null;
|
|
1894
|
-
}
|
|
1895
|
-
const map = new Map();
|
|
1896
|
-
for (const entryPath of entryPaths) {
|
|
1897
|
-
map.set(entryPath, gitTracker.isIgnoredByActiveSet(entryPath));
|
|
1898
|
-
}
|
|
1899
|
-
return map;
|
|
1900
|
-
}
|
|
1901
|
-
/**
|
|
1902
|
-
* Check whether a directory entry should be skipped during scanning.
|
|
1903
|
-
* Returns a reason string for debug logging, or null if the entry should be kept.
|
|
1904
|
-
*/
|
|
1905
|
-
function getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers) {
|
|
1906
|
-
// Always skip .git directory (not reported by git check-ignore)
|
|
1907
|
-
if (entry.isDirectory() && entry.name === '.git') {
|
|
1908
|
-
return '.git';
|
|
1909
|
-
}
|
|
1910
|
-
// Skip gitignored paths
|
|
1911
|
-
if (gitIgnoredMap !== null && gitIgnoredMap.get(fullPath) === true) {
|
|
1912
|
-
return `gitignored: ${entry.name}`;
|
|
1913
|
-
}
|
|
1914
|
-
// Each matcher answers against ITS OWN base — see {@link ExcludeMatcher}.
|
|
1915
|
-
for (const matcher of excludeMatchers) {
|
|
1916
|
-
const relativePath = safePath.relative(matcher.base, fullPath).replaceAll('\\', '/');
|
|
1917
|
-
if (isExcludedPath(matcher.isMatch, relativePath, entry.isDirectory())) {
|
|
1918
|
-
return `${matcher.label}: ${relativePath}`;
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
return null;
|
|
1922
|
-
}
|
|
1923
2267
|
/**
|
|
1924
2268
|
* The one finding a path the walk could not read produces — a directory it could
|
|
1925
2269
|
* not enter, or a file it could not open.
|
|
@@ -1936,14 +2280,11 @@ function getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers) {
|
|
|
1936
2280
|
* is no longer the ONLY thing the run says — the code, the location and the fix
|
|
1937
2281
|
* come from the registry.
|
|
1938
2282
|
*
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1944
|
-
* own, which is a change to the report's shape and belongs with issue #177
|
|
1945
|
-
* ("report counts don't describe the artifact") rather than smuggled in here. The
|
|
1946
|
-
* finding itself names the path, so nothing is hidden in the meantime.
|
|
2283
|
+
* This result is a `files[]` row but NOT a scanned file: `countFilesByStatus`
|
|
2284
|
+
* recognises it ({@link isUnreadablePathResult}) and reports it under
|
|
2285
|
+
* `summary.pathsUnreadable`, outside `filesScanned` and the status counts. A
|
|
2286
|
+
* root the OS refused therefore audits zero files and ends on the zero-files
|
|
2287
|
+
* refusal, instead of `filesScanned: 1` over a path nothing read.
|
|
1947
2288
|
*/
|
|
1948
2289
|
function unreadablePathResult(dirPath, error, locationRoot) {
|
|
1949
2290
|
// `issueLocation` is `path.relative`, so it answers `''` when the subject IS the
|
|
@@ -1954,10 +2295,14 @@ function unreadablePathResult(dirPath, error, locationRoot) {
|
|
|
1954
2295
|
// identical reason, already exists on the distributed-tree finding — see
|
|
1955
2296
|
// `anchoredTreeLocation` in ./audit/distributed-tree.ts.
|
|
1956
2297
|
const location = issueLocation(dirPath, locationRoot) || '.';
|
|
2298
|
+
// The OS names the path it refused absolutely; the report speaks scan-root
|
|
2299
|
+
// coordinates everywhere else, and an absolute path here is the build host's
|
|
2300
|
+
// `$HOME` in every CI log.
|
|
2301
|
+
const reason = withRelativePath(error instanceof Error ? error.message : String(error), dirPath, locationRoot);
|
|
1957
2302
|
const issues = [
|
|
1958
2303
|
materializeIssue('SCAN_PATH_UNREADABLE', {
|
|
1959
2304
|
location,
|
|
1960
|
-
detail: `${location}: ${
|
|
2305
|
+
detail: `${location}: ${reason}`,
|
|
1961
2306
|
}),
|
|
1962
2307
|
];
|
|
1963
2308
|
const issueCounts = countBySeverity(issues);
|
|
@@ -1971,30 +2316,36 @@ function unreadablePathResult(dirPath, error, locationRoot) {
|
|
|
1971
2316
|
};
|
|
1972
2317
|
}
|
|
1973
2318
|
/**
|
|
1974
|
-
*
|
|
2319
|
+
* Validate ONE subject the population found, degrading to a finding if the
|
|
2320
|
+
* filesystem refuses it.
|
|
1975
2321
|
*
|
|
1976
|
-
* Guarded per
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1979
|
-
* aborted the whole run with `status: error` and zero findings. Under
|
|
2322
|
+
* Guarded per SUBJECT, not just per listing: an unreadable FILE (a root-owned
|
|
2323
|
+
* `SKILL.md`, a quarantined bundle) used to throw out of `validateSkill` and
|
|
2324
|
+
* abort the whole run with `status: error` and zero findings. Under
|
|
1980
2325
|
* `~/.claude/plugins` — the flagship `vat audit --user` target, populated by sudo
|
|
1981
2326
|
* installs and macOS quarantine — a root-owned FILE is at least as likely as a
|
|
1982
|
-
* root-owned directory
|
|
2327
|
+
* root-owned directory.
|
|
1983
2328
|
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
2329
|
+
* Ownership is decided here and only here, in population order: a plugin
|
|
2330
|
+
* directory claims its subtree unconditionally; a skill claims its own only
|
|
2331
|
+
* when nothing above it already has AND its crawl actually runs
|
|
2332
|
+
* ({@link crawlOwnsSubtree}) — a repo-source skill's crawl is skipped, so
|
|
2333
|
+
* claiming its subtree would trade a double count for a missed finding.
|
|
1987
2334
|
*/
|
|
1988
|
-
async function
|
|
2335
|
+
async function validateScanSubject(subject, options, logger, scanCtx, claims) {
|
|
2336
|
+
const { locationRoot } = scanCtx;
|
|
1989
2337
|
try {
|
|
1990
|
-
if (
|
|
1991
|
-
const result = await
|
|
1992
|
-
|
|
2338
|
+
if (subject.kind === 'plugin') {
|
|
2339
|
+
const result = await validatePluginDirectory(subject.dir, logger, locationRoot);
|
|
2340
|
+
claims.claim(subject.dir);
|
|
2341
|
+
return [result];
|
|
1993
2342
|
}
|
|
1994
|
-
|
|
1995
|
-
|
|
2343
|
+
const subjectCtx = { ...scanCtx, crawledRoot: claims.owner(subject.path) };
|
|
2344
|
+
const result = await handleFileEntry({ name: basename(subject.path) }, subject.path, options, logger, subjectCtx);
|
|
2345
|
+
if (subject.kind === 'skill' && ownsOwnCrawl(subjectCtx, subject.path) && await crawlOwnsSubtree(subject.path)) {
|
|
2346
|
+
claims.claim(subject.dir);
|
|
1996
2347
|
}
|
|
1997
|
-
return [];
|
|
2348
|
+
return result === null ? [] : [result];
|
|
1998
2349
|
}
|
|
1999
2350
|
catch (error) {
|
|
2000
2351
|
// ONLY filesystem-access errors degrade. Anything else is rethrown, because
|
|
@@ -2003,87 +2354,61 @@ async function scanEntry(entry, fullPath, ctx) {
|
|
|
2003
2354
|
// prevent — it would make the tool quietest exactly when it is most wrong.
|
|
2004
2355
|
if (!isFilesystemAccessError(error))
|
|
2005
2356
|
throw error;
|
|
2006
|
-
|
|
2007
|
-
|
|
2357
|
+
const path = subject.kind === 'plugin' ? subject.dir : subject.path;
|
|
2358
|
+
logger.debug(`Unreadable entry: ${path}`);
|
|
2359
|
+
return [unreadablePathResult(path, error, locationRoot)];
|
|
2008
2360
|
}
|
|
2009
2361
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2362
|
+
/**
|
|
2363
|
+
* The remedy a refused listing names: the operator's own knob for dropping a
|
|
2364
|
+
* directory from the audit on purpose.
|
|
2365
|
+
*/
|
|
2366
|
+
function scanRefusalRemedy() {
|
|
2367
|
+
return 'To leave it out deliberately, exclude it with --exclude <pattern>.';
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Audit a directory: every plugin, skill and registry file the `crawl` lane
|
|
2371
|
+
* finds beneath it, plus one finding per directory the lane could not list.
|
|
2372
|
+
*
|
|
2373
|
+
* The enumeration is `enumerateAuditPopulation`'s — this function owns no
|
|
2374
|
+
* walk of its own. What it decides is what the audit MAKES of the population:
|
|
2375
|
+
* validation per subject, subtree ownership across subjects (see
|
|
2376
|
+
* {@link validateScanSubject}), the nested-config breadcrumb, and the refusal
|
|
2377
|
+
* findings. A refused directory is a finding, not a thrown error: `vat audit`
|
|
2378
|
+
* is a bulk linter over trees it does not own, and one root-owned directory
|
|
2379
|
+
* under `~/.claude/plugins` used to abort the flagship `vat audit --user` run
|
|
2380
|
+
* outright, losing every finding already collected. The scope of the loss is
|
|
2381
|
+
* the subtree that refused; every readable sibling is still validated.
|
|
2382
|
+
*/
|
|
2383
|
+
async function scanDirectory(dirPath, recursive, options, logger, scanCtx) {
|
|
2384
|
+
// Warm findProjectRoot's walk-up cache from the scan root: every per-skill
|
|
2385
|
+
// walk-up below lands on a cached ancestor instead of re-walking to `/`.
|
|
2023
2386
|
findProjectRoot(dirPath);
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
//
|
|
2036
|
-
//
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
excludeMatchers.push(resolvedScanCtx.projectExcludes);
|
|
2050
|
-
}
|
|
2051
|
-
// Guarded because `vat audit` is a bulk linter over trees it does not own, and
|
|
2052
|
-
// an unreadable entry is an ordinary condition there — one root-owned directory
|
|
2053
|
-
// under `~/.claude/plugins` used to abort the flagship `vat audit --user` run
|
|
2054
|
-
// outright, losing every finding already collected (issue #180). The failure is
|
|
2055
|
-
// scoped to the subtree that caused it: this returns instead of throwing, so the
|
|
2056
|
-
// CALLER's loop keeps its readable siblings and the walk continues.
|
|
2057
|
-
let entries;
|
|
2058
|
-
try {
|
|
2059
|
-
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
2060
|
-
}
|
|
2061
|
-
catch (error) {
|
|
2062
|
-
// Same predicate as the per-entry guard below — see it for why a non-IO
|
|
2063
|
-
// throw must not be degraded into a finding.
|
|
2064
|
-
if (!isFilesystemAccessError(error))
|
|
2065
|
-
throw error;
|
|
2066
|
-
logger.debug(`Unreadable directory: ${dirPath}`);
|
|
2067
|
-
return [unreadablePathResult(dirPath, error, resolvedScanCtx.locationRoot)];
|
|
2068
|
-
}
|
|
2069
|
-
const descendCtx = await contextForDescendants(dirPath, entries, resolvedScanCtx);
|
|
2070
|
-
// Build full paths and batch-check gitignore status
|
|
2071
|
-
const entryPaths = entries.map(e => safePath.join(dirPath, e.name));
|
|
2072
|
-
const gitIgnoredMap = buildGitIgnoreMap(entryPaths, resolvedScanCtx.gitTracker, options.includeArtifacts ?? false);
|
|
2073
|
-
for (const entry of entries) {
|
|
2074
|
-
const fullPath = safePath.join(dirPath, entry.name);
|
|
2075
|
-
const skipReason = getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers);
|
|
2076
|
-
if (skipReason !== null) {
|
|
2077
|
-
logger.debug(`Excluding path: ${skipReason}`);
|
|
2078
|
-
continue;
|
|
2079
|
-
}
|
|
2080
|
-
results.push(...await scanEntry(entry, fullPath, {
|
|
2081
|
-
recursive, options, logger,
|
|
2082
|
-
scanCtx: resolvedScanCtx,
|
|
2083
|
-
descendCtx,
|
|
2084
|
-
baseDir: resolvedBaseDir,
|
|
2085
|
-
nestedConfigLog: resolvedNestedLog,
|
|
2086
|
-
}));
|
|
2387
|
+
const population = await enumerateAuditPopulation({
|
|
2388
|
+
scanDir: dirPath,
|
|
2389
|
+
recursive,
|
|
2390
|
+
// No tracker means no git root, or a scan root that is itself gitignored
|
|
2391
|
+
// (the operator's explicit intent wins); either way ignored files are in.
|
|
2392
|
+
respectGitignore: scanCtx.gitTracker !== null && !(options.includeArtifacts ?? false),
|
|
2393
|
+
userExcludes: options.exclude ?? [],
|
|
2394
|
+
projectExcludes: scanCtx.projectExcludes,
|
|
2395
|
+
});
|
|
2396
|
+
// A one-time breadcrumb per nested `vibe-agent-toolkit.config.yaml` that is
|
|
2397
|
+
// NOT at the scan root. Configs do not compose across VAT projects; the
|
|
2398
|
+
// message reminds operators that only per-skill packaging rules from this
|
|
2399
|
+
// config still apply.
|
|
2400
|
+
for (const nestedConfigPath of population.nestedConfigs) {
|
|
2401
|
+
const rel = toForwardSlash(safePath.relative(dirPath, nestedConfigPath));
|
|
2402
|
+
logger.info(`Nested ${VAT_CONFIG_FILENAME} detected at ${rel} — configs do not compose across VAT projects. Per-skill packaging rules from this config still apply to skills declared in it.`);
|
|
2403
|
+
}
|
|
2404
|
+
const results = population.refusals.map((refusal) => {
|
|
2405
|
+
logger.debug(`Unreadable directory: ${refusal.directory}`);
|
|
2406
|
+
const err = new DirectoryListingRefusedError(refusal, { root: dirPath, remedy: scanRefusalRemedy() });
|
|
2407
|
+
return unreadablePathResult(refusal.directory, err, scanCtx.locationRoot);
|
|
2408
|
+
});
|
|
2409
|
+
const claims = new SubtreeClaims();
|
|
2410
|
+
for (const subject of population.subjects) {
|
|
2411
|
+
results.push(...await validateScanSubject(subject, options, logger, scanCtx, claims));
|
|
2087
2412
|
}
|
|
2088
2413
|
return results;
|
|
2089
2414
|
}
|
|
@@ -2112,13 +2437,34 @@ function calculateOverallStatus(results) {
|
|
|
2112
2437
|
function countResultsWithFindings(results) {
|
|
2113
2438
|
return results.filter((r) => r.issues.length > 0).length;
|
|
2114
2439
|
}
|
|
2115
|
-
/**
|
|
2440
|
+
/**
|
|
2441
|
+
* Whether a `files[]` row is a refused path rather than a scanned file: the
|
|
2442
|
+
* synthetic result {@link unreadablePathResult} builds, carrying nothing but
|
|
2443
|
+
* `SCAN_PATH_UNREADABLE`. Keyed on the code, not on `type: 'unknown'` alone —
|
|
2444
|
+
* an unrecognised file is also `unknown`, and it WAS examined.
|
|
2445
|
+
*/
|
|
2446
|
+
function isUnreadablePathResult(result) {
|
|
2447
|
+
return result.type === 'unknown'
|
|
2448
|
+
&& result.issues.length > 0
|
|
2449
|
+
&& result.issues.every((issue) => issue.code === 'SCAN_PATH_UNREADABLE');
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Count files by their own status.
|
|
2453
|
+
*
|
|
2454
|
+
* A refused path is NOT a scanned file. It used to be counted as one, which
|
|
2455
|
+
* made the zero-files refusal unreachable for exactly the tree it exists for:
|
|
2456
|
+
* `chmod 000` on the whole root gave `filesScanned: 1, filesPassed: 0`,
|
|
2457
|
+
* `status: warning`, exit 0 — nothing read, gate green. Kept out of every
|
|
2458
|
+
* status count too, or `filesWithWarnings` would exceed `filesScanned`.
|
|
2459
|
+
*/
|
|
2116
2460
|
function countFilesByStatus(results) {
|
|
2461
|
+
const scanned = results.filter((r) => !isUnreadablePathResult(r));
|
|
2117
2462
|
return {
|
|
2118
|
-
filesScanned:
|
|
2119
|
-
filesPassed:
|
|
2120
|
-
filesWithWarnings:
|
|
2121
|
-
filesWithErrors:
|
|
2463
|
+
filesScanned: scanned.length,
|
|
2464
|
+
filesPassed: scanned.filter((r) => r.status === 'success').length,
|
|
2465
|
+
filesWithWarnings: scanned.filter((r) => r.status === 'warning').length,
|
|
2466
|
+
filesWithErrors: scanned.filter((r) => r.status === 'error').length,
|
|
2467
|
+
pathsUnreadable: results.length - scanned.length,
|
|
2122
2468
|
};
|
|
2123
2469
|
}
|
|
2124
2470
|
/**
|
|
@@ -2146,6 +2492,24 @@ function countFilesByStatus(results) {
|
|
|
2146
2492
|
* also present in its owner's `issues`, so the nested list is a VIEW of records
|
|
2147
2493
|
* already counted, not extra records. `audit-report-coherence.integration.test.ts`
|
|
2148
2494
|
* pins that subset relation, because counting once is only correct while it holds.
|
|
2495
|
+
*
|
|
2496
|
+
* 🚨 **Zero files audited is an ERROR, not "an empty tree audits cleanly".**
|
|
2497
|
+
* `calculateOverallStatus([])` is `success`, so an existing directory holding
|
|
2498
|
+
* nothing auditable — moved plugins, a wrong subdirectory, an excluded tree, a
|
|
2499
|
+
* tree of files no lane recognises — published `status: success` beside
|
|
2500
|
+
* `filesScanned: 0`, and this command's own documentation tells CI to gate on
|
|
2501
|
+
* that `status`. Derived HERE, in the one builder both the directory lane
|
|
2502
|
+
* (`calculateSummary`) and the `--user` lane (`calculateHierarchicalSummary`)
|
|
2503
|
+
* pass through, so neither can publish a clean status over a zero denominator.
|
|
2504
|
+
* Through the shared mechanism in `run-integrity.ts`: one non-overridable
|
|
2505
|
+
* `RESOURCE_CHECK_BROKEN` at `error`, published under a top-level `issues` —
|
|
2506
|
+
* the claim is about the RUN, so it is not a `files[]` row and does not count
|
|
2507
|
+
* toward `filesScanned` — and counted in the header `issueCounts`, so the
|
|
2508
|
+
* identity becomes `issueCounts === Σ files[].issueCounts + Σ issues`.
|
|
2509
|
+
*
|
|
2510
|
+
* The exit code is NOT touched by this: `status` describes the findings and the
|
|
2511
|
+
* exit code describes whether the run completed, and this run completed. See
|
|
2512
|
+
* {@link createAuditCommand}.
|
|
2149
2513
|
*/
|
|
2150
2514
|
function buildBaseSummary(results, startTime) {
|
|
2151
2515
|
const issueCounts = { errors: 0, warnings: 0, info: 0 };
|
|
@@ -2156,11 +2520,24 @@ function buildBaseSummary(results, startTime) {
|
|
|
2156
2520
|
issueCounts.info += counts.info;
|
|
2157
2521
|
return { ...result, issueCounts: counts };
|
|
2158
2522
|
});
|
|
2523
|
+
const summary = countFilesByStatus(entries);
|
|
2524
|
+
// The denominator is files READ: a tree whose every path was refused audited
|
|
2525
|
+
// nothing, and says so through the same refusal as an empty tree.
|
|
2526
|
+
const runIssues = nothingCheckedFinding(summary.filesScanned, entries.flatMap((entry) => entry.issues), () => 'The audit ran over 0 files, so this report is not a verdict: a tree with nothing'
|
|
2527
|
+
+ ' to audit produces the same counts as a clean one. The path resolved to a tree'
|
|
2528
|
+
+ ' holding no auditable file — usually a wrong subdirectory, plugins or skills that'
|
|
2529
|
+
+ ' have moved, an excluded tree, files no audit lane recognises, or `--no-recursive`'
|
|
2530
|
+
+ ' over a tree whose resources sit deeper. Point the command at the plugin,'
|
|
2531
|
+
+ ' marketplace, registry or SKILL.md tree it should read.');
|
|
2159
2532
|
return {
|
|
2160
2533
|
entries,
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2534
|
+
// Every run-integrity finding is `error` by construction (invariant 2 of
|
|
2535
|
+
// `run-integrity.ts`), so a non-empty refusal IS the status; otherwise the
|
|
2536
|
+
// files decide it, as before.
|
|
2537
|
+
status: runIssues.length > 0 ? 'error' : calculateOverallStatus(entries),
|
|
2538
|
+
summary,
|
|
2539
|
+
issueCounts: sumSeverityCounts([issueCounts, countBySeverity(runIssues)]),
|
|
2540
|
+
...(runIssues.length === 0 ? {} : { issues: [...runIssues] }),
|
|
2164
2541
|
duration: `${Date.now() - startTime}ms`,
|
|
2165
2542
|
};
|
|
2166
2543
|
}
|
|
@@ -2169,7 +2546,7 @@ function buildBaseSummary(results, startTime) {
|
|
|
2169
2546
|
*/
|
|
2170
2547
|
function calculateHierarchicalSummary(results, hierarchical, startTime, compatMap, verbose, root) {
|
|
2171
2548
|
const { entries, ...base } = buildBaseSummary(results, startTime);
|
|
2172
|
-
const withCompat = applyCompatMap(entries, compatMap);
|
|
2549
|
+
const withCompat = applyCompatMap(entries, compatMap, root);
|
|
2173
2550
|
return {
|
|
2174
2551
|
root,
|
|
2175
2552
|
...base,
|
|
@@ -2187,15 +2564,18 @@ function calculateHierarchicalSummary(results, hierarchical, startTime, compatMa
|
|
|
2187
2564
|
/**
|
|
2188
2565
|
* Log hierarchical summary to stderr
|
|
2189
2566
|
*/
|
|
2190
|
-
function logHierarchicalSummary(results, logger) {
|
|
2567
|
+
function logHierarchicalSummary(results, runIssues, logger) {
|
|
2191
2568
|
const status = calculateOverallStatus(results);
|
|
2192
2569
|
const skillsWithIssues = countResultsWithFindings(results);
|
|
2193
2570
|
const totalSkills = results.length;
|
|
2194
|
-
//
|
|
2195
|
-
|
|
2196
|
-
if (
|
|
2571
|
+
// Same rule as the per-file lane: the exit code follows `status`.
|
|
2572
|
+
const refused = logRunIntegrity(runIssues, logger);
|
|
2573
|
+
if (refused) {
|
|
2574
|
+
// Rendered above; there is no skill verdict to print over zero skills.
|
|
2575
|
+
}
|
|
2576
|
+
else if (status === 'error') {
|
|
2197
2577
|
const errorCount = results.filter((r) => r.status === 'error').length;
|
|
2198
|
-
logger.error(`Audit found ${errorCount} skill(s) with errors (${totalSkills} scanned, ${skillsWithIssues} with issues)
|
|
2578
|
+
logger.error(`Audit found ${errorCount} skill(s) with errors (${totalSkills} scanned, ${skillsWithIssues} with issues)`);
|
|
2199
2579
|
}
|
|
2200
2580
|
else if (status === 'warning') {
|
|
2201
2581
|
const warningCount = results.filter((r) => r.status === 'warning').length;
|
|
@@ -2210,6 +2590,6 @@ function logHierarchicalSummary(results, logger) {
|
|
|
2210
2590
|
logger.info(`Audit successful: ${totalSkills} skill(s) passed`);
|
|
2211
2591
|
}
|
|
2212
2592
|
renderAuditFooter(results, logger);
|
|
2213
|
-
process.exit(
|
|
2593
|
+
process.exit(refused || status === 'error' ? ExitCode.FINDINGS : ExitCode.OK);
|
|
2214
2594
|
}
|
|
2215
2595
|
//# sourceMappingURL=audit.js.map
|