feat(#69): nest concrete skills under namespace routers at install (#883)

Emit the 6 gsd-ns-* routers as the only top-level skill bundles and nest
the ~61 concrete skills under <router>/skills/<name>/SKILL.md on runtimes
with confirmed non-recursive skill loaders (claude global, cline, qwen,
hermes, augment, trae, antigravity). Router bodies rewrite their routing
tables from Skill-tool dispatch to a Read skills/<name>/SKILL.md pattern.
Recursive/unconfirmed loaders (cursor, codex, copilot, windsurf, codebuddy,
opencode, kilo) keep the flat layout. Completes the v1.40 namespace
architecture (#2792) so the eager skill listing drops to ~6 entries.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-08 15:09:34 -04:00
committed by GitHub
parent dd81e3d120
commit 0a11d361ca
20 changed files with 975 additions and 115 deletions

View File

@@ -204,6 +204,7 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art
* @param converterName name of converter function in bin/install.js exports
* @param runtime canonical runtime ID (gates Hermes/Qwen branding in converter)
* @param configDir runtime config dir (for .gsd-source marker resolution)
* @param nested if true, nest concrete skills under their ns-* routers (#69)
*/
function skillsKind(
destSubpath: string,
@@ -211,6 +212,7 @@ function skillsKind(
converterName: string,
runtime: string,
configDir: string,
nested = false,
): ArtifactKind {
return {
kind: 'skills',
@@ -224,7 +226,7 @@ function skillsKind(
const cmdNames = installExports.readGsdCommandNames();
const wrappedConverter = (content: string, skillName: string): string =>
realConverter(content, skillName, runtime, cmdNames);
return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix);
return stageSkillsForRuntimeAsSkills(findInstallSourceRoot(configDir), resolved, wrappedConverter, prefix, nested);
},
};
}
@@ -267,6 +269,39 @@ function convertedCommandsKind(
// Public API
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Nested skill-bundle support matrix (#69)
// ---------------------------------------------------------------------------
//
// When a runtime's skill loader scans only one level deep (non-recursive), a
// concrete skill nested at `<router>/skills/<name>/SKILL.md` drops out of the
// eager top-level listing yet stays readable by file path — which is exactly
// what namespace routing needs. Recursive loaders surface every nested SKILL.md
// as a peer (zero token saving), so they stay flat. Unconfirmed loaders stay
// flat conservatively. Verified June 2026:
//
// NEST (confirmed non-recursive / one-level scan):
// claude — https://code.claude.com/docs/en/skills + anthropics/claude-code#28266
// (scans one level under ~/.claude/skills; nested skills not auto-listed)
// cline — cline/cline skills.ts scanSkillsDirectory uses flat fs.readdir
// qwen — QwenLM/qwen-code skill-load.ts flat readdir ("depth 2 enough")
// hermes — hermes-agent.nousresearch.com/docs/user-guide/features/skills
// (single-level subdir probe of the tap path)
// augment — https://docs.augmentcode.com/cli/skills (flat single-level)
// trae — docs.trae.ai/ide/skills + Trae-AI/TRAE#2253 (flat; nesting errors)
// antigravity— discuss.ai.google.dev/t/more-antigravity-issues/145875 ("will not recursive scan")
//
// FLAT (recursive loader → nesting gives no saving):
// cursor — https://cursor.com/docs/skills (walks skills root recursively)
// opencode — sst/opencode skill/index.ts glob "skills/**/SKILL.md"
// kilo — Kilo-Org/kilocode (opencode fork, same ** glob)
//
// FLAT (nested-scan behaviour unconfirmed → conservative):
// codex — developers.openai.com/codex/skills/
// copilot — docs.github.com/en/copilot/concepts/agents/about-agent-skills
// windsurf — docs.devin.ai/desktop/cascade/skills
// codebuddy — codebuddy.ai/docs/cli/skills
/**
* Resolve the artifact layout for a given runtime and config directory.
*/
@@ -290,7 +325,7 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
agentsKind('agents', 'gsd-', configDir),
];
} else {
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'claude', configDir)];
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'claude', configDir, true /* #69 nested: non-recursive scan, see matrix above */)];
}
break;
@@ -318,7 +353,7 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
break;
case 'antigravity':
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAntigravitySkill', 'antigravity', configDir)];
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAntigravitySkill', 'antigravity', configDir, true /* #69 nested */)];
break;
case 'windsurf':
@@ -328,20 +363,20 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
case 'augment':
kinds = [
commandsKind('commands', 'gsd-', configDir),
skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir),
skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir, true /* #69 nested */),
];
break;
case 'trae':
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToTraeSkill', 'trae', configDir)];
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToTraeSkill', 'trae', configDir, true /* #69 nested */)];
break;
case 'qwen':
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'qwen', configDir)];
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClaudeSkill', 'qwen', configDir, true /* #69 nested */)];
break;
case 'hermes':
kinds = [skillsKind('skills/gsd', '', 'convertClaudeCommandToClaudeSkill', 'hermes', configDir)];
kinds = [skillsKind('skills/gsd', '', 'convertClaudeCommandToClaudeSkill', 'hermes', configDir, true /* #69 nested */)];
break;
case 'codebuddy':
@@ -359,7 +394,7 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
break;
case 'cline':
kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir)] : [];
kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir, true /* #69 nested */)] : [];
break;
case 'opencode':