Skip to content

feat(manifest): unified skill scanner decoupled from legacy collectors#1859

Merged
alexeyv merged 2 commits intobmad-code-org:mainfrom
alexeyv:unified-skill-scanner
Mar 8, 2026
Merged

feat(manifest): unified skill scanner decoupled from legacy collectors#1859
alexeyv merged 2 commits intobmad-code-org:mainfrom
alexeyv:unified-skill-scanner

Conversation

@alexeyv
Copy link
Copy Markdown
Collaborator

@alexeyv alexeyv commented Mar 8, 2026

Summary

  • Add collectSkills() that recursively walks each module tree to discover type: skill directories anywhere, replacing the band-aid detection inside collectWorkflows()
  • Legacy collectors (workflows, agents, tasks, tools) now skip directories already claimed as skills
  • scanInstalledModules() recognizes skill-only modules (no agents//workflows//tasks//tools/ subdirs needed)

Test plan

  • All 206 installation component tests pass (including 9 new unified skill scanner assertions)
  • All 10 install_to_bmad contract tests pass
  • Schema validation, lint, format, markdown lint all pass
  • Manual: run installer with --modules bmm --tools claude-code and verify skill-manifest.csv output matches expectations

🤖 Generated with Claude Code

@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Mar 8, 2026

🤖 Augment PR Summary

Summary: This PR introduces a unified, recursive skill discovery pass in the manifest generator so skills can be detected anywhere in a module tree (not just under legacy collector paths).

Changes:

  • Adds collectSkills() to walk each installed module, detect directories with bmad-skill-manifest.yaml (type: skill) plus workflow.md/workflow.yaml, and populate skills[].
  • Tracks claimed skill directories via skillClaimedDirs and updates legacy collectors (workflows/agents/tasks/tools) to skip those directories to avoid double-counting.
  • Updates scanInstalledModules() to recognize “skill-only” modules via a recursive skill-manifest check.
  • Adds a new installation component test suite to verify: skills in unusual locations are discovered, are excluded from workflows/tasks, and skill-only modules are detected.

Technical Notes: Skill identification is now driven by manifest type rather than hardcoded directory conventions, and is run before other collectors to establish skip/claim behavior.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

The changes introduce automatic skill discovery and categorization to the manifest generator. A new collectSkills() method identifies skill directories containing bmad-skill-manifest.yaml with type:skill, tracks them in skillClaimedDirs, and prevents them from being processed as workflows. New test suite validates proper separation of skills from workflows and module detection.

Changes

Cohort / File(s) Summary
Test Suite: Unified Skill Scanner
test/test-installation-components.js
Adds Suite 29 with comprehensive tests validating skill discovery at unusual paths, proper categorization of skills vs workflows, task directory skills, and scanInstalledModules module recognition. Tests exercise manifest generation and module scanning with skill-manifest awareness. Note: Suite appears duplicated in patch.
Skill Collection & Integration
tools/cli/installers/lib/core/manifest-generator.js
Introduces collectSkills() async method to recursively discover skill directories by bmad-skill-manifest.yaml presence with type:skill validation. Adds _hasSkillManifestRecursive() helper for module boundary detection. Integrates skill collection into generateManifests() before workflow processing. Modifies collectWorkflows() and related methods (getAgentsFromDir, getTasksFromDir, getToolsFromDir, etc.) to skip directories already claimed by skills via skillClaimedDirs set, preventing duplicate/conflicting processing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #1839: Adds bmad-skill-manifest.yaml files that the new collectSkills() and _hasSkillManifestRecursive() logic directly detects and processes as skill modules.
  • PR #1834: Also modifies manifest-generation flow to detect and handle bmad-skill-manifest.yaml files with skill-aware scanning and collection in manifest-generator.js.
  • PR #1851: Complements skill handling in ManifestGenerator by introducing type:skill manifests, skill-manifest.csv output, and skill installation plumbing that works alongside the skill directory discovery and exclusion logic added here.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a unified skill scanner that is decoupled from legacy collectors, which matches the primary objective of the pull request.
Description check ✅ Passed The description is directly related to the changeset, covering the key implementation details (collectSkills method, legacy collector modifications, scanInstalledModules updates) and test status.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/cli/installers/lib/core/manifest-generator.js (1)

469-475: ⚠️ Potential issue | 🟠 Major

getAgentsFromDir() still processes a claimed root directory.

The new guard only skips claimed child directories. If dirPath itself is in skillClaimedDirs, this collector will still parse its .md files as agents. getTasksFromDir() and getToolsFromDir() already have the early-return pattern this method is missing.

Possible fix
   async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
+    if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return [];
     const agents = [];
     const entries = await fs.readdir(dirPath, { withFileTypes: true });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/core/manifest-generator.js` around lines 469 - 475,
getAgentsFromDir is missing the early-return that avoids processing a claimed
root directory: add the same guard used in getTasksFromDir/getToolsFromDir so
that if this.skillClaimedDirs exists and contains the incoming dirPath (or
fullPath representing the root being processed) the function immediately returns
an empty array; update the check near the top of getAgentsFromDir to
short-circuit before reading entries and before parsing any .md files to mirror
the existing pattern in getTasksFromDir/getToolsFromDir.
🧹 Nitpick comments (7)
tools/cli/installers/lib/core/manifest-generator.js (2)

222-227: Stop descending once a directory is claimed as a skill.

After a successful claim, the parent skill already owns that subtree. Continuing the walk below it can produce overlapping nested skill entries and burns extra traversal time for no upside.

Possible fix
             if (debug) {
               console.log(`[DEBUG] collectSkills: claimed skill "${workflow.name}" as ${canonicalId} at ${dir}`);
             }
-            break; // Successfully claimed — skip remaining workflow filenames
+            return; // Claimed skill owns this subtree

Also applies to: 255-259

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/core/manifest-generator.js` around lines 222 - 227,
The collectSkills traversal currently adds a claimed directory via
this.skillClaimedDirs.add(dir) and breaks out of the filename loop but continues
descending into the directory subtree; update collectSkills (and the analogous
block around the other claim at lines ~255-259) to stop descending once a
directory is claimed by returning early or marking the directory as claimed
before recursing so the walker skips its children—ensure the change occurs where
workflow.name and canonicalId are used to claim the dir so that after the
console.debug/claim call the function does not traverse into that dir's subtree.

111-112: Collapse skill discovery and skill-only module detection into one traversal.

scanInstalledModules() now recursively walks each module tree, and collectSkills() immediately walks the same trees again. Inside collectSkills() you also probe workflow.md/workflow.yaml with pathExists() after readdir() already told you what is in the directory. On larger custom modules this doubles the hot-path I/O for manifest generation.

Also applies to: 153-179, 1288-1335

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/core/manifest-generator.js` around lines 111 - 112,
scanInstalledModules() and collectSkills() are performing duplicate recursive
walks and redundant file checks; consolidate skill discovery into the single
traversal inside scanInstalledModules() so you only walk each module tree once,
populate skillClaimedDirs during that pass, and remove the separate
collectSkills() recursion. While traversing, use the directory entries returned
by readdir() to detect workflow.md / workflow.yaml instead of calling
pathExists() again (i.e., check names in the readdir() result), and remove the
extra scans referenced around collectSkills() and the duplicated ranges (lines
~153-179 and ~1288-1335). Ensure the logic that previously lived in
collectSkills() (claiming skill directories and detecting skill-only modules) is
invoked from scanInstalledModules() so callers of collectSkills() can be removed
or routed to the new single-pass implementation.
test/test-installation-components.js (5)

1668-1681: Make the skill-only-module case end-to-end, and add the negative path.

skill-only-mod is created after generateManifests(), so this only proves the helper returns its name. It never proves the module flows through a real manifest-generation run, and it does not protect against the orphan-manifest false positive (type: skill with no workflow.md/workflow.yaml).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test-installation-components.js` around lines 1668 - 1681, The test
creates skill-only-mod after generateManifests so it never verifies an
end-to-end manifest generation; move the creation of the skill-only-mod (and its
bmad-skill-manifest.yaml + workflow.md) so it exists before calling
generator29.generateManifests() or explicitly call
generator29.generateManifests() after creating it, then assert
scanInstalledModules(tempFixture29) and the generated manifests include
'skill-only-mod' (reference: generator29.generateManifests,
generator29.scanInstalledModules, tempFixture29, skill-only-mod). Also add a
negative-case directory (e.g., orphan-skill-mod) that contains only
bmad-skill-manifest.yaml with type: skill but no workflow.md/workflow.yaml and
assert it is NOT included by scanInstalledModules/generation to prevent the
orphan-manifest false positive.

1637-1681: Read the generated CSVs too.

The feature here is manifest output, but the suite only inspects generator29.skills, workflows, and tasks. A regression in skill-manifest.csv or workflow-manifest.csv serialization would still pass here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test-installation-components.js` around lines 1637 - 1681, The test
currently only checks generator29.skills/workflows/tasks but misses validating
the generated CSV outputs; update the test around ManifestGenerator/generator29
to also read and assert contents of the produced CSV files (e.g.,
skill-manifest.csv and workflow-manifest.csv in the temp fixture output
directory) to ensure the same entries appear (presence of my-custom-skill,
task-skill not appearing in tasks CSV, regular-wf only in workflow CSV, and
nested-skill present), and fail the test if the CSVs are missing or their rows
do not match the expected canonicalIds/names; use generator29 (and/or
ManifestGenerator.generateManifests/scanInstalledModules) to locate the output
CSVs and parse them (CSV parse or simple line-splitting) before asserting.

1605-1652: This does not cover the workflow-collector regression path.

The “unusual path” skill lives under core/custom-area, so collectWorkflows() would never have seen it before or after this PR. The assertion that it stays out of workflows[] will stay green even if the new claimed-dir skip in getWorkflowsFromPath() is removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test-installation-components.js` around lines 1605 - 1652, The test
misses exercising the workflow-collector path, so add a check that calls the
collector path (either invoke getWorkflowsFromPath() or the collector method
used by ManifestGenerator, e.g., collectWorkflows() /
ManifestGenerator.generateManifests() code path that scans workflows) against
the module root so claimed dirs are skipped; specifically, after creating
core/custom-area/my-skill, call the workflow-collection function used internally
(getWorkflowsFromPath or the collector helper used by ManifestGenerator) and
assert that the resulting workflows array (generator.workflows or the collector
return value) does not include the 'my-custom-skill' entry to guarantee the
claimed-dir skip remains enforced.

1623-1659: The task-skill case is vacuous against the current collector.

getTasksFromDir() only inspects files directly under core/tasks, so core/tasks/task-skill/workflow.md would not land in tasks[] even without the new skillClaimedDirs guard. This fixture does not exercise the changed branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test-installation-components.js` around lines 1623 - 1659, The test is
vacuous because getTasksFromDir only looks at files directly under core/tasks,
so putting workflow.md at core/tasks/task-skill/workflow.md never reaches the
tasks-collection logic and doesn't exercise the new skillClaimedDirs guard;
update the fixture so the task-skill workflow file is placed where
getTasksFromDir will see it (e.g., move workflow.md to core/tasks/task-skill.md
or create core/tasks/task-skill/workflow.md and adjust collector to recurse) or
else change the assertion to target skills[] behavior only; reference
getTasksFromDir and skillClaimedDirs when making the change so the test actually
exercises the guarded branch.

1593-1681: Agent/tool claimed-dir behavior still has no direct coverage.

This suite only probes workflow and task classification. The new claimed-dir branches in getAgentsFromDir() and getToolsFromDir() can regress independently and these assertions would never notice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test-installation-components.js` around lines 1593 - 1681, The test
suite misses coverage for the new "claimed-dir" branches so regressions in
getAgentsFromDir() and getToolsFromDir() won't be caught; add test steps in this
suite to create modules that use the claimed-dir pattern (e.g., create a
directory that should be treated as an agent or tool via the claimed-dir marker
the generator expects), invoke ManifestGenerator.generateManifests() and
scanInstalledModules(), then assert that entries returned by
getAgentsFromDir()/getToolsFromDir() (accessible via generator.agents or
generator.tools after generateManifests) include the claimed-dir items and are
not misclassified as workflows/tasks/skills; reference generator instance,
methods generateManifests(), scanInstalledModules(), getAgentsFromDir(), and
getToolsFromDir() when adding the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/cli/installers/lib/core/manifest-generator.js`:
- Around line 211-219: The code must not synthesize canonicalId from the
directory name; instead validate the manifest-provided canonicalId returned by
getCanonicalId(manifest, workflowFile) and if it's missing or invalid emit a
warning and skip adding this skill to this.skills; update the logic around
getCanonicalId(...) and the skills.push(...) block (where
name/description/module/path/canonicalId/install_to_bmad are assembled) to log a
clear warning (e.g. via the class logger or console.warn) referencing the
workflowFile/dir and then continue without pushing the malformed manifest entry.

---

Outside diff comments:
In `@tools/cli/installers/lib/core/manifest-generator.js`:
- Around line 469-475: getAgentsFromDir is missing the early-return that avoids
processing a claimed root directory: add the same guard used in
getTasksFromDir/getToolsFromDir so that if this.skillClaimedDirs exists and
contains the incoming dirPath (or fullPath representing the root being
processed) the function immediately returns an empty array; update the check
near the top of getAgentsFromDir to short-circuit before reading entries and
before parsing any .md files to mirror the existing pattern in
getTasksFromDir/getToolsFromDir.

---

Nitpick comments:
In `@test/test-installation-components.js`:
- Around line 1668-1681: The test creates skill-only-mod after generateManifests
so it never verifies an end-to-end manifest generation; move the creation of the
skill-only-mod (and its bmad-skill-manifest.yaml + workflow.md) so it exists
before calling generator29.generateManifests() or explicitly call
generator29.generateManifests() after creating it, then assert
scanInstalledModules(tempFixture29) and the generated manifests include
'skill-only-mod' (reference: generator29.generateManifests,
generator29.scanInstalledModules, tempFixture29, skill-only-mod). Also add a
negative-case directory (e.g., orphan-skill-mod) that contains only
bmad-skill-manifest.yaml with type: skill but no workflow.md/workflow.yaml and
assert it is NOT included by scanInstalledModules/generation to prevent the
orphan-manifest false positive.
- Around line 1637-1681: The test currently only checks
generator29.skills/workflows/tasks but misses validating the generated CSV
outputs; update the test around ManifestGenerator/generator29 to also read and
assert contents of the produced CSV files (e.g., skill-manifest.csv and
workflow-manifest.csv in the temp fixture output directory) to ensure the same
entries appear (presence of my-custom-skill, task-skill not appearing in tasks
CSV, regular-wf only in workflow CSV, and nested-skill present), and fail the
test if the CSVs are missing or their rows do not match the expected
canonicalIds/names; use generator29 (and/or
ManifestGenerator.generateManifests/scanInstalledModules) to locate the output
CSVs and parse them (CSV parse or simple line-splitting) before asserting.
- Around line 1605-1652: The test misses exercising the workflow-collector path,
so add a check that calls the collector path (either invoke
getWorkflowsFromPath() or the collector method used by ManifestGenerator, e.g.,
collectWorkflows() / ManifestGenerator.generateManifests() code path that scans
workflows) against the module root so claimed dirs are skipped; specifically,
after creating core/custom-area/my-skill, call the workflow-collection function
used internally (getWorkflowsFromPath or the collector helper used by
ManifestGenerator) and assert that the resulting workflows array
(generator.workflows or the collector return value) does not include the
'my-custom-skill' entry to guarantee the claimed-dir skip remains enforced.
- Around line 1623-1659: The test is vacuous because getTasksFromDir only looks
at files directly under core/tasks, so putting workflow.md at
core/tasks/task-skill/workflow.md never reaches the tasks-collection logic and
doesn't exercise the new skillClaimedDirs guard; update the fixture so the
task-skill workflow file is placed where getTasksFromDir will see it (e.g., move
workflow.md to core/tasks/task-skill.md or create
core/tasks/task-skill/workflow.md and adjust collector to recurse) or else
change the assertion to target skills[] behavior only; reference getTasksFromDir
and skillClaimedDirs when making the change so the test actually exercises the
guarded branch.
- Around line 1593-1681: The test suite misses coverage for the new
"claimed-dir" branches so regressions in getAgentsFromDir() and
getToolsFromDir() won't be caught; add test steps in this suite to create
modules that use the claimed-dir pattern (e.g., create a directory that should
be treated as an agent or tool via the claimed-dir marker the generator
expects), invoke ManifestGenerator.generateManifests() and
scanInstalledModules(), then assert that entries returned by
getAgentsFromDir()/getToolsFromDir() (accessible via generator.agents or
generator.tools after generateManifests) include the claimed-dir items and are
not misclassified as workflows/tasks/skills; reference generator instance,
methods generateManifests(), scanInstalledModules(), getAgentsFromDir(), and
getToolsFromDir() when adding the assertions.

In `@tools/cli/installers/lib/core/manifest-generator.js`:
- Around line 222-227: The collectSkills traversal currently adds a claimed
directory via this.skillClaimedDirs.add(dir) and breaks out of the filename loop
but continues descending into the directory subtree; update collectSkills (and
the analogous block around the other claim at lines ~255-259) to stop descending
once a directory is claimed by returning early or marking the directory as
claimed before recursing so the walker skips its children—ensure the change
occurs where workflow.name and canonicalId are used to claim the dir so that
after the console.debug/claim call the function does not traverse into that
dir's subtree.
- Around line 111-112: scanInstalledModules() and collectSkills() are performing
duplicate recursive walks and redundant file checks; consolidate skill discovery
into the single traversal inside scanInstalledModules() so you only walk each
module tree once, populate skillClaimedDirs during that pass, and remove the
separate collectSkills() recursion. While traversing, use the directory entries
returned by readdir() to detect workflow.md / workflow.yaml instead of calling
pathExists() again (i.e., check names in the readdir() result), and remove the
extra scans referenced around collectSkills() and the duplicated ranges (lines
~153-179 and ~1288-1335). Ensure the logic that previously lived in
collectSkills() (claiming skill directories and detecting skill-only modules) is
invoked from scanInstalledModules() so callers of collectSkills() can be removed
or routed to the new single-pass implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8a7cce3-0110-4554-8d51-a3542dac3ce3

📥 Commits

Reviewing files that changed from the base of the PR and between dc8293e and 47a58ad.

📒 Files selected for processing (2)
  • test/test-installation-components.js
  • tools/cli/installers/lib/core/manifest-generator.js

alexeyv and others added 2 commits March 8, 2026 08:54
Add collectSkills() that recursively walks module trees to discover
type:skill directories anywhere, replacing the band-aid detection
inside collectWorkflows(). Legacy collectors now skip claimed dirs.
scanInstalledModules recognizes skill-only modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing skillClaimedDirs guard to getAgentsFromDir (F1)
- Add skills to this.files[] in collectSkills (F2)
- Add test for type:skill inside workflows/ dir (F5)
- Warn on malformed workflow.md parse in skill dirs (F6)
- Add skills count to generateManifests return value (F9)
- Remove redundant \r? from regex after line normalization (F10)
- Normalize path.relative to forward slashes for cross-platform (F12)
- Enforce directory name as skill canonicalId, warn if manifest overrides (F13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alexeyv alexeyv force-pushed the unified-skill-scanner branch from 7427f03 to 149c404 Compare March 8, 2026 14:58
@alexeyv alexeyv merged commit 140ae57 into bmad-code-org:main Mar 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant