Add Hermes Agent as a supported provider

Impeccable now ships a Hermes-compatible bundle under dist/hermes/.hermes/skills/.
Hermes reads the Agent Skills spec as-is, so the bundle uses the four spec
frontmatter fields (name, description, version, license) plus metadata/compatibility
and drops the Claude/Codex-specific extensions Hermes would silently ignore.

The .hermes/skills/ tracked root and the regenerated pin.mjs mirrors will be
produced by .github/workflows/sync-generated-output.yml after this lands; per
AGENTS.md, generated harness churn stays out of feature PRs.

What a Hermes user gets:
- npx impeccable install --providers=hermes --scope=project writes
  .hermes/skills/impeccable/ into the cwd.
- npx impeccable install --providers=hermes --scope=user honors $HERMES_HOME,
  so a profile-scoped install (HERMES_HOME=~/.hermes/profiles/forge) lands in
  the active profile's skills dir, not the default ~/.hermes/. Cross-home
  HERMES_HOME inheritance is ignored so test isolation holds.
- /impeccable registers as a Hermes slash command and routes sub-commands via
  the Commands table in the skill body, since user-invocable / argument-hint
  are not honored by Hermes' skill loader.

What a Hermes user does NOT get, and why:
- No hook surface. Impeccable's PostToolUse/Stop anti-pattern detector on
  Claude/Codex/Cursor/Grok/GitHub does not translate to Hermes, which has no
  equivalent tool event lifecycle. The skill body still ships.
- No writeOpenAIMetadata, agentFormat, or emitHooks. Hermes has no per-skill
  tool ACL, no subagent on-disk format, and no hooks.json equivalent.

