@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.
- package/dist/bin.js +11 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/audit/distributed-tree.d.ts +172 -0
- package/dist/commands/audit/distributed-tree.d.ts.map +1 -0
- package/dist/commands/audit/distributed-tree.js +319 -0
- package/dist/commands/audit/distributed-tree.js.map +1 -0
- package/dist/commands/audit.d.ts +35 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +312 -93
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +16 -4
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.js +21 -3
- package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
- package/dist/commands/claude/marketplace/validate.d.ts +31 -3
- package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/validate.js +105 -12
- package/dist/commands/claude/marketplace/validate.js.map +1 -1
- package/dist/commands/claude/plugin/build.d.ts +9 -2
- package/dist/commands/claude/plugin/build.d.ts.map +1 -1
- package/dist/commands/claude/plugin/build.js +90 -8
- package/dist/commands/claude/plugin/build.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-files.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-files.js +6 -2
- package/dist/commands/claude/plugin/plugin-files.js.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.d.ts +40 -0
- package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.js +100 -4
- package/dist/commands/claude/plugin/tree-copy.js.map +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/positional-args.d.ts +36 -0
- package/dist/commands/positional-args.d.ts.map +1 -0
- package/dist/commands/positional-args.js +52 -0
- package/dist/commands/positional-args.js.map +1 -0
- package/dist/commands/skill/review-checklist.d.ts.map +1 -1
- package/dist/commands/skill/review-checklist.js +5 -0
- package/dist/commands/skill/review-checklist.js.map +1 -1
- package/dist/commands/skills/build.d.ts +258 -9
- package/dist/commands/skills/build.d.ts.map +1 -1
- package/dist/commands/skills/build.js +698 -111
- package/dist/commands/skills/build.js.map +1 -1
- package/dist/commands/skills/scope-guard.d.ts +64 -0
- package/dist/commands/skills/scope-guard.d.ts.map +1 -0
- package/dist/commands/skills/scope-guard.js +86 -0
- package/dist/commands/skills/scope-guard.js.map +1 -0
- package/dist/commands/skills/validate-command.d.ts.map +1 -1
- package/dist/commands/skills/validate-command.js +19 -1
- package/dist/commands/skills/validate-command.js.map +1 -1
- package/dist/commands/skills/validate.d.ts.map +1 -1
- package/dist/commands/skills/validate.js +37 -7
- package/dist/commands/skills/validate.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +18 -4
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.d.ts +77 -7
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +254 -57
- package/dist/commands/verify.js.map +1 -1
- package/dist/utils/issue-rendering.d.ts +42 -0
- package/dist/utils/issue-rendering.d.ts.map +1 -1
- package/dist/utils/issue-rendering.js +53 -0
- package/dist/utils/issue-rendering.js.map +1 -1
- package/dist/utils/issue-severity.d.ts +51 -0
- package/dist/utils/issue-severity.d.ts.map +1 -0
- package/dist/utils/issue-severity.js +63 -0
- package/dist/utils/issue-severity.js.map +1 -0
- package/dist/version.d.ts +22 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +31 -2
- package/dist/version.js.map +1 -1
- package/docs/index.md +12 -0
- package/docs/skills.md +16 -2
- package/package.json +15 -12
package/dist/commands/audit.js
CHANGED
|
@@ -4,23 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
6
|
import { existsSync as fsExistsSync } from 'node:fs';
|
|
7
|
+
import { basename } from 'node:path';
|
|
7
8
|
import { calculateValidationStatus, countBySeverity, } from '@vibe-agent-toolkit/agent-schema';
|
|
8
9
|
import { crawlAndResolveRegistry, detectDeclaredButMissing, detectMarketplacePluginSourceMissing, resetPackagingRegistryCache, detectPresentButUndeclared, detectReferenceTargetMissing, detectResourceFormat, enumerateSurfaces, validate, validateMarketplace, validateSkill, validateSkillForPackaging, } from '@vibe-agent-toolkit/agent-skills';
|
|
9
10
|
import { analyzeCompatibility, checkSettingsCompatibility, detectSkillClaudePluginNameMismatch, crawlSkillLinkRegistry, extractClaudeMarketplaceInventory, extractClaudePluginInventory, getClaudeUserPaths, readEffectiveSettings, validatePlugin, } from '@vibe-agent-toolkit/claude-marketplace';
|
|
10
11
|
import { detectFormat } from '@vibe-agent-toolkit/discovery';
|
|
11
|
-
import { findProjectRoot, gitFindRoot,
|
|
12
|
+
import { findProjectRoot, gitFindRoot, isAbsolutePath, isGitUrl, issueLocation, parseGitUrl, resetProjectRootCaches, safePath, } from '@vibe-agent-toolkit/utils';
|
|
12
13
|
import { Command } from 'commander';
|
|
13
14
|
import picomatch from 'picomatch';
|
|
14
15
|
import { resetSkillDiscoveryCache, resolveProjectDeclaredEvalSuites, resolveSkillPackagingConfig, stripValidationAllowForDisplay, } from '../skill-resolution/packaging-config.js';
|
|
15
16
|
import { handleCommandError } from '../utils/command-error.js';
|
|
16
17
|
import { ConfigLoadError, loadConfig, resetLoadedConfigCache, } from '../utils/config-loader.js';
|
|
17
|
-
import { formatIssueLines, formatSeverityBreakdown, issuesToRenderAtVerbosity, } from '../utils/issue-rendering.js';
|
|
18
|
+
import { countCollapsedFindings, formatCollapsedFindingsHint, formatIssueLines, formatSeverityBreakdown, issuesToRenderAtVerbosity, } from '../utils/issue-rendering.js';
|
|
19
|
+
import { resolveIssueSeverity } from '../utils/issue-severity.js';
|
|
18
20
|
import { createLogger } from '../utils/logger.js';
|
|
19
21
|
import { writeYamlOutput } from '../utils/output.js';
|
|
20
22
|
import { relativizePathEntries } from '../utils/relativize-paths.js';
|
|
21
23
|
import { mergeSkillPackagingConfig } from '../utils/skill-packaging-config.js';
|
|
22
24
|
import { renderSkillQualityFooter } from '../utils/skill-quality-footer.js';
|
|
23
25
|
import { computeConfigVerdicts } from '../utils/verdict-helpers.js';
|
|
26
|
+
import { crawlOwnsSubtree, distributedTreeFindings, getOrCreateGitTracker, isWithin, resetGitTrackerCache, } from './audit/distributed-tree.js';
|
|
24
27
|
import { withClonedRepo } from './audit/git-url-clone.js';
|
|
25
28
|
import { buildHierarchicalOutput } from './audit/hierarchical-output.js';
|
|
26
29
|
import { renderProvenanceHeader, rewritePathsInResults, } from './audit/provenance.js';
|
|
@@ -150,8 +153,15 @@ function packagingResultToValidationResult(skillPath, result, configTargets, loc
|
|
|
150
153
|
/**
|
|
151
154
|
* Validate a single SKILL.md file, using config-aware validation if available.
|
|
152
155
|
* Used for direct SKILL.md and vat-agent paths passed to `vat audit`.
|
|
156
|
+
*
|
|
157
|
+
* @param crawlTree Whether this call owns the presence-side crawl of the skill's
|
|
158
|
+
* directory (see {@link appendDistributedTreeFindings} below). REQUIRED
|
|
159
|
+
* and never defaulted, because the wrong answer is silent in both directions: a
|
|
160
|
+
* skill nested inside a plugin is already crawled by `validatePlugin` at any
|
|
161
|
+
* depth, so crawling again reports one file twice, while omitting it for a
|
|
162
|
+
* standalone bundle is the blindness this parameter exists to end.
|
|
153
163
|
*/
|
|
154
|
-
async function validateSingleSkill(skillPath, options, logger, locationRoot, isVATGenerated) {
|
|
164
|
+
async function validateSingleSkill(skillPath, options, logger, locationRoot, crawlTree, isVATGenerated) {
|
|
155
165
|
// Try config-aware validation: walk UP to the skill's nearest-ancestor
|
|
156
166
|
// vibe-agent-toolkit.config.yaml and apply the skill's packaging block. A
|
|
157
167
|
// broken governing config is tolerated here (audit is a bulk linter): log it
|
|
@@ -187,7 +197,9 @@ async function validateSingleSkill(skillPath, options, logger, locationRoot, isV
|
|
|
187
197
|
sharedCtx.gitTracker = gitTracker;
|
|
188
198
|
}
|
|
189
199
|
const packagingResult = await validateSkillForPackaging(skillPath, skillConfig, 'source', sharedCtx);
|
|
190
|
-
|
|
200
|
+
const configAware = packagingResultToValidationResult(skillPath, packagingResult, skillConfig.targets, locationRoot);
|
|
201
|
+
await appendDistributedTreeFindings(configAware, skillPath, locationRoot, crawlTree);
|
|
202
|
+
return configAware;
|
|
191
203
|
}
|
|
192
204
|
// Fallback: basic validation
|
|
193
205
|
const validateOptions = { skillPath, locationRoot };
|
|
@@ -197,7 +209,35 @@ async function validateSingleSkill(skillPath, options, logger, locationRoot, isV
|
|
|
197
209
|
if (isVATGenerated === true) {
|
|
198
210
|
validateOptions.isVATGenerated = true;
|
|
199
211
|
}
|
|
200
|
-
|
|
212
|
+
const result = await validateSkill(validateOptions);
|
|
213
|
+
await appendDistributedTreeFindings(result, skillPath, locationRoot, crawlTree);
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Crawl a DISTRIBUTED skill's own directory for agent-instruction files and
|
|
218
|
+
* append what it finds. Mutates `result`, like every other additive detector
|
|
219
|
+
* pass in this command.
|
|
220
|
+
*
|
|
221
|
+
* Which trees count as distributed — and why the config's declaration is NOT the
|
|
222
|
+
* discriminator any more — lives with the classifier in
|
|
223
|
+
* `./audit/distributed-tree.ts` (`classifyScannedSkillTree`). In short: a repository that never adopted
|
|
224
|
+
* VAT declares nothing, and an adopting repo's `include` globs never enumerate
|
|
225
|
+
* its drafts and fixtures, so "not declared" swept up two populations of
|
|
226
|
+
* ordinary repo source.
|
|
227
|
+
*
|
|
228
|
+
* Called from BOTH lanes — config-aware and wild — deliberately. It used to be
|
|
229
|
+
* reachable only from the wild one, which left the abandoned discriminator alive
|
|
230
|
+
* at the caller in its purest suppressing form: adding a `vibe-agent-toolkit.config.yaml`
|
|
231
|
+
* to a dotfiles repo removed findings from skills installed under `~/.claude`,
|
|
232
|
+
* because a config-aware match returned before the classifier was ever asked.
|
|
233
|
+
* Whether the project also declares a skill says nothing about where that skill
|
|
234
|
+
* lives, so it must not decide whether the question gets asked.
|
|
235
|
+
*
|
|
236
|
+
* @param crawlTree `false` when a plugin lane already crawled this subtree — see
|
|
237
|
+
* {@link validateSingleSkill}.
|
|
238
|
+
*/
|
|
239
|
+
async function appendDistributedTreeFindings(result, skillPath, locationRoot, crawlTree) {
|
|
240
|
+
appendIssues(result, await distributedTreeFindings(skillPath, locationRoot, crawlTree));
|
|
201
241
|
}
|
|
202
242
|
/**
|
|
203
243
|
* Collect repeated option values into an array (used for --exclude)
|
|
@@ -218,7 +258,12 @@ export function createAuditCommand() {
|
|
|
218
258
|
.option('--exclude <glob>', 'Exclude paths matching glob pattern (repeatable)', collect, [])
|
|
219
259
|
.option('--include-artifacts', 'Include gitignored paths (build artifacts, dependencies) that are excluded by default')
|
|
220
260
|
.option('--user', 'Audit user-level Claude resources (default: $CLAUDE_CONFIG_DIR or ~/.claude — scans plugins/, skills/, marketplaces/)')
|
|
221
|
-
|
|
261
|
+
// `-v` short form: `vat audit -v` used to print the version (the root
|
|
262
|
+
// `-v, --version` shadowed every subcommand), and after that alias was
|
|
263
|
+
// removed it exited 1 as an unknown option — while five sibling verbs
|
|
264
|
+
// accepted it. Advertising only the long form is what made the CHANGELOG's
|
|
265
|
+
// "`-v` means `--verbose` on every verb" claim untrue.
|
|
266
|
+
.option('-v, --verbose', 'Show all scanned resources, including those without issues')
|
|
222
267
|
.option('--warn-unreferenced-files', 'Warn about files not referenced in skill markdown')
|
|
223
268
|
.option('--compat', 'Run compatibility analysis for each plugin (shows claude-code, claude-cowork, claude-chat support)')
|
|
224
269
|
.option('--settings [file]', 'Check plugins against Claude settings (auto-discover or specify file; requires --compat)')
|
|
@@ -311,13 +356,17 @@ Gitignore-Aware Scanning:
|
|
|
311
356
|
Outside a git repository, no automatic exclusions apply.
|
|
312
357
|
|
|
313
358
|
Config-Aware Validation:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
359
|
+
The governing vibe-agent-toolkit.config.yaml is found by walking UP from
|
|
360
|
+
the audited path, so a built bundle deep under dist/ is still governed by
|
|
361
|
+
its project's config. Audit uses that project's build settings
|
|
362
|
+
(linkFollowDepth, files, excludeReferencesFromBundle) to validate skills,
|
|
363
|
+
which prevents false warnings for links the build pipeline resolves.
|
|
318
364
|
|
|
319
365
|
Config-aware mode never applies validation.allow — audit always shows
|
|
320
|
-
all issues. validation.severity
|
|
366
|
+
all issues. validation.severity IS applied: a code set to 'ignore' is
|
|
367
|
+
hidden. skills.defaults.validation.severity applies project-wide (skills,
|
|
368
|
+
plugins and marketplaces alike); skills.config.<name>.validation.severity
|
|
369
|
+
layers on top for that skill.
|
|
321
370
|
|
|
322
371
|
Exit Codes:
|
|
323
372
|
0 - Always (even when validation errors are surfaced)
|
|
@@ -335,6 +384,37 @@ Examples:
|
|
|
335
384
|
`);
|
|
336
385
|
return audit;
|
|
337
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Which of the Claude install roots a `--user` run actually walks.
|
|
389
|
+
*
|
|
390
|
+
* The three roots read as siblings and are NOT: `marketplacesDir` is
|
|
391
|
+
* `<claudeDir>/plugins/marketplaces`, i.e. INSIDE `pluginsDir`. A recursive walk
|
|
392
|
+
* of `plugins/` already reaches every installed marketplace, so scanning
|
|
393
|
+
* `marketplaces/` afterwards audited each marketplace-installed plugin and skill
|
|
394
|
+
* a SECOND time, in a second result, under the identical `path:`. On one real
|
|
395
|
+
* machine that turned 7 distinct agent-instruction files into 12 reported rows —
|
|
396
|
+
* and it inflates every finding class, not one code, because the whole subject
|
|
397
|
+
* is re-audited rather than one detector re-firing.
|
|
398
|
+
*
|
|
399
|
+
* The containment test is `isWithin`, the same one the provenance classifier
|
|
400
|
+
* uses, so "inside" means the same thing on both sides and on Windows.
|
|
401
|
+
*
|
|
402
|
+
* Gated on `recursive`, and that is not defensive dressing: under
|
|
403
|
+
* `--no-recursive` a walk of `plugins/` stops at its top level and never reaches
|
|
404
|
+
* `marketplaces/`, so dropping the nested root there would replace a double
|
|
405
|
+
* count with no coverage at all.
|
|
406
|
+
*
|
|
407
|
+
* Pure, and exported for that reason: the shape of the defect is "the list of
|
|
408
|
+
* roots was wrong", which is assertable as a list.
|
|
409
|
+
*
|
|
410
|
+
* @internal Exported for unit testing only — not part of the public CLI API.
|
|
411
|
+
*/
|
|
412
|
+
export function userScanTargets(candidates, recursive) {
|
|
413
|
+
const resolved = candidates.map((dir) => safePath.resolve(dir));
|
|
414
|
+
if (!recursive)
|
|
415
|
+
return resolved;
|
|
416
|
+
return resolved.filter((dir) => !resolved.some((other) => other !== dir && isWithin(other, dir)));
|
|
417
|
+
}
|
|
338
418
|
/**
|
|
339
419
|
* Handle --user audit: scans ~/.claude/plugins, ~/.claude/skills, ~/.claude/marketplaces
|
|
340
420
|
* and outputs hierarchical YAML. Calls process.exit() when done.
|
|
@@ -359,17 +439,16 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
359
439
|
process.exit(2);
|
|
360
440
|
}
|
|
361
441
|
const results = [];
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
results.push(...await getValidationResults(marketplacesDir, recursive, options, logger, scanRoot));
|
|
442
|
+
// The roots that exist, minus any one another already covers — see
|
|
443
|
+
// {@link userScanTargets}. `marketplaces/` lives inside `plugins/`.
|
|
444
|
+
const present = [
|
|
445
|
+
...(pluginsDirExists ? [pluginsDir] : []),
|
|
446
|
+
...(skillsDirExists ? [skillsDir] : []),
|
|
447
|
+
...(marketplacesDirExists ? [marketplacesDir] : []),
|
|
448
|
+
];
|
|
449
|
+
for (const target of userScanTargets(present, recursive)) {
|
|
450
|
+
logger.debug(`Auditing user-level resources at: ${target}`);
|
|
451
|
+
results.push(...await getValidationResults(target, recursive, options, logger, scanRoot));
|
|
373
452
|
}
|
|
374
453
|
// Run compatibility analysis if --compat flag is set
|
|
375
454
|
const compatMap = options.compat
|
|
@@ -385,22 +464,6 @@ async function auditUserDirectories(recursive, options, startTime, logger) {
|
|
|
385
464
|
}
|
|
386
465
|
logHierarchicalSummary(results, logger);
|
|
387
466
|
}
|
|
388
|
-
/**
|
|
389
|
-
* Apply severity filtering to validation results.
|
|
390
|
-
*
|
|
391
|
-
* Audit is advisory only: it applies `validation.severity` to decide what to
|
|
392
|
-
* show, but deliberately ignores `validation.allow`. Codes configured as
|
|
393
|
-
* `severity: 'ignore'` are stripped from the result issues before rendering.
|
|
394
|
-
*
|
|
395
|
-
* The severity config is per-skill, keyed by skill name in
|
|
396
|
-
* `config.skills.config[skillName].validation.severity`.
|
|
397
|
-
* Defaults config (`config.skills.defaults.validation.severity`) is also
|
|
398
|
-
* checked as a fallback.
|
|
399
|
-
*
|
|
400
|
-
* @param results - Raw validation results from skill/plugin validators
|
|
401
|
-
* @param config - Parsed VATConfig (may be undefined if no config file)
|
|
402
|
-
* @returns New results array with ignored codes removed from issues
|
|
403
|
-
*/
|
|
404
467
|
/** Skill resource types that can have per-skill validation config. */
|
|
405
468
|
const SKILL_RESULT_TYPES = new Set([RESOURCE_TYPE_AGENT_SKILL, 'vat-agent']);
|
|
406
469
|
/**
|
|
@@ -430,17 +493,27 @@ export function deriveScanRoot(targetPath) {
|
|
|
430
493
|
/**
|
|
431
494
|
* Derive the project root directory for config loading.
|
|
432
495
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
496
|
+
* Walks UP from the audit target through {@link findProjectRoot} — the same
|
|
497
|
+
* discovery ladder (`vibe-agent-toolkit.config.yaml`, then `.git/`) that
|
|
498
|
+
* `resolveSkillPackagingConfig` uses per skill, so the two cannot disagree about
|
|
499
|
+
* which project governs a path.
|
|
500
|
+
*
|
|
501
|
+
* It used to guess by SHAPE instead: a directory argument resolved to ITSELF and
|
|
502
|
+
* a `SKILL.md` argument to exactly two levels up. Neither reaches a project root
|
|
503
|
+
* from a built bundle — `vat audit dist/skills/demo` looked for the config inside
|
|
504
|
+
* the bundle, found none, and `applySeverityFilter` early-returned. The effect was
|
|
505
|
+
* that `severity.PACKAGED_AGENT_INSTRUCTION_FILE: ignore` — the opt-out this very
|
|
506
|
+
* command's `fix` text tells the reader to write — did nothing whenever audit was
|
|
507
|
+
* pointed at the bundle rather than at the project. Measured: `warnings: 1` with
|
|
508
|
+
* the override and without it, from the same tree, while `vat audit .` on that
|
|
509
|
+
* same project honoured it.
|
|
510
|
+
*
|
|
511
|
+
* `null` (no config and no git anywhere above) falls back to the target itself,
|
|
512
|
+
* which is what the shape-based version returned for every path.
|
|
435
513
|
*/
|
|
436
514
|
function deriveConfigRoot(targetPath) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
if (targetPath.endsWith('SKILL.md')) {
|
|
441
|
-
return safePath.resolve(safePath.join(targetPath, '..', '..'));
|
|
442
|
-
}
|
|
443
|
-
return safePath.resolve(targetPath);
|
|
515
|
+
const start = targetPath === undefined ? process.cwd() : deriveScanRoot(targetPath);
|
|
516
|
+
return findProjectRoot(start) ?? start;
|
|
444
517
|
}
|
|
445
518
|
/**
|
|
446
519
|
* Build the recalculated status and summary after issues have been filtered.
|
|
@@ -459,16 +532,55 @@ function buildFilteredResult(result, filteredIssues) {
|
|
|
459
532
|
};
|
|
460
533
|
}
|
|
461
534
|
/**
|
|
462
|
-
* Apply severity
|
|
535
|
+
* Apply severity resolution to validation results.
|
|
463
536
|
*
|
|
464
537
|
* Audit is advisory only: it applies `validation.severity` to decide what to
|
|
465
|
-
* show, but deliberately ignores `validation.allow`.
|
|
466
|
-
* `severity: 'ignore'` are stripped from the result issues before rendering.
|
|
538
|
+
* show and at what severity, but deliberately ignores `validation.allow`.
|
|
467
539
|
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
540
|
+
* **Both directions, via the one shared resolver.** This used to compute the
|
|
541
|
+
* merged override map correctly and then consult it for a single value —
|
|
542
|
+
* `issues.filter(i => effective[i.code] !== 'ignore')` — so `ignore` worked
|
|
543
|
+
* while `error`, `warning` and `info` fell through untouched. Measured end to
|
|
544
|
+
* end on a real adopter tree: `severity: error` at BOTH scopes left
|
|
545
|
+
* `severity: warning`, `errors: 0`, `status: warning`, while `vat verify` on the
|
|
546
|
+
* same config promoted correctly. Two lanes, one config key, opposite answers.
|
|
547
|
+
*
|
|
548
|
+
* Status and counts are re-derived from the resolved severities by
|
|
549
|
+
* {@link buildFilteredResult}, so a promotion moves the reported status. It does
|
|
550
|
+
* NOT move this command's exit code — `vat audit` is advisory and its exit-code
|
|
551
|
+
* behaviour is a separate, pre-existing question tracked in its own issue. That
|
|
552
|
+
* asymmetry with `vat verify` (whose exit code IS severity-derived) is
|
|
553
|
+
* deliberate here rather than overlooked.
|
|
554
|
+
*
|
|
555
|
+
* Two scopes, both live:
|
|
556
|
+
* - `skills.defaults.validation.severity` — the PROJECT-WIDE map, applied to
|
|
557
|
+
* every result this run produced, including `claude-plugin` and `marketplace`
|
|
558
|
+
* ones. It used to be applied only to skill-typed results, which left the
|
|
559
|
+
* opt-out inert for the population that motivated it: the false positive
|
|
560
|
+
* `PACKAGED_AGENT_INSTRUCTION_FILE` was kept at `warning` for — an official
|
|
561
|
+
* plugin's intentional scaffold template — is a finding on a PLUGIN tree, and
|
|
562
|
+
* no per-skill key can name it. Measured before the fix: a plugin result kept
|
|
563
|
+
* its warning with `severity: ignore` set at `skills.defaults`.
|
|
564
|
+
* - `skills.config.<name>.validation.severity` — layered on top, and only for a
|
|
565
|
+
* result that names a skill, since that key is keyed by skill name.
|
|
566
|
+
*
|
|
567
|
+
* KNOWN, DELIBERATELY NOT FIXED — per-skill scope cannot reach a finding nested
|
|
568
|
+
* inside a plugin tree, even when that finding IS attributable to a skill.
|
|
569
|
+
* `vat audit <plugin dir>` reports two findings: the plugin root's `CLAUDE.md`
|
|
570
|
+
* (attributable to no skill, which is the case the project-wide scope above was
|
|
571
|
+
* added for) and an `AGENTS.md` nested inside one of the plugin's OWN skills,
|
|
572
|
+
* which is attributable. Measured: neither moves under
|
|
573
|
+
* `skills.config.<that skill>` nor under `skills.config.<the plugin's name>`;
|
|
574
|
+
* only the project-wide `skills.defaults` moves them. The mechanism is the line
|
|
575
|
+
* below — a `claude-plugin` result is not in `SKILL_RESULT_TYPES`, so `skillName`
|
|
576
|
+
* is `undefined` and the per-skill map is never consulted, no matter which name
|
|
577
|
+
* the adopter writes.
|
|
578
|
+
*
|
|
579
|
+
* The consequence worth stating plainly: an adopter with ONE intentional
|
|
580
|
+
* agent-instruction file in ONE plugin has to silence the code project-wide in
|
|
581
|
+
* this lane, losing the detector everywhere else. The marketplace lane has the
|
|
582
|
+
* same project-wide-only limitation for a different reason, recorded there: both
|
|
583
|
+
* marketplace config schemas are `.strict()` with no `validation` key at all.
|
|
472
584
|
*
|
|
473
585
|
* @param results - Raw validation results from skill/plugin validators
|
|
474
586
|
* @param config - Parsed VATConfig (may be undefined if no config file)
|
|
@@ -481,10 +593,7 @@ function applySeverityFilter(results, config) {
|
|
|
481
593
|
const skillsConfig = config.skills;
|
|
482
594
|
const defaultSeverity = skillsConfig.defaults?.validation?.severity ?? {};
|
|
483
595
|
return results.map(result => {
|
|
484
|
-
|
|
485
|
-
return result;
|
|
486
|
-
}
|
|
487
|
-
const skillName = result.metadata?.name;
|
|
596
|
+
const skillName = SKILL_RESULT_TYPES.has(result.type) ? result.metadata?.name : undefined;
|
|
488
597
|
const perSkillSeverity = skillName === undefined
|
|
489
598
|
? {}
|
|
490
599
|
: (skillsConfig.config?.[skillName]?.validation?.severity ?? {});
|
|
@@ -493,11 +602,18 @@ function applySeverityFilter(results, config) {
|
|
|
493
602
|
if (Object.keys(effectiveSeverity).length === 0) {
|
|
494
603
|
return result;
|
|
495
604
|
}
|
|
496
|
-
const
|
|
497
|
-
|
|
605
|
+
const resolvedIssues = resolveIssueSeverity(result.issues, { severity: effectiveSeverity });
|
|
606
|
+
// Short-circuit only when NOTHING moved. Comparing lengths alone was the old
|
|
607
|
+
// test, and it is blind to the promote direction by construction: a resolved
|
|
608
|
+
// `error` keeps the array exactly as long as it was, so an early return on
|
|
609
|
+
// equal lengths discarded every re-severitied issue and reported the raw
|
|
610
|
+
// ones. The severities have to be compared too.
|
|
611
|
+
const unchanged = resolvedIssues.length === result.issues.length &&
|
|
612
|
+
resolvedIssues.every((issue, index) => issue.severity === result.issues[index]?.severity);
|
|
613
|
+
if (unchanged) {
|
|
498
614
|
return result;
|
|
499
615
|
}
|
|
500
|
-
return buildFilteredResult(result,
|
|
616
|
+
return buildFilteredResult(result, resolvedIssues);
|
|
501
617
|
});
|
|
502
618
|
}
|
|
503
619
|
/**
|
|
@@ -627,7 +743,11 @@ export async function auditCommand(targetPath, options) {
|
|
|
627
743
|
*/
|
|
628
744
|
async function validateSurface(surface, options, logger, locationRoot) {
|
|
629
745
|
if (surface.type === RESOURCE_TYPE_AGENT_SKILL) {
|
|
630
|
-
|
|
746
|
+
// No crawl: this arm only runs for a MULTI-surface root, which in practice is
|
|
747
|
+
// skill-claude-plugin (SKILL.md beside .claude-plugin/plugin.json). The plugin
|
|
748
|
+
// surface below crawls that same directory at any depth, so crawling here
|
|
749
|
+
// would report every agent-instruction file in it a second time.
|
|
750
|
+
return validateSingleSkill(surface.path, options, logger, locationRoot, false);
|
|
631
751
|
}
|
|
632
752
|
if (surface.type === RESOURCE_TYPE_CLAUDE_PLUGIN) {
|
|
633
753
|
return validatePlugin(surface.path, { locationRoot });
|
|
@@ -754,13 +874,13 @@ export async function getValidationResults(scanPath, recursive, options, logger,
|
|
|
754
874
|
// Special handling for direct SKILL.md file
|
|
755
875
|
if (format === RESOURCE_TYPE_AGENT_SKILL) {
|
|
756
876
|
logger.debug('Detected single Agent Skill');
|
|
757
|
-
return [await validateSingleSkill(scanPath, options, logger, locationRoot)];
|
|
877
|
+
return [await validateSingleSkill(scanPath, options, logger, locationRoot, true)];
|
|
758
878
|
}
|
|
759
879
|
// Special handling for VAT agent: validate its SKILL.md
|
|
760
880
|
if (format === 'vat-agent') {
|
|
761
881
|
const skillPath = safePath.join(scanPath, 'SKILL.md');
|
|
762
882
|
logger.debug('Detected VAT agent, validating SKILL.md');
|
|
763
|
-
return [await validateSingleSkill(skillPath, options, logger, locationRoot, true)];
|
|
883
|
+
return [await validateSingleSkill(skillPath, options, logger, locationRoot, true, true)];
|
|
764
884
|
}
|
|
765
885
|
// Enumerate all manifest surfaces at the directory root. If multiple are
|
|
766
886
|
// present (e.g., skill-claude-plugin: SKILL.md + .claude-plugin/plugin.json),
|
|
@@ -852,7 +972,9 @@ async function validatePluginSkillsViaInventory(scanPath, options, logger, locat
|
|
|
852
972
|
continue;
|
|
853
973
|
}
|
|
854
974
|
logger.debug(` Validating plugin-bundled skill (inventory): ${skill.files.skillMd}`);
|
|
855
|
-
|
|
975
|
+
// No crawl: `validatePlugin` already scanned the whole plugin tree, nested
|
|
976
|
+
// skill directories included, so this lane would duplicate its findings.
|
|
977
|
+
results.push(await validateSingleSkill(skill.files.skillMd, options, logger, locationRoot, false));
|
|
856
978
|
}
|
|
857
979
|
return results;
|
|
858
980
|
}
|
|
@@ -1060,6 +1182,37 @@ function renderIssueEvidence(issue, evidenceSources, logger) {
|
|
|
1060
1182
|
logger.info(formatEvidenceLine(ev));
|
|
1061
1183
|
}
|
|
1062
1184
|
}
|
|
1185
|
+
/**
|
|
1186
|
+
* The name a findings block is headed with — never the empty string.
|
|
1187
|
+
*
|
|
1188
|
+
* `issueLocation` answers "where is this, relative to the scan root", and for a
|
|
1189
|
+
* result that IS the scan root (auditing a plugin directory produces exactly
|
|
1190
|
+
* that) the honest relative path is `''`. Rendered straight into a heading, that
|
|
1191
|
+
* produced ` — 2 warnings:` — the operator was told a nameless something had two
|
|
1192
|
+
* warnings and then, because warnings collapse at default verbosity, shown
|
|
1193
|
+
* neither. Fall back in decreasing order of usefulness to the reader:
|
|
1194
|
+
*
|
|
1195
|
+
* 1. the relative path, whenever there is one (the common case, unchanged);
|
|
1196
|
+
* 2. the resource's own name from metadata already on the result — for a plugin
|
|
1197
|
+
* or skill this is the most recognisable subject there is;
|
|
1198
|
+
* 3. the last path segment, i.e. the scanned directory's own name;
|
|
1199
|
+
* 4. `'.'`, the minimum honest way to say "the root you pointed me at".
|
|
1200
|
+
*
|
|
1201
|
+
* Every step reads data already in hand — no new I/O, and no re-deriving a path
|
|
1202
|
+
* the caller already resolved.
|
|
1203
|
+
*/
|
|
1204
|
+
function findingSubject(result, root) {
|
|
1205
|
+
const location = issueLocation(result.path, root);
|
|
1206
|
+
if (location !== '')
|
|
1207
|
+
return location;
|
|
1208
|
+
const name = result.metadata?.name;
|
|
1209
|
+
if (name !== undefined && name !== '')
|
|
1210
|
+
return name;
|
|
1211
|
+
// `basename` is empty for a filesystem root ('/' or 'C:\'), which is the only
|
|
1212
|
+
// case left that '.' has to cover.
|
|
1213
|
+
const directoryName = basename(result.path);
|
|
1214
|
+
return directoryName === '' ? '.' : directoryName;
|
|
1215
|
+
}
|
|
1063
1216
|
/**
|
|
1064
1217
|
* Render supporting evidence beneath each CAPABILITY_* issue when the
|
|
1065
1218
|
* audit was invoked with --verbose. Evidence comes from the validation
|
|
@@ -1077,7 +1230,7 @@ function renderVerboseEvidence(results, root, logger) {
|
|
|
1077
1230
|
];
|
|
1078
1231
|
if (evidenceSources.length === 0)
|
|
1079
1232
|
continue;
|
|
1080
|
-
logger.info(`\n${
|
|
1233
|
+
logger.info(`\n${findingSubject(result, root)} — supporting evidence:`);
|
|
1081
1234
|
for (const issue of capabilityIssues) {
|
|
1082
1235
|
renderIssueEvidence(issue, evidenceSources, logger);
|
|
1083
1236
|
}
|
|
@@ -1154,6 +1307,15 @@ function renderAuditFooter(results, logger) {
|
|
|
1154
1307
|
* the failure direction `issue-rendering.ts` exists to prevent. The trailing
|
|
1155
1308
|
* line names the collapsed total and both ways to see it, for the same reason.
|
|
1156
1309
|
*
|
|
1310
|
+
* That trailing line points at THIS audit's YAML report, deliberately — do not
|
|
1311
|
+
* broaden it back into a claim about "the YAML report" in general. It is only
|
|
1312
|
+
* true of the verbs that emit full `issues:` arrays, and which verbs those are
|
|
1313
|
+
* has changed: `vat skills build` now publishes them too, so the sentence is
|
|
1314
|
+
* shared rather than re-spelled (see `formatCollapsedFindingsHint`, which takes
|
|
1315
|
+
* the report name for exactly this reason). Any verb that publishes counts alone
|
|
1316
|
+
* must still not use it — there, it would send a CI consumer to a document that
|
|
1317
|
+
* cannot answer the question.
|
|
1318
|
+
*
|
|
1157
1319
|
* Pure: takes results, returns lines. It reads `issues` and never rewrites it,
|
|
1158
1320
|
* because `runAuditAtPath` builds the YAML document from the very same array.
|
|
1159
1321
|
*
|
|
@@ -1166,16 +1328,23 @@ export function formatAuditFindingsLines(results, root, verbose) {
|
|
|
1166
1328
|
const rendered = issuesToRenderAtVerbosity(result.issues, verbose);
|
|
1167
1329
|
// What --verbose WOULD show, minus what this verbosity shows. Derived from
|
|
1168
1330
|
// the policy rather than re-stating it, so the count cannot contradict it.
|
|
1169
|
-
collapsed +=
|
|
1170
|
-
|
|
1331
|
+
collapsed += countCollapsedFindings(result.issues, verbose);
|
|
1332
|
+
// The heading is unconditional and counts the WHOLE set — for a
|
|
1333
|
+
// warning-only file at default verbosity it IS the report. The trailing
|
|
1334
|
+
// colon is what varies: it introduces the blocks below, so a heading with
|
|
1335
|
+
// nothing beneath it does not print one. Same rule, same reason, as
|
|
1336
|
+
// `formatPostBuildIssueReport` in `skills/build.ts` — a colon promising a
|
|
1337
|
+
// list the current verbosity will not print is a promise the renderer
|
|
1338
|
+
// already knows it will break.
|
|
1339
|
+
const heading = `\n${findingSubject(result, root)} — ${formatSeverityBreakdown(countBySeverity(result.issues))}`;
|
|
1340
|
+
lines.push(rendered.length === 0 ? heading : `${heading}:`);
|
|
1171
1341
|
for (const issue of rendered) {
|
|
1172
1342
|
lines.push(...formatIssueLines(issue, ' '));
|
|
1173
1343
|
}
|
|
1174
1344
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1345
|
+
const hint = formatCollapsedFindingsHint(collapsed, 'audit');
|
|
1346
|
+
if (hint !== undefined)
|
|
1347
|
+
lines.push(hint);
|
|
1179
1348
|
return lines;
|
|
1180
1349
|
}
|
|
1181
1350
|
/**
|
|
@@ -1201,6 +1370,53 @@ function isExcludedPath(isMatch, relativePath, isDirectory) {
|
|
|
1201
1370
|
}
|
|
1202
1371
|
return isMatch(relativePath);
|
|
1203
1372
|
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Does the SKILL.md at `skillMdPath` own its own presence-side crawl, or has an
|
|
1375
|
+
* ancestor crawl already covered it?
|
|
1376
|
+
*
|
|
1377
|
+
* The single place the walk answers that question, for both owner kinds — see
|
|
1378
|
+
* {@link ScanContext.crawledRoot}. `isWithin` rather than a bare
|
|
1379
|
+
* `crawledRoot === null`: the claim is a containment claim and it is spelled
|
|
1380
|
+
* once, in the module that already got the separator right.
|
|
1381
|
+
*/
|
|
1382
|
+
function ownsOwnCrawl(scanCtx, skillMdPath) {
|
|
1383
|
+
return scanCtx.crawledRoot === null || !isWithin(scanCtx.crawledRoot, safePath.resolve(skillMdPath));
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Hand the descending context an owner for everything below `owner`, unless one
|
|
1387
|
+
* is already claimed. First owner wins, and `null` claims nothing.
|
|
1388
|
+
*/
|
|
1389
|
+
function claimSubtree(scanCtx, owner) {
|
|
1390
|
+
if (owner === null || scanCtx.crawledRoot !== null)
|
|
1391
|
+
return scanCtx;
|
|
1392
|
+
return { ...scanCtx, crawledRoot: safePath.resolve(owner) };
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* The context this directory's SUBDIRECTORIES are walked with.
|
|
1396
|
+
*
|
|
1397
|
+
* A `SKILL.md` here means this directory's own crawl reaches every descendant at
|
|
1398
|
+
* any depth, so it — not each nested skill in turn — owns the subtree below.
|
|
1399
|
+
*
|
|
1400
|
+
* Resolved before the entry loop rather than inside it: `SKILL.md` and the
|
|
1401
|
+
* subdirectories it governs are siblings in one `readdir`, and their order is
|
|
1402
|
+
* the filesystem's to choose, so deciding this mid-loop would make the verdict
|
|
1403
|
+
* depend on it.
|
|
1404
|
+
*
|
|
1405
|
+
* Conditional on the crawl actually RUNNING ({@link crawlOwnsSubtree}), not on
|
|
1406
|
+
* the SKILL.md merely existing: a repo-source skill's crawl is skipped
|
|
1407
|
+
* entirely, so claiming its subtree would trade a double count for a missed
|
|
1408
|
+
* finding — and provenance is not monotone down a tree.
|
|
1409
|
+
*/
|
|
1410
|
+
async function contextForDescendants(dirPath, entries, scanCtx) {
|
|
1411
|
+
if (!entries.some((entry) => entry.isFile() && entry.name === 'SKILL.md'))
|
|
1412
|
+
return scanCtx;
|
|
1413
|
+
const skillMd = safePath.join(dirPath, 'SKILL.md');
|
|
1414
|
+
if (!ownsOwnCrawl(scanCtx, skillMd))
|
|
1415
|
+
return scanCtx;
|
|
1416
|
+
if (!await crawlOwnsSubtree(skillMd))
|
|
1417
|
+
return scanCtx;
|
|
1418
|
+
return claimSubtree(scanCtx, dirPath);
|
|
1419
|
+
}
|
|
1204
1420
|
/**
|
|
1205
1421
|
* Handle file entry during directory scan
|
|
1206
1422
|
*/
|
|
@@ -1230,14 +1446,21 @@ async function handleFileEntry(entry, fullPath, options, logger, scanCtx) {
|
|
|
1230
1446
|
? { locationRoot, projectSkills }
|
|
1231
1447
|
: { gitTracker: scanCtx.gitTracker, locationRoot, projectSkills };
|
|
1232
1448
|
const packagingResult = await validateSkillForPackaging(fullPath, skillConfig, 'source', sharedCtx);
|
|
1233
|
-
|
|
1449
|
+
const configAware = packagingResultToValidationResult(fullPath, packagingResult, skillConfig.targets, locationRoot);
|
|
1450
|
+
await appendDistributedTreeFindings(configAware, fullPath, locationRoot, ownsOwnCrawl(scanCtx, fullPath));
|
|
1451
|
+
return configAware;
|
|
1234
1452
|
}
|
|
1235
1453
|
// Wild mode: basic validation
|
|
1236
1454
|
const validateOptions = { skillPath: fullPath, locationRoot };
|
|
1237
1455
|
if (options.warnUnreferencedFiles) {
|
|
1238
1456
|
validateOptions.checkUnreferencedFiles = true;
|
|
1239
1457
|
}
|
|
1240
|
-
|
|
1458
|
+
const result = await validateSkill(validateOptions);
|
|
1459
|
+
// Wild mode is where the distributed-tree question is even ASKED; the
|
|
1460
|
+
// classifier answers it. Suppressed under a plugin ancestor, which already
|
|
1461
|
+
// crawled this whole subtree at any depth.
|
|
1462
|
+
await appendDistributedTreeFindings(result, fullPath, locationRoot, ownsOwnCrawl(scanCtx, fullPath));
|
|
1463
|
+
return result;
|
|
1241
1464
|
}
|
|
1242
1465
|
return null;
|
|
1243
1466
|
}
|
|
@@ -1258,9 +1481,14 @@ async function handleDirectoryEntry(fullPath, recursive, options, logger, baseDi
|
|
|
1258
1481
|
appendInventoryParseErrors(result, inv, locationRoot);
|
|
1259
1482
|
results.push(result);
|
|
1260
1483
|
}
|
|
1261
|
-
// Recurse into subdirectories (both plugin/marketplace dirs and regular dirs)
|
|
1484
|
+
// Recurse into subdirectories (both plugin/marketplace dirs and regular dirs).
|
|
1485
|
+
// Below a plugin, `validatePlugin` has already crawled the whole subtree for
|
|
1486
|
+
// agent-instruction files, so every crawl below must stand down or one file
|
|
1487
|
+
// is reported twice. See {@link ScanContext.crawledRoot} — first owner wins,
|
|
1488
|
+
// and it never clears on the way down.
|
|
1489
|
+
const childCtx = claimSubtree(scanCtx, hasClaudePlugin ? fullPath : null);
|
|
1262
1490
|
if (recursive) {
|
|
1263
|
-
const subResults = await scanDirectory(fullPath, recursive, options, logger,
|
|
1491
|
+
const subResults = await scanDirectory(fullPath, recursive, options, logger, childCtx, baseDir, nestedConfigLog);
|
|
1264
1492
|
results.push(...subResults);
|
|
1265
1493
|
}
|
|
1266
1494
|
return results;
|
|
@@ -1311,8 +1539,6 @@ function resolveProjectExcludes(scanDir, logger) {
|
|
|
1311
1539
|
}
|
|
1312
1540
|
return { isMatch, base: projectRoot, label: 'excluded by config' };
|
|
1313
1541
|
}
|
|
1314
|
-
/** Cache of (gitRoot → initialized GitTracker) to avoid re-spawning ls-files. */
|
|
1315
|
-
const gitTrackerCache = new Map();
|
|
1316
1542
|
/**
|
|
1317
1543
|
* Cache of (projectRoot → registry for the *inventory* link walk).
|
|
1318
1544
|
*
|
|
@@ -1367,23 +1593,13 @@ async function pluginInventoryAt(dir) {
|
|
|
1367
1593
|
* governing configs, or skill-discovery maps from a prior run.
|
|
1368
1594
|
*/
|
|
1369
1595
|
export function resetAuditCaches() {
|
|
1370
|
-
|
|
1596
|
+
resetGitTrackerCache();
|
|
1371
1597
|
resetPackagingRegistryCache();
|
|
1372
1598
|
inventoryRegistryCache.clear();
|
|
1373
1599
|
resetProjectRootCaches();
|
|
1374
1600
|
resetLoadedConfigCache();
|
|
1375
1601
|
resetSkillDiscoveryCache();
|
|
1376
1602
|
}
|
|
1377
|
-
async function getOrCreateGitTracker(gitRoot) {
|
|
1378
|
-
const cached = gitTrackerCache.get(gitRoot);
|
|
1379
|
-
if (cached !== undefined) {
|
|
1380
|
-
return cached;
|
|
1381
|
-
}
|
|
1382
|
-
const tracker = new GitTracker(gitRoot);
|
|
1383
|
-
await tracker.initialize();
|
|
1384
|
-
gitTrackerCache.set(gitRoot, tracker);
|
|
1385
|
-
return tracker;
|
|
1386
|
-
}
|
|
1387
1603
|
/**
|
|
1388
1604
|
* Resolve the effective git root and build a per-repo {@link GitTracker} for a
|
|
1389
1605
|
* scan. On the first call for a scan (no tracker yet), detects the git root
|
|
@@ -1398,15 +1614,15 @@ async function resolveScanContext(dirPath, locationRoot, logger) {
|
|
|
1398
1614
|
const projectExcludes = resolveProjectExcludes(dirPath, logger);
|
|
1399
1615
|
const detectedRoot = gitFindRoot(dirPath);
|
|
1400
1616
|
if (detectedRoot === null) {
|
|
1401
|
-
return { gitRoot: null, gitTracker: null, locationRoot, projectExcludes };
|
|
1617
|
+
return { gitRoot: null, gitTracker: null, locationRoot, projectExcludes, crawledRoot: null };
|
|
1402
1618
|
}
|
|
1403
1619
|
const tracker = await getOrCreateGitTracker(detectedRoot);
|
|
1404
1620
|
// If the scan root itself is gitignored (e.g. user targeted `dist/`), disable
|
|
1405
1621
|
// gitignore filtering so the user's explicit intent wins.
|
|
1406
1622
|
if (tracker.isIgnoredByActiveSet(dirPath)) {
|
|
1407
|
-
return { gitRoot: null, gitTracker: null, locationRoot, projectExcludes };
|
|
1623
|
+
return { gitRoot: null, gitTracker: null, locationRoot, projectExcludes, crawledRoot: null };
|
|
1408
1624
|
}
|
|
1409
|
-
return { gitRoot: detectedRoot, gitTracker: tracker, locationRoot, projectExcludes };
|
|
1625
|
+
return { gitRoot: detectedRoot, gitTracker: tracker, locationRoot, projectExcludes, crawledRoot: null };
|
|
1410
1626
|
}
|
|
1411
1627
|
/**
|
|
1412
1628
|
* Build the gitignore exclusion map for a set of entry paths.
|
|
@@ -1487,6 +1703,7 @@ async function scanDirectory(dirPath, recursive, options, logger, scanCtx, baseD
|
|
|
1487
1703
|
excludeMatchers.push(resolvedScanCtx.projectExcludes);
|
|
1488
1704
|
}
|
|
1489
1705
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
1706
|
+
const descendCtx = await contextForDescendants(dirPath, entries, resolvedScanCtx);
|
|
1490
1707
|
// Build full paths and batch-check gitignore status
|
|
1491
1708
|
const entryPaths = entries.map(e => safePath.join(dirPath, e.name));
|
|
1492
1709
|
const gitIgnoredMap = buildGitIgnoreMap(entryPaths, resolvedScanCtx.gitTracker, options.includeArtifacts ?? false);
|
|
@@ -1504,7 +1721,9 @@ async function scanDirectory(dirPath, recursive, options, logger, scanCtx, baseD
|
|
|
1504
1721
|
}
|
|
1505
1722
|
}
|
|
1506
1723
|
else if (entry.isDirectory()) {
|
|
1507
|
-
|
|
1724
|
+
// `descendCtx`, not `resolvedScanCtx`: this level's own skill (if any)
|
|
1725
|
+
// owns everything below, including this subdirectory.
|
|
1726
|
+
const dirResults = await handleDirectoryEntry(fullPath, recursive, options, logger, resolvedBaseDir, descendCtx, resolvedNestedLog);
|
|
1508
1727
|
results.push(...dirResults);
|
|
1509
1728
|
}
|
|
1510
1729
|
}
|