fix(acr): enumerateAgents counts only CC-registered agents — recurse + frontmatter filter + HOME dedup (M-BUG-3/4/5)
enumerateAgents previously counted every .md in an agents dir as an always-loaded agent. Per the official CC subagents docs, CC registers a subagent only when its frontmatter declares both name and description, scans agents dirs recursively, and (at a HOME self-scan) must not count ~/.claude/agents twice. - M-BUG-5: require valid name+description frontmatter; frontmatter-less files are registration no-ops costing 0 always-loaded tokens. Fixes the user-agent over-count (this machine: 13 -> 0). - M-BUG-3: listMarkdownFiles gains opt-in recursion; enumerateAgents recurses so agents in subfolders (agents/review/x.md) are counted, matching CC. - M-BUG-4: configDirs dedupes project==user paths, so a `manifest --global` self-scan (repoPath===$HOME) counts ~/.claude once (user scope), killing the spurious "project 13" double-count. Benefits rules/agents/output-styles. TDD: 4 failing tests -> green. Full suite 1305/0; --json/--raw byte-stable, frozen v5.0.0 + SC-5 + default-output snapshots untouched (no snapshot records agent enumeration rows). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EnUvKEqyEa1m9gy6Aqhdqq
This commit is contained in:
parent
a1e786ba4f
commit
7f097d524f
2 changed files with 97 additions and 5 deletions
|
|
@ -572,14 +572,20 @@ async function countPluginItems(pluginRoot) {
|
|||
return counts;
|
||||
}
|
||||
|
||||
async function listMarkdownFiles(dir) {
|
||||
async function listMarkdownFiles(dir, recursive = false) {
|
||||
const out = [];
|
||||
let entries;
|
||||
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return out; }
|
||||
for (const e of entries) {
|
||||
const full = join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
// Opt-in recursion (M-BUG-3): CC scans agents dirs recursively, so agents
|
||||
// organized into subfolders must be enumerated too. Other callers stay flat.
|
||||
if (recursive) out.push(...await listMarkdownFiles(full, true));
|
||||
continue;
|
||||
}
|
||||
if (!e.isFile()) continue;
|
||||
if (!e.name.endsWith('.md')) continue;
|
||||
const full = join(dir, e.name);
|
||||
try {
|
||||
const s = await stat(full);
|
||||
out.push({ path: full, size: s.size });
|
||||
|
|
@ -662,6 +668,11 @@ export async function enumerateSkills(pluginList = []) {
|
|||
// Rules, agents, output styles (v5.6 Foundation enumeration)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** True when `v` is a non-empty, non-whitespace string (a usable frontmatter field). */
|
||||
function hasText(v) {
|
||||
return typeof v === 'string' && v.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the project/user/plugin directory list for a per-kind enumerator.
|
||||
* Project + user dirs live under `.claude/<dir>`; plugins under each of the
|
||||
|
|
@ -669,8 +680,16 @@ export async function enumerateSkills(pluginList = []) {
|
|||
*/
|
||||
function configDirs(repoPath, pluginList, subdir, pluginSubdirs = [subdir]) {
|
||||
const home = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const dirs = [{ dir: join(repoPath, '.claude', subdir), source: 'project', pluginName: null }];
|
||||
if (home) dirs.push({ dir: join(home, '.claude', subdir), source: 'user', pluginName: null });
|
||||
const projectDir = join(repoPath, '.claude', subdir);
|
||||
const userDir = home ? join(home, '.claude', subdir) : null;
|
||||
const dirs = [];
|
||||
// M-BUG-4: when repoPath === $HOME (the `manifest --global` self-scan), the
|
||||
// project dir resolves to the same path as the user dir. Count it once, as
|
||||
// user scope, instead of enumerating the same directory twice.
|
||||
if (!(userDir && userDir === projectDir)) {
|
||||
dirs.push({ dir: projectDir, source: 'project', pluginName: null });
|
||||
}
|
||||
if (userDir) dirs.push({ dir: userDir, source: 'user', pluginName: null });
|
||||
for (const p of pluginList) {
|
||||
for (const sub of pluginSubdirs) {
|
||||
dirs.push({ dir: join(p.path, sub), source: 'plugin', pluginName: p.name });
|
||||
|
|
@ -730,8 +749,17 @@ export async function enumerateAgents(repoPath, pluginList = []) {
|
|||
const lp = deriveLoadPattern('agent');
|
||||
const dirs = configDirs(repoPath, pluginList, 'agents');
|
||||
for (const { dir, source, pluginName } of dirs) {
|
||||
const files = await listMarkdownFiles(dir);
|
||||
const files = await listMarkdownFiles(dir, true); // M-BUG-3: CC scans agents dirs recursively
|
||||
for (const f of files) {
|
||||
// M-BUG-5: CC registers a subagent only when its frontmatter declares both
|
||||
// `name` and `description` (docs: identity comes only from `name`; both are
|
||||
// required). Frontmatter-less / incomplete files are registration no-ops
|
||||
// that cost zero always-loaded tokens — don't count them as agents.
|
||||
let frontmatter;
|
||||
try {
|
||||
({ frontmatter } = parseFrontmatter(await readFile(f.path, 'utf-8')));
|
||||
} catch { continue; }
|
||||
if (!hasText(frontmatter && frontmatter.name) || !hasText(frontmatter && frontmatter.description)) continue;
|
||||
out.push({
|
||||
name: basename(f.path).replace(/\.md$/, ''),
|
||||
source,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue