Merge branch 'next' into kimi-runtime-support
This commit is contained in:
@@ -720,6 +720,7 @@ export = {
|
||||
readActiveProfile,
|
||||
writeActiveProfile,
|
||||
// Shared internals
|
||||
parseRequires,
|
||||
cleanupStagedSkills,
|
||||
// Back-compat / deprecated
|
||||
MINIMAL_SKILL_ALLOWLIST,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
66
tests/helpers/nested-layout.cjs
Normal file
66
tests/helpers/nested-layout.cjs
Normal 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,
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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')));
|
||||
|
||||
Reference in New Issue
Block a user