Merge branch 'main' into dependabot/npm_and_yarn/h3-1.15.8

This commit is contained in:
Alex Verkhovsky 2026-03-19 19:37:11 -06:00 committed by GitHub
commit 9a0a98ab5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 97 additions and 22 deletions

View File

@ -1635,6 +1635,15 @@ async function runTests() {
); );
await fs.writeFile(path.join(taskSkillDir29, 'workflow.md'), '# Task Skill\n\nSkill in tasks\n'); await fs.writeFile(path.join(taskSkillDir29, 'workflow.md'), '# Task Skill\n\nSkill in tasks\n');
// --- Native agent entrypoint inside agents/: core/agents/bmad-tea/ ---
const nativeAgentDir29 = path.join(tempFixture29, 'core', 'agents', 'bmad-tea');
await fs.ensureDir(nativeAgentDir29);
await fs.writeFile(path.join(nativeAgentDir29, 'bmad-skill-manifest.yaml'), 'type: agent\ncanonicalId: bmad-tea\n');
await fs.writeFile(
path.join(nativeAgentDir29, 'SKILL.md'),
'---\nname: bmad-tea\ndescription: Native agent entrypoint\n---\n\nPresent a capability menu.\n',
);
// Minimal agent so core module is detected // Minimal agent so core module is detected
await fs.ensureDir(path.join(tempFixture29, 'core', 'agents')); await fs.ensureDir(path.join(tempFixture29, 'core', 'agents'));
const minimalAgent29 = '<agent name="Test" title="T"><persona>p</persona></agent>'; const minimalAgent29 = '<agent name="Test" title="T"><persona>p</persona></agent>';
@ -1664,6 +1673,17 @@ async function runTests() {
const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill'); const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill');
assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]'); assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]');
// Native agent entrypoint should be installed as a verbatim skill and also
// remain visible to the agent manifest pipeline.
const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea');
assert(nativeAgentEntry29 !== undefined, 'Native type:agent SKILL.md dir appears in skills[]');
assert(
nativeAgentEntry29 && nativeAgentEntry29.path.includes('agents/bmad-tea/SKILL.md'),
'Native type:agent SKILL.md path points to the agent directory entrypoint',
);
const nativeAgentManifest29 = generator29.agents.find((a) => a.name === 'bmad-tea');
assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata');
// Regular workflow should be in workflows, NOT in skills // Regular workflow should be in workflows, NOT in skills
const regularWf29 = generator29.workflows.find((w) => w.name === 'Regular Workflow'); const regularWf29 = generator29.workflows.find((w) => w.name === 'Regular Workflow');
assert(regularWf29 !== undefined, 'Regular type:workflow appears in workflows[]'); assert(regularWf29 !== undefined, 'Regular type:workflow appears in workflows[]');
@ -1689,6 +1709,37 @@ async function runTests() {
const scannedModules29 = await generator29.scanInstalledModules(tempFixture29); const scannedModules29 = await generator29.scanInstalledModules(tempFixture29);
assert(scannedModules29.includes('skill-only-mod'), 'scanInstalledModules recognizes skill-only module'); assert(scannedModules29.includes('skill-only-mod'), 'scanInstalledModules recognizes skill-only module');
// Test scanInstalledModules recognizes native-agent-only modules too
const agentOnlyModDir29 = path.join(tempFixture29, 'agent-only-mod');
await fs.ensureDir(path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea'));
await fs.writeFile(path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea', 'bmad-skill-manifest.yaml'), 'type: agent\n');
await fs.writeFile(
path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea', 'SKILL.md'),
'---\nname: bmad-tea\ndescription: desc\n---\n\nAgent menu.\n',
);
const rescannedModules29 = await generator29.scanInstalledModules(tempFixture29);
assert(rescannedModules29.includes('agent-only-mod'), 'scanInstalledModules recognizes native-agent-only module');
// Test scanInstalledModules recognizes multi-entry manifests keyed under SKILL.md
const multiEntryModDir29 = path.join(tempFixture29, 'multi-entry-mod');
await fs.ensureDir(path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea'));
await fs.writeFile(
path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea', 'bmad-skill-manifest.yaml'),
'SKILL.md:\n type: agent\n canonicalId: bmad-tea\n',
);
await fs.writeFile(
path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea', 'SKILL.md'),
'---\nname: bmad-tea\ndescription: desc\n---\n\nAgent menu.\n',
);
const rescannedModules29b = await generator29.scanInstalledModules(tempFixture29);
assert(rescannedModules29b.includes('multi-entry-mod'), 'scanInstalledModules recognizes multi-entry native-agent module');
// skill-manifest.csv should include the native agent entrypoint
const skillManifestCsv29 = await fs.readFile(path.join(tempFixture29, '_config', 'skill-manifest.csv'), 'utf8');
assert(skillManifestCsv29.includes('bmad-tea'), 'skill-manifest.csv includes native type:agent SKILL.md entrypoint');
} catch (error) { } catch (error) {
assert(false, 'Unified skill scanner test succeeds', error.message); assert(false, 'Unified skill scanner test succeeds', error.message);
} finally { } finally {

View File

@ -50,6 +50,29 @@ class ManifestGenerator {
return getInstallToBmadShared(manifest, filename); return getInstallToBmadShared(manifest, filename);
} }
/**
* Native SKILL.md entrypoints can be packaged as either skills or agents.
* Both need verbatim installation for skill-format IDEs.
* @param {string|null} artifactType - Manifest type resolved for SKILL.md
* @returns {boolean} True when the directory should be installed verbatim
*/
isNativeSkillDirType(artifactType) {
return artifactType === 'skill' || artifactType === 'agent';
}
/**
* Check whether a loaded bmad-skill-manifest.yaml declares a native
* SKILL.md entrypoint, either as a single-entry manifest or a multi-entry map.
* @param {Object|null} manifest - Loaded manifest
* @returns {boolean} True when the manifest contains a native skill/agent entrypoint
*/
hasNativeSkillManifest(manifest) {
if (!manifest) return false;
if (manifest.__single) return this.isNativeSkillDirType(manifest.__single.type);
return Object.values(manifest).some((entry) => this.isNativeSkillDirType(entry?.type));
}
/** /**
* 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.
@ -146,9 +169,10 @@ class ManifestGenerator {
} }
/** /**
* Recursively walk a module directory tree, collecting skill directories. * Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
* A skill directory is one that contains both a bmad-skill-manifest.yaml with * A native entrypoint directory is one that contains both a
* type: skill AND a SKILL.md file with name/description frontmatter. * bmad-skill-manifest.yaml with type: skill or type: agent AND a SKILL.md file
* with name/description frontmatter.
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths). * Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
*/ */
async collectSkills() { async collectSkills() {
@ -172,11 +196,11 @@ class ManifestGenerator {
// Check this directory for skill manifest // Check this directory for skill manifest
const manifest = await this.loadSkillManifest(dir); const manifest = await this.loadSkillManifest(dir);
// Determine if this directory is a skill (type: skill in manifest) // Determine if this directory is a native SKILL.md entrypoint
const skillFile = 'SKILL.md'; const skillFile = 'SKILL.md';
const artifactType = this.getArtifactType(manifest, skillFile); const artifactType = this.getArtifactType(manifest, skillFile);
if (artifactType === 'skill' || artifactType === 'agent') { if (this.isNativeSkillDirType(artifactType)) {
const skillMdPath = path.join(dir, 'SKILL.md'); const skillMdPath = path.join(dir, 'SKILL.md');
const dirName = path.basename(dir); const dirName = path.basename(dir);
@ -190,11 +214,12 @@ class ManifestGenerator {
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}` ? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
: `${this.bmadFolderName}/${moduleName}/${skillFile}`; : `${this.bmadFolderName}/${moduleName}/${skillFile}`;
// Skills derive canonicalId from directory name — never from manifest // Native SKILL.md entrypoints derive canonicalId from directory name.
// (agent-type skills legitimately use canonicalId for agent-manifest mapping, so skip warning) // Agent entrypoints may keep canonicalId metadata for compatibility, so
// only warn for non-agent SKILL.md directories.
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') { if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
console.warn( 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)`, `Warning: Native entrypoint manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for SKILL.md directories (directory name is the canonical ID)`,
); );
} }
const canonicalId = dirName; const canonicalId = dirName;
@ -224,21 +249,21 @@ class ManifestGenerator {
} }
} }
// Warn if manifest says type:skill but directory was not claimed // Warn if manifest says this is a native entrypoint but the directory was not claimed
if (manifest && !this.skillClaimedDirs.has(dir)) { if (manifest && !this.skillClaimedDirs.has(dir)) {
let hasSkillType = false; let hasNativeSkillType = false;
if (manifest.__single) { if (manifest.__single) {
hasSkillType = manifest.__single.type === 'skill' || manifest.__single.type === 'agent'; hasNativeSkillType = this.isNativeSkillDirType(manifest.__single.type);
} else { } else {
for (const key of Object.keys(manifest)) { for (const key of Object.keys(manifest)) {
if (manifest[key]?.type === 'skill' || manifest[key]?.type === 'agent') { if (this.isNativeSkillDirType(manifest[key]?.type)) {
hasSkillType = true; hasNativeSkillType = true;
break; break;
} }
} }
} }
if (hasSkillType && debug) { if (hasNativeSkillType && debug) {
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but failed validation: ${dir}`); console.log(`[DEBUG] collectSkills: dir has native SKILL.md manifest but failed validation: ${dir}`);
} }
} }
@ -1359,7 +1384,8 @@ class ManifestGenerator {
const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks')); const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks'));
const hasTools = await fs.pathExists(path.join(modulePath, 'tools')); const hasTools = await fs.pathExists(path.join(modulePath, 'tools'));
// Check for skill-only modules: recursive scan for bmad-skill-manifest.yaml with type: skill // Check for native-entrypoint-only modules: recursive scan for
// bmad-skill-manifest.yaml with type: skill or type: agent
let hasSkills = false; let hasSkills = false;
if (!hasAgents && !hasWorkflows && !hasTasks && !hasTools) { if (!hasAgents && !hasWorkflows && !hasTasks && !hasTools) {
hasSkills = await this._hasSkillManifestRecursive(modulePath); hasSkills = await this._hasSkillManifestRecursive(modulePath);
@ -1378,7 +1404,8 @@ class ManifestGenerator {
} }
/** /**
* Recursively check if a directory tree contains a bmad-skill-manifest.yaml with type: skill. * Recursively check if a directory tree contains a bmad-skill-manifest.yaml that
* declares a native SKILL.md entrypoint (type: skill or type: agent).
* Skips directories starting with . or _. * Skips directories starting with . or _.
* @param {string} dir - Directory to search * @param {string} dir - Directory to search
* @returns {boolean} True if a skill manifest is found * @returns {boolean} True if a skill manifest is found
@ -1393,10 +1420,7 @@ class ManifestGenerator {
// Check for manifest in this directory // Check for manifest in this directory
const manifest = await this.loadSkillManifest(dir); const manifest = await this.loadSkillManifest(dir);
if (manifest) { if (this.hasNativeSkillManifest(manifest)) return true;
const type = this.getArtifactType(manifest, 'workflow.md');
if (type === 'skill') return true;
}
// Recurse into subdirectories // Recurse into subdirectories
for (const entry of entries) { for (const entry of entries) {

View File

@ -630,7 +630,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} }
/** /**
* Install verbatim skill directories (type: skill entries from skill-manifest.csv). * Install verbatim native SKILL.md directories from skill-manifest.csv.
* Copies the entire source directory as-is into the IDE skill directory. * Copies the entire source directory as-is into the IDE skill directory.
* The source SKILL.md is used directly no frontmatter transformation or file generation. * The source SKILL.md is used directly no frontmatter transformation or file generation.
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory