From 375bf3abd66ac8e491a8f65b57d6e798cf4d77a3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 5 May 2026 15:02:11 -0400 Subject: [PATCH] fix(#3126): replace hardcoded globalSkillsBase with first-class runtime-aware mapping (#3140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#3126): replace hardcoded globalSkillsBase with runtime-aware mapping Root cause: buildAgentSkillsBlock() used path.join(os.homedir(), '.claude', 'skills') for globalSkillsBase regardless of config.runtime. Cursor users (and every non-Claude runtime) saw their global: skill lookups fail with a warning pointing to the wrong directory. Fix: introduces get-shit-done/bin/lib/runtime-homes.cjs — a pure, side- effect-free module covering all 15 GSD runtimes: Runtime Config base Skills path claude ~/.claude ~/.claude/skills/ cursor ~/.cursor ~/.cursor/skills/ gemini ~/.gemini ~/.gemini/skills/ codex ~/.codex ~/.codex/skills/ copilot ~/.copilot ~/.copilot/skills/ antigravity ~/.gemini/antigravity ...antigravity/skills/ windsurf ~/.codeium/windsurf ...windsurf/skills/ augment ~/.augment ~/.augment/skills/ trae ~/.trae ~/.trae/skills/ qwen ~/.qwen ~/.qwen/skills/ hermes ~/.hermes ~/.hermes/skills/gsd/ (nested #2841) codebuddy ~/.codebuddy ~/.codebuddy/skills/ cline ~/.cline null (rules-based, no skills dir) opencode ~/.config/opencode ...opencode/skills/ kilo ~/.config/kilo ...kilo/skills/ Also adds CLAUDE_CONFIG_DIR env var support (was missing). Warning messages now show the actual runtime-specific path. Docs: INVENTORY.md CLI Modules 41→42. Regression test: 30 assertions across all runtimes. Suite: 7008/7008. Closes #3126. * fix(lint+init): allow-test-rule, fix display path duplication (skillName appended twice) --- .../fix-3126-global-skills-base-runtime.md | 5 + docs/INVENTORY-MANIFEST.json | 3 +- docs/INVENTORY.md | 3 +- get-shit-done/bin/lib/init.cjs | 18 +- get-shit-done/bin/lib/runtime-homes.cjs | 178 ++++++++++++++++ ...6-global-skills-base-runtime-path.test.cjs | 200 ++++++++++++++++++ 6 files changed, 400 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-3126-global-skills-base-runtime.md create mode 100644 get-shit-done/bin/lib/runtime-homes.cjs create mode 100644 tests/bug-3126-global-skills-base-runtime-path.test.cjs diff --git a/.changeset/fix-3126-global-skills-base-runtime.md b/.changeset/fix-3126-global-skills-base-runtime.md new file mode 100644 index 000000000..1ee3b65dd --- /dev/null +++ b/.changeset/fix-3126-global-skills-base-runtime.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3126 +--- +**`global:` skill resolution now uses the correct runtime home directory** — `buildAgentSkillsBlock()` hardcoded `globalSkillsBase` to `~/.claude/skills` regardless of the active runtime, causing every `global:` skill lookup to silently fail on non-Claude runtimes (Cursor, Gemini, Codex, Windsurf, etc.). Introduces `get-shit-done/bin/lib/runtime-homes.cjs` — a first-class runtime→directory mapping module covering all 15 supported runtimes with their canonical env-var overrides. Notable specifics: Hermes Agent uses a nested `skills/gsd//` layout (#2841); Cline is rules-based and returns `null` (no skills directory); `CLAUDE_CONFIG_DIR` env var was previously missing for Claude. Warning messages now show the actual runtime-specific path. Closes #3126. diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index fe5f395fb..be87323bb 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -1,5 +1,5 @@ { - "generated": "2026-05-03", + "generated": "2026-05-05", "families": { "agents": [ "gsd-advisor-researcher", @@ -273,6 +273,7 @@ "profile-pipeline.cjs", "roadmap-command-router.cjs", "roadmap.cjs", + "runtime-homes.cjs", "schema-detect.cjs", "secrets.cjs", "security.cjs", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index e13bacaeb..ea15cf424 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -346,7 +346,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t --- -## CLI Modules (41 shipped) +## CLI Modules (42 shipped) Full listing: `get-shit-done/bin/lib/*.cjs`. @@ -382,6 +382,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`. | `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning | | `roadmap-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools roadmap` | | `roadmap.cjs` | ROADMAP.md parsing, phase extraction, plan progress | +| `runtime-homes.cjs` | Canonical runtime → global config/skills directory mapping; first-class support for all 15 runtimes including Hermes nested layout and Cline rules-based exclusion (#3126) | | `schema-detect.cjs` | Schema-drift detection for ORM patterns (Prisma, Drizzle, etc.) | | `secrets.cjs` | Secret-config masking convention (`****`) for integration keys managed by `/gsd-config --integrations` — keeps plaintext out of `config-set` output | | `security.cjs` | Path traversal prevention, prompt injection detection, safe JSON/shell helpers | diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 952bdaaf4..0848b4212 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -1654,7 +1654,9 @@ function cmdInitRemoveWorkspace(cwd, name, raw) { function buildAgentSkillsBlock(config, agentType, projectRoot) { const { validatePath } = require('./security.cjs'); const os = require('os'); - const globalSkillsBase = path.join(os.homedir(), '.claude', 'skills'); + const { getGlobalSkillDir, getGlobalSkillDisplayPath } = require('./runtime-homes.cjs'); + const runtime = (config && config.runtime) || 'claude'; + const globalSkillsBase = require('./runtime-homes.cjs').getGlobalSkillsBase(runtime); if (!config || !config.agent_skills || !agentType) return ''; @@ -1669,7 +1671,7 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) { for (const skillPath of skillPaths) { if (typeof skillPath !== 'string') continue; - // Support global: prefix for skills installed at ~/.claude/skills/ (#1992) + // Support global: prefix for skills installed at the runtime's global skills directory (#1992, #3126) if (skillPath.startsWith('global:')) { const skillName = skillPath.slice(7); // Explicit empty-name guard before regex for clearer error message @@ -1682,10 +1684,16 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) { process.stderr.write(`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`); continue; } - const globalSkillDir = path.join(globalSkillsBase, skillName); + // Cline is rules-based and has no global skills directory + if (globalSkillsBase === null) { + process.stderr.write(`[agent-skills] WARNING: Runtime "${runtime}" does not use a skills directory — "global:${skillName}" is not supported on this runtime\n`); + continue; + } + const globalSkillDir = getGlobalSkillDir(runtime, skillName); const globalSkillMd = path.join(globalSkillDir, 'SKILL.md'); + const displayPath = getGlobalSkillDisplayPath(runtime, skillName); if (!fs.existsSync(globalSkillMd)) { - process.stderr.write(`[agent-skills] WARNING: Global skill not found at "~/.claude/skills/${skillName}/SKILL.md" — skipping\n`); + process.stderr.write(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`); continue; } // Symlink escape guard: validatePath resolves symlinks and enforces @@ -1696,7 +1704,7 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) { process.stderr.write(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`); continue; } - validPaths.push({ ref: `${globalSkillDir}/SKILL.md`, display: `~/.claude/skills/${skillName}` }); + validPaths.push({ ref: `${globalSkillDir}/SKILL.md`, display: displayPath }); continue; } diff --git a/get-shit-done/bin/lib/runtime-homes.cjs b/get-shit-done/bin/lib/runtime-homes.cjs new file mode 100644 index 000000000..9a4bd7943 --- /dev/null +++ b/get-shit-done/bin/lib/runtime-homes.cjs @@ -0,0 +1,178 @@ +'use strict'; + +/** + * runtime-homes.cjs — canonical runtime → global config/skills directory mapping. + * + * Single source of truth for resolving the global config base directory and + * the correct global skills directory for every GSD-supported runtime. + * + * Mirrors the logic in bin/install.js getGlobalDir() but as a pure, + * side-effect-free module safe to require() at any point without triggering + * the installer. bin/install.js is the authoritative source — keep in sync. + * + * Runtime-specific notes: + * hermes — GSD skills nest under skills/gsd// (not the flat + * skills// layout used by all other runtimes). This + * collapses 86 skill entries into one category in Hermes' system + * prompt (#2841). + * cline — Rules-based; commands are embedded in .clinerules. Cline does + * not use a skills/ directory. getGlobalSkillDir() returns null + * for cline so the caller can emit an appropriate warning. + */ + +const os = require('os'); +const path = require('path'); + +/** + * Expand a leading ~ to os.homedir(). + * @param {string} p + * @returns {string} + */ +function expandTilde(p) { + if (!p) return p; + if (p.startsWith('~/') || p === '~') return path.join(os.homedir(), p.slice(1)); + return p; +} + +/** + * Return the global config base directory for the given runtime. + * Respects the same env-var overrides as bin/install.js getGlobalDir(). + * + * @param {string} runtime + * @returns {string} Absolute path to the runtime's global config directory + */ +function getGlobalConfigDir(runtime) { + const home = os.homedir(); + const env = process.env; + + switch (runtime) { + // ── Claude Code ────────────────────────────────────────────────────────── + case 'claude': + return env.CLAUDE_CONFIG_DIR ? expandTilde(env.CLAUDE_CONFIG_DIR) : path.join(home, '.claude'); + + // ── Cursor ─────────────────────────────────────────────────────────────── + case 'cursor': + return env.CURSOR_CONFIG_DIR ? expandTilde(env.CURSOR_CONFIG_DIR) : path.join(home, '.cursor'); + + // ── Gemini CLI ─────────────────────────────────────────────────────────── + case 'gemini': + return env.GEMINI_CONFIG_DIR ? expandTilde(env.GEMINI_CONFIG_DIR) : path.join(home, '.gemini'); + + // ── Codex ──────────────────────────────────────────────────────────────── + case 'codex': + return env.CODEX_HOME ? expandTilde(env.CODEX_HOME) : path.join(home, '.codex'); + + // ── Copilot (VS Code) ──────────────────────────────────────────────────── + case 'copilot': + return env.COPILOT_CONFIG_DIR ? expandTilde(env.COPILOT_CONFIG_DIR) : path.join(home, '.copilot'); + + // ── Antigravity ────────────────────────────────────────────────────────── + case 'antigravity': + return env.ANTIGRAVITY_CONFIG_DIR + ? expandTilde(env.ANTIGRAVITY_CONFIG_DIR) + : path.join(home, '.gemini', 'antigravity'); + + // ── Windsurf ───────────────────────────────────────────────────────────── + case 'windsurf': + return env.WINDSURF_CONFIG_DIR + ? expandTilde(env.WINDSURF_CONFIG_DIR) + : path.join(home, '.codeium', 'windsurf'); + + // ── Augment ────────────────────────────────────────────────────────────── + case 'augment': + return env.AUGMENT_CONFIG_DIR ? expandTilde(env.AUGMENT_CONFIG_DIR) : path.join(home, '.augment'); + + // ── Trae ───────────────────────────────────────────────────────────────── + case 'trae': + return env.TRAE_CONFIG_DIR ? expandTilde(env.TRAE_CONFIG_DIR) : path.join(home, '.trae'); + + // ── Qwen Code ──────────────────────────────────────────────────────────── + case 'qwen': + return env.QWEN_CONFIG_DIR ? expandTilde(env.QWEN_CONFIG_DIR) : path.join(home, '.qwen'); + + // ── Hermes Agent ───────────────────────────────────────────────────────── + // Note: skills use a nested layout (skills/gsd//) — see getGlobalSkillDir(). + case 'hermes': + return env.HERMES_HOME ? expandTilde(env.HERMES_HOME) : path.join(home, '.hermes'); + + // ── CodeBuddy ──────────────────────────────────────────────────────────── + case 'codebuddy': + return env.CODEBUDDY_CONFIG_DIR ? expandTilde(env.CODEBUDDY_CONFIG_DIR) : path.join(home, '.codebuddy'); + + // ── Cline ──────────────────────────────────────────────────────────────── + // Note: Cline is rules-based (.clinerules) — no skills/ directory. + // getGlobalSkillDir() returns null for cline. + case 'cline': + return env.CLINE_CONFIG_DIR ? expandTilde(env.CLINE_CONFIG_DIR) : path.join(home, '.cline'); + + // ── OpenCode (XDG) ─────────────────────────────────────────────────────── + case 'opencode': { + if (env.OPENCODE_CONFIG_DIR) return expandTilde(env.OPENCODE_CONFIG_DIR); + if (env.XDG_CONFIG_HOME) return path.join(expandTilde(env.XDG_CONFIG_HOME), 'opencode'); + return path.join(home, '.config', 'opencode'); + } + + // ── Kilo (XDG) ─────────────────────────────────────────────────────────── + case 'kilo': { + if (env.KILO_CONFIG_DIR) return expandTilde(env.KILO_CONFIG_DIR); + if (env.XDG_CONFIG_HOME) return path.join(expandTilde(env.XDG_CONFIG_HOME), 'kilo'); + return path.join(home, '.config', 'kilo'); + } + + // ── Default (Claude fallback) ───────────────────────────────────────────── + default: + return env.CLAUDE_CONFIG_DIR ? expandTilde(env.CLAUDE_CONFIG_DIR) : path.join(home, '.claude'); + } +} + +/** + * Return the global skills base directory for the given runtime. + * Most runtimes: /skills + * Hermes: /skills/gsd (nested category layout — #2841) + * Cline: null (rules-based, no skills directory) + * + * @param {string} runtime + * @returns {string|null} + */ +function getGlobalSkillsBase(runtime) { + if (runtime === 'cline') return null; + const configDir = getGlobalConfigDir(runtime); + if (runtime === 'hermes') return path.join(configDir, 'skills', 'gsd'); + return path.join(configDir, 'skills'); +} + +/** + * Return the full path to a specific skill's directory for the given runtime. + * Returns null for runtimes that don't use a skills directory (cline). + * + * @param {string} runtime + * @param {string} skillName - e.g. 'gsd-executor' + * @returns {string|null} + */ +function getGlobalSkillDir(runtime, skillName) { + const base = getGlobalSkillsBase(runtime); + if (base === null) return null; + return path.join(base, skillName); +} + +/** + * Return a human-readable display path for a global skill (for log messages). + * + * @param {string} runtime + * @param {string} skillName + * @returns {string} + */ +function getGlobalSkillDisplayPath(runtime, skillName) { + const dir = getGlobalSkillDir(runtime, skillName); + if (!dir) return `(${runtime} does not use a skills directory)`; + // Replace homedir prefix with ~ for readability + const home = os.homedir(); + return dir.startsWith(home) ? '~' + dir.slice(home.length) : dir; +} + +module.exports = { + getGlobalConfigDir, + getGlobalSkillsBase, + getGlobalSkillDir, + getGlobalSkillDisplayPath, +}; diff --git a/tests/bug-3126-global-skills-base-runtime-path.test.cjs b/tests/bug-3126-global-skills-base-runtime-path.test.cjs new file mode 100644 index 000000000..deba3c344 --- /dev/null +++ b/tests/bug-3126-global-skills-base-runtime-path.test.cjs @@ -0,0 +1,200 @@ +'use strict'; +// allow-test-rule: last three tests read init.cjs source to verify delegation contract to runtime-homes.cjs — structural guard, no behavioral IR exposed + +// Regression guard for bug #3126. +// +// buildAgentSkillsBlock() in init.cjs hardcoded `globalSkillsBase` to +// `~/.claude/skills` regardless of the active runtime. On a Cursor install, +// global: skills live under `~/.cursor/skills`, causing every global: lookup +// to silently fail with: +// [agent-skills] WARNING: Global skill not found at "~/.cursor/skills/X/SKILL.md" — skipping +// +// Fix introduces get-shit-done/bin/lib/runtime-homes.cjs with first-class +// support for all 15 supported runtimes, including: +// - hermes: nested skills/gsd// layout (#2841) +// - cline: rules-based, returns null (no skills directory) +// - CLAUDE_CONFIG_DIR env var for Claude (was missing) +// - All other runtime-specific env vars + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const os = require('node:os'); + +const ROOT = path.join(__dirname, '..'); +const { + getGlobalConfigDir, + getGlobalSkillsBase, + getGlobalSkillDir, + getGlobalSkillDisplayPath, +} = require(path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'runtime-homes.cjs')); + +// Helper: run fn with an env var temporarily set +function withEnv(key, value, fn) { + const orig = process.env[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + try { return fn(); } + finally { + if (orig === undefined) delete process.env[key]; + else process.env[key] = orig; + } +} + +describe('bug #3126: runtime-homes getGlobalConfigDir — defaults', () => { + const defaults = [ + ['claude', path.join(os.homedir(), '.claude')], + ['cursor', path.join(os.homedir(), '.cursor')], + ['gemini', path.join(os.homedir(), '.gemini')], + ['codex', path.join(os.homedir(), '.codex')], + ['copilot', path.join(os.homedir(), '.copilot')], + ['antigravity', path.join(os.homedir(), '.gemini', 'antigravity')], + ['windsurf', path.join(os.homedir(), '.codeium', 'windsurf')], + ['augment', path.join(os.homedir(), '.augment')], + ['trae', path.join(os.homedir(), '.trae')], + ['qwen', path.join(os.homedir(), '.qwen')], + ['hermes', path.join(os.homedir(), '.hermes')], + ['codebuddy', path.join(os.homedir(), '.codebuddy')], + ['cline', path.join(os.homedir(), '.cline')], + ['opencode', path.join(os.homedir(), '.config', 'opencode')], + ['kilo', path.join(os.homedir(), '.config', 'kilo')], + ]; + for (const [runtime, expected] of defaults) { + test(`${runtime} default configDir`, () => { + // Clear all env vars for this runtime + const envKeys = ['CLAUDE_CONFIG_DIR','CURSOR_CONFIG_DIR','GEMINI_CONFIG_DIR', + 'CODEX_HOME','COPILOT_CONFIG_DIR','ANTIGRAVITY_CONFIG_DIR','WINDSURF_CONFIG_DIR', + 'AUGMENT_CONFIG_DIR','TRAE_CONFIG_DIR','QWEN_CONFIG_DIR','HERMES_HOME', + 'CODEBUDDY_CONFIG_DIR','CLINE_CONFIG_DIR','OPENCODE_CONFIG_DIR','KILO_CONFIG_DIR', + 'XDG_CONFIG_HOME']; + const saved = {}; + for (const k of envKeys) { saved[k] = process.env[k]; delete process.env[k]; } + try { + assert.strictEqual(getGlobalConfigDir(runtime), expected); + } finally { + for (const k of envKeys) { + if (saved[k] !== undefined) process.env[k] = saved[k]; + } + } + }); + } + test('unknown runtime falls back to ~/.claude', () => { + withEnv('CLAUDE_CONFIG_DIR', undefined, () => { + assert.strictEqual(getGlobalConfigDir('unknown-xyz'), path.join(os.homedir(), '.claude')); + }); + }); +}); + +describe('bug #3126: runtime-homes env-var overrides', () => { + test('claude respects CLAUDE_CONFIG_DIR (was missing in old code)', () => { + withEnv('CLAUDE_CONFIG_DIR', '/custom/claude', () => { + assert.strictEqual(getGlobalConfigDir('claude'), '/custom/claude'); + }); + }); + test('cursor respects CURSOR_CONFIG_DIR', () => { + withEnv('CURSOR_CONFIG_DIR', '/custom/cursor', () => { + assert.strictEqual(getGlobalConfigDir('cursor'), '/custom/cursor'); + }); + }); + test('opencode respects OPENCODE_CONFIG_DIR', () => { + withEnv('OPENCODE_CONFIG_DIR', '/custom/opencode', () => { + withEnv('XDG_CONFIG_HOME', undefined, () => { + assert.strictEqual(getGlobalConfigDir('opencode'), '/custom/opencode'); + }); + }); + }); + test('opencode uses XDG_CONFIG_HOME when OPENCODE_CONFIG_DIR absent', () => { + withEnv('OPENCODE_CONFIG_DIR', undefined, () => { + withEnv('XDG_CONFIG_HOME', '/xdg', () => { + assert.strictEqual(getGlobalConfigDir('opencode'), '/xdg/opencode'); + }); + }); + }); + test('kilo uses XDG_CONFIG_HOME when KILO_CONFIG_DIR absent', () => { + withEnv('KILO_CONFIG_DIR', undefined, () => { + withEnv('XDG_CONFIG_HOME', '/xdg', () => { + assert.strictEqual(getGlobalConfigDir('kilo'), '/xdg/kilo'); + }); + }); + }); +}); + +describe('bug #3126: runtime-homes getGlobalSkillsBase', () => { + test('most runtimes: skills at /skills', () => { + withEnv('CURSOR_CONFIG_DIR', undefined, () => { + assert.strictEqual( + getGlobalSkillsBase('cursor'), + path.join(os.homedir(), '.cursor', 'skills'), + ); + }); + }); + test('hermes: skills at /skills/gsd (nested layout #2841)', () => { + withEnv('HERMES_HOME', undefined, () => { + assert.strictEqual( + getGlobalSkillsBase('hermes'), + path.join(os.homedir(), '.hermes', 'skills', 'gsd'), + ); + }); + }); + test('cline: returns null (rules-based, no skills directory)', () => { + assert.strictEqual(getGlobalSkillsBase('cline'), null); + }); +}); + +describe('bug #3126: runtime-homes getGlobalSkillDir', () => { + test('cursor: /skills/', () => { + withEnv('CURSOR_CONFIG_DIR', undefined, () => { + assert.strictEqual( + getGlobalSkillDir('cursor', 'gsd-executor'), + path.join(os.homedir(), '.cursor', 'skills', 'gsd-executor'), + ); + }); + }); + test('hermes: /skills/gsd/', () => { + withEnv('HERMES_HOME', undefined, () => { + assert.strictEqual( + getGlobalSkillDir('hermes', 'gsd-executor'), + path.join(os.homedir(), '.hermes', 'skills', 'gsd', 'gsd-executor'), + ); + }); + }); + test('cline: returns null', () => { + assert.strictEqual(getGlobalSkillDir('cline', 'gsd-executor'), null); + }); +}); + +describe('bug #3126: init.cjs uses runtime-homes not hardcoded .claude', () => { + test('init.cjs has no hardcoded globalSkillsBase assignment to ~/.claude/skills', () => { + const fs = require('node:fs'); + const src = fs.readFileSync( + path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'init.cjs'), + 'utf8', + ); + assert.ok( + !src.includes("const globalSkillsBase = path.join(os.homedir(), '.claude', 'skills')"), + 'init.cjs still assigns globalSkillsBase to hardcoded ~/.claude/skills — fix not applied', + ); + }); + test('init.cjs requires runtime-homes', () => { + const fs = require('node:fs'); + const src = fs.readFileSync( + path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'init.cjs'), + 'utf8', + ); + assert.ok( + src.includes('runtime-homes'), + 'init.cjs does not require runtime-homes.cjs', + ); + }); + test('init.cjs warning message no longer hardcodes ~/.claude/skills', () => { + const fs = require('node:fs'); + const src = fs.readFileSync( + path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'init.cjs'), + 'utf8', + ); + assert.ok( + !src.includes("~/.claude/skills/${skillName}/SKILL.md"), + 'init.cjs warning message still hardcodes ~/.claude/skills path', + ); + }); +});