@vibe-agent-toolkit/cli 0.1.41-rc.8 → 0.1.42-rc.1

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 (76) hide show
  1. package/dist/bin.js +11 -1
  2. package/dist/bin.js.map +1 -1
  3. package/dist/commands/audit/distributed-tree.d.ts +172 -0
  4. package/dist/commands/audit/distributed-tree.d.ts.map +1 -0
  5. package/dist/commands/audit/distributed-tree.js +319 -0
  6. package/dist/commands/audit/distributed-tree.js.map +1 -0
  7. package/dist/commands/audit.d.ts +35 -0
  8. package/dist/commands/audit.d.ts.map +1 -1
  9. package/dist/commands/audit.js +312 -93
  10. package/dist/commands/audit.js.map +1 -1
  11. package/dist/commands/build.d.ts.map +1 -1
  12. package/dist/commands/build.js +16 -4
  13. package/dist/commands/build.js.map +1 -1
  14. package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
  15. package/dist/commands/claude/marketplace/git-publish.js +21 -3
  16. package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
  17. package/dist/commands/claude/marketplace/validate.d.ts +31 -3
  18. package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
  19. package/dist/commands/claude/marketplace/validate.js +105 -12
  20. package/dist/commands/claude/marketplace/validate.js.map +1 -1
  21. package/dist/commands/claude/plugin/build.d.ts +9 -2
  22. package/dist/commands/claude/plugin/build.d.ts.map +1 -1
  23. package/dist/commands/claude/plugin/build.js +90 -8
  24. package/dist/commands/claude/plugin/build.js.map +1 -1
  25. package/dist/commands/claude/plugin/plugin-files.d.ts.map +1 -1
  26. package/dist/commands/claude/plugin/plugin-files.js +6 -2
  27. package/dist/commands/claude/plugin/plugin-files.js.map +1 -1
  28. package/dist/commands/claude/plugin/tree-copy.d.ts +40 -0
  29. package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
  30. package/dist/commands/claude/plugin/tree-copy.js +100 -4
  31. package/dist/commands/claude/plugin/tree-copy.js.map +1 -1
  32. package/dist/commands/doctor.js +1 -1
  33. package/dist/commands/doctor.js.map +1 -1
  34. package/dist/commands/positional-args.d.ts +36 -0
  35. package/dist/commands/positional-args.d.ts.map +1 -0
  36. package/dist/commands/positional-args.js +52 -0
  37. package/dist/commands/positional-args.js.map +1 -0
  38. package/dist/commands/skill/review-checklist.d.ts.map +1 -1
  39. package/dist/commands/skill/review-checklist.js +5 -0
  40. package/dist/commands/skill/review-checklist.js.map +1 -1
  41. package/dist/commands/skills/build.d.ts +258 -9
  42. package/dist/commands/skills/build.d.ts.map +1 -1
  43. package/dist/commands/skills/build.js +698 -111
  44. package/dist/commands/skills/build.js.map +1 -1
  45. package/dist/commands/skills/scope-guard.d.ts +64 -0
  46. package/dist/commands/skills/scope-guard.d.ts.map +1 -0
  47. package/dist/commands/skills/scope-guard.js +86 -0
  48. package/dist/commands/skills/scope-guard.js.map +1 -0
  49. package/dist/commands/skills/validate-command.d.ts.map +1 -1
  50. package/dist/commands/skills/validate-command.js +19 -1
  51. package/dist/commands/skills/validate-command.js.map +1 -1
  52. package/dist/commands/skills/validate.d.ts.map +1 -1
  53. package/dist/commands/skills/validate.js +37 -7
  54. package/dist/commands/skills/validate.js.map +1 -1
  55. package/dist/commands/validate.d.ts.map +1 -1
  56. package/dist/commands/validate.js +18 -4
  57. package/dist/commands/validate.js.map +1 -1
  58. package/dist/commands/verify.d.ts +77 -7
  59. package/dist/commands/verify.d.ts.map +1 -1
  60. package/dist/commands/verify.js +254 -57
  61. package/dist/commands/verify.js.map +1 -1
  62. package/dist/utils/issue-rendering.d.ts +42 -0
  63. package/dist/utils/issue-rendering.d.ts.map +1 -1
  64. package/dist/utils/issue-rendering.js +53 -0
  65. package/dist/utils/issue-rendering.js.map +1 -1
  66. package/dist/utils/issue-severity.d.ts +51 -0
  67. package/dist/utils/issue-severity.d.ts.map +1 -0
  68. package/dist/utils/issue-severity.js +63 -0
  69. package/dist/utils/issue-severity.js.map +1 -0
  70. package/dist/version.d.ts +22 -2
  71. package/dist/version.d.ts.map +1 -1
  72. package/dist/version.js +31 -2
  73. package/dist/version.js.map +1 -1
  74. package/docs/index.md +12 -0
  75. package/docs/skills.md +16 -2
  76. package/package.json +15 -12
@@ -6,15 +6,19 @@
6
6
  * 2. vat skills validate (SKILL.md frontmatter validation)
7
7
  * 3. vat claude marketplace validate (strict marketplace validation, when configured)
8
8
  * 4. files-config-dests (in-process; every `files:` dest exists in the built output)
