Skip to content

feat(skills): support nested directories and lightweight injection#261

Open
Weaxs wants to merge 1 commit into
nicobailon:mainfrom
Weaxs:feat/recursive-skills-and-light-injection
Open

feat(skills): support nested directories and lightweight injection#261
Weaxs wants to merge 1 commit into
nicobailon:mainfrom
Weaxs:feat/recursive-skills-and-light-injection

Conversation

@Weaxs

@Weaxs Weaxs commented Jun 8, 2026

Copy link
Copy Markdown

Closes #183
Closes #262

Why

Two gaps surface when subagents and the host pi-coding-agent share the same skill directory.

1. Nested skill discovery mismatch

pi-coding-agent's loadSkillsFromDirInternal recursively walks subdirectories looking for SKILL.md, treating any directory that contains one as a skill root. This lets users group skills hierarchically:

skills/
  shell/
    safe-bash/SKILL.md
    tmux/SKILL.md
  web/
    chrome-devtools/SKILL.md

pi-subagents's collectFilesystemSkills only descends one level — it finds <root>/<name>/SKILL.md but not <root>/<group>/<name>/SKILL.md. The result is that a subagent's frontmatter skills: safe-bash resolves 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, c in frontmatter, buildSkillInjection always wraps each skill's full SKILL.md body 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's formatSkillsForPrompt solves 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)

collectFilesystemSkills now recurses into subdirectories of each registered skill root, stopping at any directory that contains a SKILL.md (treating it as the skill anchor for that subtree). Hidden directories and node_modules are skipped to mirror the host scanner.

Layouts that previously resolved are unchanged:

  • <root>/SKILL.md (single skill at the root)
  • <root>/<name>.md and <root>/<name>/SKILL.md

New layouts now resolved:

  • <root>/<group>/<name>/SKILL.md
  • arbitrary depth as long as no ancestor has its own SKILL.md

2. Optional lightweight skill injection

A new agent frontmatter field skillInjection: full | light (default full, fully backwards-compatible) controls how buildSkillInjection shapes 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:

<skill name="safe-bash">
# safe-bash

Use this skill when the task requires running shell commands.

## Guidelines
- ...
- ...
</skill>

<skill name="tmux">
# tmux

When the task involves long-running processes, use tmux sessions.

## Commands
- `tmux new-session -d -s <name>`
- ...
</skill>

light — only name, description, and absolute path are emitted, plus instructions to load each file on demand via the read tool:

The following skills are available for this agent. Use the read tool to load a skill's file when the task matches its description.

<available_skills>
  <skill>
    <name>safe-bash</name>
    <description>Run shell commands safely with audit trail.</description>
    <location>/abs/path/to/skills/safe-bash/SKILL.md</location>
  </skill>
  <skill>
    <name>tmux</name>
    <description>Manage tmux sessions for long-running tasks.</description>
    <location>/abs/path/to/skills/tmux/SKILL.md</location>
  </skill>
</available_skills>

The light shape mirrors pi-coding-agent's formatSkillsForPrompt output, so a model that knows how to use the host's <available_skills> listing already knows how to use this one.

Note on naming: #183 sketches the same direction with skillInjection: lazy | inline. This PR uses full | light to make the contrast read more naturally as a heaviness scale (full SKILL.md vs light listing) and to keep the default explicitly the existing behavior. Happy to rename to match the issue if maintainers prefer.

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.

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:

---
name: shell-ops
description: Long-running shell and tmux operations specialist
tools: read, grep, find, ls, bash
systemPromptMode: replace
inheritProjectContext: true
inheritSkills: false
skillInjection: light
skills: safe-bash, tmux, chrome-devtools
---

You are a shell operations specialist.

Use the read tool to load any skill listed in your `<available_skills>` block when the task matches its description. Pull only what you need.

Without skillInjection: light, the same skills: declaration would inline three full SKILL.md bodies into the system prompt at every cold start. With light, 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.ts adds 9 cases covering:

  • Two-level nested discovery (<root>/<group>/<name>/SKILL.md)
  • Recursion stops at the first SKILL.md (no nested SKILL.md leakage)
  • node_modules and dotfile directories are excluded
  • Deeply-nested skills resolve via resolveSkills
  • buildLightSkillInjection emits name/description/location, never the full body
  • Missing description gracefully omitted in light mode
  • Empty skill list returns empty string
  • XML-special characters escaped in name/description/location
  • buildSkillInjection (default) still emits full body — regression guard

Full test suite: 512 / 512 pass (npm run test:unit).

Backwards compatibility

  • Existing layouts (<root>/<name>/SKILL.md etc.) keep working unchanged.
  • skillInjection defaults to full, so agents without the field behave exactly as before.
  • Serializer only writes skillInjection when value is non-default (light), keeping existing agent files byte-identical on round-trip.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant