@vibe-agent-toolkit/agent-skills 0.2.0-rc.4 → 0.2.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +9 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/post-build-checks.d.ts +29 -0
  6. package/dist/post-build-checks.d.ts.map +1 -1
  7. package/dist/post-build-checks.js +46 -3
  8. package/dist/post-build-checks.js.map +1 -1
  9. package/dist/skill-identity.d.ts +10 -0
  10. package/dist/skill-identity.d.ts.map +1 -1
  11. package/dist/skill-identity.js +12 -0
  12. package/dist/skill-identity.js.map +1 -1
  13. package/dist/skill-packager.d.ts.map +1 -1
  14. package/dist/skill-packager.js +70 -1
  15. package/dist/skill-packager.js.map +1 -1
  16. package/dist/skill-test/build-hook.js +0 -1
  17. package/dist/skill-test/build-hook.js.map +1 -1
  18. package/dist/skill-test/eval-fragment.d.ts +26 -26
  19. package/dist/validators/mcp-tool-qualification.d.ts +158 -0
  20. package/dist/validators/mcp-tool-qualification.d.ts.map +1 -0
  21. package/dist/validators/mcp-tool-qualification.js +467 -0
  22. package/dist/validators/mcp-tool-qualification.js.map +1 -0
  23. package/dist/validators/packaged-size-limit.d.ts +296 -0
  24. package/dist/validators/packaged-size-limit.d.ts.map +1 -0
  25. package/dist/validators/packaged-size-limit.js +494 -0
  26. package/dist/validators/packaged-size-limit.js.map +1 -0
  27. package/dist/validators/packaging-validator.d.ts +10 -0
  28. package/dist/validators/packaging-validator.d.ts.map +1 -1
  29. package/dist/validators/packaging-validator.js +90 -7
  30. package/dist/validators/packaging-validator.js.map +1 -1
  31. package/dist/validators/referenced-path-missing.d.ts +202 -0
  32. package/dist/validators/referenced-path-missing.d.ts.map +1 -0
  33. package/dist/validators/referenced-path-missing.js +358 -0
  34. package/dist/validators/referenced-path-missing.js.map +1 -0
  35. package/dist/validators/skill-validator.d.ts.map +1 -1
  36. package/dist/validators/skill-validator.js +29 -0
  37. package/dist/validators/skill-validator.js.map +1 -1
  38. package/dist/walk-link-graph.d.ts +23 -1
  39. package/dist/walk-link-graph.d.ts.map +1 -1
  40. package/dist/walk-link-graph.js +11 -1
  41. package/dist/walk-link-graph.js.map +1 -1
  42. package/package.json +5 -5
@@ -0,0 +1,296 @@
1
+ /**
2
+ * PACKAGED_SIZE_EXCEEDS_API_LIMIT — the packaged skill is too large to upload to
3
+ * the Anthropic Skills API.
4
+ *
5
+ * ## Why this needs its own detector rather than a threshold on an existing one
6
+ *
7
+ * VAT ships two checks whose names suggest they already answer this, and neither
8
+ * can:
9
+ *
10
+ * - `SKILL_TOTAL_SIZE_LARGE` counts **lines** across bundled markdown (≤2000).
11
+ * - `SKILL_TOO_MANY_FILES` counts **files** (≤6).
12
+ *
13
+ * The shape that actually blocks a publish is one large binary, which has no
14
+ * lines and is one file. The measured instance is a 35.7 MB `.wasm` runtime
15
+ * bundled by three skills in a single adopter marketplace: over the ceiling on
16
+ * its own, before a byte of markdown counts, and invisible to both checks above.
17
+ * Before this module there was no byte measurement anywhere in the validators.
18
+ *
19
+ * ## Why the message names the largest files
20
+ *
21
+ * A total alone ("34.9 MB, limit 30 MB") tells an author they have a problem and
22
+ * not where it is; a bundle at that size has hundreds of files and the true cause
23
+ * is nearly always one or two of them. The adopter who reported this built the
24
+ * same thing locally and said naming the largest files "is the thing that made
25
+ * the cause obvious in one line". So the finding leads with the offenders.
26
+ *
27
+ * ## Built phase only
28
+ *
29
+ * The bytes that matter are the bytes that ship, and a `files:` config entry
30
+ * materializes files at build time — a source tree can lack the very artifact
31
+ * that blows the ceiling. Measuring the source directory would answer a
32
+ * different question and get it wrong in both directions.
33
+ *
34
+ * ## This module is the ONLY thing that stats the packaged bytes
35
+ *
36
+ * That is why an entry it cannot weigh gets a receipt rather than a zero — see
37
+ * {@link walkBundle}. No other validator revisits these files, so a silent zero
38
+ * here is a silent zero everywhere.
39
+ */
40
+ import { type ValidationIssue } from '@vibe-agent-toolkit/schema';
41
+ /**
42
+ * The Skills API's refusal ceiling, in bytes. The vendor documents it as "Total
43
+ * upload size must be under 30 MB (uncompressed)" and the 413 says "requests up
44
+ * to 30MBs", neither of which says whether MB means 10^6 or 2^20 — a 1.4 MB gap.
45
+ *
46
+ * **This is measured, not read.** VAT first assumed decimal (30,000,000) as the
47
+ * conservative reading. Uploading against the live API disproved it:
48
+ *
49
+ * | Bundle content bytes | Result |
50
+ * |---|---|
51
+ * | 30,700,000 | **accepted** — above decimal 30 MB, so decimal is wrong |
52
+ * | 31,500,000 | 413 refused |
53
+ * | 35,900,338 | 413 refused |
54
+ *
55
+ * That brackets the ceiling to (30,700,000, 31,500,000], and 2^20 × 30 =
56
+ * 31,457,280 is the only round number in the window. So the limit is mebibytes,
57
+ * and the old decimal constant warned ~1.46 MB early — a false-positive band on
58
+ * every bundle between 30.0 and 31.4 MB, which is exactly where a big bundle
59
+ * lands.
60
+ *
61
+ * ## What this counts versus what the API counts
62
+ *
63
+ * The API measures the REQUEST. This check measures the bundle's file bytes,
64
+ * because at build time there is no request to weigh — see
65
+ * {@link checkPackagedSizeLimit} for why that is left as it is rather than
66
+ * modelled.
67
+ *
68
+ * The gap between the two is not small. `buildMultipartFormData` frames every
69
+ * file part with a 51-byte boundary line, a `Content-Disposition` header of
70
+ * 61 bytes plus the filename, a 42-byte content-type-and-blank-line, and a
71
+ * 2-byte trailing CRLF — **156 bytes plus the filename's length, per file** —
72
+ * and closes the body with a 53-byte terminator. Measured: one part with a
73
+ * 14-character filename costs 170 bytes of framing, and a 1,000-file bundle
74
+ * with `resources/`-shaped names costs ~185,000. So a bundle whose FILES sit
75
+ * just under the ceiling can be a REQUEST comfortably over it. An earlier
76
+ * version of this comment put the residue at "a few hundred bytes… roughly a
77
+ * kilobyte", which understates it by two orders of magnitude on a many-file
78
+ * bundle.
79
+ *
80
+ * The uploader therefore weighs the body it built rather than the sum of its
81
+ * file bytes, and says so in its own message ({@link OversizeMeasure}). Neither
82
+ * lane pads its threshold with a fudge factor: an invented margin is a number
83
+ * nobody can re-derive.
84
+ *
85
+ * ## The two lanes compare DIFFERENTLY, and that is the correct answer
86
+ *
87
+ * The ceiling is INCLUSIVE, measured: a request body of exactly 31,457,280 bytes
88
+ * (31,456,735 file bytes plus 545 of framing) returned `status: success`, and one
89
+ * byte more returned `413`. So:
90
+ *
91
+ * - **Request bytes: `>` refuses.** The uploader knows the exact body and must
92
+ * not refuse one the API would take.
93
+ * - **File bytes: `>=` refuses** — this lane, {@link checkPackagedSizeLimit}.
94
+ * Framing is never zero, so files totalling exactly the limit ALWAYS frame up
95
+ * into a request larger than the limit and earn a real 413.
96
+ *
97
+ * An earlier version of this comment claimed "both fire AT the ceiling rather
98
+ * than only above it" and offered that as the conservative reading. It was
99
+ * reasoned, never measured, and the measurement above refutes it for the request
100
+ * lane. Do not harmonise the two operators: they are applied to different
101
+ * quantities and each is right about the one it can see.
102
+ *
103
+ * ## If the bracket ever moves, these surfaces do NOT follow automatically
104
+ *
105
+ * Nothing derives the prose from this constant, so a re-measurement is a
106
+ * multi-file edit and the list belongs here, next to the number, rather than in
107
+ * whichever file the next maintainer happens to open:
108
+ *
109
+ * 1. This constant.
110
+ * 2. `docs/validation-codes.md`, `PACKAGED_SIZE_EXCEEDS_API_LIMIT` — several
111
+ * bullets restate both the MiB figure and the exact byte count.
112
+ * 3. The `PACKAGED_SIZE_EXCEEDS_API_LIMIT` entry in
113
+ * `packages/schema/src/validation-codes.ts` (its `description`).
114
+ * 4. The `vat-skill-review` skill's checklist item for the same code.
115
+ *
116
+ * The two `vat claude org skills` help texts interpolate this constant and need
117
+ * no edit. `CHANGELOG.md` is history and is never restated.
118
+ *
119
+ * @vendor-claim reviewed=2026-09-06 verify=Re-measure rather than re-read — the docs do not state the unit. Upload two skill bundles via `vat claude org skills install`, one just under and one just over this constant, and confirm the first is accepted and the second returns 413. Adjust only if the bracket moves; if it does, update the four surfaces listed above.
120
+ */
121
+ export declare const API_SKILL_MAX_UPLOAD_BYTES = 31457280;
122
+ /**
123
+ * Directory names that never reach the Skills API, so their bytes never count
124
+ * toward its ceiling.
125
+ *
126
+ * 🔑 **This must stay identical to the uploader's exclusion set**, because the
127
+ * two halves apply the SAME constant and the SAME sentence and must therefore
128
+ * apply them to the same file set. They did not: the walk summed everything
129
+ * under the output directory while `vat claude org skills install` dropped these
130
+ * three directories from the multipart body, so a bundle of 31.0 MB of shipped
131
+ * content plus a 1.0 MB `evals/` fired at build and uploaded fine — a false
132
+ * positive on the only lane that shows the finding to the AUTHOR, which is the
133
+ * lane whose credibility the check depends on.
134
+ *
135
+ * The case is reachable and documented into existence: a root `evals/` holding
136
+ * no `evals.json` is ordinary content, so `vat build` packages it and the
137
+ * uploader drops it unconditionally.
138
+ *
139
+ * The exclusion is by DIRECTORY, so a regular FILE named `evals` is weighed here
140
+ * because it is sent there. The uploader spells its own test
141
+ * `directoryLike && NEVER_UPLOADED_DIR_NAMES.has(entry.name)`, where
142
+ * `directoryLike` is a real directory OR a symbolic link resolving to one — this
143
+ * walk cannot descend a linked directory at all (`Dirent.isDirectory()` is
144
+ * lstat-based), so it weighs one as zero and files a `SCAN_PATH_UNREADABLE`
145
+ * receipt for it. Both lanes therefore leave a linked `evals` out of the total;
146
+ * they differ only in whether they say so.
147
+ *
148
+ * ⚠️ ONE asymmetry remains, and it can only under-count: the uploader also
149
+ * withholds the skill's DECLARED eval-suite unit, which it resolves from the
150
+ * governing VAT config asynchronously. This walk is synchronous and knows only
151
+ * an output directory. In a built bundle that unit is normally absent anyway —
152
+ * the packager drops declared test input on the way in and reports what it
153
+ * dropped — so the residue is a bundle weighed slightly heavy, never light.
154
+ */
155
+ export declare const NEVER_UPLOADED_DIR_NAMES: ReadonlySet<string>;
156
+ export interface SizedFile {
157
+ /** Path relative to the packaged skill root. */
158
+ readonly path: string;
159
+ readonly bytes: number;
160
+ }
161
+ /** An entry whose bytes could not be established, and why. */
162
+ export interface UnweighedEntry {
163
+ /** Path relative to the packaged skill root; `.` for the root itself. */
164
+ readonly path: string;
165
+ /** Phrase completing "…, so its bytes were not counted": the cause, in the open. */
166
+ readonly reason: string;
167
+ /**
168
+ * Remedy for THIS entry, when the registry's generic one does not apply.
169
+ *
170
+ * 🚨 `SCAN_PATH_UNREADABLE`'s registry `fix` says "Make the path readable —
171
+ * check its permissions and ownership — then re-run the audit, or pass
172
+ * `--exclude` to drop it from the scan deliberately". That is right for a
173
+ * readdir/stat FAILURE and wrong for every entry this walk skips by POLICY:
174
+ * a symlink is refused because the uploader refuses it, not because its
175
+ * permissions are wrong, and there is nothing to make readable. Worse, both
176
+ * halves name the wrong command — this producer runs inside `packageSkill`
177
+ * (`vat build`, `vat skills package`), and `--exclude` is an `audit` flag that
178
+ * neither of those accepts, so an author following the advice gets a usage
179
+ * error on a healthy file.
180
+ *
181
+ * Omitted for the genuine unreadable cases, which fall back to the registry
182
+ * entry — that text is accurate for them, and duplicating it here would be a
183
+ * second copy to drift.
184
+ */
185
+ readonly fix?: string;
186
+ }
187
+ /** What one walk of a packaged bundle establishes — and what it could not. */
188
+ export interface BundleWalk {
189
+ readonly files: readonly SizedFile[];
190
+ readonly unweighed: readonly UnweighedEntry[];
191
+ }
192
+ /** Human-readable bytes, in the binary units the API's ceiling is expressed in. */
193
+ export declare function formatBytes(bytes: number): string;
194
+ /**
195
+ * What went on the scale, and WHICH scale it was.
196
+ *
197
+ * The two lanes cannot weigh the same thing. The build-time walk sums file bytes
198
+ * off the filesystem; there is no request at build time to weigh. The uploader
199
+ * weighs the multipart body it is holding, which is what the API measures. Both
200
+ * are correct about what they can see, and the one thing neither may do is let a
201
+ * reader believe they are the same number — an author who trims a bundle to
202
+ * "29.9 MiB of files" can still earn a 413, because the framing is ~156 bytes per
203
+ * file plus the filename (see {@link API_SKILL_MAX_UPLOAD_BYTES}).
204
+ *
205
+ * So the measure travels with the number, and the sentence says which it is.
206
+ */
207
+ export interface OversizeMeasure {
208
+ readonly of: 'packaged-files' | 'upload-request';
209
+ readonly bytes: number;
210
+ }
211
+ /**
212
+ * The one sentence describing an over-ceiling upload, shared by the build-time
213
+ * validator and by `vat claude org skills install`'s pre-flight refusal.
214
+ *
215
+ * Both refuse for the same external reason, so neither owns the wording: the
216
+ * uploader's copy would otherwise drift from the validator's, and an author who
217
+ * met the warning at build would meet a differently-worded version of it at
218
+ * upload. It takes already-measured files rather than a directory because the
219
+ * uploader knows something the walker cannot — which files it EXCLUDED (the
220
+ * declared eval suite) — and must describe the bundle it is actually sending.
221
+ * The exclusions it can state up front are shared as
222
+ * {@link NEVER_UPLOADED_DIR_NAMES}, so both sides weigh the same file set.
223
+ *
224
+ * The two lanes differ in exactly one place, {@link OversizeMeasure}: the request
225
+ * variant reports the body it weighed AND the file bytes inside it, so the
226
+ * framing is visible as the difference rather than as an unexplained discrepancy.
227
+ */
228
+ export declare function describeOversizeBundle(files: readonly SizedFile[], measure: OversizeMeasure, limitBytes?: number): string;
229
+ /**
230
+ * Emit `PACKAGED_SIZE_EXCEEDS_API_LIMIT` when the packaged bundle at `outputDir`
231
+ * is at or over the API upload ceiling, plus one `SCAN_PATH_UNREADABLE` for every
232
+ * entry whose bytes VAT could not establish.
233
+ *
234
+ * ## This weighs FILES, and deliberately not the request
235
+ *
236
+ * The API refuses on the size of the multipart REQUEST, and the uploader weighs
237
+ * exactly that — the body it is about to send. This lane cannot: there is no
238
+ * request at build time, and the framing is not a property of the bundle. Its
239
+ * per-part cost is `156 + len(filename)`, where the filename is not the packaged
240
+ * path but `<declared-name>/<packaged-path>` as the uploader keys it, and the
241
+ * body also carries whatever fields that endpoint takes — a `display_title` on
242
+ * create, none on a version add. Every one of those is decided at upload time.
243
+ *
244
+ * Adding an ESTIMATE of it here would mean re-deriving the multipart builder's
245
+ * arithmetic in a second place, where it would silently stop matching the first —
246
+ * which is the same defect as a fudge factor, only harder to spot. So this check
247
+ * measures what it can see, the message says "Packaged skill is …", and the
248
+ * uploader's says "Upload request body is …". They are different numbers on
249
+ * purpose, and a bundle that clears this one can still be refused there.
250
+ *
251
+ * One size issue for the bundle, not one per file: unlike a per-reference
252
+ * finding, the defect here IS the total — no single file is "the" problem unless
253
+ * it alone exceeds the ceiling, and emitting per file would hand an adopter a
254
+ * waiver granularity that cannot express the actual fix.
255
+ *
256
+ * ## The anchor, and why the largest file is `link`
257
+ *
258
+ * `location` is the bundle root (`.`) because the finding is about the bundle —
259
+ * but `.` is the SAME STRING for every skill in a multi-skill build, so an allow
260
+ * glob written against it waives all of them or none. That left one escape hatch,
261
+ * `severity.PACKAGED_SIZE_EXCEEDS_API_LIMIT: ignore`, which is project-wide: an
262
+ * adopter with 40 skills, one legitimately bundling a 32 MB runtime it never
263
+ * publishes to the API, had to blind themselves on the other 39.
264
+ *
265
+ * So the largest file rides as `link` — the same convention the rest of this lane
266
+ * uses (`referenced-path-missing`, `mcp-tool-qualification`), where `link` is the
267
+ * specific thing an instance is about and `location` stays the file you open.
268
+ * `applyAllowFilter` matches an allow glob against `location` OR `link`, so:
269
+ *
270
+ * ```yaml
271
+ * validation:
272
+ * allow:
273
+ * PACKAGED_SIZE_EXCEEDS_API_LIMIT:
274
+ * - paths: ["scripts/duckdb-eh.wasm"]
275
+ * reason: "Runtime this skill needs; never published to the Skills API."
276
+ * ```
277
+ *
278
+ * …waives exactly the bundles whose weight that file explains, and every other
279
+ * skill keeps the check.
280
+ *
281
+ * ⚠️ Two skills bundling the SAME oversized artifact share one anchor, so one
282
+ * entry waives both. That is the right grain for the case that produced this
283
+ * check (one `.wasm`, three skills, one decision), but it is NOT per-skill
284
+ * precision. Per-skill would need an anchor naming the skill, which this call
285
+ * site cannot mint: `location` must be a project-relative path and the packager
286
+ * passes only an absolute output directory. A caller that wants it should pass
287
+ * the bundle root as `issueLocation(outputPath, projectRoot)` and have this
288
+ * function use it in place of `.`.
289
+ *
290
+ * @param outputDir Absolute path to the packaged skill output.
291
+ * @param limitBytes Ceiling to compare against. Defaults to the documented API
292
+ * limit; parameterized so a test can drive it with a few kilobytes instead of
293
+ * writing 30 MB of fixture to disk.
294
+ */
295
+ export declare function checkPackagedSizeLimit(outputDir: string, limitBytes?: number): ValidationIssue[];
296
+ //# sourceMappingURL=packaged-size-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packaged-size-limit.d.ts","sourceRoot":"","sources":["../../src/validators/packaged-size-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAIH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AACH,eAAO,MAAM,0BAA0B,WAAa,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,wBAAwB,EAAE,WAAW,CAAC,MAAM,CAA8C,CAAC;AAUxG,MAAM,WAAW,SAAS;IACxB,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CAC/C;AAYD,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYjD;AAiLD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,gBAAgB,GAAG,gBAAgB,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,GAAE,MAAmC,GAC9C,MAAM,CAeR;AA0BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,GAAE,MAAmC,GAC9C,eAAe,EAAE,CA4BnB"}