Merge branch 'next' into kimi-runtime-support

This commit is contained in:
Viktorplus
2026-06-08 22:11:07 +02:00
committed by GitHub
7 changed files with 90 additions and 184 deletions

View File

@@ -720,6 +720,7 @@ export = {
readActiveProfile,
writeActiveProfile,
// Shared internals
parseRequires,
cleanupStagedSkills,
// Back-compat / deprecated
MINIMAL_SKILL_ALLOWLIST,

View File

@@ -37,57 +37,12 @@ const {
resolveProfile,
} = require('../gsd-core/bin/lib/install-profiles.cjs');
const { nestedSkillPath } = require('./helpers/nested-layout.cjs');
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST });
/**
* Map from concrete skill stem → ns-* router stem.
* Used for nesting runtimes (claude, cline, qwen, etc.) when the full profile
* is installed: concrete skills live at <skillsRoot>/gsd-<router>/skills/<stem>/SKILL.md.
*/
const CHILD_ROUTER = {
// ns-workflow
'discuss-phase': 'ns-workflow', 'spec-phase': 'ns-workflow', 'plan-phase': 'ns-workflow',
'execute-phase': 'ns-workflow', 'verify-work': 'ns-workflow', 'phase': 'ns-workflow',
'progress': 'ns-workflow', 'ultraplan-phase': 'ns-workflow',
'plan-review-convergence': 'ns-workflow', 'add-tests': 'ns-workflow',
'ai-integration-phase': 'ns-workflow', 'autonomous': 'ns-workflow',
'fast': 'ns-workflow', 'mvp-phase': 'ns-workflow', 'quick': 'ns-workflow',
// ns-project
'new-project': 'ns-project', 'new-milestone': 'ns-project', 'complete-milestone': 'ns-project',
'audit-milestone': 'ns-project', 'milestone-summary': 'ns-project', 'import': 'ns-project',
'ingest-docs': 'ns-project', 'profile-user': 'ns-project', 'review-backlog': 'ns-project',
// ns-review
'code-review': 'ns-review', 'audit-uat': 'ns-review', 'secure-phase': 'ns-review',
'eval-review': 'ns-review', 'ui-review': 'ns-review', 'validate-phase': 'ns-review',
'debug': 'ns-review', 'forensics': 'ns-review', 'audit-fix': 'ns-review',
'review': 'ns-review', 'ui-phase': 'ns-review',
// ns-context
'map-codebase': 'ns-context', 'graphify': 'ns-context', 'docs-update': 'ns-context',
'extract-learnings': 'ns-context',
// ns-ideate
'capture': 'ns-ideate', 'explore': 'ns-ideate', 'sketch': 'ns-ideate',
'spike': 'ns-ideate',
// ns-manage
'config': 'ns-manage', 'workspace': 'ns-manage', 'workstreams': 'ns-manage',
'thread': 'ns-manage', 'pause-work': 'ns-manage', 'resume-work': 'ns-manage',
'update': 'ns-manage', 'ship': 'ns-manage', 'inbox': 'ns-manage',
'pr-branch': 'ns-manage', 'undo': 'ns-manage', 'cleanup': 'ns-manage',
'health': 'ns-manage', 'manager': 'ns-manage', 'settings': 'ns-manage',
'stats': 'ns-manage', 'surface': 'ns-manage', 'help': 'ns-manage',
};
/**
* Returns the nested SKILL.md path for a concrete skill stem on cline
* (prefix='gsd-'): <skillsRoot>/gsd-<router>/skills/<stem>/SKILL.md
*/
function nestedClineSkillPath(skillsRoot, stem) {
const router = CHILD_ROUTER[stem];
if (!router) throw new Error(`No router mapping for stem: ${stem}`);
return path.join(skillsRoot, 'gsd-' + router, 'skills', stem, 'SKILL.md');
}
// ─── (a) Converter unit test ─────────────────────────────────────────────────
const SAMPLE_COMMAND = `---
@@ -395,7 +350,7 @@ describe('install() global cline — coexistence: skills AND .clinerules', () =>
);
// full profile: gsd-help is nested under gsd-ns-manage/skills/help/SKILL.md
const helpSkillFile = nestedClineSkillPath(skillsDir, 'help');
const helpSkillFile = nestedSkillPath(skillsDir, 'gsd-', 'help');
assert.ok(
fs.existsSync(helpSkillFile),
`${path.relative(tmpGlobalDir, helpSkillFile)} must exist under ${tmpGlobalDir} — skills emission broken for global cline`
@@ -486,7 +441,7 @@ describe('convertClaudeToCliineMarkdown — bare ~/.claude and CLAUDE_CONFIG_DIR
installRuntimeArtifacts('cline', configDir, 'global', RESOLVED_FULL);
// full profile: surface is nested under gsd-ns-manage/skills/surface/SKILL.md
const surfaceSkill = nestedClineSkillPath(path.join(configDir, 'skills'), 'surface');
const surfaceSkill = nestedSkillPath(path.join(configDir, 'skills'), 'gsd-', 'surface');
assert.ok(fs.existsSync(surfaceSkill), `${path.relative(configDir, surfaceSkill)} must exist for full profile`);
const content = fs.readFileSync(surfaceSkill, 'utf8');
@@ -546,7 +501,7 @@ describe('_applyRuntimeRewrites — cline custom-dir embedded path (Fix 1)', ()
// gsd-surface SKILL.md references config paths; with a custom configDir
// (not under $HOME), pathPrefix will be the absolute custom path.
// full profile: surface is nested under gsd-ns-manage/skills/surface/SKILL.md
const surfaceSkill = nestedClineSkillPath(path.join(configDir, 'skills'), 'surface');
const surfaceSkill = nestedSkillPath(path.join(configDir, 'skills'), 'gsd-', 'surface');
assert.ok(fs.existsSync(surfaceSkill), `${path.relative(configDir, surfaceSkill)} must exist`);
const content = fs.readFileSync(surfaceSkill, 'utf8');

View File

@@ -9,6 +9,8 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { parseRequires } = require('./helpers/nested-layout.cjs');
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const NAMESPACE_SKILLS = [
@@ -216,16 +218,6 @@ describe('gsd-health --context flag is wired into command + workflow', () => {
const NS_FILES = NAMESPACE_SKILLS.map((ns) => ns.file);
/**
* Parse the `requires:` flow-style array from a router file's raw content.
* Matches `requires: [a, b, c]` anywhere (frontmatter or body — always in fm).
*/
function parseRouterRequires(content) {
const m = content.match(/^requires:\s*\[([^\]]*)\]/m);
if (!m) return [];
return m[1].split(',').map((s) => s.trim()).filter(Boolean);
}
describe('namespace nesting completeness (#69)', () => {
// Build the concrete-skill set once (all *.md minus ns-*.md)
const allFiles = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
@@ -240,7 +232,7 @@ describe('namespace nesting completeness (#69)', () => {
for (const f of NS_FILES) {
const stem = f.replace(/\.md$/, '');
const content = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf-8');
routerRequires.set(stem, parseRouterRequires(content));
routerRequires.set(stem, parseRequires(content));
}
const allRoutedStems = new Set([...routerRequires.values()].flat());

View File

@@ -33,6 +33,7 @@ const os = require('node:os');
const { install, convertClaudeCommandToClaudeSkill } = require('../bin/install.js');
const { cleanup } = require('./helpers.cjs');
const { nestedSkillPath } = require('./helpers/nested-layout.cjs');
const REPO_ROOT = path.resolve(__dirname, '..');
const SOURCE_COMMANDS_DIR = path.join(REPO_ROOT, 'commands', 'gsd');
@@ -43,52 +44,6 @@ function makeTmpDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
/**
* Map from concrete skill stem → ns-* router stem for nesting runtimes.
* Derived from the authoritative ns-*.md `requires:` lists.
*/
const CHILD_ROUTER = {
// ns-workflow
'discuss-phase': 'ns-workflow', 'spec-phase': 'ns-workflow', 'plan-phase': 'ns-workflow',
'execute-phase': 'ns-workflow', 'verify-work': 'ns-workflow', 'phase': 'ns-workflow',
'progress': 'ns-workflow', 'ultraplan-phase': 'ns-workflow',
'plan-review-convergence': 'ns-workflow', 'add-tests': 'ns-workflow',
'ai-integration-phase': 'ns-workflow', 'autonomous': 'ns-workflow',
'fast': 'ns-workflow', 'mvp-phase': 'ns-workflow', 'quick': 'ns-workflow',
// ns-project
'new-project': 'ns-project', 'new-milestone': 'ns-project', 'complete-milestone': 'ns-project',
'audit-milestone': 'ns-project', 'milestone-summary': 'ns-project', 'import': 'ns-project',
'ingest-docs': 'ns-project', 'profile-user': 'ns-project', 'review-backlog': 'ns-project',
// ns-review
'code-review': 'ns-review', 'audit-uat': 'ns-review', 'secure-phase': 'ns-review',
'eval-review': 'ns-review', 'ui-review': 'ns-review', 'validate-phase': 'ns-review',
'debug': 'ns-review', 'forensics': 'ns-review', 'audit-fix': 'ns-review',
'review': 'ns-review', 'ui-phase': 'ns-review',
// ns-context
'map-codebase': 'ns-context', 'graphify': 'ns-context', 'docs-update': 'ns-context',
'extract-learnings': 'ns-context',
// ns-ideate
'capture': 'ns-ideate', 'explore': 'ns-ideate', 'sketch': 'ns-ideate',
'spike': 'ns-ideate',
// ns-manage
'config': 'ns-manage', 'workspace': 'ns-manage', 'workstreams': 'ns-manage',
'thread': 'ns-manage', 'pause-work': 'ns-manage', 'resume-work': 'ns-manage',
'update': 'ns-manage', 'ship': 'ns-manage', 'inbox': 'ns-manage',
'pr-branch': 'ns-manage', 'undo': 'ns-manage', 'cleanup': 'ns-manage',
'health': 'ns-manage', 'manager': 'ns-manage', 'settings': 'ns-manage',
'stats': 'ns-manage', 'surface': 'ns-manage', 'help': 'ns-manage',
};
/**
* Returns the nested SKILL.md path for a concrete skill stem on Claude
* (prefix='gsd-'): <skillsRoot>/gsd-<router>/skills/<stem>/SKILL.md
*/
function nestedClaudeSkillPath(skillsRoot, stem) {
const router = CHILD_ROUTER[stem];
if (!router) throw new Error(`No router mapping for stem: ${stem}`);
return path.join(skillsRoot, 'gsd-' + router, 'skills', stem, 'SKILL.md');
}
function readFrontmatter(mdPath) {
const content = fs.readFileSync(mdPath, 'utf8');
if (!content.startsWith('---')) return '';
@@ -299,7 +254,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-autonomous SKILL.md has context: fork after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'autonomous');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'autonomous');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^context:[ \t]*fork$/m,
`gsd-autonomous SKILL.md must have context: fork\nActual:\n${fm}`);
@@ -307,7 +262,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-autonomous SKILL.md has effort: xhigh after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'autonomous');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'autonomous');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^effort:[ \t]*xhigh$/m,
`gsd-autonomous SKILL.md must have effort: xhigh\nActual:\n${fm}`);
@@ -315,7 +270,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-execute-phase SKILL.md has context: fork after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'execute-phase');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'execute-phase');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^context:[ \t]*fork$/m,
`gsd-execute-phase SKILL.md must have context: fork\nActual:\n${fm}`);
@@ -323,7 +278,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-execute-phase SKILL.md has effort: xhigh after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'execute-phase');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'execute-phase');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^effort:[ \t]*xhigh$/m,
`gsd-execute-phase SKILL.md must have effort: xhigh\nActual:\n${fm}`);
@@ -331,7 +286,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-plan-phase SKILL.md has context: fork after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'plan-phase');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'plan-phase');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^context:[ \t]*fork$/m,
`gsd-plan-phase SKILL.md must have context: fork\nActual:\n${fm}`);
@@ -339,7 +294,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-plan-phase SKILL.md has effort: xhigh after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'plan-phase');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'plan-phase');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^effort:[ \t]*xhigh$/m,
`gsd-plan-phase SKILL.md must have effort: xhigh\nActual:\n${fm}`);
@@ -347,7 +302,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-progress SKILL.md has effort: low after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'progress');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'progress');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^effort:[ \t]*low$/m,
`gsd-progress SKILL.md must have effort: low\nActual:\n${fm}`);
@@ -355,7 +310,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
test('gsd-stats SKILL.md has effort: low after global install', () => {
runClaudeGlobalInstall(claudeHome);
const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'stats');
const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'stats');
const fm = readFrontmatter(skillPath);
assert.match(fm, /^effort:[ \t]*low$/m,
`gsd-stats SKILL.md must have effort: low\nActual:\n${fm}`);

View File

@@ -0,0 +1,66 @@
'use strict';
/**
* Shared helpers for the namespace nested-skill install-layout tests (#69).
*
* Single source of truth for the concrete-skill → namespace-router map and the
* nested SKILL.md path layout. The map is DERIVED at require-time by parsing
* each commands/gsd/ns-*.md router's `requires:` frontmatter list with the
* production parser (install-profiles.parseRequires) — never hand-maintained.
*/
const fs = require('node:fs');
const path = require('node:path');
const { parseRequires } = require('../../gsd-core/bin/lib/install-profiles.cjs');
const COMMANDS_GSD = path.join(__dirname, '..', '..', 'commands', 'gsd');
// Router stems (ns-*.md basenames), discovered from disk and sorted.
const ROUTER_STEMS = fs
.readdirSync(COMMANDS_GSD)
.filter((f) => f.startsWith('ns-') && f.endsWith('.md'))
.map((f) => f.replace(/\.md$/, ''))
.sort();
/**
* Read a router's `requires:` child list from commands/gsd/<routerStem>.md
* using the production frontmatter parser.
*/
function routerChildren(routerStem) {
const srcFile = path.join(COMMANDS_GSD, `${routerStem}.md`);
return parseRequires(fs.readFileSync(srcFile, 'utf8'));
}
// concrete-skill stem -> router stem (e.g. 'plan-phase' -> 'ns-workflow').
// A few skills are intentionally multi-owner (e.g. 'spec-phase' is required by
// both ns-ideate and ns-workflow). ROUTER_STEMS is sorted, so the
// alphabetically-last owner wins — which reproduces the prior hand-maintained
// maps (spec-phase -> ns-workflow). The enh-2792 router-completeness tests
// guard the requires: lists themselves.
const CHILD_ROUTER = {};
for (const routerStem of ROUTER_STEMS) {
for (const child of routerChildren(routerStem)) {
CHILD_ROUTER[child] = routerStem;
}
}
/**
* Nested SKILL.md path for a concrete skill stem:
* <skillsRoot>/<prefix><router>/skills/<stem>/SKILL.md
* prefix is '' for runtimes that nest under a skills/gsd parent dir (hermes),
* or 'gsd-' for flat-prefixed runtimes (claude, cline, qwen, …).
*/
function nestedSkillPath(skillsRoot, prefix, stem) {
const router = CHILD_ROUTER[stem];
if (!router) throw new Error(`No router mapping for stem: ${stem}`);
return path.join(skillsRoot, prefix + router, 'skills', stem, 'SKILL.md');
}
module.exports = {
COMMANDS_GSD,
ROUTER_STEMS,
CHILD_ROUTER,
routerChildren,
nestedSkillPath,
parseRequires,
};

View File

@@ -14,9 +14,6 @@ const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const ROOT = path.join(__dirname, '..');
const COMMANDS_GSD = path.join(ROOT, 'commands', 'gsd');
const {
installRuntimeArtifacts,
} = require('../bin/install.js');
@@ -28,6 +25,8 @@ const {
resolveProfile,
} = require('../gsd-core/bin/lib/install-profiles.cjs');
const { COMMANDS_GSD, ROUTER_STEMS, routerChildren } = require('./helpers/nested-layout.cjs');
// ---------------------------------------------------------------------------
// Runtime parity decision matrix (#69)
// ---------------------------------------------------------------------------
@@ -52,31 +51,10 @@ const FLAT = [
{ runtime: 'kilo', scope: 'global', skillsSub: 'skills' },
];
const ROUTER_STEMS = ['ns-context', 'ns-ideate', 'ns-manage', 'ns-project', 'ns-review', 'ns-workflow'];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Parse the `requires:` flow-style array from a router file's raw content.
* Matches `requires: [a, b, c]` (inline array form).
*/
function parseRouterRequires(content) {
const m = content.match(/^requires:\s*\[([^\]]*)\]/m);
if (!m) return [];
return m[1].split(',').map((s) => s.trim()).filter(Boolean);
}
/**
* Read the requires list for a router stem from the source commands/gsd dir.
*/
function routerChildren(routerStem) {
const srcFile = path.join(COMMANDS_GSD, `${routerStem}.md`);
const content = fs.readFileSync(srcFile, 'utf-8');
return parseRouterRequires(content);
}
/**
* Create a fresh temp dir, run installRuntimeArtifacts into it, and return
* the tmpDir path. Caller must cleanup in finally.

View File

@@ -53,42 +53,7 @@ const {
walk,
} = require('./helpers/install-shared.cjs');
/**
* Map from concrete skill stem → ns-* router stem for nesting runtimes.
* These runtimes nest concrete skills at <skillsRoot>/<prefix><router>/skills/<stem>/SKILL.md
* (claude/cline/qwen/trae/augment/antigravity: prefix='gsd-'; hermes: prefix='').
*/
const CHILD_ROUTER = {
// ns-workflow
'discuss-phase': 'ns-workflow', 'spec-phase': 'ns-workflow', 'plan-phase': 'ns-workflow',
'execute-phase': 'ns-workflow', 'verify-work': 'ns-workflow', 'phase': 'ns-workflow',
'progress': 'ns-workflow', 'ultraplan-phase': 'ns-workflow',
'plan-review-convergence': 'ns-workflow', 'add-tests': 'ns-workflow',
'ai-integration-phase': 'ns-workflow', 'autonomous': 'ns-workflow',
'fast': 'ns-workflow', 'mvp-phase': 'ns-workflow', 'quick': 'ns-workflow',
// ns-project
'new-project': 'ns-project', 'new-milestone': 'ns-project', 'complete-milestone': 'ns-project',
'audit-milestone': 'ns-project', 'milestone-summary': 'ns-project', 'import': 'ns-project',
'ingest-docs': 'ns-project', 'profile-user': 'ns-project', 'review-backlog': 'ns-project',
// ns-review
'code-review': 'ns-review', 'audit-uat': 'ns-review', 'secure-phase': 'ns-review',
'eval-review': 'ns-review', 'ui-review': 'ns-review', 'validate-phase': 'ns-review',
'debug': 'ns-review', 'forensics': 'ns-review', 'audit-fix': 'ns-review',
'review': 'ns-review', 'ui-phase': 'ns-review',
// ns-context
'map-codebase': 'ns-context', 'graphify': 'ns-context', 'docs-update': 'ns-context',
'extract-learnings': 'ns-context',
// ns-ideate
'capture': 'ns-ideate', 'explore': 'ns-ideate', 'sketch': 'ns-ideate',
'spike': 'ns-ideate',
// ns-manage
'config': 'ns-manage', 'workspace': 'ns-manage', 'workstreams': 'ns-manage',
'thread': 'ns-manage', 'pause-work': 'ns-manage', 'resume-work': 'ns-manage',
'update': 'ns-manage', 'ship': 'ns-manage', 'inbox': 'ns-manage',
'pr-branch': 'ns-manage', 'undo': 'ns-manage', 'cleanup': 'ns-manage',
'health': 'ns-manage', 'manager': 'ns-manage', 'settings': 'ns-manage',
'stats': 'ns-manage', 'surface': 'ns-manage', 'help': 'ns-manage',
};
const { CHILD_ROUTER, nestedSkillPath } = require('./helpers/nested-layout.cjs');
// ─── Section 1: getDirName / getGlobalConfigDir / getConfigDirFromHome ──────────
@@ -346,9 +311,7 @@ describe('install/uninstall — hermes (nested skills/gsd/<router>/skills/<stem>
assert.strictEqual(result.configDir, fs.realpathSync(targetDir));
// hermes nests: skills/gsd/<router>/skills/<stem>/SKILL.md
const hermesHelpPath = path.join(
targetDir, 'skills', 'gsd', CHILD_ROUTER['help'], 'skills', 'help', 'SKILL.md'
);
const hermesHelpPath = nestedSkillPath(path.join(targetDir, 'skills', 'gsd'), '', 'help');
assert.ok(fs.existsSync(hermesHelpPath),
`help SKILL.md must exist at nested path: ${path.relative(targetDir, hermesHelpPath)}`);
assert.ok(fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'DESCRIPTION.md')),
@@ -446,9 +409,7 @@ describe('install/uninstall — qwen (nested skills/gsd-<router>/skills/<stem>/
assert.strictEqual(result.configDir, fs.realpathSync(targetDir));
// qwen nests: skills/gsd-<router>/skills/<stem>/SKILL.md
const qwenHelpPath = path.join(
targetDir, 'skills', 'gsd-' + CHILD_ROUTER['help'], 'skills', 'help', 'SKILL.md'
);
const qwenHelpPath = nestedSkillPath(path.join(targetDir, 'skills'), 'gsd-', 'help');
assert.ok(fs.existsSync(qwenHelpPath),
`help SKILL.md must exist at nested path: ${path.relative(targetDir, qwenHelpPath)}`);
assert.ok(fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION')));
@@ -496,9 +457,7 @@ describe('install/uninstall — trae (nested skills/gsd-<router>/skills/<stem>/
});
// trae nests: skills/gsd-<router>/skills/<stem>/SKILL.md
const traeHelpPath = path.join(
targetDir, 'skills', 'gsd-' + CHILD_ROUTER['help'], 'skills', 'help', 'SKILL.md'
);
const traeHelpPath = nestedSkillPath(path.join(targetDir, 'skills'), 'gsd-', 'help');
assert.ok(fs.existsSync(traeHelpPath),
`help SKILL.md must exist at nested path: ${path.relative(targetDir, traeHelpPath)}`);
assert.ok(fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION')));