What happens
directorySkillsSource (packages/core/src/node/directory-skills-source.ts) departs from the reference file-skill sources in three places that change what the model is told and which skills load at all.
1. Scripts are advertised without a parameter schema
A discovered script is pushed with name and run only (directory-skills-source.ts:215-222); SkillScript.parametersSchema stays undefined, so the skill body renders a self-closing <script name="scripts/convert.py"/> (packages/core/src/skills/skill.ts:351-357). The model learns nothing about the argument shape.
Both references advertise a fixed schema for every file-based script:
- .NET
AgentFileSkillScript.ParametersSchema returns {"type":"array","items":{"type":"string"}} (dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScript.cs:46,68), rendered as a <parameters_schema> child in the skill body.
- Python
FileSkillScript.parameters_schema returns the same object, "so that the LLM knows to pass positional CLI arguments as a JSON array of strings" (python/packages/core/agent_framework/_skills.py:505-512).
The runtime side already exists here: SkillScriptArguments accepts readonly string[] and the runner receives it. Only the advertisement is missing, so the model has to guess between an object and an array.
2. SKILL.md discovery stops one level short
skillDirectories returns the directory itself when it holds a SKILL.md, otherwise its immediate children that do (directory-skills-source.ts:160-178), and the paths doc promises exactly that ("a parent whose immediate children are skill directories", :43-51).
Both references search two levels below a root before giving up. .NET uses MaxSkillDirectorySearchDepth = 2 and checks for SKILL.md before the depth guard (AgentFileSkillsSource.cs:34,164-197); Python uses MAX_SEARCH_DEPTH = 2 with the same shape (_skills.py:1649,3576-3610). A root, its children and its grandchildren are all candidates, and a SKILL.md stops descent. The common layout skills/<category>/<skill>/SKILL.md loads there and is silently empty here.
searchDepth (already 2) is a different knob: it bounds the resource and script walk inside one skill, matching the references' per-skill search depth. It does not affect where SKILL.md is looked for.
3. A frontmatter name that differs from the directory name is accepted
loadSkill takes frontmatter.name as the skill's name and never compares it with the directory (directory-skills-source.ts:186-187). Both references reject the skill: Python logs "frontmatter name '%s' that does not match the directory name '%s'; skipping" (_skills.py:3532-3540), .NET logs LogNameDirectoryMismatch at Error and skips (AgentFileSkillsSource.cs:773). The references treat the directory name as the skill's name. Accepting a mismatch lets two directories advertise one name, and the resourceFilter / scriptFilter callbacks, which are keyed by skill name, then see a name that exists nowhere on disk.
Required implementation
- Give every discovered script
parametersSchema: { type: 'array', items: { type: 'string' } }.
- Discover
SKILL.md at the root, its children and its grandchildren, stopping descent at the first SKILL.md found; keep the symlink refusal at every level. Update the paths doc to match.
- Report a skill whose frontmatter name differs from its directory name through
ctx.reportSkillError and skip it.
Acceptance criteria
Explicitly out of scope
Script execution itself, which stays application-supplied through scriptRunner, and the .py-only default script extension list.
What happens
directorySkillsSource(packages/core/src/node/directory-skills-source.ts) departs from the reference file-skill sources in three places that change what the model is told and which skills load at all.1. Scripts are advertised without a parameter schema
A discovered script is pushed with
nameandrunonly (directory-skills-source.ts:215-222);SkillScript.parametersSchemastaysundefined, so the skill body renders a self-closing<script name="scripts/convert.py"/>(packages/core/src/skills/skill.ts:351-357). The model learns nothing about the argument shape.Both references advertise a fixed schema for every file-based script:
AgentFileSkillScript.ParametersSchemareturns{"type":"array","items":{"type":"string"}}(dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScript.cs:46,68), rendered as a<parameters_schema>child in the skill body.FileSkillScript.parameters_schemareturns the same object, "so that the LLM knows to pass positional CLI arguments as a JSON array of strings" (python/packages/core/agent_framework/_skills.py:505-512).The runtime side already exists here:
SkillScriptArgumentsacceptsreadonly string[]and the runner receives it. Only the advertisement is missing, so the model has to guess between an object and an array.2.
SKILL.mddiscovery stops one level shortskillDirectoriesreturns the directory itself when it holds aSKILL.md, otherwise its immediate children that do (directory-skills-source.ts:160-178), and thepathsdoc promises exactly that ("a parent whose immediate children are skill directories",:43-51).Both references search two levels below a root before giving up. .NET uses
MaxSkillDirectorySearchDepth = 2and checks forSKILL.mdbefore the depth guard (AgentFileSkillsSource.cs:34,164-197); Python usesMAX_SEARCH_DEPTH = 2with the same shape (_skills.py:1649,3576-3610). A root, its children and its grandchildren are all candidates, and aSKILL.mdstops descent. The common layoutskills/<category>/<skill>/SKILL.mdloads there and is silently empty here.searchDepth(already2) is a different knob: it bounds the resource and script walk inside one skill, matching the references' per-skill search depth. It does not affect whereSKILL.mdis looked for.3. A frontmatter name that differs from the directory name is accepted
loadSkilltakesfrontmatter.nameas the skill's name and never compares it with the directory (directory-skills-source.ts:186-187). Both references reject the skill: Python logs "frontmatter name '%s' that does not match the directory name '%s'; skipping" (_skills.py:3532-3540), .NET logsLogNameDirectoryMismatchat Error and skips (AgentFileSkillsSource.cs:773). The references treat the directory name as the skill's name. Accepting a mismatch lets two directories advertise one name, and theresourceFilter/scriptFiltercallbacks, which are keyed by skill name, then see a name that exists nowhere on disk.Required implementation
parametersSchema: { type: 'array', items: { type: 'string' } }.SKILL.mdat the root, its children and its grandchildren, stopping descent at the firstSKILL.mdfound; keep the symlink refusal at every level. Update thepathsdoc to match.ctx.reportSkillErrorand skip it.Acceptance criteria
<parameters_schema>element for a discovered script, failing before the change.root/<category>/<skill>/SKILL.mdloads the skill; one with a fourth level does not; aSKILL.mdat a higher level hides the ones beneath it.CHANGELOG.mdrecords the discovery change;pnpm checkpasses.Explicitly out of scope
Script execution itself, which stays application-supplied through
scriptRunner, and the.py-only default script extension list.