feat(skills): support nested directories and lightweight injection#261
feat(skills): support nested directories and lightweight injection#261Weaxs wants to merge 1 commit into
Conversation
Two improvements that align pi-subagents' skill handling with the host
pi-coding-agent runtime:
1. Recursive skill discovery
collectFilesystemSkills now descends into subdirectories of registered
skill roots, treating any directory containing a SKILL.md as a skill
anchor (matches pi-coding-agent's loadSkillsFromDirInternal). Hidden
directories and node_modules are skipped. Layouts like
<root>/<group>/<name>/SKILL.md now resolve, in addition to the
existing <root>/SKILL.md and <root>/<name>/SKILL.md shapes.
2. Lightweight skill injection
New agent frontmatter field `skillInjection: full | light` (default
full, fully backwards compatible) controls how buildSkillInjection
shapes the system prompt:
- full: existing <skill name="...">full body</skill> per skill
- light: <available_skills> with name/description/location per skill,
plus instructions to load via the read tool when the task matches
Mirrors pi-coding-agent's formatSkillsForPrompt output so a model
that knows one knows the other. Helpful for agents that declare many
skills in `skills:` but want a small startup system prompt.
The new buildLightSkillInjection helper is exported alongside the
existing buildSkillInjection. The field is plumbed through
BuiltinAgentOverrideBase, BuiltinAgentOverrideConfig, AgentConfig,
KNOWN_FIELDS, the agent serializer, agent management input parsing,
and the subagent extension tool description.
Tests
- Add test/unit/skills-extensions.test.ts covering nested discovery
(two-level layout, recursion stop at first SKILL.md, node_modules
and dotfile exclusion, deep resolveSkills) and light injection
(no-body emission, missing-description fallback, empty-list, XML
escaping, full-mode regression guard)
- Full unit suite: 512/512 pass
|
Thanks for the PR. I would not merge this as-is, but I think it can get there with a smaller revision. The scanner still walks inside a configured skill path after adding that directory's own For Please add focused coverage for those cases, and update the user docs for the new frontmatter/override field. |
|
One more small thing for when you revise this: please add an Unreleased changelog entry for the user-visible skill discovery/light injection change, with credit to Weaxs (@Weaxs). |
|
I'm closing this for now while we clean up the PR queue. The nested-skill work still looks worth revisiting, but this branch needs a current-main revision before it can merge. Please feel free to open a fresh revised PR later if you want to pick it back up. |
Closes #183
Closes #262
Why
Two gaps surface when subagents and the host
pi-coding-agentshare the same skill directory.1. Nested skill discovery mismatch
pi-coding-agent'sloadSkillsFromDirInternalrecursively walks subdirectories looking forSKILL.md, treating any directory that contains one as a skill root. This lets users group skills hierarchically:pi-subagents'scollectFilesystemSkillsonly descends one level — it finds<root>/<name>/SKILL.mdbut not<root>/<group>/<name>/SKILL.md. The result is that a subagent's frontmatterskills: safe-bashresolves in the host but fails in the subagent runtime, even when both agents are pointed at the same skills tree.2. Heavyweight injection is the only option
When an agent declares
skills: a, b, cin frontmatter,buildSkillInjectionalways wraps each skill's fullSKILL.mdbody into the system prompt. For agents that bundle many skills, this can add 100KB+ to every cold start, even though most invocations only touch one or two skills.pi-coding-agent'sformatSkillsForPromptsolves the same problem on the host side by emitting a compact<available_skills>listing of name + description + path, and pointing the model at the read tool. Subagents have no equivalent escape hatch — they either inject everything or know nothing about declared skills.What changes
1. Recursive skill discovery (matches
pi-coding-agent)collectFilesystemSkillsnow recurses into subdirectories of each registered skill root, stopping at any directory that contains aSKILL.md(treating it as the skill anchor for that subtree). Hidden directories andnode_modulesare skipped to mirror the host scanner.Layouts that previously resolved are unchanged:
<root>/SKILL.md(single skill at the root)<root>/<name>.mdand<root>/<name>/SKILL.mdNew layouts now resolved:
<root>/<group>/<name>/SKILL.mdSKILL.md2. Optional lightweight skill injection
A new agent frontmatter field
skillInjection: full | light(defaultfull, fully backwards-compatible) controls howbuildSkillInjectionshapes the system prompt.Given an agent with
skills: safe-bash, tmux, here is the actual text appended to the subagent's system prompt in each mode.full(default, current behavior) — the entire SKILL.md body is inlined per skill:light— only name, description, and absolute path are emitted, plus instructions to load each file on demand via the read tool:The
lightshape mirrorspi-coding-agent'sformatSkillsForPromptoutput, so a model that knows how to use the host's<available_skills>listing already knows how to use this one.The new
buildLightSkillInjectionhelper is exported alongside the existingbuildSkillInjection. The field is plumbed throughBuiltinAgentOverrideBase,BuiltinAgentOverrideConfig,AgentConfig,KNOWN_FIELDS, the agent serializer, agent management input parsing, and thesubagentextension tool description.Example agent file
A user/project agent declaring many skills but opting into the lightweight listing — only the four "core" skills are injected in full; the rest stay as references the agent loads on demand:
Without
skillInjection: light, the sameskills:declaration would inline three full SKILL.md bodies into the system prompt at every cold start. Withlight, only the listing block (name + description + location) is appended, and the agent loads each SKILL.md via the read tool when actually needed.Tests
test/unit/skills-extensions.test.tsadds 9 cases covering:<root>/<group>/<name>/SKILL.md)SKILL.md(no nested SKILL.md leakage)node_modulesand dotfile directories are excludedresolveSkillsbuildLightSkillInjectionemits name/description/location, never the full bodybuildSkillInjection(default) still emits full body — regression guardFull test suite: 512 / 512 pass (
npm run test:unit).Backwards compatibility
<root>/<name>/SKILL.mdetc.) keep working unchanged.skillInjectiondefaults tofull, so agents without the field behave exactly as before.skillInjectionwhen value is non-default (light), keeping existing agent files byte-identical on round-trip.