feat(skills): add type:skill manifest for verbatim directory copying (#1851)
* feat(skills): add type:skill manifest for verbatim skill directory copying
Introduce `type: skill` in bmad-skill-manifest.yaml to signal the
installer to copy entire skill directories verbatim into IDE skill
directories, replacing the launcher-based approach.
Changes:
- skill-manifest.js: fix single-entry detection for type-only manifests,
add getArtifactType export
- manifest-generator.js: collect type:skill entries separately, write
skill-manifest.csv, derive canonicalId from directory name
- _config-driven.js: add installVerbatimSkills with YAML-safe SKILL.md
generation, stale file cleanup, and warning on parse failures
- Rename quick-dev-new-preview to bmad-quick-dev-new-preview so
directory name is the canonical ID
- Update workflow.md installed_path to reference IDE skill base directory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: replace {installed_path} with relative paths in quick-dev skill
Skills resolve paths relative to the skill root directory per the
open agent standard, so the installed_path variable is unnecessary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(skills): add install_to_bmad flag and skill: help catalog reference
Add install_to_bmad flag to skill manifests (default true) enabling
skills to opt out of _bmad/ copy while retaining .claude/skills/
installation. Support skill:<canonicalId> references in module-help.csv
workflow-file column. Fix stale quick-dev-new-preview directory
references in agent YAML and help catalog.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add install_to_bmad design contract tests
Unit tests against getInstallToBmad and loadSkillManifest that nail
down the 4 core design decisions for the install_to_bmad flag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reset skills array between runs and allow skill-only targets
- Reset this.skills and this.files in ManifestGenerator to prevent stale
data when instance is reused across multiple manifest runs
- Allow targets with empty artifact_types to still install verbatim
skills by checking skill_format before short-circuiting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve broken file references in quick-dev-new-preview workflow
- Fix step-02-plan.md templateFile path (./tech-spec-template.md → ../tech-spec-template.md)
- Teach validate-file-refs.js to skip skill: prefixed references in CSV
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8e5898e862
commit
7f1a55ca8c
|
|
@ -27,8 +27,8 @@ agent:
|
||||||
exec: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md"
|
exec: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md"
|
||||||
description: "[QD] Quick-flow Develop: Implement a story tech spec end-to-end (Core of Quick Flow)"
|
description: "[QD] Quick-flow Develop: Implement a story tech spec end-to-end (Core of Quick Flow)"
|
||||||
|
|
||||||
- trigger: QQ or fuzzy match on quick-dev-new-preview
|
- trigger: QQ or fuzzy match on bmad-quick-dev-new-preview
|
||||||
exec: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/workflow.md"
|
exec: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md"
|
||||||
description: "[QQ] Quick Dev New (Preview): Unified quick flow — clarify intent, plan, implement, review, present (experimental)"
|
description: "[QQ] Quick Dev New (Preview): Unified quick flow — clarify intent, plan, implement, review, present (experimental)"
|
||||||
|
|
||||||
- trigger: CR or fuzzy match on code-review
|
- trigger: CR or fuzzy match on code-review
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ bmm,anytime,Document Project,DP,,_bmad/bmm/workflows/document-project/workflow.y
|
||||||
bmm,anytime,Generate Project Context,GPC,,_bmad/bmm/workflows/generate-project-context/workflow.md,bmad-bmm-generate-project-context,false,analyst,Create Mode,"Scan existing codebase to generate a lean LLM-optimized project-context.md containing critical implementation rules patterns and conventions for AI agents. Essential for brownfield projects and quick-flow.",output_folder,"project context",
|
bmm,anytime,Generate Project Context,GPC,,_bmad/bmm/workflows/generate-project-context/workflow.md,bmad-bmm-generate-project-context,false,analyst,Create Mode,"Scan existing codebase to generate a lean LLM-optimized project-context.md containing critical implementation rules patterns and conventions for AI agents. Essential for brownfield projects and quick-flow.",output_folder,"project context",
|
||||||
bmm,anytime,Quick Spec,QS,,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad-bmm-quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps brownfield additions to well established patterns utilities without extensive planning",planning_artifacts,"tech spec",
|
bmm,anytime,Quick Spec,QS,,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad-bmm-quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps brownfield additions to well established patterns utilities without extensive planning",planning_artifacts,"tech spec",
|
||||||
bmm,anytime,Quick Dev,QD,,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad-bmm-quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,,
|
bmm,anytime,Quick Dev,QD,,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad-bmm-quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,,
|
||||||
bmm,anytime,Quick Dev New Preview,QQ,,_bmad/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/workflow.md,bmad-bmm-quick-dev-new-preview,false,quick-flow-solo-dev,Create Mode,"Unified quick flow (experimental): clarify intent plan implement review and present in a single workflow",implementation_artifacts,"tech spec implementation",
|
bmm,anytime,Quick Dev New Preview,QQ,,skill:bmad-quick-dev-new-preview,bmad-bmm-quick-dev-new-preview,false,quick-flow-solo-dev,Create Mode,"Unified quick flow (experimental): clarify intent plan implement review and present in a single workflow",implementation_artifacts,"tech spec implementation",
|
||||||
bmm,anytime,Correct Course,CC,,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad-bmm-correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal",
|
bmm,anytime,Correct Course,CC,,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad-bmm-correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal",
|
||||||
bmm,anytime,Write Document,WD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Describe in detail what you want, and the agent will follow the documentation best practices defined in agent memory. Multi-turn conversation with subprocess for research/review.",project-knowledge,"document",
|
bmm,anytime,Write Document,WD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Describe in detail what you want, and the agent will follow the documentation best practices defined in agent memory. Multi-turn conversation with subprocess for research/review.",project-knowledge,"document",
|
||||||
bmm,anytime,Update Standards,US,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.",_bmad/_memory/tech-writer-sidecar,"standards",
|
bmm,anytime,Update Standards,US,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.",_bmad/_memory/tech-writer-sidecar,"standards",
|
||||||
|
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
type: skill
|
||||||
|
|
@ -48,5 +48,5 @@ spec_file: '' # set at runtime before leaving this step
|
||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
||||||
- One-shot / ready-for-dev: Read fully and follow `{installed_path}/steps/step-03-implement.md`
|
- One-shot / ready-for-dev: Read fully and follow `./steps/step-03-implement.md`
|
||||||
- Plan-code-review: Read fully and follow `{installed_path}/steps/step-02-plan.md`
|
- Plan-code-review: Read fully and follow `./steps/step-02-plan.md`
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
name: 'step-02-plan'
|
name: 'step-02-plan'
|
||||||
description: 'Investigate, generate spec, present for approval'
|
description: 'Investigate, generate spec, present for approval'
|
||||||
|
|
||||||
templateFile: '{installed_path}/tech-spec-template.md'
|
templateFile: '../tech-spec-template.md'
|
||||||
wipFile: '{implementation_artifacts}/tech-spec-wip.md'
|
wipFile: '{implementation_artifacts}/tech-spec-wip.md'
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
---
|
---
|
||||||
|
|
@ -36,4 +36,4 @@ Present summary. If token count exceeded 1600 and user chose [K], include the to
|
||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
||||||
Read fully and follow `{installed_path}/steps/step-03-implement.md`
|
Read fully and follow `./steps/step-03-implement.md`
|
||||||
|
|
@ -32,4 +32,4 @@ Otherwise (`execution_mode = "plan-code-review"`): hand `{spec_file}` to a sub-a
|
||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
||||||
Read fully and follow `{installed_path}/steps/step-04-review.md`
|
Read fully and follow `./steps/step-04-review.md`
|
||||||
|
|
@ -46,7 +46,7 @@ Do NOT `git add` anything — this is read-only inspection.
|
||||||
- **reject** — noise. Drop silently. When unsure between defer and reject, prefer reject — only defer findings you are confident are real.
|
- **reject** — noise. Drop silently. When unsure between defer and reject, prefer reject — only defer findings you are confident are real.
|
||||||
3. Process findings in cascading order. If intent_gap or bad_spec findings exist, they trigger a loopback — lower findings are moot since code will be re-derived. If neither exists, process patch and defer normally. Increment `{specLoopIteration}` on each loopback. If it exceeds 5, HALT and escalate to the human. On any loopback, re-evaluate routing — if scope has grown beyond one-shot, escalate `execution_mode` to plan-code-review.
|
3. Process findings in cascading order. If intent_gap or bad_spec findings exist, they trigger a loopback — lower findings are moot since code will be re-derived. If neither exists, process patch and defer normally. Increment `{specLoopIteration}` on each loopback. If it exceeds 5, HALT and escalate to the human. On any loopback, re-evaluate routing — if scope has grown beyond one-shot, escalate `execution_mode` to plan-code-review.
|
||||||
- **intent_gap** — Root cause is inside `<frozen-after-approval>`. Revert code changes. Loop back to the human to resolve, then re-run steps 2–4.
|
- **intent_gap** — Root cause is inside `<frozen-after-approval>`. Revert code changes. Loop back to the human to resolve, then re-run steps 2–4.
|
||||||
- **bad_spec** — Root cause is outside `<frozen-after-approval>`. Before reverting code: extract KEEP instructions for positive preservation (what worked well and must survive re-derivation). Revert code changes. Read the `## Spec Change Log` in `{spec_file}` and strictly respect all logged constraints when amending the non-frozen sections that contain the root cause. Append a new change-log entry recording: the triggering finding, what was amended, the known-bad state avoided, and the KEEP instructions. Read fully and follow `{installed_path}/steps/step-03-implement.md` to re-derive the code, then this step will run again.
|
- **bad_spec** — Root cause is outside `<frozen-after-approval>`. Before reverting code: extract KEEP instructions for positive preservation (what worked well and must survive re-derivation). Revert code changes. Read the `## Spec Change Log` in `{spec_file}` and strictly respect all logged constraints when amending the non-frozen sections that contain the root cause. Append a new change-log entry recording: the triggering finding, what was amended, the known-bad state avoided, and the KEEP instructions. Read fully and follow `./steps/step-03-implement.md` to re-derive the code, then this step will run again.
|
||||||
- **patch** — Auto-fix. These are the only findings that survive loopbacks.
|
- **patch** — Auto-fix. These are the only findings that survive loopbacks.
|
||||||
- **defer** — Append to `{deferred_work_file}`.
|
- **defer** — Append to `{deferred_work_file}`.
|
||||||
- **reject** — Drop silently.
|
- **reject** — Drop silently.
|
||||||
|
|
@ -54,4 +54,4 @@ Do NOT `git add` anything — this is read-only inspection.
|
||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
||||||
Read fully and follow `{installed_path}/steps/step-05-present.md`
|
Read fully and follow `./steps/step-05-present.md`
|
||||||
|
|
@ -81,10 +81,9 @@ YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `
|
||||||
|
|
||||||
### 2. Paths
|
### 2. Paths
|
||||||
|
|
||||||
- `installed_path` = `{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev-new-preview`
|
- `templateFile` = `./tech-spec-template.md`
|
||||||
- `templateFile` = `{installed_path}/tech-spec-template.md`
|
|
||||||
- `wipFile` = `{implementation_artifacts}/tech-spec-wip.md`
|
- `wipFile` = `{implementation_artifacts}/tech-spec-wip.md`
|
||||||
|
|
||||||
### 3. First Step Execution
|
### 3. First Step Execution
|
||||||
|
|
||||||
Read fully and follow: `{installed_path}/steps/step-01-clarify-and-route.md` to begin the workflow.
|
Read fully and follow: `./steps/step-01-clarify-and-route.md` to begin the workflow.
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-quick-dev-new-preview
|
|
||||||
type: workflow
|
|
||||||
description: "Unified quick flow - clarify intent, plan, implement, review, present"
|
|
||||||
|
|
@ -21,6 +21,12 @@ description: 'Analyzes what is done and the users query and offers advice on wha
|
||||||
When `command` field has a value:
|
When `command` field has a value:
|
||||||
- Show the command prefixed with `/` (e.g., `/bmad-bmm-create-prd`)
|
- Show the command prefixed with `/` (e.g., `/bmad-bmm-create-prd`)
|
||||||
|
|
||||||
|
### Skill-Referenced Workflows
|
||||||
|
When `workflow-file` starts with `skill:`:
|
||||||
|
- The value is a skill reference (e.g., `skill:bmad-quick-dev-new-preview`), NOT a file path
|
||||||
|
- Do NOT attempt to resolve or load it as a file path
|
||||||
|
- Display using the `command` column value prefixed with `/` (same as command-based workflows)
|
||||||
|
|
||||||
### Agent-Based Workflows
|
### Agent-Based Workflows
|
||||||
When `command` field is empty:
|
When `command` field is empty:
|
||||||
- User loads agent first via `/agent-command`
|
- User loads agent first via `/agent-command`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
/**
|
||||||
|
* install_to_bmad Flag — Design Contract Tests
|
||||||
|
*
|
||||||
|
* Unit tests against the functions that implement the install_to_bmad flag.
|
||||||
|
* These nail down the 4 core design decisions:
|
||||||
|
*
|
||||||
|
* 1. true/omitted → skill stays in _bmad/ (default behavior)
|
||||||
|
* 2. false → skill removed from _bmad/ after IDE install
|
||||||
|
* 3. No platform → no cleanup runs (cleanup lives in installVerbatimSkills)
|
||||||
|
* 4. Mixed flags → each skill evaluated independently
|
||||||
|
*
|
||||||
|
* Usage: node test/test-install-to-bmad.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const { loadSkillManifest, getInstallToBmad } = require('../tools/cli/installers/lib/ide/shared/skill-manifest');
|
||||||
|
|
||||||
|
// ANSI colors
|
||||||
|
const colors = {
|
||||||
|
reset: '\u001B[0m',
|
||||||
|
green: '\u001B[32m',
|
||||||
|
red: '\u001B[31m',
|
||||||
|
yellow: '\u001B[33m',
|
||||||
|
cyan: '\u001B[36m',
|
||||||
|
dim: '\u001B[2m',
|
||||||
|
};
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function assert(condition, testName, errorMessage = '') {
|
||||||
|
if (condition) {
|
||||||
|
console.log(`${colors.green}✓${colors.reset} ${testName}`);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.red}✗${colors.reset} ${testName}`);
|
||||||
|
if (errorMessage) {
|
||||||
|
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
|
||||||
|
}
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
console.log(`${colors.cyan}========================================`);
|
||||||
|
console.log('install_to_bmad — Design Contract Tests');
|
||||||
|
console.log(`========================================${colors.reset}\n`);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 1. true/omitted → getInstallToBmad returns true (keep in _bmad/)
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Design decision 1: true or omitted → skill stays in _bmad/${colors.reset}\n`);
|
||||||
|
|
||||||
|
// Null manifest (no bmad-skill-manifest.yaml) → true
|
||||||
|
assert(getInstallToBmad(null, 'workflow.md') === true, 'null manifest defaults to true');
|
||||||
|
|
||||||
|
// Single-entry, flag omitted → true
|
||||||
|
assert(
|
||||||
|
getInstallToBmad({ __single: { type: 'skill' } }, 'workflow.md') === true,
|
||||||
|
'single-entry manifest with flag omitted defaults to true',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Single-entry, explicit true → true
|
||||||
|
assert(
|
||||||
|
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: true } }, 'workflow.md') === true,
|
||||||
|
'single-entry manifest with explicit true returns true',
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 2. false → getInstallToBmad returns false (remove from _bmad/)
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Design decision 2: false → skill removed from _bmad/${colors.reset}\n`);
|
||||||
|
|
||||||
|
// Single-entry, explicit false → false
|
||||||
|
assert(
|
||||||
|
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: false } }, 'workflow.md') === false,
|
||||||
|
'single-entry manifest with explicit false returns false',
|
||||||
|
);
|
||||||
|
|
||||||
|
// loadSkillManifest round-trip: YAML with false is preserved through load
|
||||||
|
{
|
||||||
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-itb-'));
|
||||||
|
await fs.writeFile(path.join(tmpDir, 'bmad-skill-manifest.yaml'), 'type: skill\ninstall_to_bmad: false\n');
|
||||||
|
const loaded = await loadSkillManifest(tmpDir);
|
||||||
|
assert(getInstallToBmad(loaded, 'workflow.md') === false, 'loadSkillManifest preserves install_to_bmad: false through round-trip');
|
||||||
|
await fs.remove(tmpDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 3. No platform → cleanup only runs inside installVerbatimSkills
|
||||||
|
// (This is a design invariant: getInstallToBmad is only consulted
|
||||||
|
// during IDE install. Without a platform, the flag has no effect.)
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Design decision 3: flag is a per-skill property, not a pipeline gate${colors.reset}\n`);
|
||||||
|
|
||||||
|
// The flag value is stored but doesn't trigger any side effects by itself.
|
||||||
|
// Cleanup is driven by reading the CSV column inside installVerbatimSkills.
|
||||||
|
// We verify the flag is just data — getInstallToBmad doesn't touch the filesystem.
|
||||||
|
{
|
||||||
|
const manifest = { __single: { type: 'skill', install_to_bmad: false } };
|
||||||
|
const result = getInstallToBmad(manifest, 'workflow.md');
|
||||||
|
assert(typeof result === 'boolean', 'getInstallToBmad returns a boolean (pure data, no side effects)');
|
||||||
|
assert(result === false, 'false value is faithfully returned for consumer to act on');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 4. Mixed flags → each skill evaluated independently
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Design decision 4: mixed flags — each skill independent${colors.reset}\n`);
|
||||||
|
|
||||||
|
// Multi-entry manifest: different files can have different flags
|
||||||
|
{
|
||||||
|
const manifest = {
|
||||||
|
'workflow.md': { type: 'skill', install_to_bmad: false },
|
||||||
|
'other.md': { type: 'skill', install_to_bmad: true },
|
||||||
|
};
|
||||||
|
assert(getInstallToBmad(manifest, 'workflow.md') === false, 'multi-entry: workflow.md with false returns false');
|
||||||
|
assert(getInstallToBmad(manifest, 'other.md') === true, 'multi-entry: other.md with true returns true');
|
||||||
|
assert(getInstallToBmad(manifest, 'unknown.md') === true, 'multi-entry: unknown file defaults to true');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Summary
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.cyan}========================================`);
|
||||||
|
console.log('Results:');
|
||||||
|
console.log(` Passed: ${colors.green}${passed}${colors.reset}`);
|
||||||
|
console.log(` Failed: ${colors.red}${failed}${colors.reset}`);
|
||||||
|
console.log(`========================================${colors.reset}\n`);
|
||||||
|
|
||||||
|
if (failed === 0) {
|
||||||
|
console.log(`${colors.green}All install_to_bmad contract tests passed!${colors.reset}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.red}Some install_to_bmad contract tests failed${colors.reset}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests().catch((error) => {
|
||||||
|
console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -5,7 +5,12 @@ const crypto = require('node:crypto');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest');
|
const {
|
||||||
|
loadSkillManifest: loadSkillManifestShared,
|
||||||
|
getCanonicalId: getCanonicalIdShared,
|
||||||
|
getArtifactType: getArtifactTypeShared,
|
||||||
|
getInstallToBmad: getInstallToBmadShared,
|
||||||
|
} = require('../ide/shared/skill-manifest');
|
||||||
|
|
||||||
// Load package.json for version info
|
// Load package.json for version info
|
||||||
const packageJson = require('../../../../../package.json');
|
const packageJson = require('../../../../../package.json');
|
||||||
|
|
@ -16,6 +21,7 @@ const packageJson = require('../../../../../package.json');
|
||||||
class ManifestGenerator {
|
class ManifestGenerator {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.workflows = [];
|
this.workflows = [];
|
||||||
|
this.skills = [];
|
||||||
this.agents = [];
|
this.agents = [];
|
||||||
this.tasks = [];
|
this.tasks = [];
|
||||||
this.tools = [];
|
this.tools = [];
|
||||||
|
|
@ -34,6 +40,16 @@ class ManifestGenerator {
|
||||||
return getCanonicalIdShared(manifest, filename);
|
return getCanonicalIdShared(manifest, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Delegate to shared skill-manifest module */
|
||||||
|
getArtifactType(manifest, filename) {
|
||||||
|
return getArtifactTypeShared(manifest, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delegate to shared skill-manifest module */
|
||||||
|
getInstallToBmad(manifest, filename) {
|
||||||
|
return getInstallToBmadShared(manifest, filename);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean text for CSV output by normalizing whitespace.
|
* Clean text for CSV output by normalizing whitespace.
|
||||||
* Note: Quote escaping is handled by escapeCsv() at write time.
|
* Note: Quote escaping is handled by escapeCsv() at write time.
|
||||||
|
|
@ -89,6 +105,9 @@ class ManifestGenerator {
|
||||||
// Filter out any undefined/null values from IDE list
|
// Filter out any undefined/null values from IDE list
|
||||||
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string');
|
||||||
|
|
||||||
|
// Reset files list (defensive: prevent stale data if instance is reused)
|
||||||
|
this.files = [];
|
||||||
|
|
||||||
// Collect workflow data
|
// Collect workflow data
|
||||||
await this.collectWorkflows(selectedModules);
|
await this.collectWorkflows(selectedModules);
|
||||||
|
|
||||||
|
|
@ -105,6 +124,7 @@ class ManifestGenerator {
|
||||||
const manifestFiles = [
|
const manifestFiles = [
|
||||||
await this.writeMainManifest(cfgDir),
|
await this.writeMainManifest(cfgDir),
|
||||||
await this.writeWorkflowManifest(cfgDir),
|
await this.writeWorkflowManifest(cfgDir),
|
||||||
|
await this.writeSkillManifest(cfgDir),
|
||||||
await this.writeAgentManifest(cfgDir),
|
await this.writeAgentManifest(cfgDir),
|
||||||
await this.writeTaskManifest(cfgDir),
|
await this.writeTaskManifest(cfgDir),
|
||||||
await this.writeToolManifest(cfgDir),
|
await this.writeToolManifest(cfgDir),
|
||||||
|
|
@ -127,6 +147,7 @@ class ManifestGenerator {
|
||||||
*/
|
*/
|
||||||
async collectWorkflows(selectedModules) {
|
async collectWorkflows(selectedModules) {
|
||||||
this.workflows = [];
|
this.workflows = [];
|
||||||
|
this.skills = [];
|
||||||
|
|
||||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
||||||
for (const moduleName of this.updatedModules) {
|
for (const moduleName of this.updatedModules) {
|
||||||
|
|
@ -228,6 +249,25 @@ class ManifestGenerator {
|
||||||
? `${this.bmadFolderName}/core/workflows/${relativePath}/${entry.name}`
|
? `${this.bmadFolderName}/core/workflows/${relativePath}/${entry.name}`
|
||||||
: `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/${entry.name}`;
|
: `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/${entry.name}`;
|
||||||
|
|
||||||
|
// Check if this is a type:skill entry — collect separately, skip workflow CSV
|
||||||
|
const artifactType = this.getArtifactType(skillManifest, entry.name);
|
||||||
|
if (artifactType === 'skill') {
|
||||||
|
const canonicalId = path.basename(dir);
|
||||||
|
this.skills.push({
|
||||||
|
name: workflow.name,
|
||||||
|
description: this.cleanForCSV(workflow.description),
|
||||||
|
module: moduleName,
|
||||||
|
path: installPath,
|
||||||
|
canonicalId,
|
||||||
|
install_to_bmad: this.getInstallToBmad(skillManifest, entry.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log(`[DEBUG] ✓ Added skill (skipped workflow CSV): ${workflow.name} as ${canonicalId}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Workflows with standalone: false are filtered out above
|
// Workflows with standalone: false are filtered out above
|
||||||
workflows.push({
|
workflows.push({
|
||||||
name: workflow.name,
|
name: workflow.name,
|
||||||
|
|
@ -793,6 +833,32 @@ class ManifestGenerator {
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write skill manifest CSV
|
||||||
|
* @returns {string} Path to the manifest file
|
||||||
|
*/
|
||||||
|
async writeSkillManifest(cfgDir) {
|
||||||
|
const csvPath = path.join(cfgDir, 'skill-manifest.csv');
|
||||||
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
|
|
||||||
|
let csvContent = 'canonicalId,name,description,module,path,install_to_bmad\n';
|
||||||
|
|
||||||
|
for (const skill of this.skills) {
|
||||||
|
const row = [
|
||||||
|
escapeCsv(skill.canonicalId),
|
||||||
|
escapeCsv(skill.name),
|
||||||
|
escapeCsv(skill.description),
|
||||||
|
escapeCsv(skill.module),
|
||||||
|
escapeCsv(skill.path),
|
||||||
|
escapeCsv(skill.install_to_bmad),
|
||||||
|
].join(',');
|
||||||
|
csvContent += row + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(csvPath, csvContent);
|
||||||
|
return csvPath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write agent manifest CSV
|
* Write agent manifest CSV
|
||||||
* @returns {string} Path to the manifest file
|
* @returns {string} Path to the manifest file
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const prompts = require('../../../lib/prompts');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||||
|
const csv = require('csv-parse/sync');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config-driven IDE setup handler
|
* Config-driven IDE setup handler
|
||||||
|
|
@ -116,18 +117,21 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
async installToTarget(projectDir, bmadDir, config, options) {
|
async installToTarget(projectDir, bmadDir, config, options) {
|
||||||
const { target_dir, template_type, artifact_types } = config;
|
const { target_dir, template_type, artifact_types } = config;
|
||||||
|
|
||||||
// Skip targets with explicitly empty artifact_types array
|
// Skip targets with explicitly empty artifact_types and no verbatim skills
|
||||||
// This prevents creating empty directories when no artifacts will be written
|
// This prevents creating empty directories when no artifacts will be written
|
||||||
if (Array.isArray(artifact_types) && artifact_types.length === 0) {
|
const skipStandardArtifacts = Array.isArray(artifact_types) && artifact_types.length === 0;
|
||||||
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } };
|
if (skipStandardArtifacts && !config.skill_format) {
|
||||||
|
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetPath = path.join(projectDir, target_dir);
|
const targetPath = path.join(projectDir, target_dir);
|
||||||
await this.ensureDir(targetPath);
|
await this.ensureDir(targetPath);
|
||||||
|
|
||||||
const selectedModules = options.selectedModules || [];
|
const selectedModules = options.selectedModules || [];
|
||||||
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 };
|
||||||
|
|
||||||
|
// Install standard artifacts (agents, workflows, tasks, tools)
|
||||||
|
if (!skipStandardArtifacts) {
|
||||||
// Install agents
|
// Install agents
|
||||||
if (!artifact_types || artifact_types.includes('agents')) {
|
if (!artifact_types || artifact_types.includes('agents')) {
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||||
|
|
@ -150,6 +154,12 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
results.tasks = taskToolResult.tasks || 0;
|
results.tasks = taskToolResult.tasks || 0;
|
||||||
results.tools = taskToolResult.tools || 0;
|
results.tools = taskToolResult.tools || 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install verbatim skills (type: skill)
|
||||||
|
if (config.skill_format) {
|
||||||
|
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
|
||||||
|
}
|
||||||
|
|
||||||
await this.printSummary(results, target_dir, options);
|
await this.printSummary(results, target_dir, options);
|
||||||
return { success: true, results };
|
return { success: true, results };
|
||||||
|
|
@ -164,7 +174,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
* @returns {Promise<Object>} Installation result
|
* @returns {Promise<Object>} Installation result
|
||||||
*/
|
*/
|
||||||
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
||||||
const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 };
|
||||||
|
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
||||||
|
|
@ -173,6 +183,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
allResults.workflows += result.results.workflows || 0;
|
allResults.workflows += result.results.workflows || 0;
|
||||||
allResults.tasks += result.results.tasks || 0;
|
allResults.tasks += result.results.tasks || 0;
|
||||||
allResults.tools += result.results.tools || 0;
|
allResults.tools += result.results.tools || 0;
|
||||||
|
allResults.skills += result.results.skills || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -622,6 +633,94 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
return baseName.replace(/\.md$/, extension);
|
return baseName.replace(/\.md$/, extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install verbatim skill directories (type: skill entries from skill-manifest.csv).
|
||||||
|
* Copies the entire source directory into the IDE skill directory, auto-generating SKILL.md.
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {string} targetPath - Target skills directory
|
||||||
|
* @param {Object} config - Installation configuration
|
||||||
|
* @returns {Promise<number>} Count of skills installed
|
||||||
|
*/
|
||||||
|
async installVerbatimSkills(projectDir, bmadDir, targetPath, config) {
|
||||||
|
const bmadFolderName = path.basename(bmadDir);
|
||||||
|
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(csvPath))) return 0;
|
||||||
|
|
||||||
|
const csvContent = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const records = csv.parse(csvContent, {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
const canonicalId = record.canonicalId;
|
||||||
|
if (!canonicalId) continue;
|
||||||
|
|
||||||
|
// Derive source directory from path column
|
||||||
|
// path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md"
|
||||||
|
// Strip bmadFolderName prefix and join with bmadDir, then get dirname
|
||||||
|
const relativePath = record.path.replace(new RegExp(`^${bmadFolderName}/`), '');
|
||||||
|
const sourceFile = path.join(bmadDir, relativePath);
|
||||||
|
const sourceDir = path.dirname(sourceFile);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(sourceDir))) continue;
|
||||||
|
|
||||||
|
// Clean target before copy to prevent stale files
|
||||||
|
const skillDir = path.join(targetPath, canonicalId);
|
||||||
|
await fs.remove(skillDir);
|
||||||
|
await fs.ensureDir(skillDir);
|
||||||
|
|
||||||
|
// Parse workflow.md frontmatter for description
|
||||||
|
let description = `${canonicalId} skill`;
|
||||||
|
try {
|
||||||
|
const workflowContent = await fs.readFile(sourceFile, 'utf8');
|
||||||
|
const fmMatch = workflowContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
|
if (fmMatch) {
|
||||||
|
const frontmatter = yaml.parse(fmMatch[1]);
|
||||||
|
if (frontmatter?.description) {
|
||||||
|
description = frontmatter.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await prompts.log.warn(`Failed to parse frontmatter from ${sourceFile}: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate SKILL.md with YAML-safe frontmatter
|
||||||
|
const frontmatterYaml = yaml.stringify({ name: canonicalId, description: String(description) }, { lineWidth: 0 }).trimEnd();
|
||||||
|
const skillMd = `---\n${frontmatterYaml}\n---\n\nIT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL workflow.md, READ its entire contents and follow its directions exactly!\n`;
|
||||||
|
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
|
||||||
|
|
||||||
|
// Copy all files except bmad-skill-manifest.yaml
|
||||||
|
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name === 'bmad-skill-manifest.yaml') continue;
|
||||||
|
const srcPath = path.join(sourceDir, entry.name);
|
||||||
|
const destPath = path.join(skillDir, entry.name);
|
||||||
|
await fs.copy(srcPath, destPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-install cleanup: remove _bmad/ directories for skills with install_to_bmad === "false"
|
||||||
|
for (const record of records) {
|
||||||
|
if (record.install_to_bmad === 'false') {
|
||||||
|
const relativePath = record.path.replace(new RegExp(`^${bmadFolderName}/`), '');
|
||||||
|
const sourceFile = path.join(bmadDir, relativePath);
|
||||||
|
const sourceDir = path.dirname(sourceFile);
|
||||||
|
if (await fs.pathExists(sourceDir)) {
|
||||||
|
await fs.remove(sourceDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Print installation summary
|
* Print installation summary
|
||||||
* @param {Object} results - Installation results
|
* @param {Object} results - Installation results
|
||||||
|
|
@ -634,6 +733,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
if (results.workflows > 0) parts.push(`${results.workflows} workflows`);
|
if (results.workflows > 0) parts.push(`${results.workflows} workflows`);
|
||||||
if (results.tasks > 0) parts.push(`${results.tasks} tasks`);
|
if (results.tasks > 0) parts.push(`${results.tasks} tasks`);
|
||||||
if (results.tools > 0) parts.push(`${results.tools} tools`);
|
if (results.tools > 0) parts.push(`${results.tools} tools`);
|
||||||
|
if (results.skills > 0) parts.push(`${results.skills} skills`);
|
||||||
await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`);
|
await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ async function loadSkillManifest(dirPath) {
|
||||||
const content = await fs.readFile(manifestPath, 'utf8');
|
const content = await fs.readFile(manifestPath, 'utf8');
|
||||||
const parsed = yaml.parse(content);
|
const parsed = yaml.parse(content);
|
||||||
if (!parsed || typeof parsed !== 'object') return null;
|
if (!parsed || typeof parsed !== 'object') return null;
|
||||||
if (parsed.canonicalId) return { __single: parsed };
|
if (parsed.canonicalId || parsed.type) return { __single: parsed };
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
|
console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
|
||||||
|
|
@ -45,4 +45,46 @@ function getCanonicalId(manifest, filename) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadSkillManifest, getCanonicalId };
|
/**
|
||||||
|
* Get the artifact type for a specific file from a loaded skill manifest.
|
||||||
|
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
||||||
|
* @param {string} filename - Source filename to look up
|
||||||
|
* @returns {string|null} type or null
|
||||||
|
*/
|
||||||
|
function getArtifactType(manifest, filename) {
|
||||||
|
if (!manifest) return null;
|
||||||
|
// Single-entry manifest applies to all files in the directory
|
||||||
|
if (manifest.__single) return manifest.__single.type || null;
|
||||||
|
// Multi-entry: look up by filename directly
|
||||||
|
if (manifest[filename]) return manifest[filename].type || null;
|
||||||
|
// Fallback: try alternate extensions for compiled files
|
||||||
|
const baseName = filename.replace(/\.(md|xml)$/i, '');
|
||||||
|
const agentKey = `${baseName}.agent.yaml`;
|
||||||
|
if (manifest[agentKey]) return manifest[agentKey].type || null;
|
||||||
|
const xmlKey = `${baseName}.xml`;
|
||||||
|
if (manifest[xmlKey]) return manifest[xmlKey].type || null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the install_to_bmad flag for a specific file from a loaded skill manifest.
|
||||||
|
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
||||||
|
* @param {string} filename - Source filename to look up
|
||||||
|
* @returns {boolean} install_to_bmad value (defaults to true)
|
||||||
|
*/
|
||||||
|
function getInstallToBmad(manifest, filename) {
|
||||||
|
if (!manifest) return true;
|
||||||
|
// Single-entry manifest applies to all files in the directory
|
||||||
|
if (manifest.__single) return manifest.__single.install_to_bmad !== false;
|
||||||
|
// Multi-entry: look up by filename directly
|
||||||
|
if (manifest[filename]) return manifest[filename].install_to_bmad !== false;
|
||||||
|
// Fallback: try alternate extensions for compiled files
|
||||||
|
const baseName = filename.replace(/\.(md|xml)$/i, '');
|
||||||
|
const agentKey = `${baseName}.agent.yaml`;
|
||||||
|
if (manifest[agentKey]) return manifest[agentKey].install_to_bmad !== false;
|
||||||
|
const xmlKey = `${baseName}.xml`;
|
||||||
|
if (manifest[xmlKey]) return manifest[xmlKey].install_to_bmad !== false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType, getInstallToBmad };
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,8 @@ function extractCsvRefs(filePath, content) {
|
||||||
const raw = record['workflow-file'];
|
const raw = record['workflow-file'];
|
||||||
if (!raw || raw.trim() === '') continue;
|
if (!raw || raw.trim() === '') continue;
|
||||||
if (!isResolvable(raw)) continue;
|
if (!isResolvable(raw)) continue;
|
||||||
|
// skill: prefixed references are resolved by the IDE/CLI, not as file paths
|
||||||
|
if (raw.startsWith('skill:')) continue;
|
||||||
|
|
||||||
// Line = header (1) + data row index (0-based) + 1
|
// Line = header (1) + data row index (0-based) + 1
|
||||||
const line = i + 2;
|
const line = i + 2;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue