Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/skill-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ jobs:
echo "Generated Factory SKILL.md files are stale. Run: bun run gen:skill-docs --host factory"
exit 1
}
- name: Generate Hermes skill docs
run: bun run gen:skill-docs --host hermes
- name: Verify Hermes skill docs are fresh
run: |
git diff --exit-code -- .hermes/ || {
echo "Generated Hermes SKILL.md files are stale. Run: bun run gen:skill-docs --host hermes"
exit 1
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ bin/gstack-global-discover
.slate/
.cursor/
.openclaw/
.hermes/
.context/
extension/.auth.json
.gstack-worktrees/
Expand Down
78 changes: 78 additions & 0 deletions hosts/hermes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { HostConfig } from '../scripts/host-config';

const hermes: HostConfig = {
name: 'hermes',
displayName: 'Hermes',
cliCommand: 'hermes',
cliAliases: [],

globalRoot: '.hermes/skills/gstack',
localSkillRoot: '.hermes/skills/gstack',
hostSubdir: '.hermes',
usesEnvVars: true,

frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: 1024,
descriptionLimitBehavior: 'error',
extraFields: {
version: '0.15.13.0',
},
},

generation: {
generateMetadata: false,
skipSkills: ['codex'],
includeSkills: [],
},

pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '~/.hermes/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.hermes/skills/gstack' },
{ from: '.claude/skills', to: '.hermes/skills' },
{ from: 'CLAUDE.md', to: 'HERMES.md' },
],
toolRewrites: {
'use the Bash tool': 'use the terminal tool',
'use the Read tool': 'use the read_file tool',
'use the Write tool': 'use the write_file tool',
'use the Edit tool': 'use the patch tool',
'use the Grep tool': 'use search_files with a regex pattern',
'use the Glob tool': 'use search_files to find files matching',
'use the Agent tool': 'use delegate_task',
'the Bash tool': 'the terminal tool',
'the Read tool': 'the read_file tool',
'the Write tool': 'the write_file tool',
'the Edit tool': 'the patch tool',
'WebSearch': 'web_search',
},

// Suppress Claude-specific preamble sections that don't apply to Hermes
suppressedResolvers: [
'DESIGN_OUTSIDE_VOICES',
'ADVERSARIAL_STEP',
'CODEX_SECOND_OPINION',
'CODEX_PLAN_REVIEW',
'REVIEW_ARMY',
],

runtimeRoot: {
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'],
globalFiles: {
'review': ['checklist.md', 'TODOS-format.md'],
},
},

install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
},

coAuthorTrailer: 'Co-Authored-By: Hermes Agent <agent@nousresearch.com>',
learningsMode: 'basic',

adapter: './scripts/host-adapters/hermes-adapter',
};

export default hermes;
5 changes: 3 additions & 2 deletions hosts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import opencode from './opencode';
import slate from './slate';
import cursor from './cursor';
import openclaw from './openclaw';
import hermes from './hermes';

/** All registered host configs. Add new hosts here. */
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw];
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes];

/** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
Expand Down Expand Up @@ -63,4 +64,4 @@ export function getExternalHosts(): HostConfig[] {
}

// Re-export individual configs for direct import
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw };
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes };
7 changes: 7 additions & 0 deletions scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,13 @@ function processExternalHost(
}
}

// Run host adapter for semantic transforms (after generic rewrites)
if (hostConfig.adapter) {
const adapterPath = path.resolve(ROOT, hostConfig.adapter);
const adapter = require(adapterPath);
result = adapter.transform(result, hostConfig);
}

// Config-driven: generate metadata (e.g., openai.yaml for Codex)
if (hostConfig.generation.generateMetadata && !symlinkLoop) {
const agentsDir = path.join(outputDir, 'agents');
Expand Down
63 changes: 63 additions & 0 deletions scripts/host-adapters/hermes-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Hermes host adapter — post-processing content transformer.
*
* Runs AFTER generic frontmatter/path/tool rewrites from the config system.
* Handles semantic transformations that string-replace can't cover:
*
* 1. AskUserQuestion → clarify (Hermes built-in tool)
* 2. Agent spawning → delegate_task patterns
* 3. Browse binary patterns ($B → terminal tool)
* 4. Learnings binary calls → memory tool
* 5. skill_manage hint footer
* 6. SOUL.md awareness
*
* Interface: transform(content, config) → transformed content
*/

import type { HostConfig } from '../host-config';

