fix(manifest): address PR review findings from triage
- 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>
This commit is contained in:
parent
99d2afe584
commit
149c4049fd
|
|
@ -1605,7 +1605,7 @@ async function runTests() {
|
||||||
// --- Skill at unusual path: core/custom-area/my-skill/ ---
|
// --- Skill at unusual path: core/custom-area/my-skill/ ---
|
||||||
const skillDir29 = path.join(tempFixture29, 'core', 'custom-area', 'my-skill');
|
const skillDir29 = path.join(tempFixture29, 'core', 'custom-area', 'my-skill');
|
||||||
await fs.ensureDir(skillDir29);
|
await fs.ensureDir(skillDir29);
|
||||||
await fs.writeFile(path.join(skillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\ncanonicalId: my-custom-skill\n');
|
await fs.writeFile(path.join(skillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(skillDir29, 'workflow.md'),
|
path.join(skillDir29, 'workflow.md'),
|
||||||
'---\nname: My Custom Skill\ndescription: A skill at an unusual path\n---\n\nSkill body content\n',
|
'---\nname: My Custom Skill\ndescription: A skill at an unusual path\n---\n\nSkill body content\n',
|
||||||
|
|
@ -1620,10 +1620,19 @@ async function runTests() {
|
||||||
'---\nname: Regular Workflow\ndescription: A regular workflow not a skill\n---\n\nWorkflow body\n',
|
'---\nname: Regular Workflow\ndescription: A regular workflow not a skill\n---\n\nWorkflow body\n',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- Skill inside workflows/ dir: core/workflows/wf-skill/ (exercises findWorkflows skip logic) ---
|
||||||
|
const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill');
|
||||||
|
await fs.ensureDir(wfSkillDir29);
|
||||||
|
await fs.writeFile(path.join(wfSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(wfSkillDir29, 'workflow.md'),
|
||||||
|
'---\nname: Workflow Skill\ndescription: A skill inside workflows dir\n---\n\nSkill in workflows\n',
|
||||||
|
);
|
||||||
|
|
||||||
// --- Skill inside tasks/ dir: core/tasks/task-skill/ ---
|
// --- Skill inside tasks/ dir: core/tasks/task-skill/ ---
|
||||||
const taskSkillDir29 = path.join(tempFixture29, 'core', 'tasks', 'task-skill');
|
const taskSkillDir29 = path.join(tempFixture29, 'core', 'tasks', 'task-skill');
|
||||||
await fs.ensureDir(taskSkillDir29);
|
await fs.ensureDir(taskSkillDir29);
|
||||||
await fs.writeFile(path.join(taskSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\ncanonicalId: task-skill\n');
|
await fs.writeFile(path.join(taskSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(taskSkillDir29, 'workflow.md'),
|
path.join(taskSkillDir29, 'workflow.md'),
|
||||||
'---\nname: Task Skill\ndescription: A skill inside tasks dir\n---\n\nSkill in tasks\n',
|
'---\nname: Task Skill\ndescription: A skill inside tasks dir\n---\n\nSkill in tasks\n',
|
||||||
|
|
@ -1638,7 +1647,7 @@ async function runTests() {
|
||||||
await generator29.generateManifests(tempFixture29, ['core'], [], { ides: [] });
|
await generator29.generateManifests(tempFixture29, ['core'], [], { ides: [] });
|
||||||
|
|
||||||
// Skill at unusual path should be in skills
|
// Skill at unusual path should be in skills
|
||||||
const skillEntry29 = generator29.skills.find((s) => s.canonicalId === 'my-custom-skill');
|
const skillEntry29 = generator29.skills.find((s) => s.canonicalId === 'my-skill');
|
||||||
assert(skillEntry29 !== undefined, 'Skill at unusual path appears in skills[]');
|
assert(skillEntry29 !== undefined, 'Skill at unusual path appears in skills[]');
|
||||||
assert(skillEntry29 && skillEntry29.name === 'My Custom Skill', 'Skill has correct name from frontmatter');
|
assert(skillEntry29 && skillEntry29.name === 'My Custom Skill', 'Skill has correct name from frontmatter');
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -1665,13 +1674,16 @@ async function runTests() {
|
||||||
const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf');
|
const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf');
|
||||||
assert(regularInSkills29 === undefined, 'Regular type:workflow does NOT appear in skills[]');
|
assert(regularInSkills29 === undefined, 'Regular type:workflow does NOT appear in skills[]');
|
||||||
|
|
||||||
|
// Skill inside workflows/ should be in skills[], NOT in workflows[] (exercises findWorkflows skip at lines 311/322)
|
||||||
|
const wfSkill29 = generator29.skills.find((s) => s.canonicalId === 'wf-skill');
|
||||||
|
assert(wfSkill29 !== undefined, 'Skill in workflows/ dir appears in skills[]');
|
||||||
|
const wfSkillInWorkflows29 = generator29.workflows.find((w) => w.name === 'Workflow Skill');
|
||||||
|
assert(wfSkillInWorkflows29 === undefined, 'Skill in workflows/ dir does NOT appear in workflows[]');
|
||||||
|
|
||||||
// Test scanInstalledModules recognizes skill-only modules
|
// Test scanInstalledModules recognizes skill-only modules
|
||||||
const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod');
|
const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod');
|
||||||
await fs.ensureDir(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill'));
|
await fs.ensureDir(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill'));
|
||||||
await fs.writeFile(
|
await fs.writeFile(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||||
path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'bmad-skill-manifest.yaml'),
|
|
||||||
'type: skill\ncanonicalId: nested-skill\n',
|
|
||||||
);
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'workflow.md'),
|
path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'workflow.md'),
|
||||||
'---\nname: Nested Skill\ndescription: desc\n---\nbody\n',
|
'---\nname: Nested Skill\ndescription: desc\n---\nbody\n',
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ class ManifestGenerator {
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
skills: this.skills.length,
|
||||||
workflows: this.workflows.length,
|
workflows: this.workflows.length,
|
||||||
agents: this.agents.length,
|
agents: this.agents.length,
|
||||||
tasks: this.tasks.length,
|
tasks: this.tasks.length,
|
||||||
|
|
@ -189,7 +190,7 @@ class ManifestGenerator {
|
||||||
if (workflowFile === 'workflow.yaml') {
|
if (workflowFile === 'workflow.yaml') {
|
||||||
workflow = yaml.parse(content);
|
workflow = yaml.parse(content);
|
||||||
} else {
|
} else {
|
||||||
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
if (!frontmatterMatch) {
|
if (!frontmatterMatch) {
|
||||||
if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`);
|
if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -203,12 +204,18 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build path relative from module root
|
// Build path relative from module root
|
||||||
const relativePath = path.relative(modulePath, dir);
|
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
|
||||||
const installPath = relativePath
|
const installPath = relativePath
|
||||||
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${workflowFile}`
|
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${workflowFile}`
|
||||||
: `${this.bmadFolderName}/${moduleName}/${workflowFile}`;
|
: `${this.bmadFolderName}/${moduleName}/${workflowFile}`;
|
||||||
|
|
||||||
const canonicalId = this.getCanonicalId(manifest, workflowFile) || path.basename(dir);
|
// Skills derive canonicalId from directory name — never from manifest
|
||||||
|
if (manifest && manifest.__single && manifest.__single.canonicalId) {
|
||||||
|
console.warn(
|
||||||
|
`Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const canonicalId = path.basename(dir);
|
||||||
|
|
||||||
this.skills.push({
|
this.skills.push({
|
||||||
name: workflow.name,
|
name: workflow.name,
|
||||||
|
|
@ -219,6 +226,14 @@ class ManifestGenerator {
|
||||||
install_to_bmad: this.getInstallToBmad(manifest, workflowFile),
|
install_to_bmad: this.getInstallToBmad(manifest, workflowFile),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add to files list
|
||||||
|
this.files.push({
|
||||||
|
type: 'skill',
|
||||||
|
name: workflow.name,
|
||||||
|
module: moduleName,
|
||||||
|
path: installPath,
|
||||||
|
});
|
||||||
|
|
||||||
this.skillClaimedDirs.add(dir);
|
this.skillClaimedDirs.add(dir);
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
|
|
@ -246,7 +261,9 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
if (hasSkillType && debug) {
|
if (hasSkillType && debug) {
|
||||||
const hasWorkflow = workflowFilenames.some((f) => entries.some((e) => e.name === f));
|
const hasWorkflow = workflowFilenames.some((f) => entries.some((e) => e.name === f));
|
||||||
if (!hasWorkflow) {
|
if (hasWorkflow) {
|
||||||
|
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but workflow file failed to parse: ${dir}`);
|
||||||
|
} else {
|
||||||
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md/workflow.yaml: ${dir}`);
|
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md/workflow.yaml: ${dir}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +364,7 @@ class ManifestGenerator {
|
||||||
workflow = yaml.parse(content);
|
workflow = yaml.parse(content);
|
||||||
} else {
|
} else {
|
||||||
// Parse MD workflow with YAML frontmatter
|
// Parse MD workflow with YAML frontmatter
|
||||||
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
if (!frontmatterMatch) {
|
if (!frontmatterMatch) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
|
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
|
||||||
|
|
@ -462,6 +479,8 @@ class ManifestGenerator {
|
||||||
* Only includes compiled .md files (not .agent.yaml source files)
|
* Only includes compiled .md files (not .agent.yaml source files)
|
||||||
*/
|
*/
|
||||||
async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
||||||
|
// Skip directories claimed by collectSkills
|
||||||
|
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return [];
|
||||||
const agents = [];
|
const agents = [];
|
||||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||||
// Load skill manifest for this directory (if present)
|
// Load skill manifest for this directory (if present)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue