@vibe-agent-toolkit/cli 0.1.41-rc.4 → 0.1.41-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.
- package/dist/bin.js +2 -4
- package/dist/bin.js.map +1 -1
- package/dist/commands/agent/help.d.ts.map +1 -1
- package/dist/commands/agent/help.js +2 -4
- package/dist/commands/agent/help.js.map +1 -1
- package/dist/commands/claude/org/skills.d.ts +13 -0
- package/dist/commands/claude/org/skills.d.ts.map +1 -1
- package/dist/commands/claude/org/skills.js +57 -30
- package/dist/commands/claude/org/skills.js.map +1 -1
- package/dist/commands/claude/plugin/build.d.ts +1 -1
- package/dist/commands/claude/plugin/build.d.ts.map +1 -1
- package/dist/commands/claude/plugin/build.js +239 -82
- package/dist/commands/claude/plugin/build.js.map +1 -1
- package/dist/commands/claude/plugin/install.d.ts.map +1 -1
- package/dist/commands/claude/plugin/install.js +7 -2
- package/dist/commands/claude/plugin/install.js.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.d.ts +10 -3
- package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.js.map +1 -1
- package/dist/commands/claude/plugin/uninstall.d.ts.map +1 -1
- package/dist/commands/claude/plugin/uninstall.js +10 -2
- package/dist/commands/claude/plugin/uninstall.js.map +1 -1
- package/dist/commands/rag/help.d.ts.map +1 -1
- package/dist/commands/rag/help.js +2 -4
- package/dist/commands/rag/help.js.map +1 -1
- package/dist/commands/resources/help.d.ts.map +1 -1
- package/dist/commands/resources/help.js +2 -4
- package/dist/commands/resources/help.js.map +1 -1
- package/dist/commands/resources/validate.d.ts +19 -0
- package/dist/commands/resources/validate.d.ts.map +1 -1
- package/dist/commands/resources/validate.js +51 -0
- package/dist/commands/resources/validate.js.map +1 -1
- package/dist/commands/skill/test/configure.d.ts.map +1 -1
- package/dist/commands/skill/test/configure.js +1 -1
- package/dist/commands/skill/test/configure.js.map +1 -1
- package/dist/commands/skill/test/run.d.ts +111 -24
- package/dist/commands/skill/test/run.d.ts.map +1 -1
- package/dist/commands/skill/test/run.js +328 -63
- package/dist/commands/skill/test/run.js.map +1 -1
- package/dist/commands/skills/build.d.ts.map +1 -1
- package/dist/commands/skills/build.js +3 -32
- package/dist/commands/skills/build.js.map +1 -1
- package/dist/commands/skills/install.d.ts.map +1 -1
- package/dist/commands/skills/install.js +79 -25
- package/dist/commands/skills/install.js.map +1 -1
- package/dist/commands/skills/list.d.ts.map +1 -1
- package/dist/commands/skills/list.js +9 -5
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/skill-discovery.d.ts +7 -0
- package/dist/commands/skills/skill-discovery.d.ts.map +1 -1
- package/dist/commands/skills/skill-discovery.js +5 -3
- package/dist/commands/skills/skill-discovery.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +7 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/skill-resolution/classify.d.ts +7 -2
- package/dist/skill-resolution/classify.d.ts.map +1 -1
- package/dist/skill-resolution/classify.js +7 -2
- package/dist/skill-resolution/classify.js.map +1 -1
- package/dist/skill-resolution/resolve-skill-reference.d.ts +10 -8
- package/dist/skill-resolution/resolve-skill-reference.d.ts.map +1 -1
- package/dist/skill-resolution/resolve-skill-reference.js +62 -23
- package/dist/skill-resolution/resolve-skill-reference.js.map +1 -1
- package/dist/skill-resolution/types.d.ts +3 -2
- package/dist/skill-resolution/types.d.ts.map +1 -1
- package/dist/utils/help-loader.d.ts +16 -0
- package/dist/utils/help-loader.d.ts.map +1 -1
- package/dist/utils/help-loader.js +31 -1
- package/dist/utils/help-loader.js.map +1 -1
- package/dist/utils/skill-packaging-config.d.ts +15 -6
- package/dist/utils/skill-packaging-config.d.ts.map +1 -1
- package/dist/utils/skill-packaging-config.js +23 -5
- package/dist/utils/skill-packaging-config.js.map +1 -1
- package/docs/index.md +39 -0
- package/docs/skill-test.md +213 -0
- package/docs/skills.md +29 -4
- package/package.json +13 -13
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { basename, dirname, extname } from 'node:path';
|
|
10
|
-
import { BootstrapNeededError, DuplicateStagedSkillError, isAcknowledged, mapErrorToExitCode, packageSkill, packagingConfigToPackageOptions, runPreStageBuild, runSkillTestHarness, SecurityAckError, SKILL_TEST_BUILTIN_CAPS, SkillBuildError, SkillTestExitCode, } from '@vibe-agent-toolkit/agent-skills';
|
|
11
|
-
import { findProjectRoot, safePath, toForwardSlash } from '@vibe-agent-toolkit/utils';
|
|
10
|
+
import { BootstrapNeededError, buildStaleDistWarningLines, DuplicateStagedSkillError, isAcknowledged, mapErrorToExitCode, packageSkill, packagingConfigToPackageOptions, runPreStageBuild, runSkillTestHarness, SecurityAckError, SKILL_TEST_BUILTIN_CAPS, SkillBuildError, SkillTestExitCode, } from '@vibe-agent-toolkit/agent-skills';
|
|
11
|
+
import { findProjectRoot, resolveAssetReference, safePath, toForwardSlash } from '@vibe-agent-toolkit/utils';
|
|
12
12
|
import { Command } from 'commander';
|
|
13
13
|
import { parseSourceSpec } from '../../../skill-resolution/classify.js';
|
|
14
14
|
import { findDeclaredSkillForPath, findDeclaredSkillForSourceDir, resolveSkillReference, } from '../../../skill-resolution/index.js';
|
|
15
|
-
import { ConfigLoadError, loadConfig } from '../../../utils/config-loader.js';
|
|
15
|
+
import { ConfigLoadError, loadConfig, loadConfigCached } from '../../../utils/config-loader.js';
|
|
16
16
|
import { runClaudePluginBuild } from '../../claude/plugin/build.js';
|
|
17
17
|
import { assertValidAuth, assertValidRequireAuth } from './auth-flags.js';
|
|
18
18
|
/** Extract the trailing path segment (cross-platform) from a path-like string. */
|
|
@@ -164,34 +164,62 @@ export function descriptorsToRecord(list) {
|
|
|
164
164
|
// ---------------------------------------------------------------------------
|
|
165
165
|
// Precedence resolution: flag > config > default
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
|
+
/**
|
|
168
|
+
* Read a DECLARED skill's own `skills.config.<name>.test` block from the config that
|
|
169
|
+
* governs IT — keyed by the declared name against that skill's `configRoot`, never by
|
|
170
|
+
* cwd. The one lookup used wherever "what did THIS skill declare?" is the question:
|
|
171
|
+
* {@link loadTestConfig}'s path-target arm and companion `test.build` resolution
|
|
172
|
+
* ({@link resolveCompanionSpec}), so a companion in a nested config never inherits the
|
|
173
|
+
* subject's block.
|
|
174
|
+
*/
|
|
175
|
+
function declaredSkillTestConfig(link) {
|
|
176
|
+
return loadConfigCached(link.configRoot)?.skills?.config?.[link.name]?.test;
|
|
177
|
+
}
|
|
167
178
|
/**
|
|
168
179
|
* Load the persisted `skills.config.<skill>.test` block for the subject skill,
|
|
169
|
-
* PROJECT-AWARE (mirrors the resolver — not a parallel cwd+basename
|
|
180
|
+
* PROJECT-AWARE (mirrors the resolver's ladder ORDER — not a parallel cwd+basename
|
|
181
|
+
* resolver).
|
|
182
|
+
*
|
|
183
|
+
* A PATH target is mapped back to its declared skill through the SAME two lookups
|
|
184
|
+
* {@link resolveDefinitePath} (in resolve-skill-reference.ts) uses, tried in the SAME
|
|
185
|
+
* order: first {@link findDeclaredSkillForSourceDir} (a path AT a declared skill's
|
|
186
|
+
* SOURCE dir — rung 2a, `buildable`), then {@link findDeclaredSkillForPath} (a path AT
|
|
187
|
+
* its already-built DIST dir — the `source` + `declaredSkill` back-link rung). Either
|
|
188
|
+
* hit is keyed by the DECLARED name against ITS OWN governing config via
|
|
189
|
+
* {@link declaredSkillTestConfig} — cwd-independent, so a path target honors the same
|
|
190
|
+
* model/evals/timeout/`test.build` as its name.
|
|
170
191
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
192
|
+
* The basename fallback below is a LAST RESORT, reached only when `subject` carries
|
|
193
|
+
* NEITHER link — a bare name (matched by literal string), or a path outside every
|
|
194
|
+
* declared skill's source/dist dir. It must never be the path a LINKED subject's
|
|
195
|
+
* config resolves through: a directory basename that happens to differ from its
|
|
196
|
+
* declared skill's name would otherwise silently miss that skill's `test:` block
|
|
197
|
+
* (including a required `test.build` hook) with no error — the exact failure mode
|
|
198
|
+
* this ordering exists to prevent. Missing config / missing file → undefined
|
|
199
|
+
* (defaults apply).
|
|
177
200
|
*
|
|
178
201
|
* A broken config throws {@link ConfigLoadError}; we let it propagate to the preflight
|
|
179
202
|
* guard in runSkillTestRun, which surfaces it as a clean exit-2 error rather than
|
|
180
203
|
* silently applying defaults against a config the author clearly intended.
|
|
181
204
|
*/
|
|
182
205
|
async function loadTestConfig(subject, cwd) {
|
|
183
|
-
//
|
|
184
|
-
//
|
|
206
|
+
// Rung 2a: a path AT a declared skill's SOURCE dir — tried FIRST, matching
|
|
207
|
+
// resolveDefinitePath's own precedence.
|
|
208
|
+
const sourceLink = await findDeclaredSkillForSourceDir(subject, cwd);
|
|
209
|
+
if (sourceLink !== undefined)
|
|
210
|
+
return declaredSkillTestConfig(sourceLink);
|
|
211
|
+
// Reverse rung: a path AT a declared skill's already-built DIST dir.
|
|
185
212
|
const link = await findDeclaredSkillForPath(subject, cwd);
|
|
186
|
-
if (link !== undefined)
|
|
187
|
-
return
|
|
188
|
-
}
|
|
213
|
+
if (link !== undefined)
|
|
214
|
+
return declaredSkillTestConfig(link);
|
|
189
215
|
const projectRoot = findProjectRoot(cwd);
|
|
190
216
|
if (projectRoot === null)
|
|
191
217
|
return undefined;
|
|
192
218
|
const perSkill = loadConfig(projectRoot)?.skills?.config;
|
|
193
219
|
if (perSkill === undefined)
|
|
194
220
|
return undefined;
|
|
221
|
+
// Last-resort basename fallback (see doc comment above): only reached when neither
|
|
222
|
+
// lookup above found a declared-skill link for `subject`.
|
|
195
223
|
const base = lastPathSegment(subject) || subject;
|
|
196
224
|
return perSkill[subject]?.test ?? perSkill[base]?.test;
|
|
197
225
|
}
|
|
@@ -250,8 +278,14 @@ function applyScalarMerges(opts, options, config) {
|
|
|
250
278
|
const model = options.model ?? config?.model;
|
|
251
279
|
if (model !== undefined)
|
|
252
280
|
opts.model = model;
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
// `--evals` is resolved HERE, against the process cwd, so the operator's own
|
|
282
|
+
// shell path means what they typed; `test.evals` is left alone and resolves
|
|
283
|
+
// against the skill source, because config travels with the skill. Both then
|
|
284
|
+
// reach the harness as one field, and both accept a path or an npm bare
|
|
285
|
+
// specifier (a suite published as a shared corpus).
|
|
286
|
+
const evalsRef = options.evals === undefined ? config?.evals : resolveAssetReference(options.evals, process.cwd());
|
|
287
|
+
if (evalsRef !== undefined)
|
|
288
|
+
opts.evalsSubpath = evalsRef;
|
|
255
289
|
}
|
|
256
290
|
/**
|
|
257
291
|
* Resolve one cost/runtime knob with flag > config > default precedence AND a
|
|
@@ -399,8 +433,16 @@ function sourceScaffoldFields(source, declaredSkill, cwd) {
|
|
|
399
433
|
* reference; for a declared skill, builds it (real entry points) and returns the
|
|
400
434
|
* dist dir to stage; everything else is staged as-is. Throws SkillBuildError
|
|
401
435
|
* (exit 2) for name-miss / not-found / --no-build-without-dist / build failure.
|
|
436
|
+
*
|
|
437
|
+
* `memo` is the per-run build memo (see {@link BuildMemo}) — pass the SAME map used
|
|
438
|
+
* for companion resolution so a skill that is both subject and companion builds once.
|
|
439
|
+
*
|
|
440
|
+
* `buildCommand` is the SUBJECT's own `test.build` hook, resolved by
|
|
441
|
+
* {@link loadTestConfig} (which carries a bare-name/basename fallback a raw config
|
|
442
|
+
* lookup lacks). It applies to the subject's build ONLY — companions resolve theirs
|
|
443
|
+
* from their own governing config.
|
|
402
444
|
*/
|
|
403
|
-
export async function resolveSubjectForTest(ref, cwd, flags) {
|
|
445
|
+
export async function resolveSubjectForTest(ref, cwd, flags, memo, buildCommand) {
|
|
404
446
|
const resolved = await resolveSkillReference(ref, cwd);
|
|
405
447
|
switch (resolved.kind) {
|
|
406
448
|
case 'source':
|
|
@@ -418,7 +460,7 @@ export async function resolveSubjectForTest(ref, cwd, flags) {
|
|
|
418
460
|
// A buildable ref carries `packagingConfig` — the ONE cleanly-reachable place
|
|
419
461
|
// the subject's declared executables live — so attach the grader recognition
|
|
420
462
|
// aid here (a plain source has no packaging config and leaves it undefined).
|
|
421
|
-
const subject = await resolveBuildableSubject(resolved, flags);
|
|
463
|
+
const subject = await resolveBuildableSubject(resolved, flags, memo, buildCommand);
|
|
422
464
|
const declaredExecutables = deriveDeclaredExecutableNames(resolved.packagingConfig.executables);
|
|
423
465
|
return declaredExecutables === undefined ? subject : { ...subject, declaredExecutables };
|
|
424
466
|
}
|
|
@@ -449,6 +491,98 @@ function resolveExistingDistOrThrow(ref, flags) {
|
|
|
449
491
|
}
|
|
450
492
|
throw new SkillBuildError(`--no-build: no built dist at ${ref.expectedDistDir} for '${ref.name}'. Run \`vat build\` first, or point at a built path.`);
|
|
451
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Identity of the BUILD OPERATION a {@link BuildableReference} triggers — the memo key.
|
|
496
|
+
*
|
|
497
|
+
* A `pool` skill is built by `packageSkill` into its own `expectedDistDir`, so that
|
|
498
|
+
* dir IS the operation's identity. A `plugin-local` skill is built by
|
|
499
|
+
* `runClaudePluginBuild`, which `rm -rf`s and rebuilds EVERY plugin in the whole
|
|
500
|
+
* marketplace — so the operation's identity is the (configRoot, marketplace) pair,
|
|
501
|
+
* NOT the skill. Keying plugin-local per skill would make a second companion in the
|
|
502
|
+
* same marketplace trigger a full marketplace wipe-and-rebuild that reproduces work
|
|
503
|
+
* the first call already did.
|
|
504
|
+
*/
|
|
505
|
+
export function buildMemoKey(ref) {
|
|
506
|
+
return ref.distribution.kind === 'pool'
|
|
507
|
+
? `pool:${ref.expectedDistDir}`
|
|
508
|
+
: `plugin:${ref.configRoot}:${ref.distribution.marketplaceName}`;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Assert a completed build actually MATERIALIZED `ref.expectedDistDir` with a real
|
|
512
|
+
* skill in it, and return the result to stage. A build that reports success but
|
|
513
|
+
* writes nothing (or writes an empty shell with no `SKILL.md`) would otherwise hand
|
|
514
|
+
* back a path a required companion then fails opaquely at staging, or an OPTIONAL
|
|
515
|
+
* one silently skips — the exact non-functional-companion-with-no-diagnostic
|
|
516
|
+
* symptom of issue #158. Two distinct checks, two distinct messages, so the error
|
|
517
|
+
* tells the author which failure mode they hit:
|
|
518
|
+
* 1. `expectedDistDir` itself is missing — "no output at all".
|
|
519
|
+
* 2. `expectedDistDir` exists but has no `SKILL.md` — an empty/incomplete shell.
|
|
520
|
+
*
|
|
521
|
+
* Also runs on a memo HIT, because a plugin-local hit may have been recorded by a
|
|
522
|
+
* DIFFERENT skill in the same marketplace: the dist dir always comes from THIS ref
|
|
523
|
+
* and is always checked.
|
|
524
|
+
*/
|
|
525
|
+
function verifyBuiltDist(ref) {
|
|
526
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- expectedDistDir derived from project config
|
|
527
|
+
if (!existsSync(ref.expectedDistDir)) {
|
|
528
|
+
throw new SkillBuildError(`Skill build for '${ref.name}' reported success but produced no output at ${ref.expectedDistDir}. ` +
|
|
529
|
+
`Check the skill's packaging config (\`vat build\` should create this directory).`);
|
|
530
|
+
}
|
|
531
|
+
const skillMdPath = safePath.join(ref.expectedDistDir, 'SKILL.md');
|
|
532
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- skillMdPath derived from project config
|
|
533
|
+
if (!existsSync(skillMdPath)) {
|
|
534
|
+
throw new SkillBuildError(`Skill build for '${ref.name}' reported success but produced no SKILL.md at ${ref.expectedDistDir}. ` +
|
|
535
|
+
`Check the skill's packaging config (\`vat build\` should create this file).`);
|
|
536
|
+
}
|
|
537
|
+
return { distDir: ref.expectedDistDir, rebuilt: true };
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Identity of a `test.build` PRE-STAGE HOOK invocation — the (cwd, command) pair, which
|
|
541
|
+
* is everything that distinguishes one shell invocation from another. Namespaced `hook:`
|
|
542
|
+
* so it can never collide with a {@link buildMemoKey} entry in the shared {@link BuildMemo}.
|
|
543
|
+
*/
|
|
544
|
+
function preStageHookMemoKey(configRoot, buildCommand) {
|
|
545
|
+
return `hook:${configRoot}:${buildCommand}`;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Run a skill's `test.build` pre-stage hook, at most ONCE per (configRoot, command)
|
|
549
|
+
* within a run — the "runs that shell command ONCE before staging" contract documented
|
|
550
|
+
* on {@link runPreStageBuild}, preserved now that a run may build several skills.
|
|
551
|
+
*
|
|
552
|
+
* The command ALWAYS runs with the config root of the skill that DECLARED it as cwd
|
|
553
|
+
* (both values come from the same ref), so a subject's hook can never execute against
|
|
554
|
+
* a companion's package root. Memoized only on SUCCESS: a hook that failed and was
|
|
555
|
+
* tolerated (optional companion) is retried rather than treated as done.
|
|
556
|
+
*
|
|
557
|
+
* KNOWN LIMITATION: for `plugin-local` skills, the memo key this call is gated behind
|
|
558
|
+
* ({@link buildMemoKey} in {@link buildDeclaredSkill}) is the shared `plugin:<configRoot>:
|
|
559
|
+
* <marketplace>` key, not a per-skill one — because one marketplace build serves every
|
|
560
|
+
* participant. When two declared skills share a marketplace, only the FIRST participant's
|
|
561
|
+
* `test.build` hook runs; the second skill's own hook is silently skipped, since by the
|
|
562
|
+
* time its build call is reached the memo already reports the marketplace as built and
|
|
563
|
+
* this function is never invoked for it. Fixing that requires gathering the hook set for
|
|
564
|
+
* every marketplace participant BEFORE the one marketplace build — out of scope here.
|
|
565
|
+
*/
|
|
566
|
+
function runPreStageBuildOnce(buildCommand, configRoot, memo) {
|
|
567
|
+
if (buildCommand === undefined)
|
|
568
|
+
return;
|
|
569
|
+
const key = preStageHookMemoKey(configRoot, buildCommand);
|
|
570
|
+
if (memo.has(key))
|
|
571
|
+
return;
|
|
572
|
+
runPreStageBuild({ buildCommand, configRoot });
|
|
573
|
+
memo.add(key);
|
|
574
|
+
}
|
|
575
|
+
/** Dispatch the real build for a declared skill, by how it ships. Throws the raw build error. */
|
|
576
|
+
async function runDeclaredSkillBuild(ref) {
|
|
577
|
+
if (ref.distribution.kind === 'pool') {
|
|
578
|
+
await packageSkill(ref.sourcePath, packagingConfigToPackageOptions(ref.packagingConfig, {
|
|
579
|
+
skillPath: ref.sourcePath,
|
|
580
|
+
outputPath: ref.expectedDistDir,
|
|
581
|
+
}));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
await runClaudePluginBuild(ref.configRoot, { marketplace: ref.distribution.marketplaceName });
|
|
585
|
+
}
|
|
452
586
|
/**
|
|
453
587
|
* Build a declared skill (BuildableReference) into its `expectedDistDir`, or reuse
|
|
454
588
|
* the existing dist under --no-build/--dry-run. Shared by SUBJECT resolution
|
|
@@ -457,10 +591,33 @@ function resolveExistingDistOrThrow(ref, flags) {
|
|
|
457
591
|
* skill gets the exact same build treatment as the subject (issue #158) — its
|
|
458
592
|
* `files:` injection runs before staging, instead of a raw, possibly
|
|
459
593
|
* build-artifact-incomplete, source-tree copy.
|
|
594
|
+
*
|
|
595
|
+
* `buildCommand` is the `test.build` pre-stage hook DECLARED BY THIS REF's skill —
|
|
596
|
+
* never a shared per-run flag. It runs with `ref.configRoot` as cwd, so command and
|
|
597
|
+
* cwd always come from the same skill's config (see {@link runPreStageBuildOnce}).
|
|
598
|
+
* GUARANTEED to run (once, on the first call for this ref) for a `pool`-distributed
|
|
599
|
+
* skill, whose memo key is per-skill. KNOWN LIMITATION for `plugin-local` skills: the
|
|
600
|
+
* memo key is the shared marketplace build, so when two declared skills share a
|
|
601
|
+
* marketplace, only the FIRST participant reaching this function gets its hook run —
|
|
602
|
+
* the second skill's build is served by the memo hit at the check below, and its own
|
|
603
|
+
* `test.build` hook is silently skipped (see {@link runPreStageBuildOnce}).
|
|
604
|
+
*
|
|
605
|
+
* `memo` collapses repeat work within ONE run (see {@link buildMemoKey}). It is
|
|
606
|
+
* consulted only AFTER the no-build/dry-run short-circuit and AFTER the security-ack
|
|
607
|
+
* gate: a cached build result must never be handed to a caller that has not passed
|
|
608
|
+
* the ack, and the no-build branch performs no build to memoize.
|
|
460
609
|
*/
|
|
461
|
-
async function buildDeclaredSkill(ref, flags) {
|
|
462
|
-
// --
|
|
463
|
-
|
|
610
|
+
async function buildDeclaredSkill(ref, flags, memo, buildCommand) {
|
|
611
|
+
// --dry-run DOES build once acknowledged: the "dry" part is the skill TESTING (no
|
|
612
|
+
// Claude session, no tokens), and a preview assembled from a stale dist previews
|
|
613
|
+
// something a real run would not test — which is the one job a preview has.
|
|
614
|
+
//
|
|
615
|
+
// Without the acknowledgement it still does NOT build. Building runs the repo's
|
|
616
|
+
// committed `test.build` hook, an arbitrary shell command, so an unacknowledged
|
|
617
|
+
// dry run stays the one mode that is safe to point at an untrusted clone. That
|
|
618
|
+
// preview falls back to an existing dist and says so (stale warning) — the honest
|
|
619
|
+
// trade, since accuracy requires consent to execute the repo's own build.
|
|
620
|
+
if (flags.noBuild || (flags.dryRun && !flags.explicitAck)) {
|
|
464
621
|
return resolveExistingDistOrThrow(ref, flags);
|
|
465
622
|
}
|
|
466
623
|
// SECURITY (§12): reaching here means a real build WOULD run — runPreStageBuild
|
|
@@ -471,30 +628,36 @@ async function buildDeclaredSkill(ref, flags) {
|
|
|
471
628
|
if (!flags.acknowledged) {
|
|
472
629
|
throw new SecurityAckError();
|
|
473
630
|
}
|
|
631
|
+
const key = buildMemoKey(ref);
|
|
632
|
+
// Memo HIT: this operation already ran in THIS process — skip the rebuild, but
|
|
633
|
+
// still verify this ref's own dist (the hit may be a marketplace sibling's).
|
|
634
|
+
if (memo.has(key))
|
|
635
|
+
return verifyBuiltDist(ref);
|
|
474
636
|
// test.build hook (upstream artifacts) BEFORE the skill build -- ordering matters.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
637
|
+
runPreStageBuildOnce(buildCommand, ref.configRoot, memo);
|
|
638
|
+
// ANY error thrown here is wrapped as `SkillBuildError` below, not just a narrowly-
|
|
639
|
+
// scoped build-tool failure — see {@link isSurvivableCompanionFailure} for why that
|
|
640
|
+
// matters to companion degradation. `err.stack` is also captured at construction,
|
|
641
|
+
// so it keeps reflecting THIS un-prefixed message even after a caller mutates
|
|
642
|
+
// `.message` in place (see {@link rethrowNamingCompanion}).
|
|
478
643
|
try {
|
|
479
|
-
|
|
480
|
-
await packageSkill(ref.sourcePath, packagingConfigToPackageOptions(ref.packagingConfig, {
|
|
481
|
-
skillPath: ref.sourcePath,
|
|
482
|
-
outputPath: ref.expectedDistDir,
|
|
483
|
-
}));
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
await runClaudePluginBuild(ref.configRoot, { marketplace: ref.distribution.marketplaceName });
|
|
487
|
-
}
|
|
644
|
+
await runDeclaredSkillBuild(ref);
|
|
488
645
|
}
|
|
489
646
|
catch (e) {
|
|
490
647
|
throw new SkillBuildError(`Skill build failed for '${ref.name}': ${e instanceof Error ? e.message : String(e)}`);
|
|
491
648
|
}
|
|
492
|
-
|
|
649
|
+
// Record the operation as soon as the build COMMAND succeeded — before verifying
|
|
650
|
+
// this ref's output. The build did run; a marketplace sibling must not re-run it
|
|
651
|
+
// just because this skill's own dist turned out to be missing. Verification is
|
|
652
|
+
// deliberately OUTSIDE the try/catch so its SkillBuildError is not re-wrapped into
|
|
653
|
+
// a doubled "Skill build failed for 'x': Skill build for 'x' reported success…".
|
|
654
|
+
memo.add(key);
|
|
655
|
+
return verifyBuiltDist(ref);
|
|
493
656
|
}
|
|
494
657
|
/** Build a declared skill (or stage its existing dist under --no-build/--dry-run), then return the dist to stage. */
|
|
495
|
-
async function resolveBuildableSubject(ref, flags) {
|
|
658
|
+
async function resolveBuildableSubject(ref, flags, memo, buildCommand) {
|
|
496
659
|
const scaffoldDir = dirname(ref.sourcePath);
|
|
497
|
-
const build = await buildDeclaredSkill(ref, flags);
|
|
660
|
+
const build = await buildDeclaredSkill(ref, flags, memo, buildCommand);
|
|
498
661
|
return {
|
|
499
662
|
subjectSource: { path: build.distDir },
|
|
500
663
|
subjectScaffoldDir: scaffoldDir,
|
|
@@ -505,6 +668,69 @@ async function resolveBuildableSubject(ref, flags) {
|
|
|
505
668
|
: { dryRunStagedExistingDist: build.dryRunStagedExistingDist }),
|
|
506
669
|
};
|
|
507
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* May an OPTIONAL companion's failure be DEGRADED to "stage its raw source instead"
|
|
673
|
+
* rather than failing the run? True for a genuine build failure ({@link SkillBuildError})
|
|
674
|
+
* that could not have been DESTRUCTIVE. Both narrowings matter:
|
|
675
|
+
*
|
|
676
|
+
* - **Error class.** Everything else reaching the catch is a user-fixable PREFLIGHT
|
|
677
|
+
* problem, not a degraded companion: a {@link SecurityAckError} (the fix is
|
|
678
|
+
* `--i-understand-this-runs-skill-code`, not "your companion didn't build"), a
|
|
679
|
+
* `ConfigLoadError` (a broken config reported as a build failure), or a
|
|
680
|
+
* `BuildHookError` (a genuinely misconfigured `test.build`). Swallowing those
|
|
681
|
+
* replaces the actionable message with a misleading one. Note that
|
|
682
|
+
* {@link buildDeclaredSkill} wraps ANY error `runDeclaredSkillBuild` throws —
|
|
683
|
+
* including a malformed packaging config or a stray `TypeError` deep inside
|
|
684
|
+
* `packageSkill` — into `SkillBuildError`, so "genuine build failure" here really
|
|
685
|
+
* means "anything the build call raised", not just a narrowly-scoped build error.
|
|
686
|
+
* - **Destructiveness.** A `plugin-local` build is DESTRUCTIVE once it reaches
|
|
687
|
+
* `runClaudePluginBuild`: that function `rm -rf`s the whole marketplace output
|
|
688
|
+
* tree BEFORE building, so a failure THERE may leave the user's
|
|
689
|
+
* `dist/.claude/plugins/marketplaces/<mp>/` already gone. Continuing would stage
|
|
690
|
+
* from an inconsistent dist and finish SUCCESS with the build output deleted — a
|
|
691
|
+
* test command must never destroy build output and report only a "Note:". A
|
|
692
|
+
* `pool` build writes only its own `expectedDistDir` and destroys nothing, so
|
|
693
|
+
* falling back to the raw source tree is always safe there. And under
|
|
694
|
+
* `--no-build`/`--dry-run`, `resolveExistingDistOrThrow` throws BEFORE
|
|
695
|
+
* `buildDeclaredSkill` reaches any build path at all — no build was attempted,
|
|
696
|
+
* plugin-local or not, so nothing was wiped either way and degrading is safe.
|
|
697
|
+
*
|
|
698
|
+
* A real (non `--no-build`/`--dry-run`) `plugin-local` failure stays fail-closed
|
|
699
|
+
* because it MAY have failed mid-wipe; `runClaudePluginBuild` can also throw BEFORE
|
|
700
|
+
* the `rm` (broken config, colliding plugin names), but those are indistinguishable
|
|
701
|
+
* from outside once wrapped in `SkillBuildError`, so fail-closed is deliberate there.
|
|
702
|
+
*/
|
|
703
|
+
function isSurvivableCompanionFailure(err, declared, optional, flags) {
|
|
704
|
+
if (!optional || !(err instanceof SkillBuildError))
|
|
705
|
+
return false;
|
|
706
|
+
return declared.distribution.kind === 'pool' || flags.noBuild;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Rethrow a companion failure with the `--with`/`--with-optional` ALIAS the user
|
|
710
|
+
* TYPED prefixed onto its message. Build errors name only the DECLARED skill, which
|
|
711
|
+
* with several companions leaves the user unable to tell which flag to fix.
|
|
712
|
+
*
|
|
713
|
+
* The message is prefixed IN PLACE so the error's CLASS (and therefore its
|
|
714
|
+
* `exitCode` / {@link mapErrorToExitCode} mapping) is unchanged — no new error class,
|
|
715
|
+
* no re-wrapping. Prefixing is skipped when the message is ALREADY prefixed (starts
|
|
716
|
+
* with `companion '`) — this makes the function idempotent against repeat rethrows of
|
|
717
|
+
* the SAME error object, structurally, rather than relying on a hand-maintained
|
|
718
|
+
* argument that no caught error class is ever cached/reused across companions.
|
|
719
|
+
* {@link ConfigLoadError} instances ARE cached and re-thrown per config root by
|
|
720
|
+
* `loadConfigCached`, so mutating one in place would otherwise corrupt the cache; the
|
|
721
|
+
* already-prefixed check covers this case too (in practice `ConfigLoadError` is
|
|
722
|
+
* thrown by the lookup ABOVE this try and never reaches here — belt-and-braces).
|
|
723
|
+
*
|
|
724
|
+
* NOTE: `err.stack` was captured at construction time and therefore still reflects
|
|
725
|
+
* the UN-PREFIXED message — mutating `.message` afterward does not update `.stack`.
|
|
726
|
+
* Debug output that prints `.stack` can disagree with the prefixed text on stderr.
|
|
727
|
+
*/
|
|
728
|
+
function rethrowNamingCompanion(err, alias, declaredName) {
|
|
729
|
+
if (err instanceof Error && !err.message.startsWith("companion '")) {
|
|
730
|
+
err.message = `companion '${alias}' (declared skill '${declaredName}'): ${err.message}`;
|
|
731
|
+
}
|
|
732
|
+
throw err;
|
|
733
|
+
}
|
|
508
734
|
/**
|
|
509
735
|
* Companion analog of {@link resolveBuildableSubject} (issue #158): resolve every
|
|
510
736
|
* `--with`/`--with-optional` companion whose spec is a `{ path }` pointing at a
|
|
@@ -513,37 +739,53 @@ async function resolveBuildableSubject(ref, flags) {
|
|
|
513
739
|
* url:/vendored specs, or a path outside this project's config — is "a different
|
|
514
740
|
* story" (per the issue's own framing): left untouched, staged as today.
|
|
515
741
|
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
742
|
+
* The `test.build` hook used is the COMPANION's own — read from the config that
|
|
743
|
+
* governs IT ({@link declaredSkillTestConfig}), not the subject's. The subject's hook
|
|
744
|
+
* is a different skill's command in a (possibly) different package, and running it
|
|
745
|
+
* here would execute it with the companion's config root as cwd.
|
|
746
|
+
*
|
|
747
|
+
* FAILURE HANDLING. A REQUIRED companion's failure ALWAYS propagates (the existing
|
|
748
|
+
* "--with fails if a source cannot be resolved" contract, extended to "cannot be
|
|
749
|
+
* built"), re-messaged to name the alias the user typed. An OPTIONAL companion
|
|
750
|
+
* degrades to the ORIGINAL (unbuilt) path spec — with a mandatory stderr note — only
|
|
751
|
+
* for the narrow, non-destructive case {@link isSurvivableCompanionFailure} allows;
|
|
752
|
+
* every other failure propagates for it too. Staging's existing skip-with-warning
|
|
753
|
+
* fallback still applies if the raw copy also fails to stage.
|
|
754
|
+
*
|
|
755
|
+
* DRY-RUN STALENESS: a `--dry-run` preview that staged an EXISTING
|
|
756
|
+
* companion dist WITHOUT rebuilding it warns on stderr, naming the companion +
|
|
757
|
+
* its declared skill, using {@link buildStaleDistWarningLines} — the SAME
|
|
758
|
+
* warning-construction the subject's own dry-run summary uses (never a copied
|
|
759
|
+
* string), so the identical fact ("this preview may be stale") warns for either
|
|
760
|
+
* role instead of only the subject.
|
|
522
761
|
*/
|
|
523
|
-
export async function resolveCompanionSpec(name, spec, repoRoot, flags, optional) {
|
|
762
|
+
export async function resolveCompanionSpec(name, spec, repoRoot, flags, optional, memo) {
|
|
524
763
|
if (!('path' in spec))
|
|
525
764
|
return spec;
|
|
526
765
|
const declared = await findDeclaredSkillForSourceDir(spec.path, repoRoot);
|
|
527
766
|
if (declared === undefined)
|
|
528
767
|
return spec;
|
|
529
768
|
try {
|
|
530
|
-
const build = await buildDeclaredSkill(declared, flags);
|
|
769
|
+
const build = await buildDeclaredSkill(declared, flags, memo, declaredSkillTestConfig(declared)?.build);
|
|
770
|
+
if (build.dryRunStagedExistingDist === true) {
|
|
771
|
+
process.stderr.write(buildStaleDistWarningLines(`companion '${name}' (declared skill '${declared.name}')`).join('\n') + '\n');
|
|
772
|
+
}
|
|
531
773
|
return { path: build.distDir };
|
|
532
774
|
}
|
|
533
775
|
catch (e) {
|
|
534
|
-
if (!optional)
|
|
535
|
-
|
|
776
|
+
if (!isSurvivableCompanionFailure(e, declared, optional, flags))
|
|
777
|
+
rethrowNamingCompanion(e, name, declared.name);
|
|
536
778
|
process.stderr.write(`Note: companion '${name}' (declared skill '${declared.name}') failed to build; staging its raw source instead: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
537
779
|
return spec;
|
|
538
780
|
}
|
|
539
781
|
}
|
|
540
782
|
/** Resolve every entry of a `--with`/`--with-optional` companion record (see {@link resolveCompanionSpec}). */
|
|
541
|
-
export async function resolveCompanionSources(sources, repoRoot, flags, optional) {
|
|
783
|
+
export async function resolveCompanionSources(sources, repoRoot, flags, optional, memo) {
|
|
542
784
|
if (sources === undefined)
|
|
543
785
|
return undefined;
|
|
544
786
|
const resolved = {};
|
|
545
787
|
for (const [name, spec] of Object.entries(sources)) {
|
|
546
|
-
resolved[name] = await resolveCompanionSpec(name, spec, repoRoot, flags, optional);
|
|
788
|
+
resolved[name] = await resolveCompanionSpec(name, spec, repoRoot, flags, optional, memo);
|
|
547
789
|
}
|
|
548
790
|
return resolved;
|
|
549
791
|
}
|
|
@@ -595,6 +837,19 @@ async function preflightKnobsAndConfig(subject, options, cwd) {
|
|
|
595
837
|
process.exit(SkillTestExitCode.Preflight);
|
|
596
838
|
}
|
|
597
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Exit code for an error escaping EITHER phase of {@link runSkillTestRun}.
|
|
842
|
+
*
|
|
843
|
+
* A broken governing config ({@link ConfigLoadError}) is a user-fixable PREFLIGHT
|
|
844
|
+
* problem (exit 2), not an internal harness failure — {@link mapErrorToExitCode} has
|
|
845
|
+
* no case for it and falls through to Internal (1). ONE helper shared by BOTH catch
|
|
846
|
+
* blocks so the SUBJECT arm and the COMPANION arm can never drift: a companion's
|
|
847
|
+
* governing config root can differ from the subject's, so a broken config is
|
|
848
|
+
* reachable from either arm and must report the same code from both.
|
|
849
|
+
*/
|
|
850
|
+
function exitCodeForRunError(err) {
|
|
851
|
+
return err instanceof ConfigLoadError ? SkillTestExitCode.Preflight : mapErrorToExitCode(err);
|
|
852
|
+
}
|
|
598
853
|
/**
|
|
599
854
|
* Copy the resolved-subject fields onto the harness options. Optional fields are
|
|
600
855
|
* only assigned when present (exactOptionalPropertyTypes). Extracted so
|
|
@@ -636,22 +891,28 @@ export async function runSkillTestRun(subject, options) {
|
|
|
636
891
|
acknowledgedRunsSkillCode: options.iUnderstandThisRunsSkillCode === true,
|
|
637
892
|
});
|
|
638
893
|
// Shared by subject AND companion resolution (issue #158) so a --with/--with-optional
|
|
639
|
-
// companion that maps to a declared skill gets the exact same build
|
|
640
|
-
// dry-run/security-ack) as the subject.
|
|
894
|
+
// companion that maps to a declared skill gets the exact same build GATING (no-build/
|
|
895
|
+
// dry-run/security-ack) as the subject. The `test.build` hook is deliberately NOT in
|
|
896
|
+
// here: it is per-skill (see BuildFlags), so the subject's own command is passed only
|
|
897
|
+
// to subject resolution and each companion resolves its own.
|
|
641
898
|
const buildFlags = {
|
|
642
899
|
noBuild,
|
|
643
900
|
dryRun: options.dryRun === true,
|
|
644
901
|
acknowledged,
|
|
645
|
-
|
|
902
|
+
explicitAck: options.iUnderstandThisRunsSkillCode === true,
|
|
646
903
|
};
|
|
904
|
+
// ONE memo for the whole run, shared by subject AND companion resolution: a skill
|
|
905
|
+
// that is both the subject and a companion builds exactly once, and N companions
|
|
906
|
+
// in one marketplace trigger ONE marketplace build, not N.
|
|
907
|
+
const buildMemo = new Set();
|
|
647
908
|
let resolvedSubject;
|
|
648
909
|
try {
|
|
649
|
-
resolvedSubject = await resolveSubjectForTest(subject, process.cwd(), buildFlags);
|
|
910
|
+
resolvedSubject = await resolveSubjectForTest(subject, process.cwd(), buildFlags, buildMemo, config?.build);
|
|
650
911
|
}
|
|
651
912
|
catch (err) {
|
|
652
913
|
// A broken governing config surfacing during subject resolution is a preflight
|
|
653
914
|
// problem the user must fix (exit 2), not an internal harness failure (exit 1).
|
|
654
|
-
const exitCode =
|
|
915
|
+
const exitCode = exitCodeForRunError(err);
|
|
655
916
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
656
917
|
process.exit(exitCode);
|
|
657
918
|
return;
|
|
@@ -667,10 +928,10 @@ export async function runSkillTestRun(subject, options) {
|
|
|
667
928
|
// whose source is a path into a declared skill gets built (its `files:`
|
|
668
929
|
// injection runs) exactly like the subject, instead of a raw source-tree copy.
|
|
669
930
|
const repoRoot = resolveRepoRoot();
|
|
670
|
-
const withSources = await resolveCompanionSources(harnessOpts.withSources, repoRoot, buildFlags, false);
|
|
931
|
+
const withSources = await resolveCompanionSources(harnessOpts.withSources, repoRoot, buildFlags, false, buildMemo);
|
|
671
932
|
if (withSources !== undefined)
|
|
672
933
|
harnessOpts.withSources = withSources;
|
|
673
|
-
const withOptional = await resolveCompanionSources(harnessOpts.withOptional, repoRoot, buildFlags, true);
|
|
934
|
+
const withOptional = await resolveCompanionSources(harnessOpts.withOptional, repoRoot, buildFlags, true, buildMemo);
|
|
674
935
|
if (withOptional !== undefined)
|
|
675
936
|
harnessOpts.withOptional = withOptional;
|
|
676
937
|
const result = await runSkillTestHarness(harnessOpts);
|
|
@@ -680,7 +941,10 @@ export async function runSkillTestRun(subject, options) {
|
|
|
680
941
|
return;
|
|
681
942
|
}
|
|
682
943
|
catch (err) {
|
|
683
|
-
|
|
944
|
+
// Same rule as the subject arm above (see {@link exitCodeForRunError}): companion
|
|
945
|
+
// resolution can surface a ConfigLoadError from a DIFFERENT config root than the
|
|
946
|
+
// subject's, and that must exit 2 (preflight), not 1 (internal).
|
|
947
|
+
const exitCode = exitCodeForRunError(err);
|
|
684
948
|
// BootstrapNeededError (exit 3) is the happy "wrote a template, fill it in
|
|
685
949
|
// and re-run" path — surface its message plainly, not as a hard `Error:`.
|
|
686
950
|
if (err instanceof BootstrapNeededError) {
|
|
@@ -701,16 +965,16 @@ export function createSkillTestRunCommand() {
|
|
|
701
965
|
command
|
|
702
966
|
.description('Execute a packaged skill\'s eval suite in a headless, context-isolated Claude session (runs the skill\'s code; not an OS sandbox)')
|
|
703
967
|
.argument('<skill>', 'The skill to test')
|
|
704
|
-
.option('--with <pair...>', 'Stage a REQUIRED companion skill the subject can invoke, as name=<src> (repeatable). <src> is workspace:<pkg> | npm:<spec> | url:<u> | path:<dir> | vendored (e.g. helper=npm:@scope/s@1.2.3). The run fails if a source cannot be resolved.')
|
|
705
|
-
.option('--with-optional <pair...>', 'Stage an OPTIONAL companion skill, as name=<src> (same syntax, repeatable).
|
|
706
|
-
.option('--env <pair...>', 'Inject an env var into the executor spawn as KEY=VALUE (repeatable). Values support ${fixturesDir}, ${stagedSkillDir}, ${harnessRoot}, ${resultsDir}. CLI overrides config for the same key.')
|
|
968
|
+
.option('--with <pair...>', 'Stage a REQUIRED companion skill the subject can invoke, as name=<src> (repeatable). <src> is workspace:<pkg> | npm:<spec> | url:<u> | path:<dir> | vendored (e.g. helper=npm:@scope/s@1.2.3). A path:<dir> that maps to a declared skill is BUILT like the subject (files: build artifacts included), not tree-copied as raw source. The run fails if a source cannot be resolved or its build fails.')
|
|
969
|
+
.option('--with-optional <pair...>', 'Stage an OPTIONAL companion skill, as name=<src> (same syntax, repeatable). Staged from its raw (unbuilt) source with a warning if its source cannot be resolved, or if a non-destructive build fails; the run continues.')
|
|
970
|
+
.option('--env <pair...>', 'Inject an env var into the executor spawn as KEY=VALUE (repeatable). Values support ${fixturesDir}, ${stagedSkillDir}, ${harnessRoot}, ${resultsDir}. ${fixturesDir} is per-eval and requires that eval to declare input `files`. CLI overrides config for the same key.')
|
|
707
971
|
.option('--pass-env <key...>', 'Forward a host env var by NAME to the executor spawn if present (repeatable). Protected names (PATH, auth, model) are ignored.')
|
|
708
972
|
.option('--refresh', 'Force a full re-stage (ignore existing staged content)')
|
|
709
|
-
.option('--no-build', 'Skip building
|
|
973
|
+
.option('--no-build', 'Skip building declared skills (subject and any --with/--with-optional companion); stage existing dist instead. Errors if absent for the subject or a REQUIRED companion; an OPTIONAL companion falls back to raw source with a warning.')
|
|
710
974
|
.option('--workdir <dir>', 'Override the harness working directory')
|
|
711
975
|
.option('--out <dir>', 'Override the harness output directory')
|
|
712
976
|
.option('--keep', 'Keep the harness directory after the run')
|
|
713
|
-
.option('--dry-run', '
|
|
977
|
+
.option('--dry-run', 'Build and stage exactly as a real run would, then stop without spawning Claude (no tokens spent). Combine with --no-build to skip the build too.')
|
|
714
978
|
.option('--auth <mode>', 'Auth mechanism: inherit | subscription | api-key | auto')
|
|
715
979
|
.option('--require-auth <mech>', 'Require a specific auth mechanism: subscription | api-key')
|
|
716
980
|
.option('--baseline', 'Enable A/B baseline run (with/without skill)')
|
|
@@ -719,6 +983,7 @@ export function createSkillTestRunCommand() {
|
|
|
719
983
|
.option('--i-understand-this-runs-skill-code', 'Acknowledge this command executes skill code (required)')
|
|
720
984
|
.option('--model <id>', "Model ID passed VERBATIM to `claude --model <id>` for the model UNDER TEST (the executor). VAT does no mapping/validation (e.g. opus, sonnet, claude-opus-4-8). Omit to use claude's own default model. Independent of --grader-model.")
|
|
721
985
|
.option('--grader-model <id>', 'Model for the fixed grader/judge, passed VERBATIM to `claude --model` (default claude-sonnet-5). GLOBAL (top-level `test:` config), independent of --model (the model UNDER TEST).')
|
|
986
|
+
.option('--evals <path>', "Eval suite to grade against: a path to an evals.json (resolved against the CURRENT DIRECTORY, so it may point outside the skill's tree) or an npm bare specifier honoring that package's exports map. Overrides `test.evals`, which is resolved against the skill source instead. Use this to test a skill you did not author — a correctly packaged skill ships no evals, because the suite is the answer key.")
|
|
722
987
|
.option('--max-turns <n>', 'Per-spawn cap on executor/grader turns (positive integer)')
|
|
723
988
|
.option('--max-budget-usd <n>', 'Hard USD budget cap (positive number)')
|
|
724
989
|
.option('--timeout <s>', 'Wall-clock timeout in seconds (positive integer)')
|
|
@@ -761,7 +1026,7 @@ Model:
|
|
|
761
1026
|
Exit Codes:
|
|
762
1027
|
0 - Harness ran to completion and every eval passed (or --allow-eval-failure suppressed a failing verdict)
|
|
763
1028
|
1 - Internal error (grader fragment absent/invalid, summary/expectations skew, executor/grader crash, stall/timeout)
|
|
764
|
-
2 - Preflight failed (missing binary, auth error, eval inputs absent, unsafe workdir, ack missing,
|
|
1029
|
+
2 - Preflight failed (missing binary, auth error, eval inputs absent, unsafe workdir, ack missing, broken project config, a required skill -- subject or --with companion -- failed to build, --no-build with no existing dist for one of them, or an OPTIONAL --with-optional companion hitting a non-survivable failure: a destructive plugin-local build failure, missing security ack, or broken config)
|
|
765
1030
|
3 - Bootstrap needed: evals.json was absent, so VAT wrote a starter template next to the skill source. Fill it in and re-run.
|
|
766
1031
|
4 - An eval FAILED (the harness completed and produced a valid grading.json; expectations did not all pass). This is the fail-closed DEFAULT -- suppress with --allow-eval-failure.
|
|
767
1032
|
|