9
- * 5. consistency check (in-process; skill distribution integrity — package.json, plugin assignment)
9
+ * 5. packaged-content (in-process; built bundles carry nothing that must not ship)
10
+ * 6. consistency check (in-process; skill distribution integrity — package.json, plugin assignment)
10
11
  *
11
- * 1–3 are subprocess phases chosen by {@link selectVerifyPhases}; 4–5 run here and
12
+ * 1–3 are subprocess phases chosen by {@link selectVerifyPhases}; 4–6 run here and
12
13
  * are chosen by `selectInProcessVerifyPhases`. Both sets are config-gated, and both
13
14
  * are announced on startup.
14
15
  */
16
+ import { type ValidationIssue } from '@vibe-agent-toolkit/agent-schema';
15
17
  import type { ProjectConfig } from '@vibe-agent-toolkit/resources';
16
18
  import { Command } from 'commander';
19
+ import { type ConsistencyIssueSeverity } from './consistency-check.js';
17
20
  import { type PhaseSelection } from './phase-utils.js';
21
+ import type { DiscoveredSkill } from './skills/command-helpers.js';
18
22
  export interface VerifyCommandOptions {
19
23
  /** Retired; declared only so {@link rejectRetiredOnly} can explain the removal. */
20
24
  only?: string;
@@ -32,16 +36,55 @@ export interface FilesDestCheckResult {
32
36
  /**
33
37
  * Check that all dest paths from the merged files config exist in the built output.
34
38
  *
35
- * Checks each skill's dests in the location(s) where `vat build` actually wrote them:
36
- * - Pool skills: `dist/skills/<fsName>/` (only when that dir exists)
37
- * - Tree-copy skills: `dist/.claude/plugins/.../skills/<name>/` (only when that dir exists)
38
- *
39
39
  * A dest is "missing" ONLY when absent from a candidate dir that exists. If a skill
40
40
  * has no existing candidate dir, it is not reported (build didn't run for that mode).
41
41
  *
42
+ * @param discovered - The skills this run discovered from `skills.include`. See
43
+ * {@link collectBuiltSkillOutputs} for why it is required rather than optional.
42
44
  * @returns One result per (skill, outputDir) pair where dests are absent.
43
45
  */
44
- export declare function checkFilesConfigDests(cwd: string): FilesDestCheckResult[];
46
+ export declare function checkFilesConfigDests(cwd: string, discovered: readonly DiscoveredSkill[]): FilesDestCheckResult[];
47
+ /**
48
+ * Crawl every built skill bundle for repo-internal agent-instruction files.
49
+ *
50
+ * The built-skill-bundle arm of `PACKAGED_AGENT_INSTRUCTION_FILE`. Its description
51
+ * has always claimed three surfaces — a built skill bundle, an installed plugin, a
52
+ * plugin source directory — but only the two plugin arms had a producer: the skill
53
+ * lanes inspect SKILL.md plus what links reach from it, so a file that arrives in a
54
+ * bundle with no link at all was invisible to every command. Measured on an adopter
55
+ * bundle carrying two of them: `vat audit` reported `filesScanned: 1`, zero issues,
56
+ * and `vat verify` reported `warnings: 0`.
57
+ *
58
+ * UNCONDITIONAL here, with no provenance test: `vat verify` reads the built `dist/`
59
+ * tree by definition, so every tree this enumerates is distributed output. (`vat
60
+ * audit` takes an arbitrary path and therefore must answer the provenance question
61
+ * first — see `appendDistributedTreeFindings` in audit.ts.)
62
+ *
63
+ * Explicit `files:` dests are exempt (§8.2): here the config is knowable, and
64
+ * naming a dest is an instruction to ship that file. A glob match never earns the
65
+ * exemption — a glob is a net, not a declaration — which is why the exempt set
66
+ * comes from {@link explicitFilesConfigDests} rather than from every `files:` entry.
67
+ *
68
+ * Each bundle's findings are then resolved against that skill's effective
69
+ * `validation.severity` (see {@link resolveIssueSeverity}), so the opt-out the
70
+ * code's own `fix` text prescribes actually works here. It did not: this phase
71
+ * published `detectPackagedAgentInstructionFiles`' raw output straight into the
72
+ * document, so `severity.PACKAGED_AGENT_INSTRUCTION_FILE: ignore` changed nothing
73
+ * — measured `warnings: 1` with the override at `skills.defaults`, at
74
+ * `skills.config.<name>`, and with no override at all.
75
+ *
76
+ * `validation.allow` is deliberately NOT applied. Allow is a per-PATH suppression
77
+ * whose usage is only answerable across a whole run, and this phase would have to
78
+ * drain a ledger it is not the run of — reporting ALLOW_UNUSED for every entry the
79
+ * project declares for other lanes. Severity is answerable per unit of work; allow
80
+ * is not. See `AllowUsageLedger` in `@vibe-agent-toolkit/agent-schema`.
81
+ *
82
+ * Locations anchor at `cwd`, the run's stated root, so a reader can open them.
83
+ *
84
+ * @param discovered - The skills this run discovered from `skills.include`. See
85
+ * {@link collectBuiltSkillOutputs} for why it is required rather than optional.
86
+ */
87
+ export declare function checkPackagedAgentInstructionFiles(cwd: string, discovered: readonly DiscoveredSkill[]): ValidationIssue[];
45
88
  /**
46
89
  * Decide which verification phases to run.
47
90
  *
@@ -70,4 +113,31 @@ export declare function selectVerifyPhases(config: ProjectConfig | undefined, co
70
113
  * in order. A phase that would consult nothing is not named.
71
114
  */
72
115
  export declare function formatVerifyAnnouncement(subprocessPhaseNames: readonly string[], config: ProjectConfig | undefined): string;
116
+ /** An in-process phase's finding as it appears in the archived YAML. */
117
+ interface PublishedIssue {
118
+ severity: ValidationIssue['severity'] | ConsistencyIssueSeverity;
119
+ code: string;
120
+ message: string;
121
+ fix: string;
122
+ /** Project-relative path of the file to open. Absent when the finding has none. */
123
+ location?: string;
124
+ /** 1-based line within {@link PublishedIssue.location}. */
125
+ line?: number;
126
+ /** Doc link the code's registry entry carries. */
127
+ reference?: string;
128
+ }
129
+ /**
130
+ * Project ONE finding into the archived YAML.
131
+ *
132
+ * The whole anchor rides along. This shape used to be `{severity, code, message,
133
+ * fix}` and the projection destructured exactly those four, so every
134
+ * `packaged-content` finding reached the document with no `location` — the same
135
+ * defect this PR fixed one command over in `vat skills build`, where a published
136
+ * count carried "no `code`, no location and no fix string at ANY verbosity". It
137
+ * looked survivable only because `materializeIssue` happens to interpolate the
138
+ * detail into `message` for this one code; that is a coincidence, not a contract,
139
+ * and stderr is not the document a CI consumer parses.
140
+ */
141
+ export declare function toPublishedIssue(issue: ValidationIssue): PublishedIssue;
142
+ export {};
73
143
  //# sourceMappingURL=verify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,OAAO,EAWL,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAG1B,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AASD,wBAAgB,2BAA2B,IAAI,OAAO,CA4ErD;AAED,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAwCD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAkDzE;AAkFD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,OAAO,GAChB,cAAc,CA6BhB;AAyCD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,oBAAoB,EAAE,SAAS,MAAM,EAAE,EACvC,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,MAAM,CAGR"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,kCAAkC,CAAC;AAO1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAWL,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGnE,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAYD,wBAAgB,2BAA2B,IAAI,OAAO,CAqFrD;AAED,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAyID;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,oBAAoB,EAAE,CAmBxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,kCAAkC,CAChD,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,eAAe,EAAE,CAWnB;AAwGD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,OAAO,GAChB,cAAc,CA6BhB;AA4CD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,oBAAoB,EAAE,SAAS,MAAM,EAAE,EACvC,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,MAAM,CAGR;AAED,wEAAwE;AACxE,UAAU,cAAc;IAGtB,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,GAAG,wBAAwB,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,cAAc,CAavE"}
@@ -6,24 +6,29 @@
6
6
  * 2. vat skills validate (SKILL.md frontmatter validation)
7
7
  * 3. vat claude marketplace validate (strict marketplace validation, when configured)
8
8
  * 4. files-config-dests (in-process; every `files:` dest exists in the built output)
9
- * 5. consistency check (in-process; skill distribution integrity — package.json, plugin assignment)
9
+ * 5. packaged-content (in-process; built bundles carry nothing that must not ship)
10
+ * 6. consistency check (in-process; skill distribution integrity — package.json, plugin assignment)
10
11
  *
11
- * 1–3 are subprocess phases chosen by {@link selectVerifyPhases}; 4–5 run here and
12
+ * 1–3 are subprocess phases chosen by {@link selectVerifyPhases}; 4–6 run here and
12
13
  * are chosen by `selectInProcessVerifyPhases`. Both sets are config-gated, and both
13
14
  * are announced on startup.
14
15
  */
15
16
  import { existsSync } from 'node:fs';
16
17
  import { basename } from 'node:path';
17
18
  import { calculateValidationStatus, countBySeverity, } from '@vibe-agent-toolkit/agent-schema';
18
- import { computeTreeCopiedSkillLocations, mergeFilesConfig } from '@vibe-agent-toolkit/agent-skills';
19
+ import { computeTreeCopiedSkillLocations, detectPackagedAgentInstructionFiles, explicitFilesConfigDests, } from '@vibe-agent-toolkit/agent-skills';
19
20
  import { safePath } from '@vibe-agent-toolkit/utils';
20
21
  import { Command } from 'commander';
21
22
  import { handleCommandError } from '../utils/command-error.js';
22
23
  import { loadConfig } from '../utils/config-loader.js';
24
+ import { formatIssueLines } from '../utils/issue-rendering.js';
25
+ import { resolveIssueSeverity } from '../utils/issue-severity.js';
23
26
  import { writeYamlOutput } from '../utils/output.js';
24
27
  import { requireProjectRoot } from '../utils/project-root-policy.js';
28
+ import { mergeSkillPackagingConfig } from '../utils/skill-packaging-config.js';
25
29
  import { runConsistencyChecks, } from './consistency-check.js';
26
30
  import { addRetiredOnlyOption, aggregatePhaseStatus, applyPhaseSelection, createPhaseContext, decidePhaseSelection, exitCodeForPhases, rejectRetiredOnly, runPhase, } from './phase-utils.js';
31
+ import { rejectPositionalArguments } from './positional-args.js';
27
32
  import { discoverSkillsFromConfig } from './skills/skill-discovery.js';
28
33
  /**
29
34
  * Measured full-run duration on the 90-skill / 1,041-document adopter, cited by
@@ -31,6 +36,8 @@ import { discoverSkillsFromConfig } from './skills/skill-discovery.js';
31
36
  * 1.0s + consistency under a second.
32
37
  */
33
38
  const VERIFY_FULL_RUN_SECONDS = 32;
39
+ /** How this command names itself in every user-facing diagnostic. */
40
+ const COMMAND_NAME = 'vat verify';
34
41
  export function createVerifyTopLevelCommand() {
35
42
  const command = new Command('verify');
36
43
  addRetiredOnlyOption(command)
@@ -59,11 +66,14 @@ Description:
59
66
  Phases (in-process, run after the above, when 'skills:' is configured):
60
67
  files-config-dests → every 'files:' dest exists in the built output. Appears
61
68
  in the document only when a dest is missing.
69
+ packaged-content → built skill bundles carry no repo-internal agent-instruction
70
+ file (CLAUDE.md, AGENTS.md, GEMINI.md). A dest an explicit
71
+ 'files:' entry names is honoured, not reported.
62
72
  consistency → skill distribution integrity (package.json, plugin assignment)
63
73
 
64
- A run with a 'skills:' block runs all three of skills, files-config-dests and
65
- consistency: they read that same config block. Without it neither in-process
66
- phase has anything to read, so neither runs.
74
+ A run with a 'skills:' block runs all four of skills, files-config-dests,
75
+ packaged-content and consistency: they read that same config block. Without it
76
+ no in-process phase has anything to read, so none run.
67
77
 
68
78
  The startup line on stderr names, in order, the phases that will inspect
69
79
  something. A phase that would consult nothing is not listed; a phase that does
@@ -76,9 +86,10 @@ Output:
76
86
  is a single parseable document (a phase's stdout is never streamed
77
87
  through). A phase's status comes from the child's REPORTED status, not
78
88
  from its exit code — an exit code cannot express 'warning'. In-process
79
- phases also publish issueCounts {errors, warnings, info}; 'consistency'
80
- carries its findings into the document too, while 'files-config-dests'
81
- publishes counts only and lists the missing dests on stderr.
89
+ phases also publish issueCounts {errors, warnings, info}; 'consistency' and
90
+ 'packaged-content' carry their findings into the document too, while
91
+ 'files-config-dests' publishes counts only and lists the missing dests on
92
+ stderr.
82
93
  Progress and validation errors → stderr (streamed live)
83
94
 
84
95
  By default each subprocess phase reports a per-asset summary plus the assets
@@ -90,7 +101,12 @@ Exit Codes:
90
101
  1 - Validation errors found
91
102
  2 - System error (this command's own, or propagated from a phase that could
92
103
  not run: exited 2, was killed by a signal, was never spawned, or wrote
93
- output that could not be parsed)
104
+ output that could not be parsed), or a usage error such as passing a path
105
+
106
+ Arguments:
107
+ None. Scope comes from vibe-agent-toolkit.config.yaml, never from the command
108
+ line — a path argument is rejected (exit 2) rather than discarded. To inspect
109
+ ONE skill or bundle by path, use 'vat audit <path>'.
94
110
 
95
111
  Requirements:
96
112
  projectRoot: required (errors if no vibe-agent-toolkit.config.yaml or .git/ ancestor)
@@ -110,38 +126,73 @@ Example:
110
126
  function skillNameToFsPath(name) {
111
127
  return name.replaceAll(':', '__');
112
128
  }
129
+ /** The `files:` entries governing a candidate, `[]` when the skill declares none. */
130
+ function filesOf(entry) {
131
+ return entry.packaging.files ?? [];
132
+ }
113
133
  /**
114
- * Register a check for (skillName, outputDir, mergedFiles) in the dedup map.
134
+ * Register a check for (skillName, outputDir, packaging) in the dedup map.
115
135
  *
116
136
  * Skips silently if:
117
- * - mergedFiles is empty (no entries to verify)
118
137
  * - outputDir does not exist on disk (not a candidate)
119
138
  * - the key was already added (dedup guard)
139
+ *
140
+ * An EMPTY `files:` block is registered, not skipped: {@link checkFilesConfigDests}
141
+ * has nothing to verify for such a skill and filters it out itself, but
142
+ * {@link checkPackagedAgentInstructionFiles} must still crawl that bundle — a skill
143
+ * with no `files:` block is exactly the one whose agent-instruction file arrived by
144
+ * some other route, and dropping it here would make the crawl blind to it.
120
145
  */
121
- function tryAddCheckEntry(checks, skillName, outputDir, mergedFiles) {
122
- if (mergedFiles.length === 0)
123
- return;
146
+ function tryAddCheckEntry(checks, skillName, outputDir, packaging) {
124
147
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- outputDir is resolved from config, not user input
125
148
  if (!existsSync(outputDir))
126
149
  return;
127
150
  const key = `${skillName}\0${outputDir}`;
128
151
  if (!checks.has(key)) {
129
- checks.set(key, { skillName, outputDir, mergedFiles });
152
+ checks.set(key, { skillName, outputDir, packaging });
130
153
  }
131
154
  }
132
155
  /**
133
- * Check that all dest paths from the merged files config exist in the built output.
156
+ * Every built skill bundle this project's config accounts for that EXISTS on disk,
157
+ * with the `files:` entries that govern it.
134
158
  *
135
- * Checks each skill's dests in the location(s) where `vat build` actually wrote them:
136
- * - Pool skills: `dist/skills/<fsName>/` (only when that dir exists)
137
- * - Tree-copy skills: `dist/.claude/plugins/.../skills/<name>/` (only when that dir exists)
159
+ * ONE enumeration of "where did `vat build` write this skill", shared by every
160
+ * in-process verify phase, in the two locations the build actually uses:
161
+ * - Pool skills: `dist/skills/<fsName>/`
162
+ * - Tree-copy skills: `dist/.claude/plugins/.../skills/<name>/`
138
163
  *
139
- * A dest is "missing" ONLY when absent from a candidate dir that exists. If a skill
140
- * has no existing candidate dir, it is not reported (build didn't run for that mode).
164
+ * Returns `[]` (never throws) for an unreadable config: `vat verify`'s subprocess
165
+ * phases report the real config error, and an in-process phase must not race them
166
+ * with a second, worse diagnosis.
141
167
  *
142
- * @returns One result per (skill, outputDir) pair where dests are absent.
168
+ * The pool arm enumerates the skills the run DISCOVERED, unioned with the keys of
169
+ * `skills.config`. It used to be the config keys alone, which made both in-process
170
+ * phases blind to the common case: a per-skill `config:` block is optional, so a
171
+ * skill discovered only by `skills.include` had no key and its bundle was never
172
+ * looked at. Measured on a two-skill fixture with an identical `CLAUDE.md` planted
173
+ * in each built bundle, `packaged-content` reported ONE finding and exit 0 — the
174
+ * phase was structurally blind to the population its own doc comment cites as the
175
+ * motivating incident. The same line made `files-config-dests` a total no-op for a
176
+ * project whose `files:` entries live only under `skills.defaults`, while the
177
+ * startup banner still announced that the phase ran.
178
+ *
179
+ * The config keys stay in the union rather than being replaced by discovery: a key
180
+ * naming a skill discovery does not reach (a renamed glob, a stale entry) still
181
+ * points at a bundle that may be sitting in `dist/`, and dropping it would trade
182
+ * one blindness for another.
183
+ *
184
+ * `discovered` is a REQUIRED parameter, not an optional convenience: discovery
185
+ * crawls the whole project, `vat verify` already needs the same list for its
186
+ * consistency phase, and an optional parameter here would let a call site quietly
187
+ * re-crawl (or, worse, skip discovery and reinstate the blindness above).
188
+ *
189
+ * The merge goes through {@link mergeSkillPackagingConfig} — the ONE helper every
190
+ * lane uses — so `vat verify` and `vat build` cannot disagree about a skill's
191
+ * effective config. Doing the `files:` half by hand here and leaving `validation:`
192
+ * unread is what made `severity.PACKAGED_AGENT_INSTRUCTION_FILE: ignore` a no-op in
193
+ * this command.
143
194
  */
144
- export function checkFilesConfigDests(cwd) {
195
+ function collectBuiltSkillOutputs(cwd, discovered) {
145
196
  try {
146
197
  const config = loadConfig(cwd);
147
198
  if (!config)
@@ -150,12 +201,15 @@ export function checkFilesConfigDests(cwd) {
150
201
  const defaults = skillsConfig?.defaults;
151
202
  // Dedup map: key = `skillName\0outputDir` → check entry
152
203
  const checks = new Map();
153
- // --- Pool/config skills: candidate dir is dist/skills/<fsName> ---
154
- for (const skillName of Object.keys(skillsConfig?.config ?? {})) {
204
+ // --- Pool skills: candidate dir is dist/skills/<fsName> ---
205
+ const poolNames = new Set([
206
+ ...discovered.map((skill) => skill.name),
207
+ ...Object.keys(skillsConfig?.config ?? {}),
208
+ ]);
209
+ for (const skillName of poolNames) {
155
210
  const perSkill = skillsConfig?.config?.[skillName];
156
- const mergedFiles = mergeFilesConfig(defaults?.files, perSkill?.files);
157
211
  const outputDir = safePath.resolve(cwd, 'dist', 'skills', skillNameToFsPath(skillName));
158
- tryAddCheckEntry(checks, skillName, outputDir, mergedFiles);
212
+ tryAddCheckEntry(checks, skillName, outputDir, mergeSkillPackagingConfig(defaults, perSkill));
159
213
  }
160
214
  // --- Tree-copy skills: candidate dirs are plugin output skill dirs ---
161
215
  for (const loc of computeTreeCopiedSkillLocations(config, cwd)) {
@@ -163,31 +217,94 @@ export function checkFilesConfigDests(cwd) {
163
217
  // path (`group/nested-skill` for a nested skill), so try its trailing segment
164
218
  // too — the spelling that matches for every skill whose dir is named after it.
165
219
  const dirLeaf = basename(loc.skillDirPath);
166
- const perSkill = skillsConfig?.config?.[loc.skillDirPath] ?? skillsConfig?.config?.[dirLeaf];
167
- const mergedFiles = mergeFilesConfig(defaults?.files, perSkill?.files);
168
- tryAddCheckEntry(checks, loc.skillDirPath, loc.skillOutputDir, mergedFiles);
169
- }
170
- // --- Run each pending check and collect results ---
171
- const results = [];
172
- for (const { skillName, outputDir, mergedFiles } of checks.values()) {
173
- const missing = [];
174
- for (const entry of mergedFiles) {
175
- const destPath = safePath.resolve(outputDir, entry.dest);
176
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- destPath resolved from config
177
- if (!existsSync(destPath)) {
178
- missing.push(entry.dest);
179
- }
180
- }
181
- if (missing.length > 0) {
182
- results.push({ skillName, outputDir, missing });
183
- }
220
+ const perSkill = (skillsConfig?.config?.[loc.skillDirPath] ?? skillsConfig?.config?.[dirLeaf]);
221
+ tryAddCheckEntry(checks, loc.skillDirPath, loc.skillOutputDir, mergeSkillPackagingConfig(defaults, perSkill));
184
222
  }
185
- return results;
223
+ return [...checks.values()];
186
224
  }
187
225
  catch {
188
226
  return [];
189
227
  }
190
228
  }
229
+ /**
230
+ * Check that all dest paths from the merged files config exist in the built output.
231
+ *
232
+ * A dest is "missing" ONLY when absent from a candidate dir that exists. If a skill
233
+ * has no existing candidate dir, it is not reported (build didn't run for that mode).
234
+ *
235
+ * @param discovered - The skills this run discovered from `skills.include`. See
236
+ * {@link collectBuiltSkillOutputs} for why it is required rather than optional.
237
+ * @returns One result per (skill, outputDir) pair where dests are absent.
238
+ */
239
+ export function checkFilesConfigDests(cwd, discovered) {
240
+ const results = [];
241
+ for (const check of collectBuiltSkillOutputs(cwd, discovered)) {
242
+ const { skillName, outputDir } = check;
243
+ const mergedFiles = filesOf(check);
244
+ if (mergedFiles.length === 0)
245
+ continue;
246
+ const missing = [];
247
+ for (const entry of mergedFiles) {
248
+ const destPath = safePath.resolve(outputDir, entry.dest);
249
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- destPath resolved from config
250
+ if (!existsSync(destPath)) {
251
+ missing.push(entry.dest);
252
+ }
253
+ }
254
+ if (missing.length > 0) {
255
+ results.push({ skillName, outputDir, missing });
256
+ }
257
+ }
258
+ return results;
259
+ }
260
+ /**
261
+ * Crawl every built skill bundle for repo-internal agent-instruction files.
262
+ *
263
+ * The built-skill-bundle arm of `PACKAGED_AGENT_INSTRUCTION_FILE`. Its description
264
+ * has always claimed three surfaces — a built skill bundle, an installed plugin, a
265
+ * plugin source directory — but only the two plugin arms had a producer: the skill
266
+ * lanes inspect SKILL.md plus what links reach from it, so a file that arrives in a
267
+ * bundle with no link at all was invisible to every command. Measured on an adopter
268
+ * bundle carrying two of them: `vat audit` reported `filesScanned: 1`, zero issues,
269
+ * and `vat verify` reported `warnings: 0`.
270
+ *
271
+ * UNCONDITIONAL here, with no provenance test: `vat verify` reads the built `dist/`
272
+ * tree by definition, so every tree this enumerates is distributed output. (`vat
273
+ * audit` takes an arbitrary path and therefore must answer the provenance question
274
+ * first — see `appendDistributedTreeFindings` in audit.ts.)
275
+ *
276
+ * Explicit `files:` dests are exempt (§8.2): here the config is knowable, and
277
+ * naming a dest is an instruction to ship that file. A glob match never earns the
278
+ * exemption — a glob is a net, not a declaration — which is why the exempt set
279
+ * comes from {@link explicitFilesConfigDests} rather than from every `files:` entry.
280
+ *
281
+ * Each bundle's findings are then resolved against that skill's effective
282
+ * `validation.severity` (see {@link resolveIssueSeverity}), so the opt-out the
283
+ * code's own `fix` text prescribes actually works here. It did not: this phase
284
+ * published `detectPackagedAgentInstructionFiles`' raw output straight into the
285
+ * document, so `severity.PACKAGED_AGENT_INSTRUCTION_FILE: ignore` changed nothing
286
+ * — measured `warnings: 1` with the override at `skills.defaults`, at
287
+ * `skills.config.<name>`, and with no override at all.
288
+ *
289
+ * `validation.allow` is deliberately NOT applied. Allow is a per-PATH suppression
290
+ * whose usage is only answerable across a whole run, and this phase would have to
291
+ * drain a ledger it is not the run of — reporting ALLOW_UNUSED for every entry the
292
+ * project declares for other lanes. Severity is answerable per unit of work; allow
293
+ * is not. See `AllowUsageLedger` in `@vibe-agent-toolkit/agent-schema`.
294
+ *
295
+ * Locations anchor at `cwd`, the run's stated root, so a reader can open them.
296
+ *
297
+ * @param discovered - The skills this run discovered from `skills.include`. See
298
+ * {@link collectBuiltSkillOutputs} for why it is required rather than optional.
299
+ */
300
+ export function checkPackagedAgentInstructionFiles(cwd, discovered) {
301
+ const issues = [];
302
+ for (const check of collectBuiltSkillOutputs(cwd, discovered)) {
303
+ const raw = detectPackagedAgentInstructionFiles(check.outputDir, cwd, explicitFilesConfigDests(filesOf(check)));
304
+ issues.push(...resolveIssueSeverity(raw, check.packaging.validation));
305
+ }
306
+ return issues;
307
+ }
191
308
  /**
192
309
  * Load the project config without letting a broken one abort the command.
193
310
  *
@@ -217,6 +334,25 @@ function reportFilesDestErrors(results, logger) {
217
334
  }
218
335
  }
219
336
  }
337
+ /**
338
+ * Log packaged-content findings to stderr.
339
+ *
340
+ * A companion to the document entry, never a substitute for it: this phase's
341
+ * findings are published into the YAML too (see {@link FindingsPhaseResult}).
342
+ *
343
+ * Rendered through the SHARED {@link formatIssueLines}, which `vat skills build`
344
+ * and `vat skills validate` already use. The hand-rolled renderer this replaces
345
+ * printed severity, code, location and fix — and dropped `message`, the only part
346
+ * of a finding that says what is wrong. A second renderer for the same shape is
347
+ * how one command's findings end up spelled differently from every other's.
348
+ */
349
+ function reportPackagedContentIssues(issues, logger) {
350
+ logger.error(`\n▶ Phase: ${PACKAGED_CONTENT}`);
351
+ for (const issue of issues) {
352
+ for (const line of formatIssueLines(issue, ' '))
353
+ logger.error(line);
354
+ }
355
+ }
220
356
  /**
221
357
  * Log consistency check issues to stderr.
222
358
  */
@@ -302,6 +438,8 @@ export function selectVerifyPhases(config, configError, verbose) {
302
438
  unreadableConfig: configError,
303
439
  });
304
440
  }
441
+ /** The in-process phase that crawls built skill bundles for what must not ship. */
442
+ const PACKAGED_CONTENT = 'packaged-content';
305
443
  /**
306
444
  * Which in-process phases this run performs, given the config.
307
445
  *
@@ -335,7 +473,7 @@ export function selectVerifyPhases(config, configError, verbose) {
335
473
  * though a clean run reports nothing.
336
474
  */
337
475
  function selectInProcessVerifyPhases(config) {
338
- return config?.skills === undefined ? [] : [FILES_CONFIG_DESTS, 'consistency'];
476
+ return config?.skills === undefined ? [] : [FILES_CONFIG_DESTS, PACKAGED_CONTENT, 'consistency'];
339
477
  }
340
478
  /**
341
479
  * The startup announcement: every phase this run will inspect something with,
@@ -345,6 +483,32 @@ export function formatVerifyAnnouncement(subprocessPhaseNames, config) {
345
483
  const all = [...subprocessPhaseNames, ...selectInProcessVerifyPhases(config)];
346
484
  return `🔍 vat verify (phases: ${all.join(' → ')})`;
347
485
  }
486
+ /**
487
+ * Project ONE finding into the archived YAML.
488
+ *
489
+ * The whole anchor rides along. This shape used to be `{severity, code, message,
490
+ * fix}` and the projection destructured exactly those four, so every
491
+ * `packaged-content` finding reached the document with no `location` — the same
492
+ * defect this PR fixed one command over in `vat skills build`, where a published
493
+ * count carried "no `code`, no location and no fix string at ANY verbosity". It
494
+ * looked survivable only because `materializeIssue` happens to interpolate the
495
+ * detail into `message` for this one code; that is a coincidence, not a contract,
496
+ * and stderr is not the document a CI consumer parses.
497
+ */
498
+ export function toPublishedIssue(issue) {
499
+ return {
500
+ severity: issue.severity,
501
+ code: issue.code,
502
+ message: issue.message,
503
+ fix: issue.fix ?? '',
504
+ // Conditional rather than `location: issue.location`: `exactOptionalPropertyTypes`
505
+ // distinguishes an absent key from an explicit `undefined`, and a YAML document
506
+ // carrying `location: null` claims something the finding never said.
507
+ ...(issue.location === undefined ? {} : { location: issue.location }),
508
+ ...(issue.line === undefined ? {} : { line: issue.line }),
509
+ ...(issue.reference === undefined ? {} : { reference: issue.reference }),
510
+ };
511
+ }
348
512
  /**
349
513
  * `ConsistencyIssue` speaks the same severity vocabulary as `ValidationIssue`
350
514
  * but carries a free-form `code`, so it is counted through this projection —
@@ -366,8 +530,12 @@ function asValidationIssues(issues) {
366
530
  * to go to stderr only, so the archived YAML — the artifact of record — said
367
531
  * nothing happened. And a warning-only run reported `passed`, which is the
368
532
  * reassuring answer to a question it could not represent.
533
+ *
534
+ * Discovery is handed in rather than performed here: it crawls the whole project,
535
+ * and the packaged-content and files-config-dests phases need the same list. One
536
+ * crawl per run, one answer to "which skills does this project have".
369
537
  */
370
- async function runConsistencyPhase(logger, phaseResults, config, projectRoot) {
538
+ function runConsistencyPhase(logger, phaseResults, config, projectRoot, discoveredSkills) {
371
539
  if (!config?.skills) {
372
540
  // Nothing to cross-reference, so nothing to report: a run without a
373
541
  // `skills:` block is a genuine no-op, and {@link selectInProcessVerifyPhases}
@@ -381,8 +549,7 @@ async function runConsistencyPhase(logger, phaseResults, config, projectRoot) {
381
549
  // defensively (and narrows `config.skills` for the call below).
382
550
  return;
383
551
  }
384
- const discoveredSkills = await discoverSkillsFromConfig(config.skills, projectRoot);
385
- const consistencyResult = runConsistencyChecks(discoveredSkills, config, projectRoot);
552
+ const consistencyResult = runConsistencyChecks([...discoveredSkills], config, projectRoot);
386
553
  const issues = consistencyResult.issues;
387
554
  if (issues.length > 0) {
388
555
  reportConsistencyIssues(issues, logger);
@@ -392,17 +559,21 @@ async function runConsistencyPhase(logger, phaseResults, config, projectRoot) {
392
559
  name: 'consistency',
393
560
  status: calculateValidationStatus(asValidation),
394
561
  issueCounts: countBySeverity(asValidation),
395
- issues: issues.map(({ severity, code, message, fix }) => ({ severity, code, message, fix })),
562
+ issues: asValidation.map(toPublishedIssue),
396
563
  };
397
564
  phaseResults.push(result);
398
565
  }
399
- async function verifyTopLevelCommand(options) {
566
+ async function verifyTopLevelCommand(options, command) {
567
+ // First, and before requireProjectRoot: `vat verify dist/skills/demo` used to
568
+ // be accepted, have its path discarded, run wide over the whole project and
569
+ // report success. Nothing below can un-tell that lie, so the run ends here.
570
+ rejectPositionalArguments(command.args, COMMAND_NAME, 'verifies every phase vibe-agent-toolkit.config.yaml declares, against the built dist/ tree');
400
571
  // Before requireProjectRoot: a retired flag is a usage error, and answering it
401
572
  // with "no vibe-agent-toolkit.config.yaml found" would diagnose the wrong
402
573
  // problem for anyone running the old invocation outside a project.
403
- rejectRetiredOnly(options.only, 'vat verify', VERIFY_FULL_RUN_SECONDS);
574
+ rejectRetiredOnly(options.only, COMMAND_NAME, VERIFY_FULL_RUN_SECONDS);
404
575
  // Spec §7: `vat verify` requires a projectRoot.
405
- const projectRoot = requireProjectRoot(process.cwd(), 'vat verify');
576
+ const projectRoot = requireProjectRoot(process.cwd(), COMMAND_NAME);
406
577
  const { logger, startTime, binPath } = createPhaseContext(options.debug);
407
578
  try {
408
579
  // Inside the try, deliberately: phase selection used to throw from out here
@@ -419,9 +590,17 @@ async function verifyTopLevelCommand(options) {
419
590
  logger.info(`\n▶ Phase: ${phase.name}`);
420
591
  phaseResults.push(runPhase(binPath, phase));
421
592
  }
593
+ // ONE discovery for the whole in-process half of the run. Every phase below
594
+ // asks the same question — "which skills does this project have" — and each
595
+ // answer used to be a different one: `consistency` crawled, while
596
+ // `files-config-dests` and `packaged-content` read `skills.config` keys and
597
+ // were therefore blind to every skill discovered by a glob.
598
+ const discoveredSkills = inProcess.length > 0 && config?.skills
599
+ ? await discoverSkillsFromConfig(config.skills, projectRoot)
600
+ : [];
422
601
  // Post-build files config check: verify all dest paths exist in built output
423
602
  if (inProcess.includes(FILES_CONFIG_DESTS)) {
424
- const filesDestResults = checkFilesConfigDests(projectRoot);
603
+ const filesDestResults = checkFilesConfigDests(projectRoot, discoveredSkills);
425
604
  if (filesDestResults.length > 0) {
426
605
  reportFilesDestErrors(filesDestResults, logger);
427
606
  phaseResults.push({
@@ -431,9 +610,27 @@ async function verifyTopLevelCommand(options) {
431
610
  });
432
611
  }
433
612
  }
613
+ // Packaged-content check: crawl each built skill bundle for repo-internal
614
+ // agent-instruction files. Publishes its findings INTO the document rather
615
+ // than only logging them — a file that must not ship has to be visible in
616
+ // `issueCounts`, or a CI consumer reads a clean report for a bundle carrying
617
+ // one. Warnings do not fail the run; the exit code still comes from errors.
618
+ if (inProcess.includes(PACKAGED_CONTENT)) {
619
+ const packagedIssues = checkPackagedAgentInstructionFiles(projectRoot, discoveredSkills);
620
+ if (packagedIssues.length > 0) {
621
+ reportPackagedContentIssues(packagedIssues, logger);
622
+ }
623
+ const packagedResult = {
624
+ name: PACKAGED_CONTENT,
625
+ status: calculateValidationStatus(packagedIssues),
626
+ issueCounts: countBySeverity(packagedIssues),
627
+ issues: packagedIssues.map(toPublishedIssue),
628
+ };
629
+ phaseResults.push(packagedResult);
630
+ }
434
631
  // Consistency check: cross-reference discovered skills vs package.json and plugin assignments
435
632
  if (inProcess.includes('consistency')) {
436
- await runConsistencyPhase(logger, phaseResults, config, projectRoot);
633
+ runConsistencyPhase(logger, phaseResults, config, projectRoot, discoveredSkills);
437
634
  }
438
635
  const duration = Date.now() - startTime;
439
636
  // Worst-wins across phases, with `system-error` outranking `error`: a phase