/**
* Transform generated SKILL.md content for Hermes compatibility.
* Called after all generic rewrites (paths, tools, frontmatter) have been applied.
*/
export function transform(content: string, _config: HostConfig): string {
let result = content;

// 1. AskUserQuestion references → clarify
result = result.replaceAll('AskUserQuestion', 'clarify');
result = result.replaceAll('Use AskUserQuestion', 'Use clarify');
result = result.replaceAll('use AskUserQuestion', 'use clarify');

// 2. Agent tool references → delegate_task (catch remaining patterns)
result = result.replaceAll('the Agent tool', 'delegate_task');
result = result.replaceAll('Agent tool', 'delegate_task');
result = result.replaceAll('subagent_type', 'task description');

// 3. Browse binary patterns → terminal tool invocation
result = result.replaceAll('`$B ', '`terminal $B ');

// 4. Learnings binary calls → memory tool
result = result.replace(
/~\/\.hermes\/skills\/gstack\/bin\/gstack-learnings-log\s+'([^']+)'/g,
'Use the memory tool to save: $1',
);
result = result.replace(
/~\/\.hermes\/skills\/gstack\/bin\/gstack-learnings-search/g,
'Use the memory tool to search for relevant learnings',
);

// 5. SOUL.md awareness — inject note when persona/voice config is referenced
if (result.includes('persona') || result.includes('voice configuration')) {
result = result.replace(
/^(# .+)$/m,
'$1\n\n> Voice and persona are configured via SOUL.md (~/.hermes/SOUL.md).',
);
}

// 6. skill_manage hint — add footer to generated skills
if (!result.includes('skill_manage')) {
result = result.trimEnd() + '\n\n---\n\n> If you find outdated steps in this skill, use skill_manage(action=\'patch\') to fix them.\n';
}

return result;
}
39 changes: 36 additions & 3 deletions test/host-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
slate,
cursor,
openclaw,
hermes,
} from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types';

Expand All @@ -30,8 +31,8 @@ const ROOT = path.resolve(import.meta.dir, '..');
// ─── hosts/index.ts ─────────────────────────────────────────

describe('hosts/index.ts', () => {
test('ALL_HOST_CONFIGS has 8 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(8);
test('ALL_HOST_CONFIGS has 9 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(9);
});

test('ALL_HOST_NAMES matches config names', () => {
Expand Down Expand Up @@ -493,12 +494,44 @@ describe('host config correctness', () => {
expect(openclaw.generation.includeSkills!.length).toBe(0);
});

test('hermes has tool rewrites for terminal/read_file/write_file', () => {
expect(hermes.toolRewrites).toBeDefined();
expect(hermes.toolRewrites!['use the Bash tool']).toBe('use the terminal tool');
expect(hermes.toolRewrites!['use the Read tool']).toBe('use the read_file tool');
expect(hermes.toolRewrites!['use the Edit tool']).toBe('use the patch tool');
});

test('hermes has CLAUDE.md→HERMES.md path rewrite', () => {
expect(hermes.pathRewrites.some(r => r.from === 'CLAUDE.md' && r.to === 'HERMES.md')).toBe(true);
});

test('hermes has adapter path', () => {
expect(hermes.adapter).toBeDefined();
expect(hermes.adapter).toContain('hermes-adapter');
});

test('hermes has description limit for agentskills.io', () => {
expect(hermes.frontmatter.descriptionLimit).toBe(1024);
expect(hermes.frontmatter.descriptionLimitBehavior).toBe('error');
});

test('hermes has agentskills.io version field', () => {
expect(hermes.frontmatter.extraFields).toBeDefined();
expect(hermes.frontmatter.extraFields!.version).toBeDefined();
});

test('hermes includeSkills is empty (native skills separate from generated)', () => {
expect(hermes.generation.includeSkills).toBeDefined();
expect(hermes.generation.includeSkills!.length).toBe(0);
});

test('every host has coAuthorTrailer or undefined', () => {
// Claude, Codex, Factory, OpenClaw have explicit trailers
// Claude, Codex, Factory, OpenClaw, Hermes have explicit trailers
expect(claude.coAuthorTrailer).toContain('Claude');
expect(codex.coAuthorTrailer).toContain('Codex');
expect(factory.coAuthorTrailer).toContain('Factory');
expect(openclaw.coAuthorTrailer).toContain('OpenClaw');
expect(hermes.coAuthorTrailer).toContain('Hermes');
});

test('every external host skips the codex skill', () => {
Expand Down