@vibe-agent-toolkit/cli 0.1.41-rc.7 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/bin/vat +24 -1
  2. package/dist/bin/vat.js +24 -1
  3. package/dist/bin/vat.js.map +1 -1
  4. package/dist/bin.js +23 -2
  5. package/dist/bin.js.map +1 -1
  6. package/dist/commands/agent/list.d.ts +11 -0
  7. package/dist/commands/agent/list.d.ts.map +1 -1
  8. package/dist/commands/agent/list.js +30 -11
  9. package/dist/commands/agent/list.js.map +1 -1
  10. package/dist/commands/audit/distributed-tree.d.ts +172 -0
  11. package/dist/commands/audit/distributed-tree.d.ts.map +1 -0
  12. package/dist/commands/audit/distributed-tree.js +319 -0
  13. package/dist/commands/audit/distributed-tree.js.map +1 -0
  14. package/dist/commands/audit.d.ts +69 -1
  15. package/dist/commands/audit.d.ts.map +1 -1
  16. package/dist/commands/audit.js +525 -174
  17. package/dist/commands/audit.js.map +1 -1
  18. package/dist/commands/build.d.ts +14 -0
  19. package/dist/commands/build.d.ts.map +1 -1
  20. package/dist/commands/build.js +103 -26
  21. package/dist/commands/build.js.map +1 -1
  22. package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
  23. package/dist/commands/claude/marketplace/git-publish.js +21 -3
  24. package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
  25. package/dist/commands/claude/marketplace/validate.d.ts +125 -0
  26. package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
  27. package/dist/commands/claude/marketplace/validate.js +251 -44
  28. package/dist/commands/claude/marketplace/validate.js.map +1 -1
  29. package/dist/commands/claude/org/stubs.d.ts +9 -1
  30. package/dist/commands/claude/org/stubs.d.ts.map +1 -1
  31. package/dist/commands/claude/org/stubs.js +11 -3
  32. package/dist/commands/claude/org/stubs.js.map +1 -1
  33. package/dist/commands/claude/plugin/build.d.ts +34 -4
  34. package/dist/commands/claude/plugin/build.d.ts.map +1 -1
  35. package/dist/commands/claude/plugin/build.js +187 -16
  36. package/dist/commands/claude/plugin/build.js.map +1 -1
  37. package/dist/commands/claude/plugin/plugin-files.d.ts.map +1 -1
  38. package/dist/commands/claude/plugin/plugin-files.js +6 -2
  39. package/dist/commands/claude/plugin/plugin-files.js.map +1 -1
  40. package/dist/commands/claude/plugin/plugin-json-merge.d.ts.map +1 -1
  41. package/dist/commands/claude/plugin/plugin-json-merge.js +15 -0
  42. package/dist/commands/claude/plugin/plugin-json-merge.js.map +1 -1
  43. package/dist/commands/claude/plugin/tree-copy.d.ts +40 -0
  44. package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
  45. package/dist/commands/claude/plugin/tree-copy.js +100 -4
  46. package/dist/commands/claude/plugin/tree-copy.js.map +1 -1
  47. package/dist/commands/corpus/report.d.ts +22 -4
  48. package/dist/commands/corpus/report.d.ts.map +1 -1
  49. package/dist/commands/corpus/report.js +1 -0
  50. package/dist/commands/corpus/report.js.map +1 -1
  51. package/dist/commands/corpus/runner.d.ts +16 -1
  52. package/dist/commands/corpus/runner.d.ts.map +1 -1
  53. package/dist/commands/corpus/runner.js +40 -22
  54. package/dist/commands/corpus/runner.js.map +1 -1
  55. package/dist/commands/doctor.js +1 -1
  56. package/dist/commands/doctor.js.map +1 -1
  57. package/dist/commands/inventory.d.ts +2 -0
  58. package/dist/commands/inventory.d.ts.map +1 -1
  59. package/dist/commands/inventory.js +45 -4
  60. package/dist/commands/inventory.js.map +1 -1
  61. package/dist/commands/phase-utils.d.ts +183 -14
  62. package/dist/commands/phase-utils.d.ts.map +1 -1
  63. package/dist/commands/phase-utils.js +294 -29
  64. package/dist/commands/phase-utils.js.map +1 -1
  65. package/dist/commands/positional-args.d.ts +36 -0
  66. package/dist/commands/positional-args.d.ts.map +1 -0
  67. package/dist/commands/positional-args.js +52 -0
  68. package/dist/commands/positional-args.js.map +1 -0
  69. package/dist/commands/rag/query-command.d.ts +40 -0
  70. package/dist/commands/rag/query-command.d.ts.map +1 -1
  71. package/dist/commands/rag/query-command.js +47 -20
  72. package/dist/commands/rag/query-command.js.map +1 -1
  73. package/dist/commands/resources/index.d.ts.map +1 -1
  74. package/dist/commands/resources/index.js +27 -9
  75. package/dist/commands/resources/index.js.map +1 -1
  76. package/dist/commands/resources/scan.d.ts +37 -0
  77. package/dist/commands/resources/scan.d.ts.map +1 -1
  78. package/dist/commands/resources/scan.js +47 -32
  79. package/dist/commands/resources/scan.js.map +1 -1
  80. package/dist/commands/resources/validate.d.ts +136 -1
  81. package/dist/commands/resources/validate.d.ts.map +1 -1
  82. package/dist/commands/resources/validate.js +105 -66
  83. package/dist/commands/resources/validate.js.map +1 -1
  84. package/dist/commands/skill/review-checklist.d.ts.map +1 -1
  85. package/dist/commands/skill/review-checklist.js +5 -0
  86. package/dist/commands/skill/review-checklist.js.map +1 -1
  87. package/dist/commands/skill/review.d.ts.map +1 -1
  88. package/dist/commands/skill/review.js +7 -3
  89. package/dist/commands/skill/review.js.map +1 -1
  90. package/dist/commands/skill/test/run.d.ts +1 -1
  91. package/dist/commands/skill/test/run.d.ts.map +1 -1
  92. package/dist/commands/skill/test/run.js +8 -6
  93. package/dist/commands/skill/test/run.js.map +1 -1
  94. package/dist/commands/skills/build.d.ts +349 -11
  95. package/dist/commands/skills/build.d.ts.map +1 -1
  96. package/dist/commands/skills/build.js +901 -97
  97. package/dist/commands/skills/build.js.map +1 -1
  98. package/dist/commands/skills/command-helpers.d.ts +6 -10
  99. package/dist/commands/skills/command-helpers.d.ts.map +1 -1
  100. package/dist/commands/skills/command-helpers.js +6 -22
  101. package/dist/commands/skills/command-helpers.js.map +1 -1
  102. package/dist/commands/skills/list.d.ts +18 -0
  103. package/dist/commands/skills/list.d.ts.map +1 -1
  104. package/dist/commands/skills/list.js +41 -15
  105. package/dist/commands/skills/list.js.map +1 -1
  106. package/dist/commands/skills/package.d.ts +15 -0
  107. package/dist/commands/skills/package.d.ts.map +1 -1
  108. package/dist/commands/skills/package.js +30 -20
  109. package/dist/commands/skills/package.js.map +1 -1
  110. package/dist/commands/skills/scope-guard.d.ts +64 -0
  111. package/dist/commands/skills/scope-guard.d.ts.map +1 -0
  112. package/dist/commands/skills/scope-guard.js +86 -0
  113. package/dist/commands/skills/scope-guard.js.map +1 -0
  114. package/dist/commands/skills/validate-command.d.ts.map +1 -1
  115. package/dist/commands/skills/validate-command.js +41 -7
  116. package/dist/commands/skills/validate-command.js.map +1 -1
  117. package/dist/commands/skills/validate.d.ts +44 -3
  118. package/dist/commands/skills/validate.d.ts.map +1 -1
  119. package/dist/commands/skills/validate.js +217 -53
  120. package/dist/commands/skills/validate.js.map +1 -1
  121. package/dist/commands/validate.d.ts +18 -0
  122. package/dist/commands/validate.d.ts.map +1 -1
  123. package/dist/commands/validate.js +89 -68
  124. package/dist/commands/validate.js.map +1 -1
  125. package/dist/commands/verify.d.ts +113 -6
  126. package/dist/commands/verify.d.ts.map +1 -1
  127. package/dist/commands/verify.js +440 -123
  128. package/dist/commands/verify.js.map +1 -1
  129. package/dist/index.d.ts +1 -1
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +7 -1
  132. package/dist/index.js.map +1 -1
  133. package/dist/skill-resolution/index.d.ts +1 -1
  134. package/dist/skill-resolution/index.d.ts.map +1 -1
  135. package/dist/skill-resolution/index.js +1 -1
  136. package/dist/skill-resolution/index.js.map +1 -1
  137. package/dist/skill-resolution/packaging-config.d.ts +15 -2
  138. package/dist/skill-resolution/packaging-config.d.ts.map +1 -1
  139. package/dist/skill-resolution/packaging-config.js +39 -2
  140. package/dist/skill-resolution/packaging-config.js.map +1 -1
  141. package/dist/utils/command-error.d.ts +34 -1
  142. package/dist/utils/command-error.d.ts.map +1 -1
  143. package/dist/utils/command-error.js +32 -1
  144. package/dist/utils/command-error.js.map +1 -1
  145. package/dist/utils/help-loader.d.ts +6 -2
  146. package/dist/utils/help-loader.d.ts.map +1 -1
  147. package/dist/utils/help-loader.js +9 -16
  148. package/dist/utils/help-loader.js.map +1 -1
  149. package/dist/utils/issue-rendering.d.ts +130 -0
  150. package/dist/utils/issue-rendering.d.ts.map +1 -1
  151. package/dist/utils/issue-rendering.js +147 -0
  152. package/dist/utils/issue-rendering.js.map +1 -1
  153. package/dist/utils/issue-severity.d.ts +51 -0
  154. package/dist/utils/issue-severity.d.ts.map +1 -0
  155. package/dist/utils/issue-severity.js +63 -0
  156. package/dist/utils/issue-severity.js.map +1 -0
  157. package/dist/utils/output.d.ts +54 -5
  158. package/dist/utils/output.d.ts.map +1 -1
  159. package/dist/utils/output.js +111 -17
  160. package/dist/utils/output.js.map +1 -1
  161. package/dist/utils/relativize-paths.d.ts +45 -0
  162. package/dist/utils/relativize-paths.d.ts.map +1 -0
  163. package/dist/utils/relativize-paths.js +67 -0
  164. package/dist/utils/relativize-paths.js.map +1 -0
  165. package/dist/utils/resource-loader.d.ts +21 -2
  166. package/dist/utils/resource-loader.d.ts.map +1 -1
  167. package/dist/utils/resource-loader.js +85 -11
  168. package/dist/utils/resource-loader.js.map +1 -1
  169. package/dist/utils/skill-packaging-config.d.ts +22 -1
  170. package/dist/utils/skill-packaging-config.d.ts.map +1 -1
  171. package/dist/utils/skill-packaging-config.js +26 -0
  172. package/dist/utils/skill-packaging-config.js.map +1 -1
  173. package/dist/utils/verdict-helpers.d.ts +4 -3
  174. package/dist/utils/verdict-helpers.d.ts.map +1 -1
  175. package/dist/utils/verdict-helpers.js +6 -13
  176. package/dist/utils/verdict-helpers.js.map +1 -1
  177. package/dist/version.d.ts +22 -2
  178. package/dist/version.d.ts.map +1 -1
  179. package/dist/version.js +31 -2
  180. package/dist/version.js.map +1 -1
  181. package/docs/index.md +12 -0
  182. package/docs/rag.md +9 -4
  183. package/docs/resources.md +30 -14
  184. package/docs/skills.md +25 -7
  185. package/package.json +12 -12
@@ -5,27 +5,44 @@
5
5
  * 1. vat resources validate (link integrity, collection schemas)
6
6
  * 2. vat skills validate (SKILL.md frontmatter validation)
7
7
  * 3. vat claude marketplace validate (strict marketplace validation, when configured)
8
- * 4. consistency check (skill distribution integrity — package.json, plugin assignment)
8
+ * 4. files-config-dests (in-process; every `files:` dest exists in the built output)
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)
11
+ *
12
+ * 1–3 are subprocess phases chosen by {@link selectVerifyPhases}; 4–6 run here and
13
+ * are chosen by `selectInProcessVerifyPhases`. Both sets are config-gated, and both
14
+ * are announced on startup.
9
15
  */
10
16
  import { existsSync } from 'node:fs';
11
17
  import { basename } from 'node:path';
12
18
  import { calculateValidationStatus, countBySeverity, } from '@vibe-agent-toolkit/agent-schema';
13
- import { computeTreeCopiedSkillLocations, mergeFilesConfig } from '@vibe-agent-toolkit/agent-skills';
19
+ import { computeTreeCopiedSkillLocations, detectPackagedAgentInstructionFiles, explicitFilesConfigDests, } from '@vibe-agent-toolkit/agent-skills';
14
20
  import { safePath } from '@vibe-agent-toolkit/utils';
15
21
  import { Command } from 'commander';
16
22
  import { handleCommandError } from '../utils/command-error.js';
17
23
  import { loadConfig } from '../utils/config-loader.js';
18
- import { createLogger } from '../utils/logger.js';
24
+ import { formatIssueLines } from '../utils/issue-rendering.js';
25
+ import { resolveIssueSeverity } from '../utils/issue-severity.js';
19
26
  import { writeYamlOutput } from '../utils/output.js';
20
27
  import { requireProjectRoot } from '../utils/project-root-policy.js';
28
+ import { mergeSkillPackagingConfig } from '../utils/skill-packaging-config.js';
21
29
  import { runConsistencyChecks, } from './consistency-check.js';
22
- import { aggregatePhaseStatus, exitCodeForPhases, resolveBinPath, runPhase, } from './phase-utils.js';
30
+ import { addRetiredOnlyOption, aggregatePhaseStatus, applyPhaseSelection, createPhaseContext, decidePhaseSelection, exitCodeForPhases, rejectRetiredOnly, runPhase, } from './phase-utils.js';
31
+ import { rejectPositionalArguments } from './positional-args.js';
23
32
  import { discoverSkillsFromConfig } from './skills/skill-discovery.js';
33
+ /**
34
+ * Measured full-run duration on the 90-skill / 1,041-document adopter, cited by
35
+ * the retired-`--only` message: resources 12.5s + skills 15.6s + marketplace
36
+ * 1.0s + consistency under a second.
37
+ */
38
+ const VERIFY_FULL_RUN_SECONDS = 32;
39
+ /** How this command names itself in every user-facing diagnostic. */
40
+ const COMMAND_NAME = 'vat verify';
24
41
  export function createVerifyTopLevelCommand() {
25
42
  const command = new Command('verify');
26
- command
43
+ addRetiredOnlyOption(command)
27
44
  .description('Verify built artifacts (resources + skills + marketplace + consistency); marketplace/consistency read dist/ — run after vat build')
28
- .option('--only <phase>', 'Verify only a specific phase: resources, skills, marketplace, consistency')
45
+ .option('-v, --verbose', 'Show all inspected resources, including those without issues')
29
46
  .option('--debug', 'Enable debug logging')
30
47
  .action(verifyTopLevelCommand)
31
48
  .addHelpText('after', `
@@ -36,26 +53,60 @@ Description:
36
53
  For source-only validation that needs no build (resources + skills,
37
54
  suitable for pre-commit and CI-before-build), use 'vat validate'.
38
55
 
39
- Phases:
40
- resources → link integrity, collection frontmatter schemas
41
- skills → SKILL.md frontmatter and packaging validation
42
- marketplace → strict marketplace validation (when configured)
43
- consistency → skill distribution integrity (package.json, plugin assignment)
56
+ Config-driven, exactly like 'vat validate': a phase runs only when its
57
+ config block is present. There is no phase filter — the whole run takes
58
+ about as long as its slowest two phases, so a CI gate cannot lose coverage
59
+ by naming a phase whose config key was renamed out from under it.
60
+
61
+ Phases (subprocess):
62
+ resources → link integrity, collection frontmatter schemas (when 'resources:' configured)
63
+ skills → SKILL.md frontmatter and packaging validation (when 'skills:' configured)
64
+ marketplace → strict marketplace validation (when 'claude.marketplaces:' configured)
65
+
66
+ Phases (in-process, run after the above, when 'skills:' is configured):
67
+ files-config-dests → every 'files:' dest exists in the built output. Appears
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.
72
+ consistency → skill distribution integrity (package.json, plugin assignment)
73
+
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.
77
+
78
+ The startup line on stderr names, in order, the phases that will inspect
79
+ something. A phase that would consult nothing is not listed; a phase that does
80
+ inspect its inputs is listed even when it finds nothing to report.
44
81
 
45
82
  Output:
46
- YAML summary for each phase → stdout
47
- per phase: status (success | warning | error | system-error). In-process
48
- phases (consistency, files-config-dests) also publish issueCounts
49
- {errors, warnings, info} and the findings themselves, so the archived YAML
50
- is the artifact of record rather than a summary of what stderr said.
51
- Validation errors → stderr
83
+ ONE YAML document → stdout
84
+ per phase: status (success | warning | error | system-error). A subprocess
85
+ phase's own report is captured and nested under 'report', so the whole run
86
+ is a single parseable document (a phase's stdout is never streamed
87
+ through). A phase's status comes from the child's REPORTED status, not
88
+ from its exit code — an exit code cannot express 'warning'. In-process
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.
93
+ Progress and validation errors → stderr (streamed live)
94
+
95
+ By default each subprocess phase reports a per-asset summary plus the assets
96
+ that have findings. '--verbose' is forwarded to every subprocess phase, which
97
+ then also lists the assets it inspected and found nothing to report.
52
98
 
53
99
  Exit Codes:
54
100
  0 - All phases passed (a warning does not fail the run — read status/issueCounts)
55
- 1 - Validation errors found, or '--only consistency' in a project with no
56
- skills: block (an explicit request for a phase that cannot run)
101
+ 1 - Validation errors found
57
102
  2 - System error (this command's own, or propagated from a phase that could
58
- not run: exited 2, was killed by a signal, or never spawned)
103
+ not run: exited 2, was killed by a signal, was never spawned, or wrote
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>'.
59
110
 
60
111
  Requirements:
61
112
  projectRoot: required (errors if no vibe-agent-toolkit.config.yaml or .git/ ancestor)
@@ -64,9 +115,7 @@ Requirements:
64
115
  See docs/concepts/roots-and-config.md for terminology.
65
116
 
66
117
  Example:
67
- $ vat verify # Verify everything
68
- $ vat verify --only skills # Verify skills only
69
- $ vat verify --only marketplace # Verify marketplace only
118
+ $ vat verify # Verify every configured phase
70
119
  `);
71
120
  return command;
72
121
  }
@@ -77,38 +126,73 @@ Example:
77
126
  function skillNameToFsPath(name) {
78
127
  return name.replaceAll(':', '__');
79
128
  }
129
+ /** The `files:` entries governing a candidate, `[]` when the skill declares none. */
130
+ function filesOf(entry) {
131
+ return entry.packaging.files ?? [];
132
+ }
80
133
  /**
81
- * Register a check for (skillName, outputDir, mergedFiles) in the dedup map.
134
+ * Register a check for (skillName, outputDir, packaging) in the dedup map.
82
135
  *
83
136
  * Skips silently if:
84
- * - mergedFiles is empty (no entries to verify)
85
137
  * - outputDir does not exist on disk (not a candidate)
86
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.
87
145
  */
88
- function tryAddCheckEntry(checks, skillName, outputDir, mergedFiles) {
89
- if (mergedFiles.length === 0)
90
- return;
146
+ function tryAddCheckEntry(checks, skillName, outputDir, packaging) {
91
147
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- outputDir is resolved from config, not user input
92
148
  if (!existsSync(outputDir))
93
149
  return;
94
150
  const key = `${skillName}\0${outputDir}`;
95
151
  if (!checks.has(key)) {
96
- checks.set(key, { skillName, outputDir, mergedFiles });
152
+ checks.set(key, { skillName, outputDir, packaging });
97
153
  }
98
154
  }
99
155
  /**
100
- * 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.
101
158
  *
102
- * Checks each skill's dests in the location(s) where `vat build` actually wrote them:
103
- * - Pool skills: `dist/skills/<fsName>/` (only when that dir exists)
104
- * - 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>/`
105
163
  *
106
- * A dest is "missing" ONLY when absent from a candidate dir that exists. If a skill
107
- * 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.
108
167
  *
109
- * @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.
110
194
  */
111
- export function checkFilesConfigDests(cwd) {
195
+ function collectBuiltSkillOutputs(cwd, discovered) {
112
196
  try {
113
197
  const config = loadConfig(cwd);
114
198
  if (!config)
@@ -117,12 +201,15 @@ export function checkFilesConfigDests(cwd) {
117
201
  const defaults = skillsConfig?.defaults;
118
202
  // Dedup map: key = `skillName\0outputDir` → check entry
119
203
  const checks = new Map();
120
- // --- Pool/config skills: candidate dir is dist/skills/<fsName> ---
121
- 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) {
122
210
  const perSkill = skillsConfig?.config?.[skillName];
123
- const mergedFiles = mergeFilesConfig(defaults?.files, perSkill?.files);
124
211
  const outputDir = safePath.resolve(cwd, 'dist', 'skills', skillNameToFsPath(skillName));
125
- tryAddCheckEntry(checks, skillName, outputDir, mergedFiles);
212
+ tryAddCheckEntry(checks, skillName, outputDir, mergeSkillPackagingConfig(defaults, perSkill));
126
213
  }
127
214
  // --- Tree-copy skills: candidate dirs are plugin output skill dirs ---
128
215
  for (const loc of computeTreeCopiedSkillLocations(config, cwd)) {
@@ -130,52 +217,116 @@ export function checkFilesConfigDests(cwd) {
130
217
  // path (`group/nested-skill` for a nested skill), so try its trailing segment
131
218
  // too — the spelling that matches for every skill whose dir is named after it.
132
219
  const dirLeaf = basename(loc.skillDirPath);
133
- const perSkill = skillsConfig?.config?.[loc.skillDirPath] ?? skillsConfig?.config?.[dirLeaf];
134
- const mergedFiles = mergeFilesConfig(defaults?.files, perSkill?.files);
135
- tryAddCheckEntry(checks, loc.skillDirPath, loc.skillOutputDir, mergedFiles);
220
+ const perSkill = (skillsConfig?.config?.[loc.skillDirPath] ?? skillsConfig?.config?.[dirLeaf]);
221
+ tryAddCheckEntry(checks, loc.skillDirPath, loc.skillOutputDir, mergeSkillPackagingConfig(defaults, perSkill));
136
222
  }
137
- // --- Run each pending check and collect results ---
138
- const results = [];
139
- for (const { skillName, outputDir, mergedFiles } of checks.values()) {
140
- const missing = [];
141
- for (const entry of mergedFiles) {
142
- const destPath = safePath.resolve(outputDir, entry.dest);
143
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- destPath resolved from config
144
- if (!existsSync(destPath)) {
145
- missing.push(entry.dest);
146
- }
147
- }
148
- if (missing.length > 0) {
149
- results.push({ skillName, outputDir, missing });
150
- }
151
- }
152
- return results;
223
+ return [...checks.values()];
153
224
  }
154
225
  catch {
155
226
  return [];
156
227
  }
157
228
  }
158
229
  /**
159
- * Check whether the current project has claude.marketplaces configured.
160
- * Returns the marketplace names if present, empty array otherwise.
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.
161
238
  */
162
- function getClaudeMarketplaceNames() {
163
- try {
164
- const config = loadConfig(process.cwd());
165
- if (config?.claude?.marketplaces) {
166
- return Object.keys(config.claude.marketplaces);
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 });
167
256
  }
168
- return [];
169
257
  }
170
- catch {
171
- return [];
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));
172
305
  }
