@vibe-agent-toolkit/agent-skills 0.1.41-rc.7 → 0.1.41
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/dist/content-type-routing.d.ts +18 -1
- package/dist/content-type-routing.d.ts.map +1 -1
- package/dist/content-type-routing.js +17 -0
- package/dist/content-type-routing.js.map +1 -1
- package/dist/evidence/build-evidence.d.ts +38 -0
- package/dist/evidence/build-evidence.d.ts.map +1 -0
- package/dist/evidence/build-evidence.js +60 -0
- package/dist/evidence/build-evidence.js.map +1 -0
- package/dist/evidence/index.d.ts +1 -0
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js +1 -0
- package/dist/evidence/index.js.map +1 -1
- package/dist/files-config.d.ts +225 -31
- package/dist/files-config.d.ts.map +1 -1
- package/dist/files-config.js +445 -123
- package/dist/files-config.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/post-build-checks.d.ts +10 -1
- package/dist/post-build-checks.d.ts.map +1 -1
- package/dist/post-build-checks.js +55 -6
- package/dist/post-build-checks.js.map +1 -1
- package/dist/schemas/index.d.ts +3 -2
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/installed-plugins-registry.d.ts +137 -63
- package/dist/schemas/installed-plugins-registry.d.ts.map +1 -1
- package/dist/schemas/installed-plugins-registry.js +94 -6
- package/dist/schemas/installed-plugins-registry.js.map +1 -1
- package/dist/schemas/known-marketplaces-registry.d.ts +165 -100
- package/dist/schemas/known-marketplaces-registry.d.ts.map +1 -1
- package/dist/schemas/known-marketplaces-registry.js +108 -21
- package/dist/schemas/known-marketplaces-registry.js.map +1 -1
- package/dist/schemas/registry-drift.d.ts +53 -0
- package/dist/schemas/registry-drift.d.ts.map +1 -0
- package/dist/schemas/registry-drift.js +50 -0
- package/dist/schemas/registry-drift.js.map +1 -0
- package/dist/skill-packager.d.ts +75 -5
- package/dist/skill-packager.d.ts.map +1 -1
- package/dist/skill-packager.js +304 -50
- package/dist/skill-packager.js.map +1 -1
- package/dist/skill-source/git-clone.d.ts.map +1 -1
- package/dist/skill-source/git-clone.js +32 -11
- package/dist/skill-source/git-clone.js.map +1 -1
- package/dist/skill-test/eval-inputs.js +2 -2
- package/dist/skill-test/eval-inputs.js.map +1 -1
- package/dist/skill-test/eval-lint.js +2 -2
- package/dist/skill-test/eval-lint.js.map +1 -1
- package/dist/skill-test/grader-prompt.js +2 -2
- package/dist/skill-test/grader-prompt.js.map +1 -1
- package/dist/test-input.d.ts +59 -24
- package/dist/test-input.d.ts.map +1 -1
- package/dist/test-input.js +77 -16
- package/dist/test-input.js.map +1 -1
- package/dist/validators/agent-instruction-presence.d.ts +41 -0
- package/dist/validators/agent-instruction-presence.d.ts.map +1 -0
- package/dist/validators/agent-instruction-presence.js +79 -0
- package/dist/validators/agent-instruction-presence.js.map +1 -0
- package/dist/validators/bundled-resource-link-detection.d.ts +20 -3
- package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
- package/dist/validators/bundled-resource-link-detection.js +78 -34
- package/dist/validators/bundled-resource-link-detection.js.map +1 -1
- package/dist/validators/compat-detectors.d.ts +9 -4
- package/dist/validators/compat-detectors.d.ts.map +1 -1
- package/dist/validators/compat-detectors.js +18 -30
- package/dist/validators/compat-detectors.js.map +1 -1
- package/dist/validators/index.d.ts +1 -0
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/index.js +1 -0
- package/dist/validators/index.js.map +1 -1
- package/dist/validators/packaging-validator.d.ts +68 -10
- package/dist/validators/packaging-validator.d.ts.map +1 -1
- package/dist/validators/packaging-validator.js +51 -10
- package/dist/validators/packaging-validator.js.map +1 -1
- package/dist/validators/registry-validator.d.ts.map +1 -1
- package/dist/validators/registry-validator.js +28 -7
- package/dist/validators/registry-validator.js.map +1 -1
- package/dist/validators/rule-engine/rule-context.d.ts +4 -2
- package/dist/validators/rule-engine/rule-context.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-context.js.map +1 -1
- package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
- package/dist/validators/rule-engine/rule-engine.js +15 -3
- 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 +16 -10
- package/dist/validators/skill-validator.js.map +1 -1
- package/dist/validators/source-only-codes.d.ts.map +1 -1
- package/dist/validators/source-only-codes.js +1 -0
- package/dist/validators/source-only-codes.js.map +1 -1
- package/dist/validators/validation-rules.d.ts +135 -17
- package/dist/validators/validation-rules.d.ts.map +1 -1
- package/dist/validators/validation-rules.js +192 -38
- package/dist/validators/validation-rules.js.map +1 -1
- package/dist/validators/walker-to-issues.d.ts.map +1 -1
- package/dist/validators/walker-to-issues.js +20 -7
- package/dist/validators/walker-to-issues.js.map +1 -1
- package/dist/walk-link-graph.d.ts +10 -1
- package/dist/walk-link-graph.d.ts.map +1 -1
- package/dist/walk-link-graph.js +124 -39
- package/dist/walk-link-graph.js.map +1 -1
- package/package.json +5 -5
package/dist/skill-packager.js
CHANGED
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
19
19
|
import { basename, dirname } from 'node:path';
|
|
20
|
-
import { runValidationFramework, } from '@vibe-agent-toolkit/agent-schema';
|
|
20
|
+
import { allowUnusedIssues, createAllowUsageLedger, runValidationFramework, } from '@vibe-agent-toolkit/agent-schema';
|
|
21
21
|
import { DeferredArtifacts, ResourceRegistry, loadConfig, openFrontmatter, resolveLocalHref, rewriteFrontmatterUriReferencesFromSchema, rewriteHtmlLinks, transformContent, parseMarkdown, } from '@vibe-agent-toolkit/resources';
|
|
22
|
-
import { findProjectRoot, isGlob, resolveAssetReference, toForwardSlash, safePath, } from '@vibe-agent-toolkit/utils';
|
|
23
|
-
import { getTargetSubdir } from './content-type-routing.js';
|
|
24
|
-
import { applyFilesConfig, buildArtifactHint } from './files-config.js';
|
|
22
|
+
import { findProjectRoot, isGlob, issueLocation, resolveAssetReference, toForwardSlash, safePath, } from '@vibe-agent-toolkit/utils';
|
|
23
|
+
import { CLAUDE_WEB_REFERENCES_SUBDIR, getTargetSubdir } from './content-type-routing.js';
|
|
24
|
+
import { applyFilesConfig, buildArtifactHint, droppedGlobMatchesToIssues, explicitFilesConfigDests, globEntryDest, } from './files-config.js';
|
|
25
25
|
import { checkBrokenPackagedLinks, checkUnreferencedFiles } from './post-build-checks.js';
|
|
26
26
|
import { checkPackagedTestInput, partitionTestInputFileEntries, resolveTestInputDirs, testInputExcludeRules, testInputFileEntryIssues, testInputLinkIssues, } from './test-input.js';
|
|
27
|
+
import { detectPackagedAgentInstructionFiles } from './validators/agent-instruction-presence.js';
|
|
27
28
|
import { validateSkillForPackaging } from './validators/packaging-validator.js';
|
|
29
|
+
import { materializeIssue } from './validators/rule-engine/index.js';
|
|
28
30
|
import { deferredAssetsToIssues, walkerExclusionsToIssues } from './validators/walker-to-issues.js';
|
|
29
31
|
import { walkLinkGraph } from './walk-link-graph.js';
|
|
30
32
|
const PACKAGE_JSON_FILENAME = 'package.json';
|
|
@@ -96,8 +98,15 @@ const DEFAULT_PACKAGING_TARGET = 'claude-code';
|
|
|
96
98
|
* The single canonical conversion used by BOTH `vat skills build` and
|
|
97
99
|
* `vat skill test` (the pool build), so the dist a test exercises is byte-for-byte
|
|
98
100
|
* what `vat skills build` would produce. `basePath` defaults to `dirname(skillPath)`.
|
|
101
|
+
*
|
|
102
|
+
* `projectSkills` is EVERY skill the project declares, with its effective packaging
|
|
103
|
+
* config — assembled ONCE per invocation by the calling lane and passed down, never
|
|
104
|
+
* recomputed per skill (a per-skill walk of the whole project config is an N+1 this
|
|
105
|
+
* repo has been bitten by). It is required rather than defaulted because the test-input
|
|
106
|
+
* rule is project-wide: omitting it silently packages another skill's eval answer key.
|
|
107
|
+
* A lane with genuinely no project to enumerate passes `[]` explicitly.
|
|
99
108
|
*/
|
|
100
|
-
export function packagingConfigToPackageOptions(config, anchors) {
|
|
109
|
+
export function packagingConfigToPackageOptions(config, anchors, projectSkills) {
|
|
101
110
|
return {
|
|
102
111
|
outputPath: anchors.outputPath,
|
|
103
112
|
formats: ['directory'],
|
|
@@ -106,14 +115,27 @@ export function packagingConfigToPackageOptions(config, anchors) {
|
|
|
106
115
|
...(config.resourceNaming && { resourceNaming: config.resourceNaming }),
|
|
107
116
|
...(config.stripPrefix && { stripPrefix: config.stripPrefix }),
|
|
108
117
|
...(config.linkFollowDepth !== undefined && { linkFollowDepth: config.linkFollowDepth }),
|
|
118
|
+
// `!== undefined`, not truthiness: `false` is the only value that carries
|
|
119
|
+
// information here (the packager defaults to `true`), so a truthiness spread
|
|
120
|
+
// would drop exactly the setting a user bothered to write. Dropping it made
|
|
121
|
+
// this conversion disagree with `packaging-validator.ts`, which reads the same
|
|
122
|
+
// key straight off the config — the gate predicted a README ships and the
|
|
123
|
+
// build stripped it, from one config, inside the conversion that promises
|
|
124
|
+
// byte-for-byte parity between lanes.
|
|
125
|
+
...(config.excludeNavigationFiles !== undefined && {
|
|
126
|
+
excludeNavigationFiles: config.excludeNavigationFiles,
|
|
127
|
+
}),
|
|
109
128
|
...(config.excludeReferencesFromBundle && { excludeReferencesFromBundle: config.excludeReferencesFromBundle }),
|
|
110
129
|
...(config.files && { files: config.files }),
|
|
111
130
|
...(config.validation && { validation: config.validation }),
|
|
112
131
|
// Declared test input never ships. Derived here — the ONE conversion both
|
|
113
132
|
// `vat skills build` and the plugin build go through — so no lane can package a
|
|
114
|
-
// skill without the rule applied.
|
|
133
|
+
// skill without the rule applied. PROJECT-WIDE: `projectSkills` carries every
|
|
134
|
+
// other skill's declaration too, so a link into a SIBLING skill's suite is
|
|
135
|
+
// excluded as well. Keyed to this skill alone, that sibling's answer key was
|
|
136
|
+
// ordinary content and shipped.
|
|
115
137
|
...(() => {
|
|
116
|
-
const testInputDirs = resolveTestInputDirs(config, dirname(anchors.skillPath));
|
|
138
|
+
const testInputDirs = resolveTestInputDirs(config, dirname(anchors.skillPath), projectSkills);
|
|
117
139
|
return testInputDirs.length > 0 ? { testInputDirs } : {};
|
|
118
140
|
})(),
|
|
119
141
|
};
|
|
@@ -125,9 +147,36 @@ export function packagingConfigToPackageOptions(config, anchors) {
|
|
|
125
147
|
* then packages each skill against the shared registry. This eliminates
|
|
126
148
|
* redundant I/O when building multiple skills from the same project.
|
|
127
149
|
*
|
|
150
|
+
* **One skill's failure never discards the batch.** `packageSkill` reports most
|
|
151
|
+
* problems by RETURNING a result whose `hasErrors` is set, which callers already
|
|
152
|
+
* degrade gracefully on — but it also THROWS on structural packaging failures
|
|
153
|
+
* (an absent or unreadable `files:` source). Letting that throw escape the loop
|
|
154
|
+
* made the two failure paths behave in opposite ways through one contract:
|
|
155
|
+
* measured on a 90-skill project, one such failure discarded 89 completed builds
|
|
156
|
+
* and collapsed the whole report into a single string. Each iteration is
|
|
157
|
+
* therefore contained and reported as a `failed` outcome instead.
|
|
158
|
+
*
|
|
159
|
+
* A filename collision is NOT one of the throwing paths — it is a returned
|
|
160
|
+
* `FILENAME_COLLISION` finding. Do not reach for a collision as the fixture when
|
|
161
|
+
* testing this containment: the loop completes normally either way, so such a
|
|
162
|
+
* test passes whether or not the containment exists.
|
|
163
|
+
*
|
|
164
|
+
* The registry build is deliberately OUTSIDE the containment: it is the run's
|
|
165
|
+
* shared prerequisite, so its failure really does doom every skill and must
|
|
166
|
+
* still propagate. Only per-skill work is contained.
|
|
167
|
+
*
|
|
128
168
|
* @param skills - Array of skill build specifications
|
|
129
169
|
* @param projectRoot - Absolute path to the project root directory
|
|
130
|
-
* @
|
|
170
|
+
* @param allowLedger - The RUN's allow-usage ledger. Required, not
|
|
171
|
+
* optional-with-a-default, for the same reason `runValidationFramework`'s is:
|
|
172
|
+
* this function loops, so it can never honestly conclude on its own that an
|
|
173
|
+
* allow entry matched nothing — an entry matched while building skill A is
|
|
174
|
+
* USED for the run. It is never drained here; the caller drains it once with
|
|
175
|
+
* `allowUnusedIssues()` after everything in the invocation has been seen
|
|
176
|
+
* (`vat build` also validates the SOURCE tree, whose matches count too).
|
|
177
|
+
* Containment does not change that: a skill that threw may still have matched
|
|
178
|
+
* allow entries before it threw, and those matches count for the run.
|
|
179
|
+
* @returns One outcome per input spec, in input order
|
|
131
180
|
*
|
|
132
181
|
* @example
|
|
133
182
|
* ```typescript
|
|
@@ -135,22 +184,35 @@ export function packagingConfigToPackageOptions(config, anchors) {
|
|
|
135
184
|
* { skillPath: '/project/skills/SKILL.md', options: { outputPath: '/out/skill-a' } },
|
|
136
185
|
* { skillPath: '/project/skills/SKILL2.md', options: { outputPath: '/out/skill-b' } },
|
|
137
186
|
* ];
|
|
138
|
-
* const
|
|
187
|
+
* const ledger = createAllowUsageLedger();
|
|
188
|
+
* const outcomes = await packageSkills(specs, '/project', ledger);
|
|
189
|
+
* const runIssues = allowUnusedIssues(ledger);
|
|
139
190
|
* ```
|
|
140
191
|
*/
|
|
141
|
-
export async function packageSkills(skills, projectRoot) {
|
|
192
|
+
export async function packageSkills(skills, projectRoot, allowLedger) {
|
|
142
193
|
// 1. Create one registry for the entire project. Through the shared builder:
|
|
143
194
|
// this used to call `fromCrawl` directly and omit the config, so skills built
|
|
144
195
|
// here belonged to no collection while a skill built through the single-skill
|
|
145
196
|
// fallback did.
|
|
146
197
|
const registry = await createProjectRegistry(projectRoot);
|
|
147
198
|
// 2. Package each skill against the shared registry
|
|
148
|
-
const
|
|
199
|
+
const outcomes = [];
|
|
149
200
|
for (const { skillPath, options } of skills) {
|
|
150
|
-
|
|
151
|
-
|
|
201
|
+
try {
|
|
202
|
+
const result = await packageSkill(skillPath, { ...options, registry, allowLedger });
|
|
203
|
+
outcomes.push({ status: 'built', skillPath, result });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// Not swallowed: the error is carried on the outcome so the caller reports
|
|
207
|
+
// WHICH skill failed and why, and gates the run's exit code on it.
|
|
208
|
+
outcomes.push({
|
|
209
|
+
status: 'failed',
|
|
210
|
+
skillPath,
|
|
211
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
152
214
|
}
|
|
153
|
-
return
|
|
215
|
+
return outcomes;
|
|
154
216
|
}
|
|
155
217
|
/**
|
|
156
218
|
* Package a skill with all its dependencies
|
|
@@ -179,6 +241,21 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
179
241
|
// Library callers fall back to the skill directory when canonical
|
|
180
242
|
// findProjectRoot returns null. The CLI command boundary is responsible
|
|
181
243
|
// for any user-facing warning about missing project roots.
|
|
244
|
+
//
|
|
245
|
+
// COORDINATE-SYSTEM ASSUMPTION, not an enforced invariant. This root is the base
|
|
246
|
+
// every path this call REPORTS is stated in — `namingBasePath` at step 8, and the
|
|
247
|
+
// `projectRoot` handed to `walkerExclusionsToIssues` / `testInputLinkIssues` /
|
|
248
|
+
// `deferredAssetsToIssues` at step 13b. It is derived HERE, per skill, from the
|
|
249
|
+
// skill's own path. It is NOT the `projectRoot` argument `packageSkills` was called
|
|
250
|
+
// with (that one is used only for `createProjectRegistry`), and it is not the `cwd`
|
|
251
|
+
// that `vat skills build` treats as the root of the document it emits.
|
|
252
|
+
//
|
|
253
|
+
// UNVERIFIED that these can actually diverge: in every path exercised so far the two
|
|
254
|
+
// have coincided (`vat skills build` passes its `cwd`, and the skills it discovers
|
|
255
|
+
// live under it), and no run has been observed where they differ. Nothing enforces
|
|
256
|
+
// the equality, though — a caller packaging a skill from outside its own project
|
|
257
|
+
// root would get issue locations in one coordinate system and a report header in
|
|
258
|
+
// another, with no error anywhere.
|
|
182
259
|
const projectRoot = findProjectRoot(dirname(skillPath)) ?? dirname(skillPath);
|
|
183
260
|
const skillRoot = dirname(skillPath);
|
|
184
261
|
// 3. Get or create the resource registry.
|
|
@@ -247,10 +324,15 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
247
324
|
}
|
|
248
325
|
// 8. Build path map for file copying and link rewriting
|
|
249
326
|
const namingBasePath = projectRoot;
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
|
|
327
|
+
const pathMapSkill = { path: skillPath, name: skillMetadata.name };
|
|
328
|
+
const pathMap = buildPathMap(pathMapSkill, bundledFiles, outputPath, resourceNaming, namingBasePath, stripPrefix, target);
|
|
329
|
+
// 8b. Apply files config to the path map: single-file entries by name, and glob
|
|
330
|
+
// entries by re-pointing the link-bundled files they claim at the declared dest.
|
|
331
|
+
applyFilesEntriesToPathMap(filesConfig, projectRoot, outputPath, pathMap, skillMetadata.name, bundledFiles);
|
|
332
|
+
// 8c. Collisions are judged on the FINAL destination map, so a `files:` remap is
|
|
333
|
+
// a real remedy and a `files:`-created collision is still caught. See
|
|
334
|
+
// detectDestinationCollisions for why this cannot run inside buildPathMap.
|
|
335
|
+
const collisionIssues = detectDestinationCollisions(pathMapSkill, pathMap, outputPath, resourceNaming, namingBasePath);
|
|
254
336
|
// 9. Build "to" registry for link rewriting (maps same resource IDs to output paths)
|
|
255
337
|
const outputResources = bundledResources.map(resource => ({
|
|
256
338
|
...resource,
|
|
@@ -313,9 +395,16 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
313
395
|
// Keep the dests it reports: they are what makes the orphan check below able to
|
|
314
396
|
// tell "the author forgot to document this" from "VAT put this here because the
|
|
315
397
|
// config said to." Discarding them makes the build fail on its own payload.
|
|
316
|
-
const
|
|
398
|
+
const appliedFiles = await applyFilesConfig({
|
|
317
399
|
filesConfig, projectRoot, skillOutputDir: outputPath, bundledFiles,
|
|
318
400
|
});
|
|
401
|
+
const filesConfigDests = appliedFiles.dests;
|
|
402
|
+
// Dests a glob matched and the never-package list refused. Two consumers, and
|
|
403
|
+
// both are load-bearing: the broken-link check below needs them to tell a link
|
|
404
|
+
// broken by policy from a link broken by the rewriter, and the finding channel
|
|
405
|
+
// needs them or the structured report says `warnings: 0` about a build that
|
|
406
|
+
// silently shipped less than the config asked for.
|
|
407
|
+
const droppedGlobDests = appliedFiles.dropped.map((drop) => drop.dest);
|
|
319
408
|
// 13. Post-build integrity check: no SKILL.md in subdirectories
|
|
320
409
|
// A SKILL.md is a skill definition marker — it must only exist at the root.
|
|
321
410
|
// If another skill's SKILL.md was bundled as a resource, it creates duplicate
|
|
@@ -329,8 +418,31 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
329
418
|
// Walker-exclusion issues (depth drops, missing targets, outside-project, etc.)
|
|
330
419
|
// are combined with post-build checks and run through the validation framework.
|
|
331
420
|
const rawPostBuildIssues = [
|
|
421
|
+
// Found back at step 8, reported here: the path map is decided before any
|
|
422
|
+
// file is written, but a collision is a packaging FINDING and rides the same
|
|
423
|
+
// channel as every other one rather than aborting the run from inside a
|
|
424
|
+
// helper.
|
|
425
|
+
...collisionIssues,
|
|
332
426
|
...await checkUnreferencedFiles(outputPath, filesConfigDests),
|
|
333
|
-
...await checkBrokenPackagedLinks(outputPath),
|
|
427
|
+
...await checkBrokenPackagedLinks(outputPath, droppedGlobDests),
|
|
428
|
+
// A receipt for every file a glob matched and the never-package list refused.
|
|
429
|
+
// Reported as an issue, not written to stderr: a file vanishing from a bundle
|
|
430
|
+
// has to be visible in `issueCounts`, or CI reads a clean report for a build
|
|
431
|
+
// that quietly shipped less than the config declared.
|
|
432
|
+
// Anchored at the PROJECT root, not `outputPath` like its neighbours here: a
|
|
433
|
+
// dropped file is a source file that never reached the output, so the only
|
|
434
|
+
// path a reader can open is its source path.
|
|
435
|
+
...droppedGlobMatchesToIssues(appliedFiles.dropped, projectRoot),
|
|
436
|
+
// Presence-side backstop for agent-instruction files. The walker excludes
|
|
437
|
+
// them from link-following, but a `files:` glob can still copy one in, and
|
|
438
|
+
// a file that arrives without a link is invisible to the link lane.
|
|
439
|
+
//
|
|
440
|
+
// Explicitly-declared dests are exempt (§8.2 precedence): the build KNOWS the
|
|
441
|
+
// config here, and an explicit `files:` entry is an instruction to ship that
|
|
442
|
+
// exact file. Reporting it fired this warning on the very config the guide
|
|
443
|
+
// prescribes as the escape hatch, with a remedy ("remove the file") that says
|
|
444
|
+
// to undo what the author was told to write.
|
|
445
|
+
...detectPackagedAgentInstructionFiles(outputPath, outputPath, explicitFilesConfigDests(filesConfig)),
|
|
334
446
|
// A receipt for each `files:` entry that was dropped for pointing into declared
|
|
335
447
|
// test input — the build already produced the right artifact; this just says so.
|
|
336
448
|
...testInputFileEntryIssues(droppedTestInputFiles),
|
|
@@ -346,9 +458,26 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
346
458
|
...testInputLinkIssues(excludedReferences, testInputDirs, projectRoot),
|
|
347
459
|
...deferredAssetsToIssues(deferredAssets, projectRoot),
|
|
348
460
|
];
|
|
349
|
-
|
|
461
|
+
// ONE ledger for BOTH lanes below. `options.validation.allow` governs the
|
|
462
|
+
// build-receipt lane AND the built-SKILL.md lane, but the two see disjoint
|
|
463
|
+
// issue populations — so a lane that drains its own ledger calls "unused" an
|
|
464
|
+
// entry the OTHER lane matched. (Measured before the fix: an entry that
|
|
465
|
+
// suppressed a real LINK_MISSING_TARGET error still emitted ALLOW_UNUSED from
|
|
466
|
+
// the built-output lane, and a genuinely dead entry was reported twice.) Same
|
|
467
|
+
// defect, same fix as the validate lane — see
|
|
468
|
+
// `SkillValidationSharedContext.allowLedger`.
|
|
469
|
+
//
|
|
470
|
+
// Whose run it is comes from the caller. Given a ledger, this call is one unit
|
|
471
|
+
// of a larger run (a batch, and/or a source-tree validation pass that matches
|
|
472
|
+
// entries this build's two lanes structurally cannot) and must NOT drain —
|
|
473
|
+
// draining here is what reported a package-scoped entry matched while building
|
|
474
|
+
// skill A as unused while building skill B. Given none, the caller has claimed
|
|
475
|
+
// this call IS the run, so the drain below is honest.
|
|
476
|
+
const ownsRun = options.allowLedger === undefined;
|
|
477
|
+
const buildRunLedger = options.allowLedger ?? createAllowUsageLedger();
|
|
478
|
+
const framework = runValidationFramework([...rawLinkIssues, ...rawPostBuildIssues], options.validation ?? {}, buildRunLedger);
|
|
350
479
|
// 13c. Run full validation suite on built output
|
|
351
|
-
const postBuildValidation = await runPostBuildValidation(outputPath, options.validation);
|
|
480
|
+
const postBuildValidation = await runPostBuildValidation(outputPath, options.validation, buildRunLedger);
|
|
352
481
|
// 14. Generate distribution artifacts
|
|
353
482
|
const artifacts = await generatePackageArtifacts(outputPath, skillMetadata, formats, target);
|
|
354
483
|
// Get relative paths for result
|
|
@@ -360,18 +489,37 @@ export async function packageSkill(skillPath, options = {}) {
|
|
|
360
489
|
relativeLinkedFiles,
|
|
361
490
|
artifacts,
|
|
362
491
|
postBuildValidation,
|
|
363
|
-
|
|
492
|
+
// Drain point — but only when this call owns the run. Both lanes have now
|
|
493
|
+
// contributed; whether anything ELSE still will is the caller's claim.
|
|
494
|
+
framework: ownsRun ? withRunAllowUnused(framework, buildRunLedger) : framework,
|
|
364
495
|
excludedReferences,
|
|
365
496
|
skillRoot,
|
|
366
497
|
});
|
|
367
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Fold the build run's ALLOW_UNUSED verdict into the build-issue lane.
|
|
501
|
+
*
|
|
502
|
+
* ALLOW_UNUSED belongs to the RUN, not to whichever lane happened to be looking
|
|
503
|
+
* when an entry went unmatched — so it is drained once, here, and reported on
|
|
504
|
+
* the single channel (`postBuildIssues`) rather than on both.
|
|
505
|
+
*/
|
|
506
|
+
function withRunAllowUnused(framework, ledger) {
|
|
507
|
+
const unused = allowUnusedIssues(ledger);
|
|
508
|
+
if (unused.length === 0)
|
|
509
|
+
return framework;
|
|
510
|
+
const emitted = [...framework.emitted, ...unused];
|
|
511
|
+
return { ...framework, emitted, hasErrors: emitted.some(i => i.severity === 'error') };
|
|
512
|
+
}
|
|
368
513
|
/**
|
|
369
514
|
* Run full validation suite against built output (context = 'built').
|
|
370
515
|
* Source-only codes are automatically filtered out by validateSkillForPackaging.
|
|
516
|
+
*
|
|
517
|
+
* `allowLedger` is required, not optional: this lane is one half of a build, so
|
|
518
|
+
* it must never conclude on its own that an allow entry matched nothing.
|
|
371
519
|
*/
|
|
372
|
-
async function runPostBuildValidation(outputPath, validation) {
|
|
520
|
+
async function runPostBuildValidation(outputPath, validation, allowLedger) {
|
|
373
521
|
const builtSkillPath = safePath.join(outputPath, 'SKILL.md');
|
|
374
|
-
return validateSkillForPackaging(builtSkillPath, validation ? { validation } : undefined, 'built');
|
|
522
|
+
return validateSkillForPackaging(builtSkillPath, validation ? { validation } : undefined, 'built', { allowLedger });
|
|
375
523
|
}
|
|
376
524
|
/**
|
|
377
525
|
* Assemble the final PackageSkillResult from intermediate data.
|
|
@@ -387,7 +535,7 @@ function assemblePackageResult(input) {
|
|
|
387
535
|
},
|
|
388
536
|
artifacts: input.artifacts,
|
|
389
537
|
postBuildValidation: input.postBuildValidation,
|
|
390
|
-
hasErrors: input.framework.hasErrors || input.postBuildValidation.
|
|
538
|
+
hasErrors: input.framework.hasErrors || input.postBuildValidation.status === 'error',
|
|
391
539
|
};
|
|
392
540
|
if (input.framework.emitted.length > 0) {
|
|
393
541
|
result.postBuildIssues = input.framework.emitted;
|
|
@@ -597,17 +745,53 @@ function buildSyntheticAssetResource(assetPath, outputFilePath) {
|
|
|
597
745
|
};
|
|
598
746
|
}
|
|
599
747
|
/**
|
|
600
|
-
*
|
|
748
|
+
* Re-point every LINK-BUNDLED file a GLOB `files:` entry also claims at the dest
|
|
749
|
+
* that entry declares.
|
|
750
|
+
*
|
|
751
|
+
* A glob's expansion is late-bound to copy time, so its matches used to be absent
|
|
752
|
+
* from the path map entirely — and a match that link traversal ALSO discovered was
|
|
753
|
+
* therefore parked at its type-derived location (`resources/GUIDE.md`) while
|
|
754
|
+
* `applyFilesConfig` copied the same bytes to the declared dest
|
|
755
|
+
* (`packs/alpha/GUIDE.md`). Two identical files shipped, the rewritten link pointed
|
|
756
|
+
* at traversal's copy, the declared `dest:` was dead, and nothing said so — a size
|
|
757
|
+
* regression for anyone shipping multi-megabyte artifacts, and a `dest:` that lies
|
|
758
|
+
* whenever the same file is also referenced from prose.
|
|
759
|
+
*
|
|
760
|
+
* Only files already in `bundledFiles` are considered: this is not a second
|
|
761
|
+
* expansion of the glob (that stays with {@link copyGlobEntry}), it is the path map
|
|
762
|
+
* — the authority on where a file ends up — learning what the config already said.
|
|
763
|
+
*/
|
|
764
|
+
function applyGlobEntryToPathMap(entry, projectRoot, outputPath, pathMap, bundledFiles) {
|
|
765
|
+
for (const bundled of bundledFiles) {
|
|
766
|
+
const dest = globEntryDest(entry, projectRoot, bundled);
|
|
767
|
+
if (dest === undefined)
|
|
768
|
+
continue;
|
|
769
|
+
// joinUnderRoot mirrors the non-glob branch's zip-slip guard below.
|
|
770
|
+
pathMap.set(toForwardSlash(bundled), safePath.joinUnderRoot(outputPath, dest));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Register `files:` config entries in the path map.
|
|
601
775
|
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
776
|
+
* Two passes, GLOBS FIRST, and the order is the precedence rule: an explicit entry
|
|
777
|
+
* naming a file outranks a glob that merely caught it, exactly as it does on the
|
|
778
|
+
* copy side (see `partitionNeverPackaged` — a glob is a net, not a declaration).
|
|
779
|
+
* Applying globs second would let one silently overwrite the dest an author spelled
|
|
780
|
+
* out, and the explicit entry's own copy is then skipped as "already bundled at its
|
|
781
|
+
* dest" — so the declared file would never be written at all.
|
|
605
782
|
*
|
|
606
783
|
* Called from step 8b of packageSkill to keep the main function under the
|
|
607
784
|
* cognitive-complexity limit.
|
|
608
785
|
*/
|
|
609
|
-
function
|
|
786
|
+
function applyFilesEntriesToPathMap(filesConfig, projectRoot, outputPath, pathMap, skillName, bundledFiles) {
|
|
610
787
|
for (const fileEntry of filesConfig) {
|
|
788
|
+
if (isGlob(fileEntry.source)) {
|
|
789
|
+
applyGlobEntryToPathMap(fileEntry, projectRoot, outputPath, pathMap, bundledFiles);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const fileEntry of filesConfig) {
|
|
793
|
+
// A glob source contains magic, never exists as a literal path, and would
|
|
794
|
+
// wrongly throw the existence check below; its dests were handled above.
|
|
611
795
|
if (isGlob(fileEntry.source))
|
|
612
796
|
continue;
|
|
613
797
|
const absoluteSource = safePath.resolve(safePath.join(projectRoot, fileEntry.source));
|
|
@@ -760,37 +944,107 @@ function stampDeferredDestResolvedId(resources, absDestTarget, id) {
|
|
|
760
944
|
*/
|
|
761
945
|
export function getResourceSubdirForFile(filePath, target) {
|
|
762
946
|
if (target === 'claude-web') {
|
|
763
|
-
return
|
|
947
|
+
return CLAUDE_WEB_REFERENCES_SUBDIR;
|
|
764
948
|
}
|
|
765
949
|
return getTargetSubdir(filePath);
|
|
766
950
|
}
|
|
767
951
|
/**
|
|
768
|
-
*
|
|
769
|
-
*
|
|
952
|
+
* The FILENAME_COLLISION finding for one pair of sources that package to one dest.
|
|
953
|
+
*
|
|
954
|
+
* Naming the skill is the whole point of the first line: neither colliding file
|
|
955
|
+
* need be referenced by SKILL.md directly (both are commonly reached by deep
|
|
956
|
+
* link traversal), so without it the only way to find the owner in a large
|
|
957
|
+
* batch is to bisect it one skill at a time.
|
|
958
|
+
*
|
|
959
|
+
* By NAME, not by absolute path: this message is published verbatim in
|
|
960
|
+
* `vat skills build`'s stdout payload, the name is what that payload's per-skill
|
|
961
|
+
* rows already key on, and a `/Users/<someone>/…` prefix answers no question the
|
|
962
|
+
* reader has while naming the machine the build ran on. The colliding files
|
|
963
|
+
* follow the same rule — stated in the project's coordinates, like every other
|
|
964
|
+
* "where" this package renders (`issueLocation`).
|
|
965
|
+
*
|
|
966
|
+
* `location` is the SECOND file: of the two, it is the one whose packaging the
|
|
967
|
+
* first pre-empted, so it is the one an author renames. The remedy that is not
|
|
968
|
+
* a rename (switch `resourceNaming`) comes from the registry `fix`.
|
|
770
969
|
*/
|
|
771
|
-
function
|
|
970
|
+
function filenameCollisionIssue(skill, existingSource, linkedFile, targetRelPath, resourceNaming, namingBasePath) {
|
|
971
|
+
const location = issueLocation(linkedFile, namingBasePath);
|
|
972
|
+
return materializeIssue('FILENAME_COLLISION', {
|
|
973
|
+
location,
|
|
974
|
+
message: `Filename collision detected when packaging skill: ${skill.name} — ` +
|
|
975
|
+
`File 1: ${issueLocation(existingSource, namingBasePath)}, ` +
|
|
976
|
+
`File 2: ${location}; both would be packaged as ${targetRelPath} ` +
|
|
977
|
+
`(current resourceNaming strategy: ${resourceNaming})`,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Build a map of source paths (forward-slash normalized) to output paths, plus
|
|
982
|
+
* a FILENAME_COLLISION finding for every pair of sources that land on one dest.
|
|
983
|
+
*
|
|
984
|
+
* A collision is REPORTED, not thrown. It used to throw a raw `Error`, which
|
|
985
|
+
* escaped the contract every other packaging finding honours — the caller got a
|
|
986
|
+
* bare string instead of a coded, located, fixable `ValidationIssue`, and the
|
|
987
|
+
* batch lane had to special-case it. The build still fails: the finding's
|
|
988
|
+
* registry severity is `error`, so it flips `hasErrors`.
|
|
989
|
+
*
|
|
990
|
+
* The colliding source keeps its path-map entry rather than being dropped. The
|
|
991
|
+
* build is failing either way, and keeping it means the link rewriter still
|
|
992
|
+
* resolves both links to a file that exists in the output — dropping it would
|
|
993
|
+
* add a second, derived PACKAGED_BROKEN_LINK on top of the real finding and
|
|
994
|
+
* point the surviving link at an unrewritable source path.
|
|
995
|
+
*
|
|
996
|
+
* @param namingBasePath - The project root. Resource names are generated
|
|
997
|
+
* relative to it AND every path this function REPORTS is stated in its
|
|
998
|
+
* coordinates, so no message leaves here in machine-specific terms.
|
|
999
|
+
*/
|
|
1000
|
+
function buildPathMap(skill, bundledFiles, outputPath, resourceNaming, namingBasePath, stripPrefix, target = DEFAULT_PACKAGING_TARGET) {
|
|
772
1001
|
const pathMap = new Map();
|
|
773
|
-
pathMap.set(toForwardSlash(
|
|
1002
|
+
pathMap.set(toForwardSlash(skill.path), safePath.join(outputPath, 'SKILL.md'));
|
|
774
1003
|
for (const linkedFile of bundledFiles) {
|
|
775
1004
|
const targetRelPath = generateTargetPath(linkedFile, namingBasePath, resourceNaming, stripPrefix);
|
|
776
1005
|
const fileSubdir = getResourceSubdirForFile(linkedFile, target);
|
|
777
|
-
|
|
778
|
-
// Check for filename collisions
|
|
779
|
-
const existingSource = [...pathMap.entries()].find(([_src, target]) => target === targetPath)?.[0];
|
|
780
|
-
if (existingSource !== undefined && existingSource !== toForwardSlash(linkedFile)) {
|
|
781
|
-
throw new Error(`Filename collision detected when packaging skill:\n` +
|
|
782
|
-
` File 1: ${existingSource}\n` +
|
|
783
|
-
` File 2: ${linkedFile}\n` +
|
|
784
|
-
` Both would be packaged as: ${targetRelPath}\n` +
|
|
785
|
-
`\n` +
|
|
786
|
-
` To resolve: Use a different resourceNaming strategy or ensure unique filenames.\n` +
|
|
787
|
-
` Current strategy: ${resourceNaming}\n` +
|
|
788
|
-
` Try 'resource-id' or 'preserve-path' for path-based naming.`);
|
|
789
|
-
}
|
|
790
|
-
pathMap.set(toForwardSlash(linkedFile), targetPath);
|
|
1006
|
+
pathMap.set(toForwardSlash(linkedFile), safePath.join(outputPath, fileSubdir, targetRelPath));
|
|
791
1007
|
}
|
|
792
1008
|
return pathMap;
|
|
793
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Report a FILENAME_COLLISION for every destination claimed by more than one source.
|
|
1012
|
+
*
|
|
1013
|
+
* MUST run against the FINAL destination map — after `files:` single-file entries
|
|
1014
|
+
* have overridden the naming-strategy destinations. Detecting collisions while
|
|
1015
|
+
* building the map (where this logic used to live) answers the question one step
|
|
1016
|
+
* too early: `files:` is a legitimate remedy for a basename collision, and an
|
|
1017
|
+
* adopter who remapped both sides to distinct dests still had the build failed at
|
|
1018
|
+
* `error` severity for a collision that no longer physically occurred. It also
|
|
1019
|
+
* could not see the inverse — two `files:` entries pointing at ONE dest is a real
|
|
1020
|
+
* collision the naming-strategy map contains no trace of.
|
|
1021
|
+
*
|
|
1022
|
+
* Scope: glob `files:` entries expand later (`applyFilesConfig`) and so are not in
|
|
1023
|
+
* this map; their destinations are directories, which cannot collide this way.
|
|
1024
|
+
*/
|
|
1025
|
+
function detectDestinationCollisions(skill, pathMap, outputPath, resourceNaming, namingBasePath) {
|
|
1026
|
+
const sourcesByDest = new Map();
|
|
1027
|
+
for (const [source, dest] of pathMap) {
|
|
1028
|
+
const existing = sourcesByDest.get(dest);
|
|
1029
|
+
if (existing)
|
|
1030
|
+
existing.push(source);
|
|
1031
|
+
else
|
|
1032
|
+
sourcesByDest.set(dest, [source]);
|
|
1033
|
+
}
|
|
1034
|
+
const issues = [];
|
|
1035
|
+
for (const [dest, sources] of sourcesByDest) {
|
|
1036
|
+
// Map iteration is insertion-ordered, so sources[0] is the entry that "won"
|
|
1037
|
+
// the destination — the same File 1 / File 2 framing the message always used.
|
|
1038
|
+
const [winner, ...losers] = sources;
|
|
1039
|
+
if (winner === undefined || losers.length === 0)
|
|
1040
|
+
continue;
|
|
1041
|
+
const targetRelPath = toForwardSlash(safePath.relative(outputPath, dest));
|
|
1042
|
+
for (const loser of losers) {
|
|
1043
|
+
issues.push(filenameCollisionIssue(skill, winner, loser, targetRelPath, resourceNaming, namingBasePath));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return issues;
|
|
1047
|
+
}
|
|
794
1048
|
// ============================================================================
|
|
795
1049
|
// Rewrite Rules
|
|
796
1050
|
// ============================================================================
|