@vibe-agent-toolkit/cli 0.2.0-rc.7 → 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 +0 -1
- 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 +5 -2
- package/dist/bin.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 +72 -23
- package/dist/commands/ard/emit.d.ts.map +1 -1
- package/dist/commands/ard/emit.js +109 -56
- package/dist/commands/ard/emit.js.map +1 -1
- package/dist/commands/ard/index.d.ts +1 -1
- package/dist/commands/ard/index.d.ts.map +1 -1
- package/dist/commands/ard/index.js +4 -3
- package/dist/commands/ard/index.js.map +1 -1
- 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 +16 -14
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +304 -389
- 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 +127 -28
- package/dist/commands/claude/budget.d.ts.map +1 -1
- package/dist/commands/claude/budget.js +76 -33
- 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 +3 -2
- 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.map +1 -1
- package/dist/commands/claude/marketplace/validate.js +25 -15
- 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 +19 -1
- package/dist/commands/claude/org/skills.d.ts.map +1 -1
- package/dist/commands/claude/org/skills.js +141 -105
- 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.map +1 -1
- package/dist/commands/claude/plugin/build.js +12 -31
- 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 +2 -1
- package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.js +15 -14
- 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/report.d.ts.map +1 -1
- package/dist/commands/corpus/report.js +1 -2
- package/dist/commands/corpus/report.js.map +1 -1
- package/dist/commands/corpus/runner.d.ts.map +1 -1
- package/dist/commands/corpus/runner.js +2 -6
- 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 -3
- 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.map +1 -1
- package/dist/commands/doctor.js +55 -37
- 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.map +1 -1
- package/dist/commands/okf/index.js +14 -2
- package/dist/commands/okf/index.js.map +1 -1
- package/dist/commands/okf/validate.d.ts +157 -48
- package/dist/commands/okf/validate.d.ts.map +1 -1
- package/dist/commands/okf/validate.js +109 -76
- package/dist/commands/okf/validate.js.map +1 -1
- package/dist/commands/phase-utils.d.ts +11 -10
- package/dist/commands/phase-utils.d.ts.map +1 -1
- package/dist/commands/phase-utils.js +30 -14
- 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.map +1 -1
- package/dist/commands/rag/index-command.js +4 -1
- package/dist/commands/rag/index-command.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.map +1 -1
- package/dist/commands/resources/check-progress.js +5 -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 +135 -4
- package/dist/commands/resources/check.d.ts.map +1 -1
- package/dist/commands/resources/check.js +97 -80
- 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 +24 -21
- package/dist/commands/resources/index.js.map +1 -1
- package/dist/commands/resources/query.d.ts.map +1 -1
- package/dist/commands/resources/query.js +4 -8
- 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 +1 -1
- package/dist/commands/resources/validate.d.ts.map +1 -1
- package/dist/commands/resources/validate.js +0 -1
- 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 +90 -60
- 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 +118 -63
- 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 +112 -39
- 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.map +1 -1
- package/dist/commands/skills/list.js +14 -18
- 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.map +1 -1
- package/dist/commands/skills/skill-discovery.js +0 -1
- 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/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 +8 -2
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +86 -56
- 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/resolve-skill-reference.d.ts.map +1 -1
- package/dist/skill-resolution/resolve-skill-reference.js +2 -3
- 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 +63 -62
- package/dist/utils/command-error.d.ts.map +1 -1
- package/dist/utils/command-error.js +80 -76
- 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/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.map +1 -1
- package/dist/utils/optional-backend.js +2 -1
- 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.map +1 -1
- package/dist/utils/project-root-policy.js +0 -2
- package/dist/utils/project-root-policy.js.map +1 -1
- package/dist/utils/resource-loader.js +2 -2
- package/dist/utils/resource-loader.js.map +1 -1
- 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/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.map +1 -1
- package/dist/utils/user-context-scanner.js +0 -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 +3 -4
- 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 +44 -53
- package/docs/doctor.md +1 -0
- package/docs/index.md +18 -10
- package/docs/rag.md +0 -1
- package/docs/resources.md +0 -1
- package/docs/skill-test.md +10 -9
- package/docs/skills.md +6 -7
- package/package.json +22 -27
- 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
|
@@ -9,14 +9,13 @@ import { conventionalSuiteProbe, crawlAndResolveRegistry, detectDeclaredButMissi
|
|
|
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
11
|
import { listingRefusalRemedy } from '@vibe-agent-toolkit/resources';
|
|
12
|
-
import { calculateValidationStatus, countBySeverity, } from '@vibe-agent-toolkit/schema';
|
|
13
|
-
import { findProjectRoot,
|
|
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
14
|
import { DirectoryListingRefusedError } from '@vibe-agent-toolkit/utils/crawl';
|
|
15
15
|
import { gitFindRoot, isGitUrl, parseGitUrl, } from '@vibe-agent-toolkit/utils/git';
|
|
16
16
|
import { Command } from 'commander';
|
|
17
|
-
import picomatch from 'picomatch';
|
|
18
17
|
import { resetSkillDiscoveryCache, resolveProjectDeclaredEvalSuites, resolveSkillPackagingConfig, stripValidationAllowForDisplay, } from '../skill-resolution/packaging-config.js';
|
|
19
|
-
import { handleCommandError } from '../utils/command-error.js';
|
|
18
|
+
import { handleCommandError, handleExpectedFailure } from '../utils/command-error.js';
|
|
20
19
|
import { ConfigLoadError, loadConfig, resetLoadedConfigCache, } from '../utils/config-loader.js';
|
|
21
20
|
import { countCollapsedFindings, formatCollapsedFindingsHint, formatIssueLines, formatSeverityBreakdown, issuesToRenderAtVerbosity, sumSeverityCounts, } from '../utils/issue-rendering.js';
|
|
22
21
|
import { resolveIssueSeverity } from '../utils/issue-severity.js';
|
|
@@ -31,13 +30,13 @@ import { crawlOwnsSubtree, distributedTreeFindings, getOrCreateGitTracker, gitTr
|
|
|
31
30
|
import { withClonedRepo } from './audit/git-url-clone.js';
|
|
32
31
|
import { buildHierarchicalOutput } from './audit/hierarchical-output.js';
|
|
33
32
|
import { renderProvenanceHeader, rewritePathsInResults, } from './audit/provenance.js';
|
|
33
|
+
import { enumerateAuditPopulation, resolveProjectExcludes, VAT_CONFIG_FILENAME, } from './audit/scan-population.js';
|
|
34
34
|
import { createAuditSettingsCommand } from './audit-settings.js';
|
|
35
35
|
import { discoverSkillsFromConfig, SKILLS_INCLUDE_REMEDY, } from './skills/skill-discovery.js';
|
|
36
36
|
/** Resource type constant for agent skills, avoiding duplicate string literals. */
|
|
37
37
|
const RESOURCE_TYPE_AGENT_SKILL = 'agent-skill';
|
|
38
38
|
/** Resource type constant for Claude plugins, avoiding duplicate string literals. */
|
|
39
39
|
const RESOURCE_TYPE_CLAUDE_PLUGIN = 'claude-plugin';
|
|
40
|
-
const VAT_CONFIG_FILENAME = 'vibe-agent-toolkit.config.yaml';
|
|
41
40
|
/**
|
|
42
41
|
* Build config-aware context for a single VAT project at `scanRoot`.
|
|
43
42
|
*
|
|
@@ -56,7 +55,6 @@ const VAT_CONFIG_FILENAME = 'vibe-agent-toolkit.config.yaml';
|
|
|
56
55
|
*/
|
|
57
56
|
async function buildVATProjectContext(scanRoot, logger, locationRoot) {
|
|
58
57
|
const configPath = safePath.join(scanRoot, VAT_CONFIG_FILENAME);
|
|
59
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- scanRoot is a controlled parameter
|
|
60
58
|
if (!fs.existsSync(configPath)) {
|
|
61
59
|
return null;
|
|
62
60
|
}
|
|
@@ -355,8 +353,8 @@ function degradingDiscovery(configRoot, logger, locationRoot) {
|
|
|
355
353
|
* config-aware lane makes. Every other verb hands this crawl `'refuse'` (a build
|
|
356
354
|
* must not ship a shorter bundle), and while the registry carried that ruling
|
|
357
355
|
* INSIDE itself `vat audit` on a tree with one `chmod 000` sibling exited 2
|
|
358
|
-
* with `status: error` and ZERO findings —
|
|
359
|
-
*
|
|
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:
|
|
360
358
|
* status describes what was found, exit describes whether the run completed.
|
|
361
359
|
*
|
|
362
360
|
* Recorded through the same per-directory ledger as a refused discovery, so a
|
|
@@ -487,8 +485,8 @@ async function validateSingleSkill(skillPath, options, logger, locationRoot, cra
|
|
|
487
485
|
await appendDistributedTreeFindings(configAware, skillPath, locationRoot, crawlTree);
|
|
488
486
|
return configAware;
|
|
489
487
|
}
|
|
490
|
-
// Fallback: basic validation
|
|
491
|
-
const validateOptions = { skillPath, locationRoot };
|
|
488
|
+
// Fallback: basic validation. Severity is applied by `applySeverityFilter`.
|
|
489
|
+
const validateOptions = { skillPath, locationRoot, validation: {} };
|
|
492
490
|
if (options.warnUnreferencedFiles) {
|
|
493
491
|
validateOptions.checkUnreferencedFiles = true;
|
|
494
492
|
}
|
|
@@ -535,19 +533,13 @@ function collect(value, previous) {
|
|
|
535
533
|
* Create audit command
|
|
536
534
|
* Top-level command: vat audit [path]
|
|
537
535
|
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* `
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* ⚠️ Do NOT "reconcile" the two by making one follow the other. Making the exit
|
|
546
|
-
* code follow `status` turns an advisory report into a gate — the thing
|
|
547
|
-
* `vat validate` already is, and the thing this command's published contract
|
|
548
|
-
* promises it is not. Making `status` follow the exit code deletes the only
|
|
549
|
-
* machine-readable verdict a CI consumer has, and a stamped `success` over a
|
|
550
|
-
* report full of errors is a stronger falsehood than the mismatch ever was.
|
|
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.
|
|
551
543
|
*/
|
|
552
544
|
export function createAuditCommand() {
|
|
553
545
|
const audit = new Command('audit');
|
|
@@ -614,22 +606,16 @@ Description:
|
|
|
614
606
|
done
|
|
615
607
|
(See packages/cli/docs/audit.md for details.)
|
|
616
608
|
|
|
617
|
-
Output
|
|
618
|
-
The YAML 'status' describes the
|
|
619
|
-
|
|
620
|
-
and both are correct.
|
|
621
|
-
|
|
622
|
-
Note that 'status' means something narrower here than elsewhere in this CLI,
|
|
623
|
-
where it moves with the exit code. Gate CI on the report — 'status' and
|
|
624
|
-
'issueCounts' — never on this command's exit code.
|
|
609
|
+
Output:
|
|
610
|
+
The YAML 'status' describes the findings, and the exit code follows it, as
|
|
611
|
+
in every other command of this CLI.
|
|
625
612
|
|
|
626
613
|
A run that audited ZERO files is refused, not passed: 'status: error' with one
|
|
627
614
|
non-overridable RESOURCE_CHECK_BROKEN under a top-level 'issues:' (the claim is
|
|
628
|
-
about the run, so it is not a files[] row),
|
|
629
|
-
completed; it just is not a verdict.
|
|
615
|
+
about the run, so it is not a files[] row), and exit 1.
|
|
630
616
|
|
|
631
617
|
Validation Behavior:
|
|
632
|
-
|
|
618
|
+
Audit surfaces all validation issues for inspection.
|
|
633
619
|
Unlike 'vat skills validate', audit:
|
|
634
620
|
- NEVER applies validation.allow (allowed codes are always shown)
|
|
635
621
|
- Respects validation.severity: a code set to 'ignore' is hidden; warnings
|
|
@@ -653,11 +639,16 @@ Validation Checks:
|
|
|
653
639
|
|
|
654
640
|
Warnings (should fix):
|
|
655
641
|
- Skill exceeds recommended length (>5000 lines)
|
|
656
|
-
-
|
|
657
|
-
(
|
|
658
|
-
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)
|
|
659
644
|
- Unreferenced files detected (with --warn-unreferenced-files)
|
|
660
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
|
+
|
|
661
652
|
Gitignore-Aware Scanning:
|
|
662
653
|
When scanning inside a git repository, paths matched by .gitignore are
|
|
663
654
|
skipped by default. This excludes build artifacts (dist/), dependencies
|
|
@@ -667,8 +658,9 @@ Gitignore-Aware Scanning:
|
|
|
667
658
|
Use --include-artifacts to scan gitignored paths (e.g., to audit a
|
|
668
659
|
bundled marketplace plugin in dist/).
|
|
669
660
|
|
|
670
|
-
User-supplied --exclude patterns are always applied on top.
|
|
671
|
-
|
|
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.
|
|
672
664
|
|
|
673
665
|
Config-Aware Validation:
|
|
674
666
|
The governing vibe-agent-toolkit.config.yaml is found by walking UP from
|
|
@@ -687,14 +679,19 @@ Config-Aware Validation:
|
|
|
687
679
|
skills.config.<name>.validation... that one skill
|
|
688
680
|
|
|
689
681
|
Exit Codes:
|
|
690
|
-
0 -
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
|
698
695
|
no report to read.
|
|
699
696
|
|
|
700
697
|
Examples:
|
|
@@ -749,11 +746,8 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
749
746
|
// --user scans three sibling directories; they share ONE enclosing Claude
|
|
750
747
|
// config dir, so that is the run's single stated root.
|
|
751
748
|
const scanRoot = safePath.resolve(claudeDir);
|
|
752
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- safe: path constructed from os.homedir()
|
|
753
749
|
const pluginsDirExists = fs.existsSync(pluginsDir);
|
|
754
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- safe: path constructed from os.homedir()
|
|
755
750
|
const skillsDirExists = fs.existsSync(skillsDir);
|
|
756
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- safe: path constructed from os.homedir()
|
|
757
751
|
const marketplacesDirExists = fs.existsSync(marketplacesDir);
|
|
758
752
|
if (!pluginsDirExists && !skillsDirExists && !marketplacesDirExists) {
|
|
759
753
|
logger.error(`No user-level Claude directories found:`);
|
|
@@ -761,7 +755,7 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
761
755
|
logger.error(` Skills: ${skillsDir}`);
|
|
762
756
|
logger.error(` Marketplaces: ${marketplacesDir}`);
|
|
763
757
|
logger.error('Claude plugins/skills/marketplaces have not been installed yet.');
|
|
764
|
-
process.exit(
|
|
758
|
+
process.exit(ExitCode.ERROR);
|
|
765
759
|
}
|
|
766
760
|
const scanned = [];
|
|
767
761
|
// The roots that exist, minus any one another already covers — see
|
|
@@ -815,7 +809,6 @@ const SKILL_RESULT_TYPES = new Set([RESOURCE_TYPE_AGENT_SKILL, 'vat-agent']);
|
|
|
815
809
|
*/
|
|
816
810
|
export function deriveScanRoot(targetPath) {
|
|
817
811
|
const resolved = safePath.resolve(targetPath);
|
|
818
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- resolved is the operator's own audit target
|
|
819
812
|
const isDirectory = fs.existsSync(resolved) && fs.statSync(resolved).isDirectory();
|
|
820
813
|
return isDirectory ? resolved : safePath.resolve(resolved, '..');
|
|
821
814
|
}
|
|
@@ -863,7 +856,7 @@ function buildFilteredResult(result, filteredIssues) {
|
|
|
863
856
|
/**
|
|
864
857
|
* Apply severity resolution to validation results.
|
|
865
858
|
*
|
|
866
|
-
* Audit
|
|
859
|
+
* Audit shows everything: it applies `validation.severity` to decide what to
|
|
867
860
|
* show and at what severity, but deliberately ignores `validation.allow`.
|
|
868
861
|
*
|
|
869
862
|
* **Both directions, via the one shared resolver.** This used to compute the
|
|
@@ -1045,7 +1038,7 @@ export async function buildAuditReport(scanPath, options, startTime, logger) {
|
|
|
1045
1038
|
logger.warn(`Config could not be read; severity overrides are not applied: ${String(err)}`);
|
|
1046
1039
|
}
|
|
1047
1040
|
// Apply severity filtering: hide codes whose effective severity is 'ignore'.
|
|
1048
|
-
// Allow is deliberately NOT applied — audit
|
|
1041
|
+
// Allow is deliberately NOT applied — audit shows every finding.
|
|
1049
1042
|
const results = applySeverityFilter(rawResults, config);
|
|
1050
1043
|
const effectiveSettings = await resolveEffectiveSettings(options, scanPath, logger);
|
|
1051
1044
|
// Run compatibility analysis if --compat flag is set
|
|
@@ -1108,7 +1101,6 @@ export async function auditCommand(targetPath, options) {
|
|
|
1108
1101
|
// cloned https://github.com/<that>.git instead, reaching out to the
|
|
1109
1102
|
// network with a name derived from the caller's own directory layout.
|
|
1110
1103
|
// Shorthand is a fallback for arguments that name nothing on disk.
|
|
1111
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- targetPath is the user's own audit target; existence is the question being asked
|
|
1112
1104
|
if (targetPath !== undefined && !fsExistsSync(targetPath) && isGitUrl(targetPath)) {
|
|
1113
1105
|
await runUrlAudit(targetPath, options);
|
|
1114
1106
|
return;
|
|
@@ -1120,6 +1112,12 @@ export async function auditCommand(targetPath, options) {
|
|
|
1120
1112
|
return;
|
|
1121
1113
|
}
|
|
1122
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
|
+
}
|
|
1123
1121
|
await runAuditAtPath(scanPath, options);
|
|
1124
1122
|
}
|
|
1125
1123
|
catch (error) {
|
|
@@ -1129,6 +1127,41 @@ export async function auditCommand(targetPath, options) {
|
|
|
1129
1127
|
handleCommandError(error, logger, startTime, 'Audit');
|
|
1130
1128
|
}
|
|
1131
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
|
+
}
|
|
1132
1165
|
/**
|
|
1133
1166
|
* Dispatch a single surface to the matching validator. Keeps the multi-surface
|
|
1134
1167
|
* branch of {@link getValidationResults} focused so cognitive complexity stays
|
|
@@ -1246,21 +1279,35 @@ async function appendPluginInventoryToSurfaceResults(surfaceResults, scanPath, l
|
|
|
1246
1279
|
* not (plugin.json) and a format it is not (JSON), at error severity beside
|
|
1247
1280
|
* the skill lane's warning, and carrying the build host's absolute path.
|
|
1248
1281
|
*
|
|
1249
|
-
* What remains — hooks/hooks.json, .mcp.json
|
|
1250
|
-
*
|
|
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.
|
|
1251
1287
|
*/
|
|
1252
1288
|
function appendInventoryParseErrors(result, inv, locationRoot) {
|
|
1253
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));
|
|
1254
1293
|
const parseIssues = [];
|
|
1255
1294
|
for (const err of inv.parseErrors) {
|
|
1256
1295
|
if (err.path.endsWith(pluginJsonSuffix) || basename(err.path) === 'SKILL.md')
|
|
1257
1296
|
continue;
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
}
|
|
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}` }));
|
|
1264
1311
|
}
|
|
1265
1312
|
// Status was hand-set to 'error' here while `issueCounts` and `summary` kept
|
|
1266
1313
|
// describing the pre-append list. One appender derives all three.
|
|
@@ -1284,7 +1331,8 @@ function appendInventoryParseErrors(result, inv, locationRoot) {
|
|
|
1284
1331
|
* way into a validator unprotected: a `SKILL.md` named directly on the command
|
|
1285
1332
|
* line, a plugin directory, a marketplace, each surface of a multi-surface tree.
|
|
1286
1333
|
* All of them still ended the whole run with `status: error`, exit 2 and zero
|
|
1287
|
-
* findings — the exact symptom of
|
|
1334
|
+
* findings — the exact symptom of the unreadable-directory defect, in the lanes
|
|
1335
|
+
* nobody had checked.
|
|
1288
1336
|
*
|
|
1289
1337
|
* Guarding the seven dispatch sites individually would have been the same mistake
|
|
1290
1338
|
* a seventh time. This function is what every one of them is reached through, so
|
|
@@ -1451,26 +1499,45 @@ async function validateAuditSubject(scanPath, recursive, options, logger, locati
|
|
|
1451
1499
|
return [result];
|
|
1452
1500
|
}
|
|
1453
1501
|
// If unknown format, check if it's a directory we can scan
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
// merged into `options.exclude`, whose patterns are relative to the
|
|
1462
|
-
// directory the operator named. See resolveProjectExcludes.
|
|
1463
|
-
return scanDirectory(scanPath, recursive, options, logger, await resolveScanContext(scanPath, locationRoot, logger));
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
catch {
|
|
1467
|
-
// 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));
|
|
1468
1509
|
}
|
|
1469
1510
|
// Unknown resource type - use unified validator which will return appropriate error
|
|
1470
1511
|
logger.debug(`Unknown resource type at: ${scanPath}`);
|
|
1471
1512
|
const result = await validate(scanPath, { locationRoot });
|
|
1472
1513
|
return [result];
|
|
1473
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
|
+
}
|
|
1474
1541
|
/**
|
|
1475
1542
|
* Inventory-driven plugin skill walker. Builds the plugin inventory via
|
|
1476
1543
|
* `extractClaudePluginInventory` and dispatches `validateSingleSkill` for each
|
|
@@ -1536,9 +1603,9 @@ function resolveConfigTargetsForPlugin(pluginDir, vatContext) {
|
|
|
1536
1603
|
const union = new Set();
|
|
1537
1604
|
for (const [skillPathAbs, packagingConfig] of vatContext.skillConfigs) {
|
|
1538
1605
|
const rel = safePath.relative(pluginDirAbs, skillPathAbs);
|
|
1539
|
-
// Skill lives inside the plugin directory iff relative path
|
|
1540
|
-
//
|
|
1541
|
-
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))
|
|
1542
1609
|
continue;
|
|
1543
1610
|
const skillTargets = packagingConfig.targets;
|
|
1544
1611
|
if (skillTargets === undefined)
|
|
@@ -1788,16 +1855,6 @@ function findingSubject(result, root) {
|
|
|
1788
1855
|
const directoryName = basename(result.path);
|
|
1789
1856
|
return directoryName === '' ? '.' : directoryName;
|
|
1790
1857
|
}
|
|
1791
|
-
/**
|
|
1792
|
-
* The clause every "audit found errors" line ends with.
|
|
1793
|
-
*
|
|
1794
|
-
* One string because the two summary lanes — per-file and per-skill — both make
|
|
1795
|
-
* the same claim and used to word it differently, which is how one of them ended
|
|
1796
|
-
* up saying "Audit failed" over an exit code of 0. Naming the command that DOES
|
|
1797
|
-
* gate is the actionable half: an adopter reading "advisory" still has to be
|
|
1798
|
-
* told what to reach for instead.
|
|
1799
|
-
*/
|
|
1800
|
-
const ADVISORY_EXIT_NOTE = '— advisory, exit 0; use `vat validate` to gate on this';
|
|
1801
1858
|
/**
|
|
1802
1859
|
* Render supporting evidence beneath each CAPABILITY_* issue when the
|
|
1803
1860
|
* audit was invoked with --verbose. Evidence comes from the validation
|
|
@@ -1826,15 +1883,15 @@ function renderVerboseEvidence(results, root, logger) {
|
|
|
1826
1883
|
* document says. The document is derived in `buildBaseSummary`; this only
|
|
1827
1884
|
* RENDERS it. Both stderr lanes call it first, because "Audit successful: 0
|
|
1828
1885
|
* file(s) passed" over `status: error` is the exact disagreement the refusal
|
|
1829
|
-
* exists to end (invariant 6 of `run-integrity.ts`).
|
|
1830
|
-
*
|
|
1886
|
+
* exists to end (invariant 6 of `run-integrity.ts`). The run ends on
|
|
1887
|
+
* `FINDINGS`, like every other `status: error` this command publishes.
|
|
1831
1888
|
*
|
|
1832
1889
|
* @returns Whether a refusal was rendered, so the caller skips its verdict line
|
|
1833
1890
|
*/
|
|
1834
1891
|
function logRunIntegrity(runIssues, logger) {
|
|
1835
1892
|
if (runIssues === undefined || runIssues.length === 0)
|
|
1836
1893
|
return false;
|
|
1837
|
-
logger.error(
|
|
1894
|
+
logger.error('Audit is not a verdict: it audited 0 files');
|
|
1838
1895
|
for (const issue of runIssues) {
|
|
1839
1896
|
for (const line of formatIssueLines(issue, ' '))
|
|
1840
1897
|
logger.error(line);
|
|
@@ -1879,26 +1936,17 @@ function logSettingsTotals(files, logger) {
|
|
|
1879
1936
|
function handleAuditResults(results, summary, logger, verbose) {
|
|
1880
1937
|
const { filesWithErrors: errorCount, filesWithWarnings: warningCount, filesPassed: successCount, } = summary.summary;
|
|
1881
1938
|
logSettingsTotals(summary.files ?? [], logger);
|
|
1882
|
-
//
|
|
1883
|
-
//
|
|
1884
|
-
//
|
|
1885
|
-
//
|
|
1886
|
-
//
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
// `status: error` over exit 0 — measured on
|
|
1890
|
-
// `packages/agent-skills/test/fixtures/skill-files` — and that is the
|
|
1891
|
-
// published contract, not a leftover: `status` describes the FINDINGS, the
|
|
1892
|
-
// exit code describes whether the RUN completed. An earlier version of this
|
|
1893
|
-
// comment claimed all three signals had been reconciled; two of the three
|
|
1894
|
-
// had, and the third is not a signal to reconcile. Where a reader is told
|
|
1895
|
-
// which is which is {@link createAuditCommand}'s help — see its `Output`
|
|
1896
|
-
// section, and `docs/audit.md`.
|
|
1897
|
-
if (logRunIntegrity(summary.issues, 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) {
|
|
1898
1946
|
// Rendered above; there is no file verdict to print over zero files.
|
|
1899
1947
|
}
|
|
1900
1948
|
else if (errorCount > 0) {
|
|
1901
|
-
logger.error(`Audit found ${errorCount} file(s) with errors
|
|
1949
|
+
logger.error(`Audit found ${errorCount} file(s) with errors`);
|
|
1902
1950
|
logFindingsForStatus(results, 'error', summary.root, logger.error.bind(logger), verbose);
|
|
1903
1951
|
}
|
|
1904
1952
|
else if (warningCount > 0) {
|
|
@@ -1914,7 +1962,7 @@ function handleAuditResults(results, summary, logger, verbose) {
|
|
|
1914
1962
|
: `Audit successful: ${successCount} file(s) passed`);
|
|
1915
1963
|
}
|
|
1916
1964
|
renderAuditFooter(results, logger);
|
|
1917
|
-
process.exit(0);
|
|
1965
|
+
process.exit(refused || errorCount > 0 ? ExitCode.FINDINGS : ExitCode.OK);
|
|
1918
1966
|
}
|
|
1919
1967
|
/**
|
|
1920
1968
|
* Render the skill-quality checklist footer when audit results contain
|
|
@@ -2009,17 +2057,6 @@ function logFindingsForStatus(results, status, root, logFn, verbose) {
|
|
|
2009
2057
|
logFn(line);
|
|
2010
2058
|
}
|
|
2011
2059
|
}
|
|
2012
|
-
/**
|
|
2013
|
-
* Check whether a path should be excluded during directory scanning.
|
|
2014
|
-
* For directories, checks both bare path and path with trailing slash
|
|
2015
|
-
* so patterns like "dist/**" prune the directory itself.
|
|
2016
|
-
*/
|
|
2017
|
-
function isExcludedPath(isMatch, relativePath, isDirectory) {
|
|
2018
|
-
if (isDirectory) {
|
|
2019
|
-
return isMatch(relativePath) || isMatch(relativePath + '/');
|
|
2020
|
-
}
|
|
2021
|
-
return isMatch(relativePath);
|
|
2022
|
-
}
|
|
2023
2060
|
/**
|
|
2024
2061
|
* Does the SKILL.md at `skillMdPath` own its own presence-side crawl, or has an
|
|
2025
2062
|
* ancestor crawl already covered it?
|
|
@@ -2033,39 +2070,27 @@ function ownsOwnCrawl(scanCtx, skillMdPath) {
|
|
|
2033
2070
|
return scanCtx.crawledRoot === null || !isWithin(scanCtx.crawledRoot, safePath.resolve(skillMdPath));
|
|
2034
2071
|
}
|
|
2035
2072
|
/**
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
|
|
2039
|
-
function claimSubtree(scanCtx, owner) {
|
|
2040
|
-
if (owner === null || scanCtx.crawledRoot !== null)
|
|
2041
|
-
return scanCtx;
|
|
2042
|
-
return { ...scanCtx, crawledRoot: safePath.resolve(owner) };
|
|
2043
|
-
}
|
|
2044
|
-
/**
|
|
2045
|
-
* The context this directory's SUBDIRECTORIES are walked with.
|
|
2046
|
-
*
|
|
2047
|
-
* A `SKILL.md` here means this directory's own crawl reaches every descendant at
|
|
2048
|
-
* 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.
|
|
2049
2076
|
*
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2053
|
-
*
|
|
2054
|
-
*
|
|
2055
|
-
* Conditional on the crawl actually RUNNING ({@link crawlOwnsSubtree}), not on
|
|
2056
|
-
* the SKILL.md merely existing: a repo-source skill's crawl is skipped
|
|
2057
|
-
* entirely, so claiming its subtree would trade a double count for a missed
|
|
2058
|
-
* 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.
|
|
2059
2081
|
*/
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
return
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
+
}
|
|
2069
2094
|
}
|
|
2070
2095
|
/**
|
|
2071
2096
|
* Handle file entry during directory scan
|
|
@@ -2116,8 +2141,8 @@ async function handleFileEntry(entry, fullPath, options, logger, scanCtx) {
|
|
|
2116
2141
|
await appendDistributedTreeFindings(configAware, fullPath, locationRoot, ownsOwnCrawl(scanCtx, fullPath));
|
|
2117
2142
|
return configAware;
|
|
2118
2143
|
}
|
|
2119
|
-
// Wild mode: basic validation
|
|
2120
|
-
const validateOptions = { skillPath: fullPath, locationRoot };
|
|
2144
|
+
// Wild mode: basic validation. Severity is applied by `applySeverityFilter`.
|
|
2145
|
+
const validateOptions = { skillPath: fullPath, locationRoot, validation: {} };
|
|
2121
2146
|
if (options.warnUnreferencedFiles) {
|
|
2122
2147
|
validateOptions.checkUnreferencedFiles = true;
|
|
2123
2148
|
}
|
|
@@ -2131,89 +2156,15 @@ async function handleFileEntry(entry, fullPath, options, logger, scanCtx) {
|
|
|
2131
2156
|
return null;
|
|
2132
2157
|
}
|
|
2133
2158
|
/**
|
|
2134
|
-
*
|
|
2135
|
-
|
|
2136
|
-
async function handleDirectoryEntry(fullPath, recursive, options, logger, baseDir, scanCtx, nestedConfigLog) {
|
|
2137
|
-
const fs = await import('node:fs/promises');
|
|
2138
|
-
const { locationRoot } = scanCtx;
|
|
2139
|
-
const results = [];
|
|
2140
|
-
// Check if directory contains a plugin or marketplace
|
|
2141
|
-
const claudePluginDir = safePath.join(fullPath, '.claude-plugin');
|
|
2142
|
-
const hasClaudePlugin = await fs.access(claudePluginDir).then(() => true).catch(() => false);
|
|
2143
|
-
if (hasClaudePlugin) {
|
|
2144
|
-
logger.debug(`Validating resource directory: ${fullPath}`);
|
|
2145
|
-
const result = await validate(fullPath, { validatePlugin, locationRoot });
|
|
2146
|
-
const inv = await pluginInventoryAt(fullPath);
|
|
2147
|
-
appendInventoryParseErrors(result, inv, locationRoot);
|
|
2148
|
-
results.push(result);
|
|
2149
|
-
}
|
|
2150
|
-
// Recurse into subdirectories (both plugin/marketplace dirs and regular dirs).
|
|
2151
|
-
// Below a plugin, `validatePlugin` has already crawled the whole subtree for
|
|
2152
|
-
// agent-instruction files, so every crawl below must stand down or one file
|
|
2153
|
-
// is reported twice. See {@link ScanContext.crawledRoot} — first owner wins,
|
|
2154
|
-
// and it never clears on the way down.
|
|
2155
|
-
const childCtx = claimSubtree(scanCtx, hasClaudePlugin ? fullPath : null);
|
|
2156
|
-
if (recursive) {
|
|
2157
|
-
const subResults = await scanDirectory(fullPath, recursive, options, logger, childCtx, baseDir, nestedConfigLog);
|
|
2158
|
-
results.push(...subResults);
|
|
2159
|
-
}
|
|
2160
|
-
return results;
|
|
2161
|
-
}
|
|
2162
|
-
/**
|
|
2163
|
-
* Compile the governing project's `resources.exclude` for a scan.
|
|
2164
|
-
*
|
|
2165
|
-
* The config is found by walking UP from the scan directory ({@link findProjectRoot}),
|
|
2166
|
-
* not by looking in it. Looking only in the scan directory is how a path argument
|
|
2167
|
-
* silently voided every exclude the project had declared: `vat audit
|
|
2168
|
-
* packages/x/resources/skills/` found no config there, so a package that excludes
|
|
2169
|
-
* its deliberately-broken eval fixtures had them audited as production skills the
|
|
2170
|
-
* moment anyone named a subdirectory. Commit 8a466b0c fixed the same defect for
|
|
2171
|
-
* `vat resources validate|scan` and `vat rag index`; this is the lane it missed.
|
|
2172
|
-
* The rule is the same one that commit established: a path argument says WHICH
|
|
2173
|
-
* tree to audit, and `exclude` applies either way.
|
|
2174
|
-
*
|
|
2175
|
-
* With one deliberate exception, mirroring the gitignore rule in
|
|
2176
|
-
* {@link resolveScanContext}: when the operator points the scan AT an excluded
|
|
2177
|
-
* tree, their explicit intent wins and the excludes are dropped for that run.
|
|
2178
|
-
* Otherwise naming an excluded directory would report `filesScanned: 0,
|
|
2179
|
-
* status: success` — a green run that scanned nothing.
|
|
2159
|
+
* Validate a directory the population found holding `.claude-plugin/` — a
|
|
2160
|
+
* plugin or a marketplace, whichever the unified validator decides.
|
|
2180
2161
|
*/
|
|
2181
|
-
function
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
patterns = loadConfig(projectRoot)?.resources?.exclude ?? [];
|
|
2188
|
-
}
|
|
2189
|
-
catch (err) {
|
|
2190
|
-
// Audit is a bulk linter over trees it does not own; a broken governing
|
|
2191
|
-
// config must not abort the scan. Say so rather than dropping it silently.
|
|
2192
|
-
//
|
|
2193
|
-
// 🚨 `warn`, not `debug`. This comment already said "say so" while logging at
|
|
2194
|
-
// a level nobody sees without `--debug`, so what actually happened was the
|
|
2195
|
-
// silent drop it forbids — and the consequence is the one this function's own
|
|
2196
|
-
// docstring spells out: EVERY `resources.exclude` the project declared is
|
|
2197
|
-
// void, so a package that excludes its deliberately-broken eval fixtures has
|
|
2198
|
-
// them audited as production skills. That is findings APPEARING, not findings
|
|
2199
|
-
// disappearing, which is exactly the direction an operator will read as VAT
|
|
2200
|
-
// being wrong rather than as their config being broken.
|
|
2201
|
-
logger.warn(`Config at ${projectRoot} could not be read; every resources.exclude it declares is`
|
|
2202
|
-
+ ` dropped for this run, so excluded trees are audited as ordinary source: ${String(err)}`);
|
|
2203
|
-
return null;
|
|
2204
|
-
}
|
|
2205
|
-
if (patterns.length === 0)
|
|
2206
|
-
return null;
|
|
2207
|
-
// dot:true so excludes like `**/.cache/*` match through dotfile dirs;
|
|
2208
|
-
// without it the exclude silently never fires.
|
|
2209
|
-
const isMatch = picomatch([...patterns], { dot: true });
|
|
2210
|
-
const scanRelToProject = safePath.relative(projectRoot, safePath.resolve(scanDir)).replaceAll('\\', '/');
|
|
2211
|
-
if (scanRelToProject !== '' && isExcludedPath(isMatch, scanRelToProject, true)) {
|
|
2212
|
-
logger.debug(`Scan root ${scanRelToProject} is excluded by the config at ${projectRoot}; ` +
|
|
2213
|
-
'auditing it anyway because it was named explicitly');
|
|
2214
|
-
return null;
|
|
2215
|
-
}
|
|
2216
|
-
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;
|
|
2217
2168
|
}
|
|
2218
2169
|
/**
|
|
2219
2170
|
* Cache of (projectRoot → registry for the *inventory* link walk).
|
|
@@ -2313,43 +2264,6 @@ async function resolveScanContext(dirPath, locationRoot, logger) {
|
|
|
2313
2264
|
}
|
|
2314
2265
|
return { gitRoot: detectedRoot, gitTracker: tracker, locationRoot, projectExcludes, crawledRoot: null };
|
|
2315
2266
|
}
|
|
2316
|
-
/**
|
|
2317
|
-
* Build the gitignore exclusion map for a set of entry paths.
|
|
2318
|
-
* Returns null when gitignore filtering is not applicable (no git root,
|
|
2319
|
-
* --include-artifacts, or outside a git repo).
|
|
2320
|
-
*/
|
|
2321
|
-
function buildGitIgnoreMap(entryPaths, gitTracker, includeArtifacts) {
|
|
2322
|
-
if (gitTracker === null || includeArtifacts) {
|
|
2323
|
-
return null;
|
|
2324
|
-
}
|
|
2325
|
-
const map = new Map();
|
|
2326
|
-
for (const entryPath of entryPaths) {
|
|
2327
|
-
map.set(entryPath, gitTracker.isIgnoredByActiveSet(entryPath));
|
|
2328
|
-
}
|
|
2329
|
-
return map;
|
|
2330
|
-
}
|
|
2331
|
-
/**
|
|
2332
|
-
* Check whether a directory entry should be skipped during scanning.
|
|
2333
|
-
* Returns a reason string for debug logging, or null if the entry should be kept.
|
|
2334
|
-
*/
|
|
2335
|
-
function getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers) {
|
|
2336
|
-
// Always skip .git directory (not reported by git check-ignore)
|
|
2337
|
-
if (entry.isDirectory() && entry.name === '.git') {
|
|
2338
|
-
return '.git';
|
|
2339
|
-
}
|
|
2340
|
-
// Skip gitignored paths
|
|
2341
|
-
if (gitIgnoredMap !== null && gitIgnoredMap.get(fullPath) === true) {
|
|
2342
|
-
return `gitignored: ${entry.name}`;
|
|
2343
|
-
}
|
|
2344
|
-
// Each matcher answers against ITS OWN base — see {@link ExcludeMatcher}.
|
|
2345
|
-
for (const matcher of excludeMatchers) {
|
|
2346
|
-
const relativePath = safePath.relative(matcher.base, fullPath).replaceAll('\\', '/');
|
|
2347
|
-
if (isExcludedPath(matcher.isMatch, relativePath, entry.isDirectory())) {
|
|
2348
|
-
return `${matcher.label}: ${relativePath}`;
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
return null;
|
|
2352
|
-
}
|
|
2353
2267
|
/**
|
|
2354
2268
|
* The one finding a path the walk could not read produces — a directory it could
|
|
2355
2269
|
* not enter, or a file it could not open.
|
|
@@ -2366,14 +2280,11 @@ function getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers) {
|
|
|
2366
2280
|
* is no longer the ONLY thing the run says — the code, the location and the fix
|
|
2367
2281
|
* come from the registry.
|
|
2368
2282
|
*
|
|
2369
|
-
*
|
|
2370
|
-
*
|
|
2371
|
-
*
|
|
2372
|
-
*
|
|
2373
|
-
*
|
|
2374
|
-
* own, which is a change to the report's shape and belongs with issue #177
|
|
2375
|
-
* ("report counts don't describe the artifact") rather than smuggled in here. The
|
|
2376
|
-
* 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.
|
|
2377
2288
|
*/
|
|
2378
2289
|
function unreadablePathResult(dirPath, error, locationRoot) {
|
|
2379
2290
|
// `issueLocation` is `path.relative`, so it answers `''` when the subject IS the
|
|
@@ -2405,30 +2316,36 @@ function unreadablePathResult(dirPath, error, locationRoot) {
|
|
|
2405
2316
|
};
|
|
2406
2317
|
}
|
|
2407
2318
|
/**
|
|
2408
|
-
*
|
|
2319
|
+
* Validate ONE subject the population found, degrading to a finding if the
|
|
2320
|
+
* filesystem refuses it.
|
|
2409
2321
|
*
|
|
2410
|
-
* Guarded per
|
|
2411
|
-
*
|
|
2412
|
-
*
|
|
2413
|
-
* 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
|
|
2414
2325
|
* `~/.claude/plugins` — the flagship `vat audit --user` target, populated by sudo
|
|
2415
2326
|
* installs and macOS quarantine — a root-owned FILE is at least as likely as a
|
|
2416
|
-
* root-owned directory
|
|
2327
|
+
* root-owned directory.
|
|
2417
2328
|
*
|
|
2418
|
-
*
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
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.
|
|
2421
2334
|
*/
|
|
2422
|
-
async function
|
|
2335
|
+
async function validateScanSubject(subject, options, logger, scanCtx, claims) {
|
|
2336
|
+
const { locationRoot } = scanCtx;
|
|
2423
2337
|
try {
|
|
2424
|
-
if (
|
|
2425
|
-
const result = await
|
|
2426
|
-
|
|
2338
|
+
if (subject.kind === 'plugin') {
|
|
2339
|
+
const result = await validatePluginDirectory(subject.dir, logger, locationRoot);
|
|
2340
|
+
claims.claim(subject.dir);
|
|
2341
|
+
return [result];
|
|
2427
2342
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
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);
|
|
2430
2347
|
}
|
|
2431
|
-
return [];
|
|
2348
|
+
return result === null ? [] : [result];
|
|
2432
2349
|
}
|
|
2433
2350
|
catch (error) {
|
|
2434
2351
|
// ONLY filesystem-access errors degrade. Anything else is rethrown, because
|
|
@@ -2437,87 +2354,61 @@ async function scanEntry(entry, fullPath, ctx) {
|
|
|
2437
2354
|
// prevent — it would make the tool quietest exactly when it is most wrong.
|
|
2438
2355
|
if (!isFilesystemAccessError(error))
|
|
2439
2356
|
throw error;
|
|
2440
|
-
|
|
2441
|
-
|
|
2357
|
+
const path = subject.kind === 'plugin' ? subject.dir : subject.path;
|
|
2358
|
+
logger.debug(`Unreadable entry: ${path}`);
|
|
2359
|
+
return [unreadablePathResult(path, error, locationRoot)];
|
|
2442
2360
|
}
|
|
2443
2361
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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 `/`.
|
|
2457
2386
|
findProjectRoot(dirPath);
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
//
|
|
2470
|
-
//
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
excludeMatchers.push(resolvedScanCtx.projectExcludes);
|
|
2484
|
-
}
|
|
2485
|
-
// Guarded because `vat audit` is a bulk linter over trees it does not own, and
|
|
2486
|
-
// an unreadable entry is an ordinary condition there — one root-owned directory
|
|
2487
|
-
// under `~/.claude/plugins` used to abort the flagship `vat audit --user` run
|
|
2488
|
-
// outright, losing every finding already collected (issue #180). The failure is
|
|
2489
|
-
// scoped to the subtree that caused it: this returns instead of throwing, so the
|
|
2490
|
-
// CALLER's loop keeps its readable siblings and the walk continues.
|
|
2491
|
-
let entries;
|
|
2492
|
-
try {
|
|
2493
|
-
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
2494
|
-
}
|
|
2495
|
-
catch (error) {
|
|
2496
|
-
// Same predicate as the per-entry guard below — see it for why a non-IO
|
|
2497
|
-
// throw must not be degraded into a finding.
|
|
2498
|
-
if (!isFilesystemAccessError(error))
|
|
2499
|
-
throw error;
|
|
2500
|
-
logger.debug(`Unreadable directory: ${dirPath}`);
|
|
2501
|
-
return [unreadablePathResult(dirPath, error, resolvedScanCtx.locationRoot)];
|
|
2502
|
-
}
|
|
2503
|
-
const descendCtx = await contextForDescendants(dirPath, entries, resolvedScanCtx);
|
|
2504
|
-
// Build full paths and batch-check gitignore status
|
|
2505
|
-
const entryPaths = entries.map(e => safePath.join(dirPath, e.name));
|
|
2506
|
-
const gitIgnoredMap = buildGitIgnoreMap(entryPaths, resolvedScanCtx.gitTracker, options.includeArtifacts ?? false);
|
|
2507
|
-
for (const entry of entries) {
|
|
2508
|
-
const fullPath = safePath.join(dirPath, entry.name);
|
|
2509
|
-
const skipReason = getSkipReason(entry, fullPath, gitIgnoredMap, excludeMatchers);
|
|
2510
|
-
if (skipReason !== null) {
|
|
2511
|
-
logger.debug(`Excluding path: ${skipReason}`);
|
|
2512
|
-
continue;
|
|
2513
|
-
}
|
|
2514
|
-
results.push(...await scanEntry(entry, fullPath, {
|
|
2515
|
-
recursive, options, logger,
|
|
2516
|
-
scanCtx: resolvedScanCtx,
|
|
2517
|
-
descendCtx,
|
|
2518
|
-
baseDir: resolvedBaseDir,
|
|
2519
|
-
nestedConfigLog: resolvedNestedLog,
|
|
2520
|
-
}));
|
|
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));
|
|
2521
2412
|
}
|
|
2522
2413
|
return results;
|
|
2523
2414
|
}
|
|
@@ -2546,13 +2437,34 @@ function calculateOverallStatus(results) {
|
|
|
2546
2437
|
function countResultsWithFindings(results) {
|
|
2547
2438
|
return results.filter((r) => r.issues.length > 0).length;
|
|
2548
2439
|
}
|
|
2549
|
-
/**
|
|
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
|
+
*/
|
|
2550
2460
|
function countFilesByStatus(results) {
|
|
2461
|
+
const scanned = results.filter((r) => !isUnreadablePathResult(r));
|
|
2551
2462
|
return {
|
|
2552
|
-
filesScanned:
|
|
2553
|
-
filesPassed:
|
|
2554
|
-
filesWithWarnings:
|
|
2555
|
-
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,
|
|
2556
2468
|
};
|
|
2557
2469
|
}
|
|
2558
2470
|
/**
|
|
@@ -2608,7 +2520,10 @@ function buildBaseSummary(results, startTime) {
|
|
|
2608
2520
|
issueCounts.info += counts.info;
|
|
2609
2521
|
return { ...result, issueCounts: counts };
|
|
2610
2522
|
});
|
|
2611
|
-
const
|
|
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'
|
|
2612
2527
|
+ ' to audit produces the same counts as a clean one. The path resolved to a tree'
|
|
2613
2528
|
+ ' holding no auditable file — usually a wrong subdirectory, plugins or skills that'
|
|
2614
2529
|
+ ' have moved, an excluded tree, files no audit lane recognises, or `--no-recursive`'
|
|
@@ -2620,7 +2535,7 @@ function buildBaseSummary(results, startTime) {
|
|
|
2620
2535
|
// `run-integrity.ts`), so a non-empty refusal IS the status; otherwise the
|
|
2621
2536
|
// files decide it, as before.
|
|
2622
2537
|
status: runIssues.length > 0 ? 'error' : calculateOverallStatus(entries),
|
|
2623
|
-
summary
|
|
2538
|
+
summary,
|
|
2624
2539
|
issueCounts: sumSeverityCounts([issueCounts, countBySeverity(runIssues)]),
|
|
2625
2540
|
...(runIssues.length === 0 ? {} : { issues: [...runIssues] }),
|
|
2626
2541
|
duration: `${Date.now() - startTime}ms`,
|
|
@@ -2653,14 +2568,14 @@ function logHierarchicalSummary(results, runIssues, logger) {
|
|
|
2653
2568
|
const status = calculateOverallStatus(results);
|
|
2654
2569
|
const skillsWithIssues = countResultsWithFindings(results);
|
|
2655
2570
|
const totalSkills = results.length;
|
|
2656
|
-
//
|
|
2657
|
-
|
|
2658
|
-
if (
|
|
2571
|
+
// Same rule as the per-file lane: the exit code follows `status`.
|
|
2572
|
+
const refused = logRunIntegrity(runIssues, logger);
|
|
2573
|
+
if (refused) {
|
|
2659
2574
|
// Rendered above; there is no skill verdict to print over zero skills.
|
|
2660
2575
|
}
|
|
2661
2576
|
else if (status === 'error') {
|
|
2662
2577
|
const errorCount = results.filter((r) => r.status === 'error').length;
|
|
2663
|
-
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)`);
|
|
2664
2579
|
}
|
|
2665
2580
|
else if (status === 'warning') {
|
|
2666
2581
|
const warningCount = results.filter((r) => r.status === 'warning').length;
|
|
@@ -2675,6 +2590,6 @@ function logHierarchicalSummary(results, runIssues, logger) {
|
|
|
2675
2590
|
logger.info(`Audit successful: ${totalSkills} skill(s) passed`);
|
|
2676
2591
|
}
|
|
2677
2592
|
renderAuditFooter(results, logger);
|
|
2678
|
-
process.exit(
|
|
2593
|
+
process.exit(refused || status === 'error' ? ExitCode.FINDINGS : ExitCode.OK);
|
|
2679
2594
|
}
|
|
2680
2595
|
//# sourceMappingURL=audit.js.map
|