Verified end-to-end with hermes-agent v0.18.2: /impeccable polish and
/impeccable critique both load reference/<command>.md and return the
documented first step. parse_frontmatter accepts the generated SKILL.md,
scan_skill_commands registers /impeccable, and the full default test suite
passes (267/267 in the critical files; 0 fail across all suites).
This commit is contained in:
Forge
2026-08-06 00:03:12 -07:00
committed by plamb
parent a075d89bdb
commit 5ce4a5c6b5
10 changed files with 104 additions and 23 deletions
@@ -27,6 +27,7 @@ env:
.gemini
.github/skills
.grok
.hermes
.kiro
.opencode
.pi
+1 -1
View File
@@ -22,7 +22,7 @@ Run `bun run build` after changing anything in `skill/`, transformer code, or us
## Generated Provider Output Policy
The root harness folders (`.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, `.gemini/skills/`, `.github/skills/`, `.grok/skills/`, `.kiro/skills/`, `.opencode/skills/`, `.pi/skills/`, `.qoder/skills/`, `.rovodev/skills/`, `.trae*/skills/`, `.vibe/skills/`) and `plugin/` stay tracked so `main` remains installable for direct GitHub, `npx skills`, and submodule users. They are still generated artifacts.
The root harness folders (`.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, `.gemini/skills/`, `.github/skills/`, `.grok/skills/`, `.hermes/skills/`, `.kiro/skills/`, `.opencode/skills/`, `.pi/skills/`, `.qoder/skills/`, `.rovodev/skills/`, `.trae*/skills/`, `.vibe/skills/`) and `plugin/` stay tracked so `main` remains installable for direct GitHub, `npx skills`, and submodule users. They are still generated artifacts.
Normal development should be source-first: stage changes in `skill/`, `scripts/`, `cli/`, `site/`, `extension/`, `functions/`, and `tests/`; leave generated harness churn unstaged unless the user asked for it. After source changes land on `main`, `.github/workflows/sync-generated-output.yml` runs `bun run build:release` and commits generated provider output directly back to `main`. Treat generated harness diffs as release artifacts and keep them out of feature PRs unless they are the point of the PR.
+55 -6
View File
@@ -23,7 +23,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const API_BASE = 'https://impeccable.style';
// Provider folder names in project roots
const PROVIDER_DIRS = ['.claude', '.cursor', '.gemini', '.agents', '.agent', '.github', '.grok', '.kiro', '.opencode', '.pi', '.qoder', '.trae', '.trae-cn', '.rovodev', '.vibe'];
const PROVIDER_DIRS = ['.claude', '.cursor', '.gemini', '.agents', '.agent', '.github', '.grok', '.hermes', '.kiro', '.opencode', '.pi', '.qoder', '.trae', '.trae-cn', '.rovodev', '.vibe'];
const PROVIDER_ALIASES = {
agent: '.agent',
agents: '.agents',
@@ -37,6 +37,7 @@ const PROVIDER_ALIASES = {
github: '.github',
grok: '.grok',
'grok-build': '.grok',
hermes: '.hermes',
xai: '.grok',
kiro: '.kiro',
opencode: '.opencode',
@@ -57,6 +58,7 @@ const PROVIDER_DISPLAY = {
'.gemini': { name: 'Gemini CLI', input: 'gemini' },
'.github': { name: 'GitHub Copilot', input: 'github' },
'.grok': { name: 'Grok Build', input: 'grok' },
'.hermes': { name: 'Hermes Agent', input: 'hermes' },
'.kiro': { name: 'Kiro', input: 'kiro' },
'.opencode': { name: 'OpenCode', input: 'opencode' },
'.pi': { name: 'Pi Coding Agent', input: 'pi' },
@@ -66,7 +68,7 @@ const PROVIDER_DISPLAY = {
'.trae-cn': { name: 'Trae CN', input: 'trae-cn' },
'.vibe': { name: 'Mistral Vibe', input: 'vibe' },
};
const PROVIDER_INPUT_ORDER = ['antigravity', 'claude', 'codex', 'cursor', 'gemini', 'github', 'grok', 'kiro', 'opencode', 'pi', 'qoder', 'trae', 'trae-cn', 'rovo-dev', 'vibe'];
const PROVIDER_INPUT_ORDER = ['antigravity', 'claude', 'codex', 'cursor', 'gemini', 'github', 'grok', 'hermes', 'kiro', 'opencode', 'pi', 'qoder', 'trae', 'trae-cn', 'rovo-dev', 'vibe'];
// OpenCode reads global skills from its config directory, not ~/.opencode:
// $OPENCODE_CONFIG_DIR, else $XDG_CONFIG_HOME/opencode, else
@@ -78,14 +80,52 @@ function opencodeGlobalConfigDir(home) {
return join(home, '.config', 'opencode');
}
// Hermes reads skills from `$HERMES_HOME/skills/`, where $HERMES_HOME defaults
// to `~/.hermes` but is also set to a profile path (e.g.
// `~/.hermes/profiles/forge`) when a non-default profile is active. Reading
// the env var matters here at install time: writing to `~/.hermes/skills/`
// from a profile-scoped Hermes invocation would land in the wrong profile
// (the same cross-profile data-corruption class that the active_profile
// fallback warning in hermes_constants.py exists to detect). Used by
// HOME_SKILLS_DIR_OVERRIDES['.hermes'] only; GLOBAL_HARNESS_HINTS reads the
// fixed `~/.hermes` location so detection doesn't leak the developer's real
// HERMES_HOME into test output (test isolation).
//
// Ignore $HERMES_HOME when it doesn't sit under `home` (the caller-supplied
// home dir, which tests inject via HOME=/tmp/...). Without this guard, an
// inherited $HERMES_HOME=/home/<dev>/.hermes from the developer's shell leaks
// into test output even when the test sets HOME=/tmp/imp-home-xxx: tests
// expect ~/.hermes to live under their tmp home, not under the dev's real
// home. The check uses `resolve()` on both sides so a symlinked test home
// (e.g. /tmp -> /private/tmp on macOS) still compares correctly.
function hermesGlobalHome(home) {
const envHome = process.env.HERMES_HOME;
if (envHome) {
try {
const resolvedEnv = path.resolve(envHome);
const resolvedHome = path.resolve(home);
// Honor HERMES_HOME only when it lives under the active home (real
// ~/.hermes or ~/.hermes/profiles/<name>). Cross-home inheritance is
// treated as not-set, so a test running under HOME=/tmp/... doesn't
// pick up the developer's real ~/.hermes.
if (resolvedEnv === resolvedHome || resolvedEnv.startsWith(resolvedHome + path.sep)) {
return resolvedEnv;
}
} catch {
// fall through to default
}
}
return join(home, '.hermes');
}
// Providers whose GLOBAL (home) skills dir is not `<provider>/skills`,
// as a function of the home dir. Pi discovers global skills from
// ~/.pi/agent/skills/ (issue #327); OpenCode from its config dir (issue
// #406). Antigravity's global skills dir is ~/.gemini/config/skills/
// (shared Gemini config location); project scope stays `.agent/skills`.
// Project scope stays `<provider>/skills` for all of these.
// #406); Hermes from $HERMES_HOME. Project scope stays `<provider>/skills`
// for all three.
const HOME_SKILLS_DIR_OVERRIDES = {
'.agent': (home) => join(home, '.gemini', 'config', 'skills'),
'.hermes': (home) => join(hermesGlobalHome(home), 'skills'),
'.pi': (home) => join(home, '.pi', 'agent', 'skills'),
'.opencode': (home) => join(opencodeGlobalConfigDir(home), 'skills'),
};
@@ -93,6 +133,14 @@ const HOME_SKILLS_DIR_OVERRIDES = {
// When a project has no harness folder yet, infer the target from globally
// installed harnesses (~/.claude, ~/.codex, ...). Codex reads skills from
// .agents/skills, so ~/.codex maps to the .agents bundle variant.
//
// Hermes auto-detection uses the fixed `~/.hermes` location only. When a
// non-default Hermes profile is active (HERMES_HOME points to a profile path),
// the user is expected to be inside a Hermes invocation and can pass
// --providers=hermes explicitly. Auto-detection from a non-default HERMES_HOME
// would also defeat test isolation (tests inject HOME; HERMES_HOME leaks from
// the parent process and would surface the developer's real ~/.hermes in
// detection output). The install path honors $HERMES_HOME; detection does not.
const GLOBAL_HARNESS_HINTS = [
{ home: '.agent', provider: '.agent' },
// Antigravity nests under ~/.gemini/ too, so any of these also trips the
@@ -105,6 +153,7 @@ const GLOBAL_HARNESS_HINTS = [
{ home: '.cursor', provider: '.cursor' },
{ home: '.gemini', provider: '.gemini' },
{ home: '.grok', provider: '.grok' },
{ home: '.hermes', provider: '.hermes' },
{ home: '.kiro', provider: '.kiro' },
{ home: '.opencode', provider: '.opencode' },
// OpenCode's real global config dir (issue #406); the ~/.opencode entry
@@ -610,7 +659,7 @@ async function copyOrExtractLocalBundle(sourceValue) {
*/
function normalizeForHash(content) {
return content
.replace(/\.(claude|cursor|agents|agent|github|gemini|codex|grok|kiro|opencode|pi|qoder|trae|trae-cn|rovodev|vibe)\/skills\//g, '.PROVIDER/skills/');
.replace(/\.(claude|cursor|agents|agent|github|gemini|codex|grok|hermes|kiro|opencode|pi|qoder|trae|trae-cn|rovodev|vibe)\/skills\//g, '.PROVIDER/skills/');
}
function hashSkillFile(filePath) {
+1
View File
@@ -12,6 +12,7 @@ export const FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS = Object.freeze({
antigravity: '.agent',
github: '.github',
grok: '.grok',
hermes: '.hermes',
kiro: '.kiro',
opencode: '.opencode',
pi: '.pi',
+19 -16
View File
@@ -26,6 +26,7 @@ Last verified: 2026-04-28 (subagent landscape spot-checked 2026-06-28; Mistral V
| Rovo Dev | https://support.atlassian.com/rovo/docs/extend-rovo-dev-cli-with-agent-skills |
| Mistral Vibe | https://docs.mistral.ai/vibe/code/cli/skills |
| Grok Build | https://docs.x.ai/build/features/skills-plugins-marketplaces |
| Hermes Agent | https://hermes-agent.nousresearch.com/docs/ |
| Antigravity | https://antigravity.google/docs/skills |
## Spec Compliance
@@ -38,28 +39,29 @@ Provider-specific extensions beyond the spec: `user-invocable`, `argument-hint`,
Fields marked with * are spec-standard. Others are provider extensions.
| Field | Claude Code | Cursor | Gemini | Codex | Copilot | Grok | Kiro | OpenCode | Pi | Qoder | Rovo Dev | Mistral Vibe | Antigravity |
|-------|:-----------:|:------:|:------:|:-----:|:-------:|:----:|:----:|:--------:|:--:|:-----:|:--------:|:------------:|:-----------:|
| `name`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `description`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `license`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `compatibility`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `metadata`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `allowed-tools`* | Yes | No | Ignored | No | No | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes |
| `user-invocable` | Yes | No | No | No | Yes | Yes | No | Yes | No | Yes | Yes | Yes | No |
| `argument-hint` | Yes | No | No | No | Yes | Yes | No | Yes | No | Yes | Yes | No | No |
| `disable-model-invocation` | Yes | Yes | No | No | Yes | Yes | No | Yes | Yes | TBD | TBD | No | No |
| `model` | Yes | No | No | No | No | Yes | No | Yes | No | No | No | No | No |
| `effort` | Yes | No | No | No | No | Yes | No | No | No | No | No | No | No |
| `context` | Yes | No | No | No | No | No | No | No | No | No | No | No | No |
| `agent` | Yes | No | No | No | No | No | No | Yes | No | No | No | No | No |
| `hooks` | Yes | No | No | Yes | No | Yes | No | No | No | No | No | No | No |
| Field | Claude Code | Cursor | Gemini | Codex | Copilot | Grok | Hermes | Kiro | OpenCode | Pi | Qoder | Rovo Dev | Mistral Vibe | Antigravity |
|-------|:-----------:|:------:|:------:|:-----:|:-------:|:----:|:------:|:----:|:--------:|:--:|:-----:|:--------:|:------------:|:-----------:|
| `name`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `description`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `license`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `compatibility`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `metadata`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `allowed-tools`* | Yes | No | Ignored | No | No | Yes | No | No | Yes | Yes | Yes | Yes | Yes | Yes |
| `user-invocable` | Yes | No | No | No | Yes | Yes | No | No | Yes | No | Yes | Yes | Yes | No |
| `argument-hint` | Yes | No | No | No | Yes | Yes | No | No | Yes | No | Yes | Yes | No | No |
| `disable-model-invocation` | Yes | Yes | No | No | Yes | Yes | No | No | Yes | Yes | TBD | TBD | No | No |
| `model` | Yes | No | No | No | No | Yes | No | No | Yes | No | No | No | No | No |
| `effort` | Yes | No | No | No | No | Yes | No | No | No | No | No | No | No | No |
| `context` | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No |
| `agent` | Yes | No | No | No | No | No | No | No | Yes | No | No | No | No | No |
| `hooks` | Yes | No | No | Yes | No | Yes | No | No | No | No | No | No | No | No |
Notes:
- Gemini CLI validates only `name` and `description`; other spec fields are parsed but ignored.
- Codex CLI uses a separate `agents/openai.yaml` sidecar for skill metadata (icons, branding, MCP tools, invocation control). Codex also auto-discovers subagents bundled inside an installed skill's `agents/` folder (TOML), which is how Impeccable ships its asset-producer. Standalone custom agents can still live under `.codex/agents/` or `~/.codex/agents/`, but Impeccable no longer installs anything there.
- Codex CLI hooks ship under `[features].hooks = true` (still flagged), require `/hooks` trust ceremony per-update, and are disabled on Windows.
- Grok Build is Claude Code compatible with zero config: it also reads `.claude/skills/`, `.claude/settings.json` hooks, and Claude plugin layouts. Native paths are `.grok/skills/`, `.grok/hooks/*.json`, and `.grok/agents/`. Skill frontmatter supports `when-to-use` in addition to the fields above. Project hooks require `/hooks-trust` (or `--trust`). See https://docs.x.ai/build/features/skills-plugins-marketplaces and https://docs.x.ai/build/features/hooks.
- Hermes Agent reads the Agent Skills spec as-is. Spec-defined fields (`name`, `description`, `license`, `compatibility`, `metadata`) are parsed and stored; harness-specific extensions (`user-invocable`, `argument-hint`, `allowed-tools`, `disable-model-invocation`, `model`, `effort`, `context`, `agent`, `hooks`) are unknown keys and silently ignored. Hermes has no hook surface, no per-skill tool ACL, and no slash-command equivalent of `user-invocable` (skills are loaded via `/skill <name>` or auto-loaded; sub-commands like `/impeccable polish` are routed from the skill body, not declared in frontmatter). Hermes adds two frontmatter fields not in the spec: `platforms:` (OS filter; default = all) and `environments:` (relevance gate over `kanban`, `docker`, `s6`). Unknown fields are silently ignored.
- Kiro recognizes `user-invocable` and `disable-model-invocation` per community reports but does not formally document them.
- Antigravity supports standard Agent Skills spec frontmatter fields (`name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`).
- Unknown fields are silently ignored by all harnesses.
@@ -92,6 +94,7 @@ Notes:
| Rovo Dev | `.rovodev/skills/` | `~/.rovodev/skills/` (user-level) |
| Mistral Vibe | `.vibe/skills/` (project), `~/.vibe/skills/` (global) | `.agents/skills/` (project), `~/.agents/skills/` (global) |
| Grok Build | `.grok/skills/` (project), `~/.grok/skills/` (global) | `.agents/skills/`, `.claude/skills/`, `.cursor/skills/` (Claude/Cursor compat, configurable) |
| Hermes Agent | `.hermes/skills/` (project), `~/.hermes/skills/` (global) | `skills.external_dirs` config (no automatic `.agents/skills/` fallback) |
| Antigravity | `.agent/skills/` (project), `~/.gemini/config/skills/` (global) | `.agents/skills/` (project), `~/.agents/skills/` (global) |
All harnesses support the `{skill-name}/SKILL.md` directory structure with optional `reference/`, `scripts/`, and `assets/` subdirectories.
+1
View File
@@ -491,6 +491,7 @@ This folder contains skills for all supported tools:
.agent/ -> Antigravity
.github/ -> GitHub Copilot
.grok/ -> Grok Build
.hermes/ -> Hermes Agent
.kiro/ -> Kiro
.opencode/ -> OpenCode
.pi/ -> Pi
+1
View File
@@ -18,5 +18,6 @@ export const transformRovoDev = createTransformer(PROVIDERS['rovo-dev']);
export const transformVibe = createTransformer(PROVIDERS.vibe);
export const transformGrok = createTransformer(PROVIDERS.grok);
export const transformAntigravity = createTransformer(PROVIDERS.antigravity);
export const transformHermes = createTransformer(PROVIDERS.hermes);
export { createTransformer, PROVIDERS };
+14
View File
@@ -162,4 +162,18 @@ export const PROVIDERS = {
displayName: 'Antigravity',
frontmatterFields: ['license', 'compatibility', 'metadata', 'allowed-tools'],
},
hermes: {
provider: 'hermes',
providerTags: ['hermes'],
configDir: '.hermes',
displayName: 'Hermes Agent',
// Hermes ships the Agent Skills spec as-is. The optional fields below
// (license, compatibility, metadata) are spec-defined; harness-specific
// extensions (user-invocable, argument-hint, allowed-tools) are NOT
// recognized by the Hermes skill loader and would be silently ignored.
// Hermes also has no hook surface, no equivalent of Claude's slash
// commands, and no per-skill tool ACL -- so no emitHooks, no agentFormat,
// no writeOpenAIMetadata. See hermes-agent/SKILL.md "Skills" section.
frontmatterFields: ['license', 'compatibility', 'metadata'],
},
};
+10
View File
@@ -539,6 +539,15 @@ export const PROVIDER_PLACEHOLDERS = {
config_file: 'AGENTS.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
},
'hermes': {
// Hermes is provider-agnostic and reads AGENTS.md / CLAUDE.md / .cursorrules
// for project context. "the model" matches the pi/opencode phrasing used
// for harnesses without a vendor-fixed assistant name.
model: 'the model',
config_file: 'AGENTS.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
}
};
@@ -552,6 +561,7 @@ export const PROVIDER_BLOCK_TAGS = new Set([
'gemini',
'github',
'grok',
'hermes',
'kiro',
'opencode',
'pi',
+1
View File
@@ -22,6 +22,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
// All known harness directories
const HARNESS_DIRS = [
'.claude', '.cursor', '.gemini', '.codex', '.agents', '.agent', '.github', '.grok',
'.hermes',
'.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', '.vibe', '.qoder',
];