@vibe-agent-toolkit/agent-skills 0.1.42 → 0.2.0-rc.10
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 +5 -5
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +7 -17
- package/dist/builder.js.map +1 -1
- package/dist/content-type-routing.d.ts +32 -0
- package/dist/content-type-routing.d.ts.map +1 -1
- package/dist/content-type-routing.js +23 -0
- package/dist/content-type-routing.js.map +1 -1
- package/dist/files-config.d.ts +34 -1
- package/dist/files-config.d.ts.map +1 -1
- package/dist/files-config.js +277 -39
- package/dist/files-config.js.map +1 -1
- package/dist/fs-attribution.d.ts +45 -0
- package/dist/fs-attribution.d.ts.map +1 -0
- package/dist/fs-attribution.js +57 -0
- package/dist/fs-attribution.js.map +1 -0
- package/dist/import.d.ts.map +1 -1
- package/dist/import.js +0 -4
- package/dist/import.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -7
- package/dist/index.js.map +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts +1 -1
- package/dist/inventory/detectors/declared-but-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts +1 -1
- package/dist/inventory/detectors/marketplace-source-missing.d.ts.map +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts +1 -1
- package/dist/inventory/detectors/present-but-undeclared.d.ts.map +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts +1 -1
- package/dist/inventory/detectors/reference-target-missing.d.ts.map +1 -1
- package/dist/inventory/index.d.ts +1 -1
- package/dist/inventory/index.d.ts.map +1 -1
- package/dist/inventory/index.js +1 -1
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/serialize.d.ts +4 -4
- package/dist/inventory/serialize.d.ts.map +1 -1
- package/dist/inventory/serialize.js +6 -6
- package/dist/inventory/serialize.js.map +1 -1
- package/dist/inventory/types.d.ts +8 -0
- package/dist/inventory/types.d.ts.map +1 -1
- package/dist/inventory/types.js.map +1 -1
- package/dist/link-graph-members.d.ts +18 -0
- package/dist/link-graph-members.d.ts.map +1 -0
- package/dist/link-graph-members.js +18 -0
- package/dist/link-graph-members.js.map +1 -0
- package/dist/parsers/frontmatter-parser.d.ts.map +1 -1
- package/dist/parsers/frontmatter-parser.js +30 -6
- package/dist/parsers/frontmatter-parser.js.map +1 -1
- package/dist/plugin-distribution-layout.d.ts +70 -6
- package/dist/plugin-distribution-layout.d.ts.map +1 -1
- package/dist/plugin-distribution-layout.js +84 -11
- package/dist/plugin-distribution-layout.js.map +1 -1
- package/dist/post-build-checks.d.ts +38 -1
- package/dist/post-build-checks.d.ts.map +1 -1
- package/dist/post-build-checks.js +68 -17
- package/dist/post-build-checks.js.map +1 -1
- package/dist/projection/skill-extent.d.ts +247 -0
- package/dist/projection/skill-extent.d.ts.map +1 -0
- package/dist/projection/skill-extent.js +560 -0
- package/dist/projection/skill-extent.js.map +1 -0
- package/dist/schemas/agent-skill-frontmatter.js +1 -1
- package/dist/schemas/agent-skill-frontmatter.js.map +1 -1
- package/dist/schemas/allowed-tools.d.ts +31 -0
- package/dist/schemas/allowed-tools.d.ts.map +1 -0
- package/dist/schemas/allowed-tools.js +88 -0
- package/dist/schemas/allowed-tools.js.map +1 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/installed-plugins-registry.d.ts.map +1 -1
- package/dist/schemas/installed-plugins-registry.js +7 -1
- package/dist/schemas/installed-plugins-registry.js.map +1 -1
- package/dist/schemas/marketplace-manifest.d.ts.map +1 -1
- package/dist/schemas/marketplace-manifest.js +17 -6
- package/dist/schemas/marketplace-manifest.js.map +1 -1
- package/dist/skill-identity.d.ts +18 -3
- package/dist/skill-identity.d.ts.map +1 -1
- package/dist/skill-identity.js +25 -6
- package/dist/skill-identity.js.map +1 -1
- package/dist/skill-packager.d.ts +144 -20
- package/dist/skill-packager.d.ts.map +1 -1
- package/dist/skill-packager.js +447 -105
- package/dist/skill-packager.js.map +1 -1
- package/dist/skill-source/content-hash.d.ts.map +1 -1
- package/dist/skill-source/content-hash.js +21 -10
- package/dist/skill-source/content-hash.js.map +1 -1
- package/dist/skill-source/fetch-cache.d.ts.map +1 -1
- package/dist/skill-source/fetch-cache.js +0 -5
- package/dist/skill-source/fetch-cache.js.map +1 -1
- package/dist/skill-source/git-clone.d.ts +1 -1
- package/dist/skill-source/git-clone.d.ts.map +1 -1
- package/dist/skill-source/git-clone.js +10 -10
- package/dist/skill-source/git-clone.js.map +1 -1
- package/dist/skill-source/sources/url-source.d.ts.map +1 -1
- package/dist/skill-source/sources/url-source.js +2 -2
- package/dist/skill-source/sources/url-source.js.map +1 -1
- package/dist/skill-source/stage.d.ts.map +1 -1
- package/dist/skill-source/stage.js +0 -6
- package/dist/skill-source/stage.js.map +1 -1
- package/dist/skill-test/baseline-delta.d.ts +1575 -0
- package/dist/skill-test/baseline-delta.d.ts.map +1 -0
- package/dist/skill-test/baseline-delta.js +272 -0
- package/dist/skill-test/baseline-delta.js.map +1 -0
- package/dist/skill-test/baseline-integrity.d.ts +944 -0
- package/dist/skill-test/baseline-integrity.d.ts.map +1 -0
- package/dist/skill-test/baseline-integrity.js +3090 -0
- package/dist/skill-test/baseline-integrity.js.map +1 -0
- package/dist/skill-test/build-hook.d.ts +3 -2
- package/dist/skill-test/build-hook.d.ts.map +1 -1
- package/dist/skill-test/build-hook.js +4 -5
- package/dist/skill-test/build-hook.js.map +1 -1
- package/dist/skill-test/configure-writer.js +1 -1
- package/dist/skill-test/configure-writer.js.map +1 -1
- package/dist/skill-test/declared-env.d.ts +5 -4
- package/dist/skill-test/declared-env.d.ts.map +1 -1
- package/dist/skill-test/declared-env.js +8 -9
- package/dist/skill-test/declared-env.js.map +1 -1
- package/dist/skill-test/eval-executor.d.ts +21 -5
- package/dist/skill-test/eval-executor.d.ts.map +1 -1
- package/dist/skill-test/eval-executor.js +6 -6
- package/dist/skill-test/eval-executor.js.map +1 -1
- package/dist/skill-test/eval-fragment.d.ts +106 -28
- package/dist/skill-test/eval-fragment.d.ts.map +1 -1
- package/dist/skill-test/eval-fragment.js +78 -16
- package/dist/skill-test/eval-fragment.js.map +1 -1
- package/dist/skill-test/eval-grader.d.ts +65 -7
- package/dist/skill-test/eval-grader.d.ts.map +1 -1
- package/dist/skill-test/eval-grader.js +263 -64
- package/dist/skill-test/eval-grader.js.map +1 -1
- package/dist/skill-test/eval-inputs.d.ts +110 -8
- package/dist/skill-test/eval-inputs.d.ts.map +1 -1
- package/dist/skill-test/eval-inputs.js +177 -22
- package/dist/skill-test/eval-inputs.js.map +1 -1
- package/dist/skill-test/eval-lint.d.ts +17 -4
- package/dist/skill-test/eval-lint.d.ts.map +1 -1
- package/dist/skill-test/eval-lint.js +22 -9
- package/dist/skill-test/eval-lint.js.map +1 -1
- package/dist/skill-test/eval-suite-isolation.d.ts.map +1 -1
- package/dist/skill-test/eval-suite-isolation.js +8 -5
- package/dist/skill-test/eval-suite-isolation.js.map +1 -1
- package/dist/skill-test/evals-template.d.ts +1 -1
- package/dist/skill-test/evals-template.d.ts.map +1 -1
- package/dist/skill-test/evals-template.js +1 -3
- package/dist/skill-test/evals-template.js.map +1 -1
- package/dist/skill-test/executor-prompt.d.ts +15 -7
- package/dist/skill-test/executor-prompt.d.ts.map +1 -1
- package/dist/skill-test/executor-prompt.js +10 -7
- package/dist/skill-test/executor-prompt.js.map +1 -1
- package/dist/skill-test/failure-reason.d.ts +113 -0
- package/dist/skill-test/failure-reason.d.ts.map +1 -0
- package/dist/skill-test/failure-reason.js +124 -0
- package/dist/skill-test/failure-reason.js.map +1 -0
- package/dist/skill-test/fragment-merge.d.ts +3 -20
- package/dist/skill-test/fragment-merge.d.ts.map +1 -1
- package/dist/skill-test/fragment-merge.js +67 -8
- package/dist/skill-test/fragment-merge.js.map +1 -1
- package/dist/skill-test/grader-prompt.d.ts +29 -9
- package/dist/skill-test/grader-prompt.d.ts.map +1 -1
- package/dist/skill-test/grader-prompt.js +264 -68
- package/dist/skill-test/grader-prompt.js.map +1 -1
- package/dist/skill-test/grader-text.d.ts +145 -0
- package/dist/skill-test/grader-text.d.ts.map +1 -0
- package/dist/skill-test/grader-text.js +410 -0
- package/dist/skill-test/grader-text.js.map +1 -0
- package/dist/skill-test/grading-adapter.d.ts +38 -2
- package/dist/skill-test/grading-adapter.d.ts.map +1 -1
- package/dist/skill-test/grading-adapter.js +57 -9
- package/dist/skill-test/grading-adapter.js.map +1 -1
- package/dist/skill-test/grading-schema.d.ts +160 -5
- package/dist/skill-test/grading-schema.d.ts.map +1 -1
- package/dist/skill-test/grading-schema.js +66 -7
- package/dist/skill-test/grading-schema.js.map +1 -1
- package/dist/skill-test/harness-location.d.ts +3 -2
- package/dist/skill-test/harness-location.d.ts.map +1 -1
- package/dist/skill-test/harness-location.js +5 -16
- package/dist/skill-test/harness-location.js.map +1 -1
- package/dist/skill-test/index.d.ts +2 -2
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +2 -2
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/lock.d.ts +58 -1
- package/dist/skill-test/lock.d.ts.map +1 -1
- package/dist/skill-test/lock.js +78 -6
- package/dist/skill-test/lock.js.map +1 -1
- package/dist/skill-test/pipeline.d.ts +19 -1
- package/dist/skill-test/pipeline.d.ts.map +1 -1
- package/dist/skill-test/pipeline.js +21 -10
- package/dist/skill-test/pipeline.js.map +1 -1
- package/dist/skill-test/preflight.d.ts +1 -1
- package/dist/skill-test/preflight.d.ts.map +1 -1
- package/dist/skill-test/preflight.js +59 -16
- package/dist/skill-test/preflight.js.map +1 -1
- package/dist/skill-test/prompt-invariants.d.ts +4 -2
- package/dist/skill-test/prompt-invariants.d.ts.map +1 -1
- package/dist/skill-test/prompt-invariants.js +5 -4
- package/dist/skill-test/prompt-invariants.js.map +1 -1
- package/dist/skill-test/run-harness.d.ts +665 -85
- package/dist/skill-test/run-harness.d.ts.map +1 -1
- package/dist/skill-test/run-harness.js +1522 -237
- package/dist/skill-test/run-harness.js.map +1 -1
- package/dist/skill-test/staging.d.ts +20 -8
- package/dist/skill-test/staging.d.ts.map +1 -1
- package/dist/skill-test/staging.js +25 -18
- package/dist/skill-test/staging.js.map +1 -1
- package/dist/skill-test/tier-plan.d.ts +1 -1
- package/dist/skill-test/tier-plan.js +1 -1
- package/dist/skill-test/tool-eval-schema.d.ts +6 -6
- package/dist/skill-test/tool-eval-schema.js +2 -2
- package/dist/skill-test/vendor-manifest.d.ts +2 -1
- package/dist/skill-test/vendor-manifest.d.ts.map +1 -1
- package/dist/skill-test/vendor-manifest.js +10 -10
- package/dist/skill-test/vendor-manifest.js.map +1 -1
- package/dist/test-input.d.ts +59 -8
- package/dist/test-input.d.ts.map +1 -1
- package/dist/test-input.js +64 -19
- package/dist/test-input.js.map +1 -1
- package/dist/validators/agent-instruction-presence.d.ts +1 -1
- package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
- package/dist/validators/agent-instruction-presence.js +45 -2
- package/dist/validators/agent-instruction-presence.js.map +1 -1
- package/dist/validators/bundled-resource-link-detection.d.ts +6 -4
- package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.js +39 -39
- package/dist/validators/bundled-resource-link-detection.js.map +1 -1
- package/dist/validators/compat-detectors.d.ts +18 -1
- package/dist/validators/compat-detectors.d.ts.map +1 -1
- package/dist/validators/compat-detectors.js +47 -20
- package/dist/validators/compat-detectors.js.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts +1 -1
- package/dist/validators/cross-skill-dependency-detection.d.ts.map +1 -1
- package/dist/validators/cross-skill-dependency-detection.js +1 -1
- package/dist/validators/cross-skill-dependency-detection.js.map +1 -1
- package/dist/validators/description-style-detection.d.ts +1 -1
- package/dist/validators/description-style-detection.d.ts.map +1 -1
- package/dist/validators/description-style-detection.js +1 -1
- package/dist/validators/description-style-detection.js.map +1 -1
- package/dist/validators/format-detection.d.ts +3 -0
- package/dist/validators/format-detection.d.ts.map +1 -1
- package/dist/validators/format-detection.js +41 -26
- package/dist/validators/format-detection.js.map +1 -1
- package/dist/validators/frontmatter-validation.d.ts +1 -1
- package/dist/validators/frontmatter-validation.d.ts.map +1 -1
- package/dist/validators/frontmatter-validation.js +2 -2
- package/dist/validators/frontmatter-validation.js.map +1 -1
- package/dist/validators/imperative-body-detection.d.ts +1 -1
- package/dist/validators/imperative-body-detection.d.ts.map +1 -1
- package/dist/validators/imperative-body-detection.js +1 -1
- package/dist/validators/imperative-body-detection.js.map +1 -1
- package/dist/validators/index.d.ts +1 -1
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.d.ts +1 -1
- package/dist/validators/kebab-case-detection.d.ts.map +1 -1
- package/dist/validators/kebab-case-detection.js +1 -1
- package/dist/validators/kebab-case-detection.js.map +1 -1
- package/dist/validators/marketplace-validator.d.ts.map +1 -1
- package/dist/validators/marketplace-validator.js +7 -2
- package/dist/validators/marketplace-validator.js.map +1 -1
- package/dist/validators/mcp-tool-qualification.d.ts +158 -0
- package/dist/validators/mcp-tool-qualification.d.ts.map +1 -0
- package/dist/validators/mcp-tool-qualification.js +467 -0
- package/dist/validators/mcp-tool-qualification.js.map +1 -0
- package/dist/validators/packaged-size-limit.d.ts +296 -0
- package/dist/validators/packaged-size-limit.d.ts.map +1 -0
- package/dist/validators/packaged-size-limit.js +492 -0
- package/dist/validators/packaged-size-limit.js.map +1 -0
- package/dist/validators/packaging-validator.d.ts +149 -10
- package/dist/validators/packaging-validator.d.ts.map +1 -1
- package/dist/validators/packaging-validator.js +371 -55
- package/dist/validators/packaging-validator.js.map +1 -1
- package/dist/validators/plugin-hosted-shape.d.ts +4 -2
- package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
- package/dist/validators/plugin-hosted-shape.js +6 -7
- package/dist/validators/plugin-hosted-shape.js.map +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts +1 -1
- package/dist/validators/plugin-recommended-fields.d.ts.map +1 -1
- package/dist/validators/plugin-recommended-fields.js +1 -1
- package/dist/validators/plugin-recommended-fields.js.map +1 -1
- package/dist/validators/referenced-path-missing.d.ts +211 -0
- package/dist/validators/referenced-path-missing.d.ts.map +1 -0
- package/dist/validators/referenced-path-missing.js +378 -0
- package/dist/validators/referenced-path-missing.js.map +1 -0
- package/dist/validators/registry-validator.d.ts.map +1 -1
- package/dist/validators/registry-validator.js +1 -2
- package/dist/validators/registry-validator.js.map +1 -1
- package/dist/validators/rule-engine/index.d.ts +1 -1
- package/dist/validators/rule-engine/rule-context.d.ts +20 -3
- package/dist/validators/rule-engine/rule-context.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-context.js +3 -1
- package/dist/validators/rule-engine/rule-context.js.map +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts +2 -2
- package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-engine.js +52 -13
- package/dist/validators/rule-engine/rule-engine.js.map +1 -1
- package/dist/validators/skill-validator.d.ts.map +1 -1
- package/dist/validators/skill-validator.js +91 -54
- package/dist/validators/skill-validator.js.map +1 -1
- package/dist/validators/source-only-codes.js +1 -1
- package/dist/validators/source-only-codes.js.map +1 -1
- package/dist/validators/types.d.ts +21 -1
- package/dist/validators/types.d.ts.map +1 -1
- package/dist/validators/unified-validator.d.ts.map +1 -1
- package/dist/validators/unified-validator.js +4 -5
- package/dist/validators/unified-validator.js.map +1 -1
- package/dist/validators/validation-rules.d.ts +4 -4
- package/dist/validators/validation-rules.d.ts.map +1 -1
- package/dist/validators/validation-rules.js +9 -38
- package/dist/validators/validation-rules.js.map +1 -1
- package/dist/validators/walker-to-issues.d.ts +9 -3
- package/dist/validators/walker-to-issues.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.js +64 -19
- package/dist/validators/walker-to-issues.js.map +1 -1
- package/dist/walk-link-graph.d.ts +64 -5
- package/dist/walk-link-graph.d.ts.map +1 -1
- package/dist/walk-link-graph.js +376 -92
- package/dist/walk-link-graph.js.map +1 -1
- package/package.json +14 -22
- package/dist/skill-test/exit-codes.d.ts +0 -97
- package/dist/skill-test/exit-codes.d.ts.map +0 -1
- package/dist/skill-test/exit-codes.js +0 -141
- package/dist/skill-test/exit-codes.js.map +0 -1
|
@@ -15,19 +15,21 @@
|
|
|
15
15
|
import { existsSync, statSync } from 'node:fs';
|
|
16
16
|
import { readFile } from 'node:fs/promises';
|
|
17
17
|
import { basename, dirname } from 'node:path';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { findProjectRoot, issueLocation, normalizedTmpdir, toForwardSlash, safePath } from '@vibe-agent-toolkit/utils';
|
|
18
|
+
import { DeferredArtifacts, listingRefusalRemedy, loadConfig, parseFileCached, ResourceRegistry, } from '@vibe-agent-toolkit/resources';
|
|
19
|
+
import { CODE_REGISTRY, runSingleUnitValidation, runValidationFramework, } from '@vibe-agent-toolkit/schema';
|
|
20
|
+
import { findProjectRoot, issueLocation, normalizedTmpdir, toForwardSlash, toForwardSlashAnyPlatform, safePath, } from '@vibe-agent-toolkit/utils';
|
|
21
|
+
import { DirectoryListingRefusedError } from '@vibe-agent-toolkit/utils/crawl';
|
|
21
22
|
import { collectPreBuildGlobFindings, preBuildGlobFindingsToIssues } from '../files-config.js';
|
|
22
|
-
import {
|
|
23
|
+
import { conventionalSuiteProbe, partitionTestInputFileEntries, resolveTestInputDirs, testInputExcludeRules, testInputLinkIssues, } from '../test-input.js';
|
|
23
24
|
import { walkLinkGraph } from '../walk-link-graph.js';
|
|
24
25
|
import { observationToIssue, runCompatDetectors } from './compat-detectors.js';
|
|
25
26
|
import { detectUndeclaredCrossSkillAuth } from './cross-skill-dependency-detection.js';
|
|
26
27
|
import { validateFrontmatterRules, validateFrontmatterSchema } from './frontmatter-validation.js';
|
|
28
|
+
import { collectUnqualifiedMcpToolIssues } from './mcp-tool-qualification.js';
|
|
27
29
|
import { materializeIssue } from './rule-engine/index.js';
|
|
28
30
|
import { SOURCE_ONLY_CODES } from './source-only-codes.js';
|
|
29
31
|
import { VALIDATION_RULES, VALIDATION_THRESHOLDS, } from './validation-rules.js';
|
|
30
|
-
import { deferredAssetsToIssues, walkerExclusionsToIssues } from './walker-to-issues.js';
|
|
32
|
+
import { deferredAssetsToIssues, outsideSkillDirLinksToIssues, walkerExclusionsToIssues } from './walker-to-issues.js';
|
|
31
33
|
/** Exclude reason constants to avoid duplicate string literals */
|
|
32
34
|
const EXCLUDE_REASON_DIRECTORY = 'directory-target';
|
|
33
35
|
const EXCLUDE_REASON_OUTSIDE_PROJECT = 'outside-project';
|
|
@@ -60,7 +62,7 @@ function validateFilesConfig(files, projectRoot, locationRoot) {
|
|
|
60
62
|
const issues = [];
|
|
61
63
|
const destSet = new Set();
|
|
62
64
|
for (const entry of files) {
|
|
63
|
-
const normalized =
|
|
65
|
+
const normalized = toForwardSlashAnyPlatform(entry.dest);
|
|
64
66
|
if (destSet.has(normalized)) {
|
|
65
67
|
issues.push({
|
|
66
68
|
severity: 'error',
|
|
@@ -72,7 +74,6 @@ function validateFilesConfig(files, projectRoot, locationRoot) {
|
|
|
72
74
|
// Check if an existing source resolves to a directory — a typed single-file
|
|
73
75
|
// slot cannot be satisfied by a directory.
|
|
74
76
|
const resolvedSource = safePath.resolve(safePath.join(projectRoot, entry.source));
|
|
75
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- resolvedSource derived from config-supplied path
|
|
76
77
|
if (existsSync(resolvedSource) && statSync(resolvedSource).isDirectory()) {
|
|
77
78
|
// Anchor at the resolved source, expressed in the run's ONE coordinate
|
|
78
79
|
// system — not the raw config value, which is project-relative and so
|
|
@@ -88,10 +89,10 @@ function validateFilesConfig(files, projectRoot, locationRoot) {
|
|
|
88
89
|
* anchored at a project-relative `location`.
|
|
89
90
|
*
|
|
90
91
|
* Thin wrapper over the shared {@link materializeIssue} so severity / fix /
|
|
91
|
-
* reference come from the single CODE_REGISTRY source
|
|
92
|
+
* reference come from the single CODE_REGISTRY source; the
|
|
92
93
|
* caller supplies a fully-formed `message`.
|
|
93
94
|
*
|
|
94
|
-
* Deliberately NOT named `createRegistryIssue`: `@vibe-agent-toolkit/
|
|
95
|
+
* Deliberately NOT named `createRegistryIssue`: `@vibe-agent-toolkit/schema`
|
|
95
96
|
* exports a function by that name whose third parameter is an EXTRAS OBJECT, not
|
|
96
97
|
* a location string. Two same-named functions with incompatible third arguments
|
|
97
98
|
* is a trap for anyone reading a call site.
|
|
@@ -99,13 +100,28 @@ function validateFilesConfig(files, projectRoot, locationRoot) {
|
|
|
99
100
|
function registryIssueAt(code, message, location) {
|
|
100
101
|
return materializeIssue(code, { message, location });
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Settle the memoized refusals under THIS caller's policy — see {@link MemoizedRegistry}.
|
|
105
|
+
*
|
|
106
|
+
* @param refusals - What the one crawl of `projectRoot` was refused
|
|
107
|
+
* @param unreadable - This caller's ruling
|
|
108
|
+
* @param projectRoot - The crawl's root, for the refuse sentence
|
|
109
|
+
*/
|
|
110
|
+
function replayRefusals(refusals, unreadable, projectRoot) {
|
|
111
|
+
for (const refusal of refusals) {
|
|
112
|
+
if (unreadable === 'refuse') {
|
|
113
|
+
throw new DirectoryListingRefusedError(refusal, { root: projectRoot, remedy: listingRefusalRemedy(projectRoot) });
|
|
114
|
+
}
|
|
115
|
+
unreadable.degrade(refusal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
102
118
|
/**
|
|
103
119
|
* Build a fresh ResourceRegistry for a single skill's projectRoot and resolve
|
|
104
120
|
* internal links. Extracted so the skill validator can fall back to a private
|
|
105
121
|
* registry when the caller does not supply a shared one.
|
|
106
122
|
*
|
|
107
123
|
* Crawls markdown AND HTML (`.html`/`.htm`) so the live audit/validate path
|
|
108
|
-
* sees the same link graph the built path does
|
|
124
|
+
* sees the same link graph the built path does. The registry
|
|
109
125
|
* parses HTML via parse5 and surfaces its `local_file` links, so the walker
|
|
110
126
|
* traverses HTML references and catches HTML broken links at source time — not
|
|
111
127
|
* just at build time. (Previously the crawl was markdown-only, so source HTML
|
|
@@ -113,29 +129,89 @@ function registryIssueAt(code, message, location) {
|
|
|
113
129
|
*
|
|
114
130
|
* Exported so external callers (e.g. the inventory layer) can build a registry
|
|
115
131
|
* once and pass it down rather than re-crawling per skill.
|
|
132
|
+
*
|
|
133
|
+
* ## Why the config is READ here rather than threaded in
|
|
134
|
+
*
|
|
135
|
+
* A collection may declare a `mimeType` that overrides `mime-type.ts`'s
|
|
136
|
+
* extension tables and so decides which parser runs; `ResourceRegistry` honours
|
|
137
|
+
* those declarations only when it was handed a config, and the projection lane
|
|
138
|
+
* behind `populationSource` reads them off the root. A config-less registry
|
|
139
|
+
* therefore disagrees with the population that enumerated it about whether a
|
|
140
|
+
* file is prose, inside a single command.
|
|
141
|
+
*
|
|
142
|
+
* Threading was considered and does not fit this function. Its callers discover
|
|
143
|
+
* `projectRoot` **per skill** (`findProjectRoot` from each SKILL.md's directory,
|
|
144
|
+
* and one `vat audit --user` run reached at least 72 distinct roots), so no
|
|
145
|
+
* caller holds one config that governs them all. Worse, the memo below is keyed
|
|
146
|
+
* on the root ALONE: an optional config argument would let two callers of one
|
|
147
|
+
* root disagree and silently share whichever registry was built first.
|
|
148
|
+
*
|
|
149
|
+
* Reading it here is what `createProjectRegistry` in `skill-packager.ts` already
|
|
150
|
+
* does, with the same `loadConfig`, for the same stated reason — "a registry
|
|
151
|
+
* built without config silently belongs to no collection, so a lane that built
|
|
152
|
+
* its own config-less registry rewrote frontmatter differently from the lane
|
|
153
|
+
* that used this one". One read per root per process, behind the memo, against a
|
|
154
|
+
* crawl that parses every document under it.
|
|
155
|
+
*
|
|
156
|
+
* ## A broken config falls back here, and is reported by the caller
|
|
157
|
+
*
|
|
158
|
+
* Unlike `createProjectRegistry`, this must NOT throw on a config that exists
|
|
159
|
+
* and will not parse. Its principal caller is `vat audit`, whose pinned
|
|
160
|
+
* behaviour is "tolerates, falls back to config-free validation" — a bulk linter
|
|
161
|
+
* over trees VAT does not own must not be aborted by someone else's YAML
|
|
162
|
+
* (`cli/test/integration/config-broken-behavior.integration.test.ts` is the map).
|
|
163
|
+
*
|
|
164
|
+
* The fallback is not the silent conflation that rule normally forbids, because
|
|
165
|
+
* nothing is being hidden: `vat audit` reads the same config at its own layer,
|
|
166
|
+
* catches `ConfigLoadError` there, and reports it. What this does is decline to
|
|
167
|
+
* abort a scan from four frames down. A config that will not parse also gives
|
|
168
|
+
* the projection lane no declarations, so the two lanes still AGREE — which is
|
|
169
|
+
* the property this function exists to hold.
|
|
116
170
|
*/
|
|
117
|
-
export async function crawlAndResolveRegistry(projectRoot) {
|
|
171
|
+
export async function crawlAndResolveRegistry(projectRoot, options) {
|
|
172
|
+
const { populationSource, unreadable } = options;
|
|
173
|
+
const cache = registryCacheFor(populationSource);
|
|
118
174
|
const key = toForwardSlash(safePath.resolve(projectRoot));
|
|
119
|
-
const cached =
|
|
175
|
+
const cached = cache.get(key);
|
|
120
176
|
if (cached !== undefined) {
|
|
121
|
-
|
|
177
|
+
const { registry, refusals } = await cached;
|
|
178
|
+
replayRefusals(refusals, unreadable, projectRoot);
|
|
179
|
+
return registry;
|
|
122
180
|
}
|
|
123
181
|
// The PROMISE is cached, not the resolved value, so two overlapping requests
|
|
124
182
|
// for one root can never start two crawls.
|
|
125
183
|
const pending = (async () => {
|
|
184
|
+
const config = await loadConfig(projectRoot).catch(() => undefined);
|
|
185
|
+
// Every refusal the crawl hands over is kept for the memo's later callers —
|
|
186
|
+
// see {@link MemoizedRegistry}. Under `'refuse'` the crawl throws instead and
|
|
187
|
+
// this stays empty.
|
|
188
|
+
const refusals = [];
|
|
189
|
+
const recorded = unreadable === 'refuse'
|
|
190
|
+
? 'refuse'
|
|
191
|
+
: { degrade: (refusal) => { refusals.push(refusal); unreadable.degrade(refusal); } };
|
|
126
192
|
const registry = await ResourceRegistry.fromCrawl({
|
|
127
193
|
baseDir: projectRoot,
|
|
128
194
|
include: ['**/*.md', '**/*.html', '**/*.htm'],
|
|
129
|
-
|
|
195
|
+
unreadable: recorded,
|
|
196
|
+
// Enumeration only. `ResourceRegistry.crawl` re-applies the include set
|
|
197
|
+
// above — and the crawl's default exclude — to whatever the source
|
|
198
|
+
// offers, through the same compiled matcher the walk itself uses, so a
|
|
199
|
+
// source that enumerates a whole tree still yields exactly this crawl's
|
|
200
|
+
// markdown and HTML. Handing this builder a projection is a cost change,
|
|
201
|
+
// not a scope change.
|
|
202
|
+
...(populationSource !== undefined && { populationSource }),
|
|
203
|
+
}, config === undefined ? undefined : { config });
|
|
130
204
|
registry.resolveLinks();
|
|
131
|
-
return registry;
|
|
205
|
+
return { registry, refusals };
|
|
132
206
|
})();
|
|
133
|
-
|
|
134
|
-
return pending;
|
|
207
|
+
cache.set(key, pending);
|
|
208
|
+
return (await pending).registry;
|
|
135
209
|
}
|
|
136
210
|
/**
|
|
137
|
-
* Cache of (resolved project root → registry)
|
|
138
|
-
* per root per process rather than once per skill.
|
|
211
|
+
* Cache of (resolved project root → registry) for the INCUMBENT WALK, so the
|
|
212
|
+
* crawl above is paid ONCE per root per process rather than once per skill.
|
|
213
|
+
* Source-backed callers get {@link sourcedRegistryCache} instead, and the two
|
|
214
|
+
* never meet — see there for why that separation is the whole point.
|
|
139
215
|
*
|
|
140
216
|
* It lives beside the crawl because every caller wants the same thing and the
|
|
141
217
|
* cost is a property of the crawl, not of any one lane. `vat audit` had already
|
|
@@ -149,8 +225,81 @@ export async function crawlAndResolveRegistry(projectRoot) {
|
|
|
149
225
|
* Safe to hold for a process lifetime: every VAT entry point is a short-lived
|
|
150
226
|
* CLI invocation. Note the crawl excludes build output (`BUILD_OUTPUT_GLOBS` via
|
|
151
227
|
* the crawler's defaults), so packaging a skill mid-run cannot invalidate it.
|
|
228
|
+
*
|
|
229
|
+
* 🚨 That exclusion is ALSO the cause of a live defect, and this paragraph is the
|
|
230
|
+
* reason the defect must not be fixed by widening the crawl. The packager's
|
|
231
|
+
* post-build lane sees an EMPTY link graph because the built tree it validates
|
|
232
|
+
* falls under `**\/dist\/**` — measured, and now pinned by a committed test; the
|
|
233
|
+
* numbers, the control that proves the cause, and the fixture all live in the
|
|
234
|
+
* `runPostBuildValidation` docstring in `../skill-packager.ts`, and are not
|
|
235
|
+
* restated here.
|
|
236
|
+
*
|
|
237
|
+
* ⛔ The exclusion is the stated safety argument DIRECTLY ABOVE, for a memo shared
|
|
238
|
+
* by the audit lane, the validate lane AND the built lane. Widening the crawl so
|
|
239
|
+
* one lane can see its own output retracts that argument for every lane sharing
|
|
240
|
+
* this memo — it is not a one-lane fix, and it lands here, not there.
|
|
241
|
+
*/
|
|
242
|
+
const walkRegistryCache = new Map();
|
|
243
|
+
/**
|
|
244
|
+
* The same memo for the SOURCE-BACKED lanes, keyed first by the population
|
|
245
|
+
* source's IDENTITY and only then by root.
|
|
246
|
+
*
|
|
247
|
+
* ## Why the source is part of the key
|
|
248
|
+
*
|
|
249
|
+
* This crawl is memoized for a process and SHARED across commands that do not
|
|
250
|
+
* know about each other: `vat skills build` reaches it per skill through
|
|
251
|
+
* {@link validateSkillForPackaging}, `vat audit` reaches it directly, and the
|
|
252
|
+
* pipeline oracles reach it as a lane. Keyed on the root ALONE, the FIRST
|
|
253
|
+
* caller's population binds for the whole process, and a later caller asking a
|
|
254
|
+
* different question transparently receives the earlier caller's answer — with
|
|
255
|
+
* nothing in any output saying so. The projection lane does not merely enumerate
|
|
256
|
+
* the same set faster: it sees uncommitted markdown, so it genuinely answers a
|
|
257
|
+
* different membership question and ADDS `LINK_BROKEN_FILE` findings. Serving
|
|
258
|
+
* one caller's answer to the other is a silent wrong answer in both directions.
|
|
259
|
+
*
|
|
260
|
+
* ## Why IDENTITY, and not a lane descriptor
|
|
261
|
+
*
|
|
262
|
+
* A `'walk' | 'projection'` tag would key two projection sources the same, and
|
|
263
|
+
* two projection sources can differ in their ignore oracle (a run with no
|
|
264
|
+
* `GitTracker` admits the ignored half of a tree), in which store answers them,
|
|
265
|
+
* and in whether that store is still OPEN — `withResourcePopulationSource`
|
|
266
|
+
* closes it when its bracket ends. A key that cannot see those differences is
|
|
267
|
+
* the same bug one level down. The closure itself can never be wrong about
|
|
268
|
+
* which of them it is.
|
|
269
|
+
*
|
|
270
|
+
* The win survives: one run holds ONE source closure for its whole bracket and
|
|
271
|
+
* hands the same one to every skill, so a `vat skills build` still pays a single
|
|
272
|
+
* crawl for the run rather than one per skill. Two independent callers cannot
|
|
273
|
+
* hold the same closure by accident, so they cannot collide.
|
|
274
|
+
*
|
|
275
|
+
* A `WeakMap` rather than a `Map` because the entry's useful life is the
|
|
276
|
+
* bracket's: once the source is unreachable its store is closed and its
|
|
277
|
+
* registries can never be served again, so holding them would be a leak with no
|
|
278
|
+
* upside. (`registryCacheFor` is what reads it; `resetPackagingRegistryCache`
|
|
279
|
+
* replaces it wholesale, since a `WeakMap` cannot be cleared in place.)
|
|
152
280
|
*/
|
|
153
|
-
|
|
281
|
+
let sourcedRegistryCache = new WeakMap();
|
|
282
|
+
/**
|
|
283
|
+
* The (root → registry) memo this call belongs in: the walk's, or the one
|
|
284
|
+
* private to this population source.
|
|
285
|
+
*
|
|
286
|
+
* Separate maps rather than a composite key, because the two are keyed on
|
|
287
|
+
* different KINDS — a path string and an object identity — and a composite key
|
|
288
|
+
* would have to stringify the source, which is exactly the information loss
|
|
289
|
+
* {@link sourcedRegistryCache} exists to avoid.
|
|
290
|
+
*/
|
|
291
|
+
function registryCacheFor(populationSource) {
|
|
292
|
+
if (populationSource === undefined) {
|
|
293
|
+
return walkRegistryCache;
|
|
294
|
+
}
|
|
295
|
+
const existing = sourcedRegistryCache.get(populationSource);
|
|
296
|
+
if (existing !== undefined) {
|
|
297
|
+
return existing;
|
|
298
|
+
}
|
|
299
|
+
const created = new Map();
|
|
300
|
+
sourcedRegistryCache.set(populationSource, created);
|
|
301
|
+
return created;
|
|
302
|
+
}
|
|
154
303
|
/**
|
|
155
304
|
* Drop every memoized registry.
|
|
156
305
|
*
|
|
@@ -161,7 +310,34 @@ const registryCache = new Map();
|
|
|
161
310
|
* as a fresh one. `resetAuditCaches` calls this alongside its own caches.
|
|
162
311
|
*/
|
|
163
312
|
export function resetPackagingRegistryCache() {
|
|
164
|
-
|
|
313
|
+
walkRegistryCache.clear();
|
|
314
|
+
// Both lanes, or the reset is a half-truth: a caller that reset the cache and
|
|
315
|
+
// then re-supplied the SAME source closure would be served the pre-reset parse
|
|
316
|
+
// of a tree it may have changed. A `WeakMap` has no `clear`, so it is replaced.
|
|
317
|
+
sourcedRegistryCache = new WeakMap();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The registry a skill is validated against: the caller's, when it covers the
|
|
321
|
+
* skill; otherwise one crawled here under the caller's `unreadable` ruling.
|
|
322
|
+
*
|
|
323
|
+
* No shared context is a single-skill lane whose result is acted on whole (the
|
|
324
|
+
* packager's post-build check passes one; a library caller that passes nothing
|
|
325
|
+
* is the same shape): it refuses. A caller with a context has ruled on
|
|
326
|
+
* `shared.unreadable` — see that field.
|
|
327
|
+
*
|
|
328
|
+
* @param skillPath - Path to SKILL.md
|
|
329
|
+
* @param projectRoot - The root the fallback crawl covers
|
|
330
|
+
* @param shared - The batching caller's context, if any
|
|
331
|
+
* @returns A registry whose links are resolved and that covers `skillPath`
|
|
332
|
+
*/
|
|
333
|
+
async function registryForSkill(skillPath, projectRoot, shared) {
|
|
334
|
+
if (shared?.registry !== undefined && registryCoversSkill(shared.registry, skillPath)) {
|
|
335
|
+
return shared.registry;
|
|
336
|
+
}
|
|
337
|
+
return crawlAndResolveRegistry(projectRoot, {
|
|
338
|
+
unreadable: shared === undefined ? 'refuse' : shared.unreadable,
|
|
339
|
+
...(shared?.populationSource !== undefined && { populationSource: shared.populationSource }),
|
|
340
|
+
});
|
|
165
341
|
}
|
|
166
342
|
/**
|
|
167
343
|
* True iff the shared registry's baseDir is an ancestor of (or equals) the
|
|
@@ -181,6 +357,34 @@ function registryCoversSkill(registry, skillPath) {
|
|
|
181
357
|
// eslint-disable-next-line local/no-path-startswith -- both sides normalized via toForwardSlash above
|
|
182
358
|
return skillDir.startsWith(`${normalizedBase}/`);
|
|
183
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* The refusal for a matched file that has no frontmatter object — either no
|
|
362
|
+
* `---` block at all, or one whose YAML did not parse (`frontmatterError`).
|
|
363
|
+
*
|
|
364
|
+
* Either way the file is not a skill, and it is refused with the SAME
|
|
365
|
+
* non-overridable code `validateSkill` in skill-validator.ts emits for it:
|
|
366
|
+
* `SKILL_MISSING_FRONTMATTER` has no CODE_REGISTRY entry, so the framework's
|
|
367
|
+
* `finalize()` passes it through and `validation.severity` cannot name it.
|
|
368
|
+
*
|
|
369
|
+
* This refusal used to be absent from the packaging lane — its frontmatter
|
|
370
|
+
* checks ran only `if (parseResult.frontmatter)` — so a plain `# Readme` a
|
|
371
|
+
* `skills.include` glob drifted onto, or a SKILL.md that lost its fence, ran
|
|
372
|
+
* zero frontmatter checks and came back `success` under its H1 as a name:
|
|
373
|
+
* `vat skills validate`, `vat validate` and `vat verify` were green on a
|
|
374
|
+
* project that shipped no skill.
|
|
375
|
+
*/
|
|
376
|
+
function missingFrontmatterIssue(frontmatterError, location) {
|
|
377
|
+
return {
|
|
378
|
+
severity: 'error',
|
|
379
|
+
code: 'SKILL_MISSING_FRONTMATTER',
|
|
380
|
+
message: frontmatterError === undefined
|
|
381
|
+
? 'No YAML frontmatter found — a skill starts with a `---` block that names and describes it'
|
|
382
|
+
: `Frontmatter did not parse as YAML: ${frontmatterError}`,
|
|
383
|
+
location,
|
|
384
|
+
line: 1,
|
|
385
|
+
fix: 'Add YAML frontmatter with name and description fields, or narrow `skills.include` so it no longer matches this file',
|
|
386
|
+
};
|
|
387
|
+
}
|
|
184
388
|
/**
|
|
185
389
|
* Validate a skill for packaging
|
|
186
390
|
*
|
|
@@ -201,7 +405,7 @@ export async function validateSkillForPackaging(skillPath, packagingConfig, cont
|
|
|
201
405
|
// Validation-POLICY boundary (config root -> git root -> skill dir): what is
|
|
202
406
|
// "outside the project", where `files:` sources resolve from, what the
|
|
203
407
|
// registry crawls. Library fallback to skill dir keeps callers null-safe; CLI
|
|
204
|
-
// command boundary owns any user-facing warning.
|
|
408
|
+
// command boundary owns any user-facing warning.
|
|
205
409
|
const projectRoot = findProjectRoot(dirname(skillPath)) ?? dirname(skillPath);
|
|
206
410
|
// ANCHOR base — deliberately a separate variable from projectRoot above.
|
|
207
411
|
// Every emitted location is relative to this ONE root, computed once before
|
|
@@ -210,14 +414,19 @@ export async function validateSkillForPackaging(skillPath, packagingConfig, cont
|
|
|
210
414
|
const locationRoot = shared?.locationRoot ?? projectRoot;
|
|
211
415
|
const skillLocation = issueLocation(skillPath, locationRoot);
|
|
212
416
|
// Parse SKILL.md
|
|
213
|
-
const parseResult = await
|
|
214
|
-
//
|
|
215
|
-
|
|
417
|
+
const parseResult = await parseFileCached(skillPath, 'markdown');
|
|
418
|
+
// The parser already decoded these bytes; a second whole-file read of the same
|
|
419
|
+
// path would only add a syscall and a TOCTOU window. `content` is the raw
|
|
420
|
+
// source verbatim, so fenced code blocks reach `runCompatDetectors` intact.
|
|
421
|
+
const skillContent = parseResult.content;
|
|
216
422
|
const skillLines = skillContent.split('\n').length;
|
|
217
423
|
// Validate frontmatter schema (name format, required fields, etc.)
|
|
218
424
|
if (parseResult.frontmatter) {
|
|
219
425
|
rawIssues.push(...validateFrontmatterSchema(parseResult.frontmatter, false, skillLocation), ...validateFrontmatterRules(parseResult.frontmatter, skillLocation));
|
|
220
426
|
}
|
|
427
|
+
else {
|
|
428
|
+
rawIssues.push(missingFrontmatterIssue(parseResult.frontmatterError, skillLocation));
|
|
429
|
+
}
|
|
221
430
|
// Compat capability detection: collect observations from SKILL.md and
|
|
222
431
|
// surface each as a CAPABILITY_* issue. Observations are also returned
|
|
223
432
|
// on the result so downstream verdict computation (CLI layer) can recover
|
|
@@ -240,21 +449,39 @@ export async function validateSkillForPackaging(skillPath, packagingConfig, cont
|
|
|
240
449
|
// per-file markdown parse is paid for exactly once across the batch. Only
|
|
241
450
|
// reuse when the shared registry covers this skill's projectRoot; otherwise
|
|
242
451
|
// fall back to a fresh crawl to avoid leaking resources across roots.
|
|
243
|
-
const registry =
|
|
244
|
-
? shared.registry
|
|
245
|
-
: await crawlAndResolveRegistry(projectRoot);
|
|
452
|
+
const registry = await registryForSkill(skillPath, projectRoot, shared);
|
|
246
453
|
const skillResource = registry.getResource(safePath.resolve(skillPath));
|
|
247
454
|
// Only the entries the PACKAGER will actually copy defer a link: an entry pointing
|
|
248
455
|
// into declared test input is dropped at build time, so its dest never appears and
|
|
249
456
|
// a link to it is a genuine broken link — the same verdict `vat skills build`
|
|
250
|
-
// reaches.
|
|
457
|
+
// reaches. Same derivation `packagedFileEntries` performs for the lanes that have
|
|
458
|
+
// no second use for the dirs — see test-input.ts.
|
|
251
459
|
//
|
|
252
460
|
// `projectSkills` is the PROJECT's declared eval suites, assembled once by the
|
|
253
461
|
// calling lane (see SkillValidationSharedContext.projectSkills). Empty when the
|
|
254
462
|
// caller has no project to enumerate — a single-skill audit of a tree with no
|
|
255
463
|
// config — which narrows this lane to the subject's own suite and nothing else.
|
|
256
464
|
const projectSkills = shared?.projectSkills ?? [];
|
|
257
|
-
|
|
465
|
+
// ONE resolution per skill, shared by both consumers below (the `files:` filter and
|
|
466
|
+
// the walker's exclude rules), and ONE conventional-suite probe per RUN behind it.
|
|
467
|
+
//
|
|
468
|
+
// Two multipliers on the same filesystem question, both now closed. Resolving it
|
|
469
|
+
// once per CONSUMER doubled the term for no new information (the two call sites took
|
|
470
|
+
// identical arguments) — 6,844 probes on a 58-skill adopter declaring no `test:`
|
|
471
|
+
// block, of which 3,422 re-asked a question already answered. That fix removed the
|
|
472
|
+
// second call site and left the QUADRATIC standing: the resolution still probes the
|
|
473
|
+
// subject AND every entry in `projectSkills`, so a lane looping over S skills asked
|
|
474
|
+
// S^2 questions about S paths. Measured with the lab on a 103-skill adopter,
|
|
475
|
+
// `vat resources validate`: 10,815 probes over 103 distinct paths (~105 repeats
|
|
476
|
+
// each) — exactly 50% of that command's 21,648 user filesystem calls. With the run's
|
|
477
|
+
// probe threaded here it is ~103 probes and ~10,900 calls on the same tree.
|
|
478
|
+
//
|
|
479
|
+
// A caller that omits `suiteProbe` is claiming this call IS the run (see the field's
|
|
480
|
+
// docstring); the narrowing is written here, at the call site, so it cannot happen by
|
|
481
|
+
// omission inside `resolveTestInputDirs`.
|
|
482
|
+
const suiteProbe = shared?.suiteProbe ?? conventionalSuiteProbe();
|
|
483
|
+
const testInputDirs = resolveTestInputDirs(packagingConfig ?? {}, dirname(skillPath), projectSkills, suiteProbe);
|
|
484
|
+
const packagedFiles = partitionTestInputFileEntries(packagingConfig?.files ?? [], projectRoot, testInputDirs).kept;
|
|
258
485
|
const deferred = DeferredArtifacts.from([{ files: packagedFiles, skillDir: dirname(skillPath) }], projectRoot);
|
|
259
486
|
// What the `files:` GLOBs will do to this build, reported HERE — before any
|
|
260
487
|
// build — because this is where it is still cheap to act on: `vat skills
|
|
@@ -270,7 +497,6 @@ export async function validateSkillForPackaging(skillPath, packagingConfig, cont
|
|
|
270
497
|
// while staying silent about the fatal zero-match was the asymmetry that
|
|
271
498
|
// let `vat skills validate` return success on a config that cannot build.
|
|
272
499
|
rawIssues.push(...preBuildGlobFindingsToIssues(await collectPreBuildGlobFindings(packagedFiles, projectRoot), locationRoot));
|
|
273
|
-
const testInputDirs = resolveTestInputDirs(packagingConfig ?? {}, dirname(skillPath), projectSkills);
|
|
274
500
|
const walkOptions = {
|
|
275
501
|
maxDepth,
|
|
276
502
|
// The SAME test-input exclusion the packager applies. Without it this lane
|
|
@@ -289,20 +515,20 @@ export async function validateSkillForPackaging(skillPath, packagingConfig, cont
|
|
|
289
515
|
if (shared?.gitTracker !== undefined) {
|
|
290
516
|
walkOptions.gitTracker = shared.gitTracker;
|
|
291
517
|
}
|
|
292
|
-
const { bundledResources, bundledAssets, excludedReferences, maxBundledDepth, deferredAssets } = walkLinkGraph(skillResource?.id ?? '', registry, walkOptions);
|
|
518
|
+
const { bundledResources, bundledAssets, excludedReferences, outsideSkillDirLinks, maxBundledDepth, deferredAssets } = walkLinkGraph(skillResource?.id ?? '', registry, walkOptions);
|
|
293
519
|
const bundledFiles = [...bundledResources.map(r => r.filePath), ...bundledAssets];
|
|
294
520
|
// Count direct links that actually made it into the bundle
|
|
295
521
|
const directLinks = getResolvedMarkdownLinks(parseResult.links, skillPath);
|
|
296
522
|
const bundledFileSet = new Set(bundledFiles);
|
|
297
523
|
const directFileCount = directLinks.filter(p => bundledFileSet.has(p)).length;
|
|
298
|
-
//
|
|
299
|
-
// the walker dropped or deferred, and all three anchor on `locationRoot`:
|
|
524
|
+
// Four producers, one append, order significant, all anchored on `locationRoot`:
|
|
300
525
|
// 1. walker exclusions (LINK_OUTSIDE_PROJECT, LINK_TARGETS_DIRECTORY, etc.)
|
|
301
526
|
// 2. links dropped for pointing into declared test input — the same issue, at
|
|
302
527
|
// the same location, the packager emits for it. `projectRoot` scopes WHICH
|
|
303
528
|
// dirs count as declared test input; `locationRoot` only anchors.
|
|
304
529
|
// 3. one info issue per deferred asset declared in files: config
|
|
305
|
-
|
|
530
|
+
// 4. followed links bundled from outside the skill directory (LINK_OUTSIDE_SKILL_DIR)
|
|
531
|
+
rawIssues.push(...walkerExclusionsToIssues(excludedReferences, locationRoot), ...testInputLinkIssues(excludedReferences, testInputDirs, projectRoot, locationRoot), ...deferredAssetsToIssues(deferredAssets, locationRoot), ...outsideSkillDirLinksToIssues(outsideSkillDirLinks, locationRoot));
|
|
306
532
|
const fileCount = bundledFiles.length + 1; // +1 for SKILL.md itself
|
|
307
533
|
const maxLinkDepth = maxBundledDepth;
|
|
308
534
|
// Calculate total lines from bundled markdown files only, and scan each
|
|
@@ -311,13 +537,20 @@ export async function validateSkillForPackaging(skillPath, packagingConfig, cont
|
|
|
311
537
|
let totalLines = skillLines;
|
|
312
538
|
for (const bundledFile of bundledFiles) {
|
|
313
539
|
if (bundledFile.endsWith('.md')) {
|
|
314
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- bundledFile resolved from markdown parser
|
|
315
540
|
const content = await readFile(bundledFile, 'utf-8');
|
|
316
541
|
totalLines += content.split('\n').length;
|
|
317
542
|
// Anchor contract: never hand a producer an absolute path as `location`.
|
|
318
543
|
const bundledLocation = issueLocation(bundledFile, locationRoot);
|
|
319
544
|
collectNonPortableAssetReferenceIssues(content, bundledLocation, rawIssues);
|
|
320
545
|
collectNonPortableCommandIssues(content, bundledLocation, rawIssues);
|
|
546
|
+
// Whole-file bytes, frontmatter included — safe because the detector
|
|
547
|
+
// strips leading frontmatter itself. It did not always, and this lane was
|
|
548
|
+
// the half of the invariant nobody was enforcing: a bundled file carrying
|
|
549
|
+
// `allowed-tools: [mcp__x__do_thing]` seeded the vocabulary the SKILL.md
|
|
550
|
+
// lane's comment (below) says must come from prose, then fired on its own
|
|
551
|
+
// body. Pinned by "does not let a bundled file's allowed-tools frontmatter
|
|
552
|
+
// supply the vocabulary" in packaging-validator.test.ts.
|
|
553
|
+
collectUnqualifiedMcpToolIssues(content, bundledLocation, rawIssues);
|
|
321
554
|
}
|
|
322
555
|
}
|
|
323
556
|
const excludedDetails = deduplicateExcludedReferences(excludedReferences, skillPath);
|
|
@@ -329,8 +562,19 @@ export async function validateSkillForPackaging(skillPath, packagingConfig, cont
|
|
|
329
562
|
collectTimeSensitiveContentIssues(parseResult.content, skillLocation, rawIssues);
|
|
330
563
|
collectNonPortableAssetReferenceIssues(parseResult.content, skillLocation, rawIssues);
|
|
331
564
|
collectNonPortableCommandIssues(parseResult.content, skillLocation, rawIssues);
|
|
565
|
+
// An `allowed-tools:` list of `mcp__…` names does not by itself supply the
|
|
566
|
+
// vocabulary: the qualified spelling has to appear in the prose an agent
|
|
567
|
+
// reads, or "the document contradicts itself" is not the finding. That holds
|
|
568
|
+
// because the DETECTOR strips frontmatter. ⛔ It was NEVER true that this call
|
|
569
|
+
// site passed a post-frontmatter slice — `parseResult.content` is the raw file
|
|
570
|
+
// verbatim, as the comment 190 lines above says — so before the detector
|
|
571
|
+
// stripped, this lane did read `allowed-tools:` as prose and did ship the
|
|
572
|
+
// findings that came of it.
|
|
573
|
+
collectUnqualifiedMcpToolIssues(parseResult.content, skillLocation, rawIssues);
|
|
332
574
|
// Cross-skill dependency smell: body declares a requires/depends token the
|
|
333
|
-
// description does not mention.
|
|
575
|
+
// description does not mention. Reads the RAW file, frontmatter included —
|
|
576
|
+
// which is what this detector wants, since the declaration it looks for can be
|
|
577
|
+
// written either side of the fence.
|
|
334
578
|
if (parseResult.frontmatter) {
|
|
335
579
|
rawIssues.push(...detectUndeclaredCrossSkillAuth(parseResult.frontmatter, parseResult.content, skillLocation));
|
|
336
580
|
}
|
|
@@ -510,9 +754,23 @@ const RELATIVE_PATH_HINT = 'Reference bundled files by a path relative to the sk
|
|
|
510
754
|
* plain form; falling to (2) is what still flags the operator forms — a
|
|
511
755
|
* lone `\$\{NAME\}` alternative silently missed `${NAME:-default}`, an
|
|
512
756
|
* idiomatic non-portable reference.
|
|
757
|
+
* 3. `\$\{?env:NAME\b` — PowerShell's expansion, BOTH spellings. A skill that
|
|
758
|
+
* documents a Windows invocation writes `$env:CLAUDE_SKILL_DIR` or
|
|
759
|
+
* `${env:CLAUDE_SKILL_DIR}`, neither of which the two POSIX alternatives
|
|
760
|
+
* match: after `$`/`${` comes `env:`, not the variable name. The forms are
|
|
761
|
+
* equally non-portable, so leaving them out flagged the bash line of a skill
|
|
762
|
+
* and waved through the PowerShell line directly beneath it.
|
|
763
|
+
*
|
|
764
|
+
* ⚠️ Compiled with the `i` flag, and that is about alternative 3 specifically.
|
|
765
|
+
* PowerShell variable and drive names are case-INSENSITIVE, so `$Env:NAME` and
|
|
766
|
+
* `$ENV:NAME` are as valid — and as commonly authored — as `$env:NAME`. A
|
|
767
|
+
* case-SENSITIVE `\$env:` matched one of the three and silently passed the other
|
|
768
|
+
* two, reintroducing exactly the asymmetry this alternative was added to remove.
|
|
769
|
+
* The variable names themselves are uppercase literals passed in by the call
|
|
770
|
+
* sites below, so case-insensitivity costs nothing there.
|
|
513
771
|
*/
|
|
514
772
|
// eslint-disable-next-line security/detect-non-literal-regexp -- composed from a compile-time constant name, no user input
|
|
515
|
-
const envVarPattern = (name) => new RegExp(String.raw `\$\{${name}\}|\$\{?${name}\b
|
|
773
|
+
const envVarPattern = (name) => new RegExp(String.raw `\$\{${name}\}|\$\{?env:${name}\b|\$\{?${name}\b`, 'i');
|
|
516
774
|
const NON_PORTABLE_ASSET_VARIANTS = [
|
|
517
775
|
{
|
|
518
776
|
label: 'claude-plugin-root',
|
|
@@ -530,6 +788,40 @@ const NON_PORTABLE_ASSET_VARIANTS = [
|
|
|
530
788
|
pattern: envVarPattern('CLAUDE_PROJECT_DIR'),
|
|
531
789
|
fix: '`CLAUDE_PROJECT_DIR` is a Claude Code-only variable, so a skill relying on it will not resolve the user\'s project on other runtimes. There is no skill-relative equivalent — if the skill genuinely operates on the user\'s repository, take the location as an explicit parameter with `$CLAUDE_PROJECT_DIR` as a fallback, and make sure the skill\'s declared `targets` reflect the Claude Code dependency.',
|
|
532
790
|
},
|
|
791
|
+
{
|
|
792
|
+
// Load-bearing in Claude Code and MEANINGLESS in the API code-execution
|
|
793
|
+
// container. Both facts are true for good reasons, which is why this is a
|
|
794
|
+
// warning and not a rename: in Claude Code a skill may live under
|
|
795
|
+
// `~/.claude/skills/`, a plugin tree, a project dir or a marketplace install
|
|
796
|
+
// path while cwd is the user's repo, and the consumer needing a real path is
|
|
797
|
+
// often a PROCESS (a script argument) that does not share the model's
|
|
798
|
+
// context — there is no literal an author can write. The API container has
|
|
799
|
+
// the opposite property: the location is a stable literal (`/skills/<name>/`,
|
|
800
|
+
// cwd `/`) so nothing is parameterised and no equivalent variable is set,
|
|
801
|
+
// measured live against the Messages API. `${CLAUDE_SKILL_DIR}` therefore
|
|
802
|
+
// expands to empty there and `node "/scripts/x.mjs"` does not exist.
|
|
803
|
+
label: 'claude-skill-dir',
|
|
804
|
+
pattern: envVarPattern('CLAUDE_SKILL_DIR'),
|
|
805
|
+
fix: `\`CLAUDE_SKILL_DIR\` is set by Claude Code and is absent in the API code-execution container, where it expands to empty. ${RELATIVE_PATH_HINT} A relative path resolves on every target because the MODEL resolves it against the skill directory — not because cwd happens to be right. When a process genuinely needs an absolute path, instruct the agent to cd into the skill directory first rather than reaching for another variable.`,
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
// The same mistake pointed the other way: the API container's literal mount
|
|
809
|
+
// point hardcoded into a skill, which resolves nowhere in Claude Code. Anchored
|
|
810
|
+
// at a path boundary so `~/.claude/skills/x/` and `.claude/skills/` in prose
|
|
811
|
+
// (both preceded by a word character) are not flagged.
|
|
812
|
+
//
|
|
813
|
+
// ⚠️ The TRAILING separator is optional, and that is the common spelling —
|
|
814
|
+
// not an edge case. `/skills/<name>` with nothing after it is the mount
|
|
815
|
+
// POINT, which is what the sibling `claude-skill-dir` fix text tells authors
|
|
816
|
+
// to reach for ("cd into the skill directory first"). Requiring the trailing
|
|
817
|
+
// `/` made `cd /skills/my-skill && node scripts/run.mjs` invisible while
|
|
818
|
+
// firing on `node /skills/my-skill/scripts/run.mjs` on the next line. The
|
|
819
|
+
// `(?![\w.-])` branch keeps the name from being truncated mid-token, so a
|
|
820
|
+
// longer name is still matched whole rather than at a prefix.
|
|
821
|
+
label: 'api-skill-mount',
|
|
822
|
+
pattern: /(?:^|[\s"'`(])\/skills\/[A-Za-z0-9._-]+(?:\/|(?![\w.-]))/,
|
|
823
|
+
fix: `\`/skills/<name>/\` is the Anthropic API code-execution container's mount point and does not exist in Claude Code, a claude.ai upload, or any other host. ${RELATIVE_PATH_HINT} When a process genuinely needs an absolute path, instruct the agent to cd into the skill directory first.`,
|
|
824
|
+
},
|
|
533
825
|
{
|
|
534
826
|
label: 'absolute-script-path',
|
|
535
827
|
pattern: /\b(?:node|bun|deno|python3?|ruby|sh|bash|uv)\s+["']?\/[^\s"']+\.(?:mjs|cjs|js|ts|py|rb|sh)\b/,
|
|
@@ -656,7 +948,7 @@ function collectPortabilityFamilyIssues(content, docLocation, issues, family) {
|
|
|
656
948
|
* Collect NON_PORTABLE_ASSET_REFERENCE issues — scan one skill document for any
|
|
657
949
|
* member of the non-portable asset-reference family.
|
|
658
950
|
*/
|
|
659
|
-
function collectNonPortableAssetReferenceIssues(content, docLocation, issues) {
|
|
951
|
+
export function collectNonPortableAssetReferenceIssues(content, docLocation, issues) {
|
|
660
952
|
collectPortabilityFamilyIssues(content, docLocation, issues, {
|
|
661
953
|
code: 'NON_PORTABLE_ASSET_REFERENCE',
|
|
662
954
|
variants: NON_PORTABLE_ASSET_VARIANTS,
|
|
@@ -670,7 +962,7 @@ function collectNonPortableAssetReferenceIssues(content, docLocation, issues) {
|
|
|
670
962
|
* Collect NON_PORTABLE_COMMAND issues — scan one skill document for any member of
|
|
671
963
|
* the non-portable shell-command family.
|
|
672
964
|
*/
|
|
673
|
-
function collectNonPortableCommandIssues(content, docLocation, issues) {
|
|
965
|
+
export function collectNonPortableCommandIssues(content, docLocation, issues) {
|
|
674
966
|
collectPortabilityFamilyIssues(content, docLocation, issues, {
|
|
675
967
|
code: 'NON_PORTABLE_COMMAND',
|
|
676
968
|
variants: NON_PORTABLE_COMMAND_VARIANTS,
|
|
@@ -689,10 +981,24 @@ function collectProgressiveDisclosureIssue(skillLines, referenceFileCount, skill
|
|
|
689
981
|
}
|
|
690
982
|
}
|
|
691
983
|
/**
|
|
692
|
-
* Process links from parsed markdown and return resolved .md file paths
|
|
984
|
+
* Process links from parsed markdown and return resolved .md file paths.
|
|
985
|
+
*
|
|
986
|
+
* Exported for unit test: the deduplication contract (one entry, and one existence
|
|
987
|
+
* probe, per DISTINCT target however many times it is linked) is not observable
|
|
988
|
+
* through `validateSkillForPackaging`'s result alone.
|
|
693
989
|
*/
|
|
694
|
-
function getResolvedMarkdownLinks(links, markdownPath) {
|
|
695
|
-
const
|
|
990
|
+
export function getResolvedMarkdownLinks(links, markdownPath) {
|
|
991
|
+
const markdownDir = dirname(markdownPath);
|
|
992
|
+
// Collapse to DISTINCT targets before touching the filesystem. `links` is one
|
|
993
|
+
// entry per link OCCURRENCE, and documents routinely point many occurrences at
|
|
994
|
+
// one file (a routing table linking every row to the same sub-skill). Two
|
|
995
|
+
// consequences, both fixed here rather than downstream:
|
|
996
|
+
// - the caller derives `directFileCount` from this list, so occurrences made
|
|
997
|
+
// it report more direct files than the whole bundle holds;
|
|
998
|
+
// - the existence probe below ran once per occurrence. On this repo's own
|
|
999
|
+
// cat-agents skill that was 42 probes over 9 distinct paths.
|
|
1000
|
+
// A Set preserves insertion order, so the surviving order is unchanged.
|
|
1001
|
+
const candidates = new Set();
|
|
696
1002
|
for (const link of links) {
|
|
697
1003
|
if (link.type !== 'local_file') {
|
|
698
1004
|
continue;
|
|
@@ -702,22 +1008,24 @@ function getResolvedMarkdownLinks(links, markdownPath) {
|
|
|
702
1008
|
if (hrefWithoutAnchor === '') {
|
|
703
1009
|
continue;
|
|
704
1010
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (!resolvedPath.endsWith('.md') || !existsSync(resolvedPath)) {
|
|
710
|
-
continue;
|
|
1011
|
+
const resolvedPath = safePath.resolve(markdownDir, hrefWithoutAnchor);
|
|
1012
|
+
// Only .md targets are candidates; existence is settled once, below.
|
|
1013
|
+
if (resolvedPath.endsWith('.md')) {
|
|
1014
|
+
candidates.add(resolvedPath);
|
|
711
1015
|
}
|
|
712
|
-
resolvedPaths.push(resolvedPath);
|
|
713
1016
|
}
|
|
714
|
-
return
|
|
1017
|
+
return [...candidates].filter(candidate => existsSync(candidate));
|
|
715
1018
|
}
|
|
716
1019
|
/**
|
|
717
1020
|
* Reasons that are reported as validation errors instead of excluded references.
|
|
718
1021
|
* These are filtered out of the excluded references list.
|
|
719
1022
|
*/
|
|
720
|
-
const
|
|
1023
|
+
const EXCLUDE_REASON_UNREADABLE = 'unreadable-target';
|
|
1024
|
+
const VALIDATION_ERROR_REASONS = new Set([
|
|
1025
|
+
EXCLUDE_REASON_DIRECTORY,
|
|
1026
|
+
EXCLUDE_REASON_OUTSIDE_PROJECT,
|
|
1027
|
+
EXCLUDE_REASON_UNREADABLE,
|
|
1028
|
+
]);
|
|
721
1029
|
/**
|
|
722
1030
|
* Deduplicate excluded references by path, preserving detail from first occurrence.
|
|
723
1031
|
* Filters out entries reported as validation errors (directory-target, outside-project).
|
|
@@ -751,9 +1059,15 @@ function mapExcludeReason(excludeReason) {
|
|
|
751
1059
|
case 'agent-instruction-file': return 'agent-instruction-file';
|
|
752
1060
|
case 'skill-definition': return 'skill-definition';
|
|
753
1061
|
case 'gitignored': return 'gitignored';
|
|
1062
|
+
// Reported under its own name rather than falling into the default arm.
|
|
1063
|
+
// The default answers `depth-exceeded`, which for this reason would be
|
|
1064
|
+
// false: the walk never reached the edge at all, because the file holding
|
|
1065
|
+
// it is not one VAT routes through.
|
|
1066
|
+
case 'non-routable-source': return 'non-routable-source';
|
|
754
1067
|
case 'depth-exceeded':
|
|
755
1068
|
case EXCLUDE_REASON_DIRECTORY:
|
|
756
1069
|
case EXCLUDE_REASON_OUTSIDE_PROJECT:
|
|
1070
|
+
case EXCLUDE_REASON_UNREADABLE:
|
|
757
1071
|
case 'missing-target':
|
|
758
1072
|
case undefined:
|
|
759
1073
|
default:
|
|
@@ -769,9 +1083,11 @@ function extractSkillName(parseResult, skillPath) {
|
|
|
769
1083
|
if (frontmatterName && typeof frontmatterName === 'string') {
|
|
770
1084
|
return frontmatterName;
|
|
771
1085
|
}
|
|
772
|
-
// Try H1 title
|
|
773
|
-
//
|
|
774
|
-
|
|
1086
|
+
// Try H1 title. `[ \t]` is a single fixed-width class, not a quantifier, so it
|
|
1087
|
+
// cannot compete with the `[^\n]*` capture for the same space — that ambiguity
|
|
1088
|
+
// is what made the old `\s+([^\n]+)` form backtrack super-linearly. Trailing
|
|
1089
|
+
// whitespace is removed by the .trim() below, exactly as before.
|
|
1090
|
+
const h1Match = /^#[ \t]([^\n]*)$/m.exec(parseResult.content);
|
|
775
1091
|
if (h1Match?.[1]) {
|
|
776
1092
|
return h1Match[1].trim();
|
|
777
1093
|
}
|