306
+ return issues;
173
307
  }
308
+ /**
309
+ * Load the project config without letting a broken one abort the command.
310
+ *
311
+ * A config that exists but does not parse is reported as `error` rather than
312
+ * swallowed as "no config": `vat verify` must not answer "that phase is not
313
+ * configured" when it could not read the configuration at all.
314
+ */
315
+ function loadConfigTolerant(cwd) {
316
+ try {
317
+ return { config: loadConfig(cwd) };
318
+ }
319
+ catch (error) {
320
+ return { config: undefined, error: error instanceof Error ? error.message : String(error) };
321
+ }
322
+ }
323
+ /** The in-process phase that checks `files:` dests against the built output. */
324
+ const FILES_CONFIG_DESTS = 'files-config-dests';
174
325
  /**
175
326
  * Log files-config-dests errors to stderr.
176
327
  */
177
328
  function reportFilesDestErrors(results, logger) {
178
- logger.error('\n▶ Phase: files-config-dests');
329
+ logger.error(`\n▶ Phase: ${FILES_CONFIG_DESTS}`);
179
330
  for (const { skillName, outputDir, missing } of results) {
180
331
  logger.error(` Skill '${skillName}': missing dest file(s) in ${outputDir}/:`);
181
332
  for (const dest of missing) {
@@ -183,6 +334,25 @@ function reportFilesDestErrors(results, logger) {
183
334
  }
184
335
  }
185
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
+ }
186
356
  /**
187
357
  * Log consistency check issues to stderr.
188
358
  */
@@ -203,27 +373,141 @@ function reportConsistencyIssues(issues, logger) {
203
373
  logger.info(` INFO [${issue.code}]: ${issue.message}`);
204
374
  }
205
375
  }
