From c3a42d66f936c801037a6fd1f63f933a6f74f208 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 29 Apr 2026 07:44:49 -0400 Subject: [PATCH] Revert "feat(install): add Hermes Agent runtime support" (#2849) --- README.md | 11 +- bin/install.js | 142 ++--------- get-shit-done/bin/lib/core.cjs | 12 +- tests/hermes-install.test.cjs | 316 ------------------------- tests/hermes-skills-migration.test.cjs | 287 ---------------------- tests/kilo-install.test.cjs | 6 +- tests/multi-runtime-select.test.cjs | 69 +++--- 7 files changed, 51 insertions(+), 792 deletions(-) delete mode 100644 tests/hermes-install.test.cjs delete mode 100644 tests/hermes-skills-migration.test.cjs diff --git a/README.md b/README.md index 4826da8d8..f6a4b5427 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **English** · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [한국어](README.ko-KR.md) -**A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Cursor, Windsurf, Antigravity, Augment, Trae, Qwen Code, Hermes Agent, Cline, and CodeBuddy.** +**A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Cursor, Windsurf, Antigravity, Augment, Trae, Qwen Code, Cline, and CodeBuddy.** **Solves context rot — the quality degradation that happens as Claude fills its context window.** @@ -104,12 +104,11 @@ npx get-shit-done-cc@latest ``` The installer prompts you to choose: -1. **Runtime** — Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Cursor, Windsurf, Antigravity, Augment, Trae, Qwen Code, Hermes Agent, CodeBuddy, Cline, or all (interactive multi-select — pick multiple runtimes in a single install session) +1. **Runtime** — Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Cursor, Windsurf, Antigravity, Augment, Trae, Qwen Code, CodeBuddy, Cline, or all (interactive multi-select — pick multiple runtimes in a single install session) 2. **Location** — Global (all projects) or local (current project only) Verify with: - Claude Code / Gemini / Copilot / Antigravity / Qwen Code: `/gsd-help` -- Hermes Agent: ask the agent to use `gsd-help` (skills are natural-language-triggered) - OpenCode / Kilo / Augment / Trae / CodeBuddy: `/gsd-help` - Codex: `$gsd-help` - Cline: GSD installs via `.clinerules` — verify by checking `.clinerules` exists @@ -180,10 +179,6 @@ npx get-shit-done-cc --trae --local # Install to ./.trae/ npx get-shit-done-cc --qwen --global # Install to ~/.qwen/ npx get-shit-done-cc --qwen --local # Install to ./.qwen/ -# Hermes Agent -npx get-shit-done-cc --hermes --global # Install to ~/.hermes/ (honors $HERMES_HOME) -npx get-shit-done-cc --hermes --local # Install to ./.hermes/ - # CodeBuddy npx get-shit-done-cc --codebuddy --global # Install to ~/.codebuddy/ npx get-shit-done-cc --codebuddy --local # Install to ./.codebuddy/ @@ -197,7 +192,7 @@ npx get-shit-done-cc --all --global # Install to all directories ``` Use `--global` (`-g`) or `--local` (`-l`) to skip the location prompt. -Use `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--windsurf`, `--antigravity`, `--augment`, `--trae`, `--qwen`, `--hermes`, `--codebuddy`, `--cline`, or `--all` to skip the runtime prompt. +Use `--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--cursor`, `--windsurf`, `--antigravity`, `--augment`, `--trae`, `--qwen`, `--codebuddy`, `--cline`, or `--all` to skip the runtime prompt. The GSD SDK CLI (`gsd-sdk`) is installed automatically (required by `/gsd-*` commands). Pass `--no-sdk` to skip the SDK install, or `--sdk` to force a reinstall. diff --git a/bin/install.js b/bin/install.js index 29da0342d..5f48fc6e9 100755 --- a/bin/install.js +++ b/bin/install.js @@ -93,7 +93,6 @@ const hasWindsurf = args.includes('--windsurf'); const hasAugment = args.includes('--augment'); const hasTrae = args.includes('--trae'); const hasQwen = args.includes('--qwen'); -const hasHermes = args.includes('--hermes'); const hasCodebuddy = args.includes('--codebuddy'); const hasCline = args.includes('--cline'); const hasBoth = args.includes('--both'); // Legacy flag, keeps working @@ -114,7 +113,7 @@ if (hasSdk && hasNoSdk) { // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; if (hasAll) { - selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline']; + selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy', 'cline']; } else if (hasBoth) { selectedRuntimes = ['claude', 'opencode']; } else { @@ -130,7 +129,6 @@ if (hasAll) { if (hasAugment) selectedRuntimes.push('augment'); if (hasTrae) selectedRuntimes.push('trae'); if (hasQwen) selectedRuntimes.push('qwen'); - if (hasHermes) selectedRuntimes.push('hermes'); if (hasCodebuddy) selectedRuntimes.push('codebuddy'); if (hasCline) selectedRuntimes.push('cline'); } @@ -182,7 +180,6 @@ function getDirName(runtime) { if (runtime === 'augment') return '.augment'; if (runtime === 'trae') return '.trae'; if (runtime === 'qwen') return '.qwen'; - if (runtime === 'hermes') return '.hermes'; if (runtime === 'codebuddy') return '.codebuddy'; if (runtime === 'cline') return '.cline'; return '.claude'; @@ -218,7 +215,6 @@ function getConfigDirFromHome(runtime, isGlobal) { if (runtime === 'augment') return "'.augment'"; if (runtime === 'trae') return "'.trae'"; if (runtime === 'qwen') return "'.qwen'"; - if (runtime === 'hermes') return "'.hermes'"; if (runtime === 'codebuddy') return "'.codebuddy'"; if (runtime === 'cline') return "'.cline'"; return "'.claude'"; @@ -393,19 +389,6 @@ function getGlobalDir(runtime, explicitDir = null) { return path.join(os.homedir(), '.qwen'); } - if (runtime === 'hermes') { - // Hermes Agent: --config-dir > HERMES_HOME > ~/.hermes - // Honors HERMES_HOME which Hermes users set for profile mode / Docker - // deploys (docs: https://hermes-agent.nousresearch.com/docs). - if (explicitDir) { - return expandTilde(explicitDir); - } - if (process.env.HERMES_HOME) { - return expandTilde(process.env.HERMES_HOME); - } - return path.join(os.homedir(), '.hermes'); - } - if (runtime === 'codebuddy') { // CodeBuddy: --config-dir > CODEBUDDY_CONFIG_DIR > ~/.codebuddy if (explicitDir) { @@ -448,7 +431,7 @@ const banner = '\n' + '\n' + ' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' + ' A meta-prompting, context engineering and spec-driven\n' + - ' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and CodeBuddy by TÂCHES.\n'; + ' development system for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Cline and CodeBuddy by TÂCHES.\n'; // Parse --config-dir argument function parseConfigDirArg() { @@ -486,7 +469,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--minimal${reset} Install only the main-loop skills (new-project,\n discuss-phase, plan-phase, execute-phase, help, update)\n and zero gsd-* subagents. Cuts cold-start system-prompt\n overhead from ~12k tokens to ~700 — useful for local LLMs\n with 32K–128K context. Re-run \`gsd update\` (without --minimal)\n to expand to the full surface. Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx get-shit-done-cc --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx get-shit-done-cc --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx get-shit-done-cc --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx get-shit-done-cc --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx get-shit-done-cc --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx get-shit-done-cc --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx get-shit-done-cc --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx get-shit-done-cc --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx get-shit-done-cc --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx get-shit-done-cc --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx get-shit-done-cc --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx get-shit-done-cc --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n`); + console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--minimal${reset} Install only the main-loop skills (new-project,\n discuss-phase, plan-phase, execute-phase, help, update)\n and zero gsd-* subagents. Cuts cold-start system-prompt\n overhead from ~12k tokens to ~700 — useful for local LLMs\n with 32K–128K context. Re-run \`gsd update\` (without --minimal)\n to expand to the full surface. Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx get-shit-done-cc --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx get-shit-done-cc --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx get-shit-done-cc --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx get-shit-done-cc --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx get-shit-done-cc --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx get-shit-done-cc --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx get-shit-done-cc --trae --local\n\n ${dim}# Install for Cline locally${reset}\n npx get-shit-done-cc --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx get-shit-done-cc --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx get-shit-done-cc --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n`); process.exit(0); } @@ -5114,21 +5097,12 @@ function copyCommandsAsClaudeSkills(srcDir, skillsDir, prefix, pathPrefix, runti content = content.replace(/~\/\.qwen\//g, pathPrefix); content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix); content = content.replace(/\.\/\.qwen\//g, `./${getDirName(runtime)}/`); - content = content.replace(/~\/\.hermes\//g, pathPrefix); - content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix); - content = content.replace(/\.\/\.hermes\//g, `./${getDirName(runtime)}/`); // Qwen reuses Claude skill format but needs runtime-specific content replacement if (runtime === 'qwen') { content = content.replace(/CLAUDE\.md/g, 'QWEN.md'); content = content.replace(/\bClaude Code\b/g, 'Qwen Code'); content = content.replace(/\.claude\//g, '.qwen/'); } - // Hermes Agent reuses Claude skill format; rewrite branding + paths. - if (runtime === 'hermes') { - content = content.replace(/CLAUDE\.md/g, 'HERMES.md'); - content = content.replace(/\bClaude Code\b/g, 'Hermes Agent'); - content = content.replace(/\.claude\//g, '.hermes/'); - } content = processAttribution(content, getCommitAttribution(runtime)); content = convertClaudeCommandToClaudeSkill(content, skillName); @@ -5266,7 +5240,6 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand const isAugment = runtime === 'augment'; const isTrae = runtime === 'trae'; const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; const isCline = runtime === 'cline'; const dirName = getDirName(runtime); @@ -5298,9 +5271,6 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand content = content.replace(/~\/\.qwen\//g, pathPrefix); content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix); content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`); - content = content.replace(/~\/\.hermes\//g, pathPrefix); - content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix); - content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`); } content = processAttribution(content, getCommitAttribution(runtime)); @@ -5349,11 +5319,6 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand content = content.replace(/\bClaude Code\b/g, 'Qwen Code'); content = content.replace(/\.claude\//g, '.qwen/'); fs.writeFileSync(destPath, content); - } else if (isHermes) { - content = content.replace(/CLAUDE\.md/g, 'HERMES.md'); - content = content.replace(/\bClaude Code\b/g, 'Hermes Agent'); - content = content.replace(/\.claude\//g, '.hermes/'); - fs.writeFileSync(destPath, content); } else { fs.writeFileSync(destPath, content); } @@ -5405,13 +5370,6 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand jsContent = jsContent.replace(/CLAUDE\.md/g, 'QWEN.md'); jsContent = jsContent.replace(/\bClaude Code\b/g, 'Qwen Code'); fs.writeFileSync(destPath, jsContent); - } else if (isHermes && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { - let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/\.claude\/skills\//g, '.hermes/skills/'); - jsContent = jsContent.replace(/\.claude\//g, '.hermes/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, 'HERMES.md'); - jsContent = jsContent.replace(/\bClaude Code\b/g, 'Hermes Agent'); - fs.writeFileSync(destPath, jsContent); } else { fs.copyFileSync(srcPath, destPath); } @@ -5590,7 +5548,6 @@ function uninstall(isGlobal, runtime = 'claude') { const isAugment = runtime === 'augment'; const isTrae = runtime === 'trae'; const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; const isCodebuddy = runtime === 'codebuddy'; const dirName = getDirName(runtime); @@ -5615,7 +5572,6 @@ function uninstall(isGlobal, runtime = 'claude') { if (runtime === 'augment') runtimeLabel = 'Augment'; if (runtime === 'trae') runtimeLabel = 'Trae'; if (runtime === 'qwen') runtimeLabel = 'Qwen Code'; - if (runtime === 'hermes') runtimeLabel = 'Hermes Agent'; if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy'; console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); @@ -5763,32 +5719,6 @@ function uninstall(isGlobal, runtime = 'claude') { } } - const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd'); - if (fs.existsSync(legacyCommandsDir)) { - const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']); - fs.rmSync(legacyCommandsDir, { recursive: true }); - removedCount++; - console.log(` ${green}✓${reset} Removed legacy commands/gsd/`); - restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts); - } - } else if (isHermes) { - // Hermes Agent: remove skills/gsd-*/ directories (same layout as Qwen) - const skillsDir = path.join(targetDir, 'skills'); - if (fs.existsSync(skillsDir)) { - let skillCount = 0; - const entries = fs.readdirSync(skillsDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory() && entry.name.startsWith('gsd-')) { - fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); - skillCount++; - } - } - if (skillCount > 0) { - removedCount++; - console.log(` ${green}✓${reset} Removed ${skillCount} Hermes Agent skills`); - } - } - const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd'); if (fs.existsSync(legacyCommandsDir)) { const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']); @@ -6613,7 +6543,6 @@ function install(isGlobal, runtime = 'claude') { const isAugment = runtime === 'augment'; const isTrae = runtime === 'trae'; const isQwen = runtime === 'qwen'; - const isHermes = runtime === 'hermes'; const isCodebuddy = runtime === 'codebuddy'; const isCline = runtime === 'cline'; const dirName = getDirName(runtime); @@ -6658,7 +6587,6 @@ function install(isGlobal, runtime = 'claude') { if (isAugment) runtimeLabel = 'Augment'; if (isTrae) runtimeLabel = 'Trae'; if (isQwen) runtimeLabel = 'Qwen Code'; - if (isHermes) runtimeLabel = 'Hermes Agent'; if (isCodebuddy) runtimeLabel = 'CodeBuddy'; if (isCline) runtimeLabel = 'Cline'; @@ -6784,32 +6712,6 @@ function install(isGlobal, runtime = 'claude') { failures.push('skills/gsd-*'); } - const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd'); - if (fs.existsSync(legacyCommandsDir)) { - const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']); - fs.rmSync(legacyCommandsDir, { recursive: true }); - console.log(` ${green}✓${reset} Removed legacy commands/gsd/ directory`); - restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts); - } - } else if (isHermes) { - // Hermes Agent: reuses Claude-skill-format pipeline (skills/gsd-*/SKILL.md). - // Hermes' skill loader accepts unknown frontmatter keys (allowed-tools, - // argument-hint) and renders the gsd-* skills under ~/.hermes/skills/. - const skillsDir = path.join(targetDir, 'skills'); - const gsdSrc = stageSkillsForMode(path.join(src, 'commands', 'gsd'), installMode); - copyCommandsAsClaudeSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime, isGlobal); - if (fs.existsSync(skillsDir)) { - const count = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length; - if (count > 0) { - console.log(` ${green}✓${reset} Installed ${count} skills to skills/`); - } else { - failures.push('skills/gsd-*'); - } - } else { - failures.push('skills/gsd-*'); - } - const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd'); if (fs.existsSync(legacyCommandsDir)) { const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']); @@ -7017,10 +6919,6 @@ function install(isGlobal, runtime = 'claude') { content = content.replace(/CLAUDE\.md/g, 'QWEN.md'); content = content.replace(/\bClaude Code\b/g, 'Qwen Code'); content = content.replace(/\.claude\//g, '.qwen/'); - } else if (isHermes) { - content = content.replace(/CLAUDE\.md/g, 'HERMES.md'); - content = content.replace(/\bClaude Code\b/g, 'Hermes Agent'); - content = content.replace(/\.claude\//g, '.hermes/'); } const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name; fs.writeFileSync(path.join(agentsDest, destName), content); @@ -7085,10 +6983,6 @@ function install(isGlobal, runtime = 'claude') { content = content.replace(/CLAUDE\.md/g, 'QWEN.md'); content = content.replace(/\bClaude Code\b/g, 'Qwen Code'); } - if (isHermes) { - content = content.replace(/CLAUDE\.md/g, 'HERMES.md'); - content = content.replace(/\bClaude Code\b/g, 'Hermes Agent'); - } content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version); fs.writeFileSync(destFile, content); // Ensure hook files are executable (fixes #1162 — missing +x permission) @@ -7832,7 +7726,6 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'trae') program = 'Trae'; if (runtime === 'cline') program = 'Cline'; if (runtime === 'qwen') program = 'Qwen Code'; - if (runtime === 'hermes') program = 'Hermes Agent'; let command = '/gsd-new-project'; if (runtime === 'opencode') command = '/gsd-new-project'; @@ -7846,7 +7739,6 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'trae') command = '/gsd-new-project'; if (runtime === 'cline') command = '/gsd-new-project'; if (runtime === 'qwen') command = '/gsd-new-project'; - if (runtime === 'hermes') command = 'gsd-new-project (mention the skill name, e.g. "use gsd-new-project")'; console.log(` ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}. @@ -7934,14 +7826,13 @@ function promptRuntime(callback) { '7': 'copilot', '8': 'cursor', '9': 'gemini', - '10': 'hermes', - '11': 'kilo', - '12': 'opencode', - '13': 'qwen', - '14': 'trae', - '15': 'windsurf' + '10': 'kilo', + '11': 'opencode', + '12': 'qwen', + '13': 'trae', + '14': 'windsurf' }; - const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf']; + const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf']; console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset} ${cyan}2${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset} @@ -7952,13 +7843,12 @@ function promptRuntime(callback) { ${cyan}7${reset}) Copilot ${dim}(~/.copilot)${reset} ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset} ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset} - ${cyan}10${reset}) Hermes Agent ${dim}(~/.hermes)${reset} - ${cyan}11${reset}) Kilo ${dim}(~/.config/kilo)${reset} - ${cyan}12${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - ${cyan}13${reset}) Qwen Code ${dim}(~/.qwen)${reset} - ${cyan}14${reset}) Trae ${dim}(~/.trae)${reset} - ${cyan}15${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset} - ${cyan}16${reset}) All + ${cyan}10${reset}) Kilo ${dim}(~/.config/kilo)${reset} + ${cyan}11${reset}) OpenCode ${dim}(~/.config/opencode)${reset} + ${cyan}12${reset}) Qwen Code ${dim}(~/.qwen)${reset} + ${cyan}13${reset}) Trae ${dim}(~/.trae)${reset} + ${cyan}14${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset} + ${cyan}15${reset}) All ${dim}Select multiple: 1,2,6 or 1 2 6${reset} `); @@ -7969,7 +7859,7 @@ function promptRuntime(callback) { const input = answer.trim() || '1'; // "All" shortcut - if (input === '16') { + if (input === '15') { callback(allRuntimes); return; } diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 617582a15..808208e09 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -1610,16 +1610,6 @@ const RUNTIME_PROFILE_MAP = { sonnet: { model: 'claude-sonnet-4-6' }, haiku: { model: 'claude-haiku-4-5' }, }, - hermes: { - // Hermes Agent is provider-agnostic; users pick any provider in ~/.hermes/config.yaml. - // Defaults use OpenRouter slugs because (a) OpenRouter is Hermes' default provider and - // (b) the same slugs resolve on OpenRouter, native Anthropic, and Copilot via Hermes' - // aggregator-aware resolver. Users on a different provider override per-tier via - // model_profile_overrides.hermes.{opus,sonnet,haiku} in .planning/config.json. - opus: { model: 'anthropic/claude-opus-4-7' }, - sonnet: { model: 'anthropic/claude-sonnet-4-6' }, - haiku: { model: 'anthropic/claude-haiku-4-5' }, - }, }; const RUNTIMES_WITH_REASONING_EFFORT = new Set(['codex']); @@ -1642,7 +1632,7 @@ const RUNTIME_OVERRIDE_TIERS = new Set(['opus', 'sonnet', 'haiku']); const KNOWN_RUNTIMES = new Set([ 'claude', 'codex', 'opencode', 'kilo', 'gemini', 'qwen', 'copilot', 'cursor', 'windsurf', 'augment', 'trae', 'codebuddy', - 'antigravity', 'cline', 'hermes', + 'antigravity', 'cline', ]); const _warnedConfigKeys = new Set(); diff --git a/tests/hermes-install.test.cjs b/tests/hermes-install.test.cjs deleted file mode 100644 index 240efb69f..000000000 --- a/tests/hermes-install.test.cjs +++ /dev/null @@ -1,316 +0,0 @@ -process.env.GSD_TEST_MODE = '1'; - -const { test, describe, beforeEach, afterEach } = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const os = require('node:os'); -const { createTempDir, cleanup } = require('./helpers.cjs'); - -const { - getDirName, - getGlobalDir, - getConfigDirFromHome, - install, - uninstall, - writeManifest, -} = require('../bin/install.js'); - -describe('Hermes Agent runtime directory mapping', () => { - test('maps Hermes to .hermes for local installs', () => { - assert.strictEqual(getDirName('hermes'), '.hermes'); - }); - - test('maps Hermes to ~/.hermes for global installs', () => { - assert.strictEqual(getGlobalDir('hermes'), path.join(os.homedir(), '.hermes')); - }); - - test('returns .hermes config fragments for local and global installs', () => { - assert.strictEqual(getConfigDirFromHome('hermes', false), "'.hermes'"); - assert.strictEqual(getConfigDirFromHome('hermes', true), "'.hermes'"); - }); -}); - -describe('getGlobalDir (Hermes Agent)', () => { - let originalHermesConfigDir; - - beforeEach(() => { - originalHermesConfigDir = process.env.HERMES_HOME; - }); - - afterEach(() => { - if (originalHermesConfigDir !== undefined) { - process.env.HERMES_HOME = originalHermesConfigDir; - } else { - delete process.env.HERMES_HOME; - } - }); - - test('returns ~/.hermes with no env var or explicit dir', () => { - delete process.env.HERMES_HOME; - const result = getGlobalDir('hermes'); - assert.strictEqual(result, path.join(os.homedir(), '.hermes')); - }); - - test('returns explicit dir when provided', () => { - const result = getGlobalDir('hermes', '/custom/hermes-path'); - assert.strictEqual(result, '/custom/hermes-path'); - }); - - test('respects HERMES_HOME env var', () => { - process.env.HERMES_HOME = '~/custom-hermes'; - const result = getGlobalDir('hermes'); - assert.strictEqual(result, path.join(os.homedir(), 'custom-hermes')); - }); - - test('explicit dir takes priority over HERMES_HOME', () => { - process.env.HERMES_HOME = '~/from-env'; - const result = getGlobalDir('hermes', '/explicit/path'); - assert.strictEqual(result, '/explicit/path'); - }); - - test('does not break other runtimes', () => { - assert.strictEqual(getGlobalDir('claude'), path.join(os.homedir(), '.claude')); - assert.strictEqual(getGlobalDir('codex'), path.join(os.homedir(), '.codex')); - }); -}); - -describe('Hermes Agent local install/uninstall', () => { - let tmpDir; - let previousCwd; - - beforeEach(() => { - tmpDir = createTempDir('gsd-hermes-install-'); - previousCwd = process.cwd(); - process.chdir(tmpDir); - }); - - afterEach(() => { - process.chdir(previousCwd); - cleanup(tmpDir); - }); - - test('installs GSD into ./.hermes and removes it cleanly', () => { - const result = install(false, 'hermes'); - const targetDir = path.join(tmpDir, '.hermes'); - - assert.strictEqual(result.runtime, 'hermes'); - assert.strictEqual(result.configDir, fs.realpathSync(targetDir)); - - assert.ok(fs.existsSync(path.join(targetDir, 'skills', 'gsd-help', 'SKILL.md'))); - assert.ok(fs.existsSync(path.join(targetDir, 'get-shit-done', 'VERSION'))); - assert.ok(fs.existsSync(path.join(targetDir, 'agents'))); - - const manifest = writeManifest(targetDir, 'hermes'); - assert.ok(Object.keys(manifest.files).some(file => file.startsWith('skills/gsd-help/')), manifest); - - uninstall(false, 'hermes'); - - assert.ok(!fs.existsSync(path.join(targetDir, 'skills', 'gsd-help')), 'Hermes skill directory removed'); - assert.ok(!fs.existsSync(path.join(targetDir, 'get-shit-done')), 'get-shit-done removed'); - }); -}); - -describe('E2E: Hermes Agent uninstall skills cleanup', () => { - let tmpDir; - let previousCwd; - - beforeEach(() => { - tmpDir = createTempDir('gsd-hermes-uninstall-'); - previousCwd = process.cwd(); - process.chdir(tmpDir); - }); - - afterEach(() => { - process.chdir(previousCwd); - cleanup(tmpDir); - }); - - test('removes all gsd-* skill directories on --hermes --uninstall', () => { - const targetDir = path.join(tmpDir, '.hermes'); - install(false, 'hermes'); - - const skillsDir = path.join(targetDir, 'skills'); - assert.ok(fs.existsSync(skillsDir), 'skills dir exists after install'); - - const installedSkills = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.ok(installedSkills.length > 0, `found ${installedSkills.length} gsd-* skill dirs before uninstall`); - - uninstall(false, 'hermes'); - - if (fs.existsSync(skillsDir)) { - const remainingGsd = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(remainingGsd.length, 0, - `Expected 0 gsd-* skill dirs after uninstall, found: ${remainingGsd.map(e => e.name).join(', ')}`); - } - }); - - test('preserves non-GSD skill directories during --hermes --uninstall', () => { - const targetDir = path.join(tmpDir, '.hermes'); - install(false, 'hermes'); - - const customSkillDir = path.join(targetDir, 'skills', 'my-custom-skill'); - fs.mkdirSync(customSkillDir, { recursive: true }); - fs.writeFileSync(path.join(customSkillDir, 'SKILL.md'), '# My Custom Skill\n'); - - assert.ok(fs.existsSync(path.join(customSkillDir, 'SKILL.md')), 'custom skill exists before uninstall'); - - uninstall(false, 'hermes'); - - assert.ok(fs.existsSync(path.join(customSkillDir, 'SKILL.md')), - 'Non-GSD skill directory should be preserved after Hermes uninstall'); - }); - - test('removes engine directory on --hermes --uninstall', () => { - const targetDir = path.join(tmpDir, '.hermes'); - install(false, 'hermes'); - - assert.ok(fs.existsSync(path.join(targetDir, 'get-shit-done', 'VERSION')), - 'engine exists before uninstall'); - - uninstall(false, 'hermes'); - - assert.ok(!fs.existsSync(path.join(targetDir, 'get-shit-done')), - 'get-shit-done engine should be removed after Hermes uninstall'); - }); -}); - -// ─── Regression: no Claude references leak into Hermes install (parity with Qwen regression #2112) ────────── - -describe('Hermes install contains no leaked Claude references (parity with Qwen regression #2112)', () => { - let tmpDir; - let previousCwd; - - beforeEach(() => { - tmpDir = createTempDir('gsd-hermes-refs-'); - previousCwd = process.cwd(); - process.chdir(tmpDir); - install(false, 'hermes'); - }); - - afterEach(() => { - process.chdir(previousCwd); - cleanup(tmpDir); - }); - - /** - * Recursively walk a directory and return all file paths. - */ - function walk(dir) { - const results = []; - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...walk(full)); - } else { - results.push(full); - } - } - return results; - } - - /** - * Return files under .hermes/ that contain Claude references, - * excluding CHANGELOG.md (historical accuracy) and VERSION (no prose). - */ - function findClaudeLeaks() { - const hermesDir = path.join(tmpDir, '.hermes'); - const allFiles = walk(hermesDir); - const textFiles = allFiles.filter(f => - f.endsWith('.md') || f.endsWith('.cjs') || f.endsWith('.js') - ); - const excluded = ['CHANGELOG.md']; - const candidates = textFiles.filter(f => - !excluded.includes(path.basename(f)) - ); - const leaks = []; - for (const file of candidates) { - const content = fs.readFileSync(file, 'utf8'); - if (/\bCLAUDE\.md\b/.test(content) || - /\bClaude Code\b/.test(content) || - /\.claude\//.test(content)) { - leaks.push(path.relative(tmpDir, file)); - } - } - return leaks; - } - - test('skills contain no CLAUDE.md or Claude Code references', () => { - const hermesDir = path.join(tmpDir, '.hermes'); - const skillsDir = path.join(hermesDir, 'skills'); - assert.ok(fs.existsSync(skillsDir), 'skills directory exists'); - - const skillFiles = walk(skillsDir).filter(f => f.endsWith('.md')); - assert.ok(skillFiles.length > 0, 'at least one skill file exists'); - - const leaks = []; - for (const file of skillFiles) { - const content = fs.readFileSync(file, 'utf8'); - if (/\bCLAUDE\.md\b/.test(content) || /\bClaude Code\b/.test(content)) { - leaks.push(path.relative(tmpDir, file)); - } - } - assert.strictEqual(leaks.length, 0, - [ - 'Skills should not contain Claude references after Hermes install.', - 'Leaking files:', - ...leaks, - ].join('\n')); - }); - - test('agents contain no CLAUDE.md or Claude Code references', () => { - const agentsDir = path.join(tmpDir, '.hermes', 'agents'); - assert.ok(fs.existsSync(agentsDir), 'agents directory exists'); - - const agentFiles = walk(agentsDir).filter(f => f.endsWith('.md')); - assert.ok(agentFiles.length > 0, 'at least one agent file exists'); - - const leaks = []; - for (const file of agentFiles) { - const content = fs.readFileSync(file, 'utf8'); - if (/\bCLAUDE\.md\b/.test(content) || /\bClaude Code\b/.test(content)) { - leaks.push(path.relative(tmpDir, file)); - } - } - assert.strictEqual(leaks.length, 0, - [ - 'Agents should not contain Claude references after Hermes install.', - 'Leaking files:', - ...leaks, - ].join('\n')); - }); - - test('hooks contain no .claude/ path references', () => { - const hooksDir = path.join(tmpDir, '.hermes', 'hooks'); - if (!fs.existsSync(hooksDir)) { - return; // hooks may not be present in local installs - } - - const hookFiles = walk(hooksDir).filter(f => f.endsWith('.js')); - const leaks = []; - for (const file of hookFiles) { - const content = fs.readFileSync(file, 'utf8'); - if (/\.claude\//.test(content)) { - leaks.push(path.relative(tmpDir, file)); - } - } - assert.strictEqual(leaks.length, 0, - [ - 'Hooks should not contain .claude/ path references after Hermes install.', - 'Leaking files:', - ...leaks, - ].join('\n')); - }); - - test('full tree scan finds zero Claude references outside CHANGELOG.md', () => { - const leaks = findClaudeLeaks(); - assert.strictEqual(leaks.length, 0, - [ - 'No files under .hermes/ (except CHANGELOG.md) should contain Claude references.', - `Found ${leaks.length} leaking file(s):`, - ...leaks, - ].join('\n')); - }); -}); diff --git a/tests/hermes-skills-migration.test.cjs b/tests/hermes-skills-migration.test.cjs deleted file mode 100644 index 5a26c90dc..000000000 --- a/tests/hermes-skills-migration.test.cjs +++ /dev/null @@ -1,287 +0,0 @@ -/** - * GSD Tools Tests - Hermes Agent Skills Migration - * - * Tests for installing GSD for Hermes Agent using the standard - * skills/gsd-xxx/SKILL.md format (same open standard as Claude Code 2.1.88+). - * - * Uses node:test and node:assert (NOT Jest). - */ - -process.env.GSD_TEST_MODE = '1'; - -const { test, describe, beforeEach, afterEach } = require('node:test'); -const assert = require('node:assert/strict'); -const path = require('path'); -const os = require('os'); -const fs = require('fs'); - -const { - convertClaudeCommandToClaudeSkill, - copyCommandsAsClaudeSkills, -} = require('../bin/install.js'); - -// ─── convertClaudeCommandToClaudeSkill (used by Hermes via copyCommandsAsClaudeSkills) ── - -describe('Hermes Agent: convertClaudeCommandToClaudeSkill', () => { - test('preserves allowed-tools multiline YAML list', () => { - const input = [ - '---', - 'name: gsd:next', - 'description: Advance to the next step', - 'allowed-tools:', - ' - Read', - ' - Bash', - ' - Grep', - '---', - '', - 'Body content here.', - ].join('\n'); - - const result = convertClaudeCommandToClaudeSkill(input, 'gsd-next'); - assert.ok(result.includes('allowed-tools:'), 'allowed-tools field is present'); - assert.ok(result.includes('Read'), 'Read tool preserved'); - assert.ok(result.includes('Bash'), 'Bash tool preserved'); - assert.ok(result.includes('Grep'), 'Grep tool preserved'); - }); - - test('preserves argument-hint', () => { - const input = [ - '---', - 'name: gsd:debug', - 'description: Debug issues', - 'argument-hint: "[issue description]"', - 'allowed-tools:', - ' - Read', - ' - Bash', - '---', - '', - 'Debug body.', - ].join('\n'); - - const result = convertClaudeCommandToClaudeSkill(input, 'gsd-debug'); - assert.ok(result.includes('argument-hint:'), 'argument-hint field is present'); - assert.ok( - result.includes('[issue description]'), - 'argument-hint value preserved' - ); - }); - - test('emits hyphen-form name (gsd-) from hyphen-form dir (#2808)', () => { - const input = [ - '---', - 'name: gsd:next', - 'description: Advance workflow', - '---', - '', - 'Body.', - ].join('\n'); - - // Directory name is gsd-next (hyphen, Windows-safe), frontmatter name is - // gsd-next (hyphen, #2808 — canonical invocation form for Claude Code autocomplete). - const result = convertClaudeCommandToClaudeSkill(input, 'gsd-next'); - assert.ok(result.includes('name: gsd-next'), 'frontmatter name uses hyphen form (#2808)'); - }); - - test('preserves body content unchanged', () => { - const body = '\n\nDo the thing.\n\n\n\nStep 1.\nStep 2.\n\n'; - const input = [ - '---', - 'name: gsd:test', - 'description: Test command', - '---', - body, - ].join(''); - - const result = convertClaudeCommandToClaudeSkill(input, 'gsd-test'); - assert.ok(result.includes(''), 'objective tag preserved'); - assert.ok(result.includes('Do the thing.'), 'body text preserved'); - assert.ok(result.includes(''), 'process tag preserved'); - }); - - test('produces valid SKILL.md frontmatter starting with ---', () => { - const input = [ - '---', - 'name: gsd:plan', - 'description: Plan a phase', - '---', - '', - 'Plan body.', - ].join('\n'); - - const result = convertClaudeCommandToClaudeSkill(input, 'gsd-plan'); - assert.ok(result.startsWith('---\n'), 'frontmatter starts with ---'); - assert.ok(result.includes('\n---\n'), 'frontmatter closes with ---'); - }); -}); - -// ─── copyCommandsAsClaudeSkills (used for Hermes skills install) ───────────── - -describe('Hermes Agent: copyCommandsAsClaudeSkills', () => { - let tmpDir; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-hermes-test-')); - }); - - afterEach(() => { - if (fs.existsSync(tmpDir)) { - fs.rmSync(tmpDir, { recursive: true }); - } - }); - - test('creates skills/gsd-xxx/SKILL.md directory structure', () => { - // Create source command files - const srcDir = path.join(tmpDir, 'src', 'commands', 'gsd'); - fs.mkdirSync(srcDir, { recursive: true }); - fs.writeFileSync(path.join(srcDir, 'quick.md'), [ - '---', - 'name: gsd:quick', - 'description: Execute a quick task', - 'allowed-tools:', - ' - Read', - ' - Bash', - '---', - '', - 'Quick task body', - ].join('\n')); - - const skillsDir = path.join(tmpDir, 'dest', 'skills'); - copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '/test/prefix/', 'hermes', false); - - // Verify SKILL.md was created - const skillPath = path.join(skillsDir, 'gsd-quick', 'SKILL.md'); - assert.ok(fs.existsSync(skillPath), 'gsd-quick/SKILL.md exists'); - - // Verify content - const content = fs.readFileSync(skillPath, 'utf8'); - assert.ok(content.includes('name: gsd-quick'), 'frontmatter name uses hyphen form (#2808)'); - assert.ok(content.includes('description:'), 'description present'); - assert.ok(content.includes('allowed-tools:'), 'allowed-tools preserved'); - assert.ok(content.includes(''), 'body content preserved'); - }); - - test('replaces ~/.claude/ paths with pathPrefix', () => { - const srcDir = path.join(tmpDir, 'src', 'commands', 'gsd'); - fs.mkdirSync(srcDir, { recursive: true }); - fs.writeFileSync(path.join(srcDir, 'next.md'), [ - '---', - 'name: gsd:next', - 'description: Next step', - '---', - '', - 'Reference: @~/.claude/get-shit-done/workflows/next.md', - ].join('\n')); - - const skillsDir = path.join(tmpDir, 'dest', 'skills'); - copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.hermes/', 'hermes', false); - - const content = fs.readFileSync(path.join(skillsDir, 'gsd-next', 'SKILL.md'), 'utf8'); - assert.ok(content.includes('$HOME/.hermes/'), 'path replaced to .hermes/'); - assert.ok(!content.includes('~/.claude/'), 'old claude path removed'); - }); - - test('replaces $HOME/.claude/ paths with pathPrefix', () => { - const srcDir = path.join(tmpDir, 'src', 'commands', 'gsd'); - fs.mkdirSync(srcDir, { recursive: true }); - fs.writeFileSync(path.join(srcDir, 'plan.md'), [ - '---', - 'name: gsd:plan', - 'description: Plan phase', - '---', - '', - 'Reference: $HOME/.claude/get-shit-done/workflows/plan.md', - ].join('\n')); - - const skillsDir = path.join(tmpDir, 'dest', 'skills'); - copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.hermes/', 'hermes', false); - - const content = fs.readFileSync(path.join(skillsDir, 'gsd-plan', 'SKILL.md'), 'utf8'); - assert.ok(content.includes('$HOME/.hermes/'), 'path replaced to .hermes/'); - assert.ok(!content.includes('$HOME/.claude/'), 'old claude path removed'); - }); - - test('removes stale gsd- skills before installing new ones', () => { - const srcDir = path.join(tmpDir, 'src', 'commands', 'gsd'); - fs.mkdirSync(srcDir, { recursive: true }); - fs.writeFileSync(path.join(srcDir, 'quick.md'), [ - '---', - 'name: gsd:quick', - 'description: Quick task', - '---', - '', - 'Body', - ].join('\n')); - - const skillsDir = path.join(tmpDir, 'dest', 'skills'); - // Pre-create a stale skill - fs.mkdirSync(path.join(skillsDir, 'gsd-old-skill'), { recursive: true }); - fs.writeFileSync(path.join(skillsDir, 'gsd-old-skill', 'SKILL.md'), 'old'); - - copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '/test/', 'hermes', false); - - assert.ok(!fs.existsSync(path.join(skillsDir, 'gsd-old-skill')), 'stale skill removed'); - assert.ok(fs.existsSync(path.join(skillsDir, 'gsd-quick', 'SKILL.md')), 'new skill installed'); - }); - - test('preserves agent field in frontmatter', () => { - const srcDir = path.join(tmpDir, 'src', 'commands', 'gsd'); - fs.mkdirSync(srcDir, { recursive: true }); - fs.writeFileSync(path.join(srcDir, 'execute.md'), [ - '---', - 'name: gsd:execute', - 'description: Execute phase', - 'agent: gsd-executor', - 'allowed-tools:', - ' - Read', - ' - Bash', - ' - Task', - '---', - '', - 'Execute body', - ].join('\n')); - - const skillsDir = path.join(tmpDir, 'dest', 'skills'); - copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '/test/', 'hermes', false); - - const content = fs.readFileSync(path.join(skillsDir, 'gsd-execute', 'SKILL.md'), 'utf8'); - assert.ok(content.includes('agent: gsd-executor'), 'agent field preserved'); - }); -}); - -// ─── Integration: SKILL.md format validation ──────────────────────────────── - -describe('Hermes Agent: SKILL.md format validation', () => { - test('SKILL.md frontmatter is valid YAML structure', () => { - const input = [ - '---', - 'name: gsd:review', - 'description: Code review with quality checks', - 'argument-hint: "[PR number or branch]"', - 'agent: gsd-code-reviewer', - 'allowed-tools:', - ' - Read', - ' - Grep', - ' - Bash', - '---', - '', - 'Review code', - ].join('\n'); - - const result = convertClaudeCommandToClaudeSkill(input, 'gsd-review'); - - // Parse the frontmatter - const fmMatch = result.match(/^---\n([\s\S]*?)\n---/); - assert.ok(fmMatch, 'has frontmatter block'); - - const fmLines = fmMatch[1].split('\n'); - const hasName = fmLines.some(l => l.startsWith('name: gsd-review')); - const hasDesc = fmLines.some(l => l.startsWith('description:')); - const hasAgent = fmLines.some(l => l.startsWith('agent:')); - const hasTools = fmLines.some(l => l.startsWith('allowed-tools:')); - - assert.ok(hasName, 'name field correct'); - assert.ok(hasDesc, 'description field present'); - assert.ok(hasAgent, 'agent field present'); - assert.ok(hasTools, 'allowed-tools field present'); - }); -}); diff --git a/tests/kilo-install.test.cjs b/tests/kilo-install.test.cjs index c3fec323f..69d5efaec 100644 --- a/tests/kilo-install.test.cjs +++ b/tests/kilo-install.test.cjs @@ -221,12 +221,12 @@ describe('Source code integration (Kilo)', () => { assert.ok(src.includes("'kilo'"), '--all includes kilo runtime'); }); - test('promptRuntime runtimeMap has Kilo as option 11', () => { - assert.ok(src.includes("'11': 'kilo'"), 'runtimeMap has 11 -> kilo'); + test('promptRuntime runtimeMap has Kilo as option 10', () => { + assert.ok(src.includes("'10': 'kilo'"), 'runtimeMap has 10 -> kilo'); }); test('prompt text shows Kilo above OpenCode without marketing copy', () => { - assert.ok(src.includes('11${reset}) Kilo'), 'prompt lists Kilo as option 11'); + assert.ok(src.includes('10${reset}) Kilo'), 'prompt lists Kilo as option 10'); assert.ok(!src.includes('the #1 AI coding platform on OpenRouter'), 'prompt does not include marketing tagline'); }); diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 25536110a..c0b1b0004 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -27,14 +27,13 @@ const runtimeMap = { '7': 'copilot', '8': 'cursor', '9': 'gemini', - '10': 'hermes', - '11': 'kilo', - '12': 'opencode', - '13': 'qwen', - '14': 'trae', - '15': 'windsurf' + '10': 'kilo', + '11': 'opencode', + '12': 'qwen', + '13': 'trae', + '14': 'windsurf' }; -const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf']; +const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf']; /** * Simulate the parsing logic from promptRuntime without requiring readline. @@ -43,7 +42,7 @@ const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', ' function parseRuntimeInput(input) { input = input.trim() || '1'; - if (input === '16') { + if (input === '15') { return allRuntimes; } @@ -79,7 +78,7 @@ describe('multi-runtime selection parsing', () => { test('space-separated choices return multiple runtimes', () => { assert.deepStrictEqual(parseRuntimeInput('1 7 9'), ['claude', 'copilot', 'gemini']); - assert.deepStrictEqual(parseRuntimeInput('8 11'), ['cursor', 'kilo']); + assert.deepStrictEqual(parseRuntimeInput('8 10'), ['cursor', 'kilo']); }); test('mixed comma and space separators work', () => { @@ -87,32 +86,24 @@ describe('multi-runtime selection parsing', () => { assert.deepStrictEqual(parseRuntimeInput('2 , 8'), ['antigravity', 'cursor']); }); - test('single choice for hermes', () => { - assert.deepStrictEqual(parseRuntimeInput('10'), ['hermes']); - }); - - test('single choice for kilo', () => { - assert.deepStrictEqual(parseRuntimeInput('11'), ['kilo']); - }); - test('single choice for opencode', () => { - assert.deepStrictEqual(parseRuntimeInput('12'), ['opencode']); + assert.deepStrictEqual(parseRuntimeInput('11'), ['opencode']); }); test('single choice for qwen', () => { - assert.deepStrictEqual(parseRuntimeInput('13'), ['qwen']); + assert.deepStrictEqual(parseRuntimeInput('12'), ['qwen']); }); test('single choice for trae', () => { - assert.deepStrictEqual(parseRuntimeInput('14'), ['trae']); + assert.deepStrictEqual(parseRuntimeInput('13'), ['trae']); }); test('single choice for windsurf', () => { - assert.deepStrictEqual(parseRuntimeInput('15'), ['windsurf']); + assert.deepStrictEqual(parseRuntimeInput('14'), ['windsurf']); }); - test('choice 16 returns all runtimes', () => { - assert.deepStrictEqual(parseRuntimeInput('16'), allRuntimes); + test('choice 15 returns all runtimes', () => { + assert.deepStrictEqual(parseRuntimeInput('15'), allRuntimes); }); test('empty input defaults to claude', () => { @@ -121,13 +112,13 @@ describe('multi-runtime selection parsing', () => { }); test('invalid choices are ignored, falls back to claude if all invalid', () => { - assert.deepStrictEqual(parseRuntimeInput('17'), ['claude']); + assert.deepStrictEqual(parseRuntimeInput('16'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']); }); test('invalid choices mixed with valid are filtered out', () => { - assert.deepStrictEqual(parseRuntimeInput('1,17,7'), ['claude', 'copilot']); + assert.deepStrictEqual(parseRuntimeInput('1,16,7'), ['claude', 'copilot']); assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['augment']); }); @@ -138,12 +129,12 @@ describe('multi-runtime selection parsing', () => { test('preserves selection order', () => { assert.deepStrictEqual(parseRuntimeInput('9,1,7'), ['gemini', 'claude', 'copilot']); - assert.deepStrictEqual(parseRuntimeInput('11,2,8'), ['kilo', 'antigravity', 'cursor']); + assert.deepStrictEqual(parseRuntimeInput('10,2,8'), ['kilo', 'antigravity', 'cursor']); }); }); describe('install.js source contains multi-select support', () => { - test('runtimeMap is defined with all 15 runtimes', () => { + test('runtimeMap is defined with all 14 runtimes', () => { for (const [key, name] of Object.entries(runtimeMap)) { assert.ok( installSrc.includes(`'${key}': '${name}'`), @@ -160,29 +151,25 @@ describe('install.js source contains multi-select support', () => { } }); - test('all shortcut uses option 16', () => { + test('all shortcut uses option 15', () => { assert.ok( - installSrc.includes("if (input === '16')"), - 'all shortcut uses option 16' + installSrc.includes("if (input === '15')"), + 'all shortcut uses option 15' ); }); - test('prompt lists Hermes Agent as option 10, Qwen Code as option 13, and All as option 16', () => { + test('prompt lists Qwen Code as option 12, Trae as option 13 and All as option 15', () => { assert.ok( - installSrc.includes('10${reset}) Hermes Agent'), - 'prompt lists Hermes Agent as option 10' + installSrc.includes('12${reset}) Qwen Code'), + 'prompt lists Qwen Code as option 12' ); assert.ok( - installSrc.includes('13${reset}) Qwen Code'), - 'prompt lists Qwen Code as option 13' + installSrc.includes('13${reset}) Trae'), + 'prompt lists Trae as option 13' ); assert.ok( - installSrc.includes('14${reset}) Trae'), - 'prompt lists Trae as option 14' - ); - assert.ok( - installSrc.includes('16${reset}) All'), - 'prompt lists All as option 16' + installSrc.includes('15${reset}) All'), + 'prompt lists All as option 15' ); });