Revert "feat(install): add Hermes Agent runtime support" (#2849)

This commit is contained in:
Tom Boucher
2026-04-29 07:44:49 -04:00
committed by GitHub
parent 0acf1de88c
commit c3a42d66f9
7 changed files with 51 additions and 792 deletions

View File

@@ -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.
</details>

File diff suppressed because one or more lines are too long

View File

@@ -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();

View File

@@ -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'));
});
});

View File

@@ -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-<cmd>) 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<objective>\nDo the thing.\n</objective>\n\n<process>\nStep 1.\nStep 2.\n</process>\n';
const input = [
'---',
'name: gsd:test',
'description: Test command',
'---',
body,
].join('');
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-test');
assert.ok(result.includes('<objective>'), 'objective tag preserved');
assert.ok(result.includes('Do the thing.'), 'body text preserved');
assert.ok(result.includes('<process>'), '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',
'---',
'',
'<objective>Quick task body</objective>',
].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('<objective>'), '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',
'---',
'',
'<objective>Review code</objective>',
].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');
});
});

View File

@@ -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');
});

View File

@@ -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'
);
});