206
- function buildPhaseList(options) {
207
- const { only } = options;
376
+ /**
377
+ * Phases `vat verify` knows how to run, in stable execution order.
378
+ *
379
+ * `PhaseVocabulary.validNames` is what `--only` was checked against; with
380
+ * `--only` retired from this command it is documentation only — the arm that
381
+ * reads it in {@link decidePhaseSelection} is unreachable from here.
382
+ */
383
+ const VALID_PHASES = ['resources', 'skills', 'marketplace', 'consistency'];
384
+ const VERIFY_VOCABULARY = {
385
+ noun: 'Phase',
386
+ verb: 'verify',
387
+ validNames: VALID_PHASES,
388
+ noop: {
389
+ warning: 'No resources:, skills: or claude.marketplaces: block found in vibe-agent-toolkit.config.yaml — nothing to verify. If this is unexpected, check your config.',
390
+ note: 'No configured phases (no resources, skills or claude.marketplaces block in vibe-agent-toolkit.config.yaml).',
391
+ },
392
+ };
393
+ /**
394
+ * Decide which verification phases to run.
395
+ *
396
+ * Config-gated, exactly as `vat validate` is. It used to push `resources` and
397
+ * `skills` unconditionally, so `vat verify --only skills` in a project with no
398
+ * `skills:` block exited 0 while `vat validate --only skills` on the same
399
+ * project exited 1 — same question, opposite verdicts, and a CI gate pinned to
400
+ * the verify form stayed green forever the moment the config key was renamed.
401
+ *
402
+ * `--only` is gone from this command entirely. Measured on a 90-skill project a
403
+ * full `vat verify` is ~32s, of which the two slowest phases are ~28s — the
404
+ * filter bought at most ~18s and repeatedly bought a wrong answer with it. Every
405
+ * run is now a whole run, and `only` is passed to {@link decidePhaseSelection}
406
+ * as `undefined` (that helper still routes `--only` for `vat validate` and
407
+ * `vat build`).
408
+ *
409
+ * @param configError - The config-load failure, when the config could not be
410
+ * read. Every subprocess phase still runs so the CHILD reports the real
411
+ * config error (exit 2) instead of this command guessing.
412
+ * @param verbose - Forwarded to each subprocess phase as `--verbose`. The
413
+ * children own their own summarization; this command only relays the request.
414
+ */
415
+ export function selectVerifyPhases(config, configError, verbose) {
208
416
  const phases = [];
209
- if (!only || only === 'resources') {
210
- phases.push({ name: 'resources', args: ['resources', 'validate'] });
417
+ const unreadable = configError !== undefined;
418
+ const detail = verbose === true ? ['--verbose'] : [];
419
+ if (unreadable || config?.resources) {
420
+ phases.push({ name: 'resources', args: ['resources', 'validate', ...detail] });
211
421
  }
212
- if (!only || only === 'skills') {
213
- phases.push({ name: 'skills', args: ['skills', 'validate'] });
422
+ if (unreadable || config?.skills) {
423
+ phases.push({ name: 'skills', args: ['skills', 'validate', ...detail] });
214
424
  }
215
- if (!only || only === 'marketplace') {
216
- // Only add marketplace phase(s) when config exists and has marketplaces
217
- const marketplaceNames = getClaudeMarketplaceNames();
218
- for (const name of marketplaceNames) {
219
- const marketplaceBuildPath = `dist/.claude/plugins/marketplaces/${name}`;
220
- phases.push({
221
- name: `marketplace:${name}`,
222
- args: ['claude', 'marketplace', 'validate', marketplaceBuildPath],
223
- });
224
- }
425
+ for (const name of Object.keys(config?.claude?.marketplaces ?? {})) {
426
+ phases.push({
427
+ name: `marketplace:${name}`,
428
+ args: [
429
+ 'claude',
430
+ 'marketplace',
431
+ 'validate',
432
+ `dist/.claude/plugins/marketplaces/${name}`,
433
+ ...detail,
434
+ ],
435
+ });
225
436
  }
226
- return phases;
437
+ return decidePhaseSelection(undefined, phases, VERIFY_VOCABULARY, {
438
+ unreadableConfig: configError,
439
+ });
440
+ }
441
+ /** The in-process phase that crawls built skill bundles for what must not ship. */
442
+ const PACKAGED_CONTENT = 'packaged-content';
443
+ /**
444
+ * Which in-process phases this run performs, given the config.
445
+ *
446
+ * The SINGLE source for that question: {@link verifyTopLevelCommand} gates
447
+ * execution on this list and {@link formatVerifyAnnouncement} announces the same
448
+ * list, so the printed phase list cannot drift from the phases that run. It used
449
+ * to be announced from the subprocess phases alone while these two were gated by
450
+ * hand-written conditions further down, so a run printed '(phases: skills)' and
451
+ * then also ran `consistency`, which put a second entry in the emitted document.
452
+ * A status that under-reports what it did is the same defect class as
453
+ * {@link selectVerifyPhases}' silent exit-0 pass.
454
+ *
455
+ * The contract is **the phases that will inspect something** — not "code paths
456
+ * this run enters". An earlier fix traded the under-reporting for
457
+ * over-reporting: on a project with `resources:` and no `skills:`, a run
458
+ * announced 'resources → files-config-dests → consistency' and emitted a
459
+ * document holding `resources` and nothing else, so an operator read a claim
460
+ * that distribution consistency had been checked. Both in-process phases read
461
+ * the same input, the `skills:` block — without it {@link checkFilesConfigDests}
462
+ * has no `files:` entry to resolve (both `defaults.files` and
463
+ * `config.<skill>.files` live under `skills:`, so every merge is empty) and
464
+ * {@link runConsistencyPhase} returns before its first lookup. Neither can
465
+ * produce a finding, so neither is named.
466
+ *
467
+ * This is a truthfulness change, not a behaviour change: the condition here is
468
+ * the one the phases already applied internally, hoisted so the announcement can
469
+ * see it.
470
+ *
471
+ * Note "will inspect something" is not a prediction of the findings:
472
+ * `files-config-dests` is named whenever a `skills:` block engages it, even
473
+ * though a clean run reports nothing.
474
+ */
475
+ function selectInProcessVerifyPhases(config) {
476
+ return config?.skills === undefined ? [] : [FILES_CONFIG_DESTS, PACKAGED_CONTENT, 'consistency'];
477
+ }
478
+ /**
479
+ * The startup announcement: every phase this run will inspect something with,
480
+ * in order. A phase that would consult nothing is not named.
481
+ */
482
+ export function formatVerifyAnnouncement(subprocessPhaseNames, config) {
483
+ const all = [...subprocessPhaseNames, ...selectInProcessVerifyPhases(config)];
484
+ return `🔍 vat verify (phases: ${all.join(' → ')})`;
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
+ };
227
511
  }
228
512
  /**
229
513
  * `ConsistencyIssue` speaks the same severity vocabulary as `ValidationIssue`
@@ -246,27 +530,26 @@ function asValidationIssues(issues) {
246
530
  * to go to stderr only, so the archived YAML — the artifact of record — said
247
531
  * nothing happened. And a warning-only run reported `passed`, which is the
248
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".
249
537
  */
250
- async function runConsistencyPhase(logger, phaseResults, explicitlyRequested) {
251
- const config = loadConfig(process.cwd());
538
+ function runConsistencyPhase(logger, phaseResults, config, projectRoot, discoveredSkills) {
252
539
  if (!config?.skills) {
253
- // Nothing to cross-reference. Inside a full `vat verify` that is a genuine
254
- // no-op; but `--only consistency` asked for THIS phase specifically, and
255
- // answering an explicit request with an empty phase list and `success` is
256
- // the same silent pass `vat validate --only <unconfigured surface>`
257
- // deliberately refuses to give.
258
- if (explicitlyRequested) {
259
- logger.error("Phase 'consistency' needs a skills: block in vibe-agent-toolkit.config.yaml — there is nothing to cross-reference.");
260
- phaseResults.push({
261
- name: 'consistency',
262
- status: 'error',
263
- error: "No skills: block in vibe-agent-toolkit.config.yaml — 'consistency' has nothing to check.",
264
- });
265
- }
540
+ // Nothing to cross-reference, so nothing to report: a run without a
541
+ // `skills:` block is a genuine no-op, and {@link selectInProcessVerifyPhases}
542
+ // has already decided not to name this phase. There used to be a second arm
543
+ // here that pushed an ERROR result instead — because `--only consistency`
544
+ // had asked for THIS phase specifically, and answering an explicit request
545
+ // with an empty phase list and `success` is the same silent pass
546
+ // `vat validate --only <unconfigured surface>` refuses to give. With
547
+ // `--only` retired from `vat verify` there is no way to ask for this phase
548
+ // specifically, so that arm went with it. This guard remains reachable only
549
+ // defensively (and narrows `config.skills` for the call below).
266
550
  return;
267
551
  }
268
- const discoveredSkills = await discoverSkillsFromConfig(config.skills, process.cwd());
269
- const consistencyResult = runConsistencyChecks(discoveredSkills, config, process.cwd());
552
+ const consistencyResult = runConsistencyChecks([...discoveredSkills], config, projectRoot);
270
553
  const issues = consistencyResult.issues;
271
554
  if (issues.length > 0) {
272
555
  reportConsistencyIssues(issues, logger);
@@ -276,44 +559,78 @@ async function runConsistencyPhase(logger, phaseResults, explicitlyRequested) {
276
559
  name: 'consistency',
277
560
  status: calculateValidationStatus(asValidation),
278
561
  issueCounts: countBySeverity(asValidation),
279
- issues: issues.map(({ severity, code, message, fix }) => ({ severity, code, message, fix })),
562
+ issues: asValidation.map(toPublishedIssue),
280
563
  };
281
564
  phaseResults.push(result);
282
565
  }
283
- 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');
571
+ // Before requireProjectRoot: a retired flag is a usage error, and answering it
572
+ // with "no vibe-agent-toolkit.config.yaml found" would diagnose the wrong
573
+ // problem for anyone running the old invocation outside a project.
574
+ rejectRetiredOnly(options.only, COMMAND_NAME, VERIFY_FULL_RUN_SECONDS);
284
575
  // Spec §7: `vat verify` requires a projectRoot.
285
- requireProjectRoot(process.cwd(), 'vat verify');
286
- const phases = buildPhaseList(options);
287
- // Consistency is an in-process phase, not a subprocess. Allow --only consistency
288
- // to produce an empty subprocess phase list without throwing.
289
- if (phases.length === 0 && options.only !== 'consistency') {
290
- throw new Error(`Unknown phase: ${options.only ?? ''}. Valid phases: resources, skills, marketplace, consistency`);
291
- }
292
- const logger = createLogger(options.debug ? { debug: true } : {});
293
- const startTime = Date.now();
294
- const binPath = resolveBinPath();
576
+ const projectRoot = requireProjectRoot(process.cwd(), COMMAND_NAME);
577
+ const { logger, startTime, binPath } = createPhaseContext(options.debug);
295
578
  try {
296
- logger.info(`🔍 vat verify (phases: ${phases.map((p) => p.name).join(' → ')})`);
579
+ // Inside the try, deliberately: phase selection used to throw from out here
580
+ // (on an unroutable `--only`), so the user got a raw Node stack trace and
581
+ // zero bytes of the structured document a scripted caller parses.
582
+ const { config, error: configError } = loadConfigTolerant(projectRoot);
583
+ const phases = applyPhaseSelection(selectVerifyPhases(config, configError, options.verbose), logger, startTime);
584
+ // Announced from the same list the in-process gates below read, so the
585
+ // printed phases and the executed phases cannot disagree.
586
+ const inProcess = selectInProcessVerifyPhases(config);
587
+ logger.info(formatVerifyAnnouncement(phases.map((p) => p.name), config));
297
588
  const phaseResults = [];
298
589
  for (const phase of phases) {
299
590
  logger.info(`\n▶ Phase: ${phase.name}`);
300
591
  phaseResults.push(runPhase(binPath, phase));
301
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
+ : [];
302
601
  // Post-build files config check: verify all dest paths exist in built output
303
- if (!options.only || options.only === 'skills') {
304
- const filesDestResults = checkFilesConfigDests(process.cwd());
602
+ if (inProcess.includes(FILES_CONFIG_DESTS)) {
603
+ const filesDestResults = checkFilesConfigDests(projectRoot, discoveredSkills);
305
604
  if (filesDestResults.length > 0) {
306
605
  reportFilesDestErrors(filesDestResults, logger);
307
606
  phaseResults.push({
308
- name: 'files-config-dests',
607
+ name: FILES_CONFIG_DESTS,
309
608
  status: 'error',
310
609
  issueCounts: { errors: filesDestResults.length, warnings: 0, info: 0 },
311
610
  });
312
611
  }
313
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
+ }
314
631
  // Consistency check: cross-reference discovered skills vs package.json and plugin assignments
315
- if (!options.only || options.only === 'consistency' || options.only === 'skills') {
316
- await runConsistencyPhase(logger, phaseResults, options.only === 'consistency');
632
+ if (inProcess.includes('consistency')) {
633
+ runConsistencyPhase(logger, phaseResults, config, projectRoot, discoveredSkills);
317
634
  }
318
635
  const duration = Date.now() - startTime;
319
636
  // Worst-wins across phases, with `system-error` outranking `error`: a phase