fix(skills): honor HERMES_HOME when discovering Hermes skills

A relocated Hermes profile (hermes -p <profile>) moves the whole tree, so the default ~/.hermes path discovered nothing.
This commit is contained in:
Neil
2026-08-21 23:54:06 -07:00
parent 6da33fafab
commit 6985fa38ba
4 changed files with 63 additions and 9 deletions
+33
View File
@@ -53,6 +53,7 @@ beforeEach(() => {
afterEach(() => {
unavailableRootPath = null
vi.unstubAllEnvs()
vi.restoreAllMocks()
})
@@ -509,6 +510,38 @@ describe('skill discovery', () => {
})
})
it('discovers Hermes Skills under a relocated HERMES_HOME profile', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skills-'))
const home = join(root, 'home')
const hermesHome = join(root, 'profiles', 'coder')
const skillDir = join(hermesHome, 'skills', 'research')
await mkdir(skillDir, { recursive: true })
await writeFile(
join(skillDir, 'SKILL.md'),
['---', 'name: profile-research', 'description: Research sources.', '---', ''].join('\n')
)
vi.stubEnv('HERMES_HOME', hermesHome)
const result = await discoverSkills({
homeDir: home,
cwd: join(root, 'missing-cwd')
})
expect(result.skills).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'profile-research',
sourceLabel: 'Hermes home',
directoryPath: skillDir
})
])
)
expect(result.sources.find((source) => source.id === 'home-hermes')).toMatchObject({
path: join(hermesHome, 'skills'),
exists: true
})
})
it('discovers worktree .agents skill symlinks from the requested cwd', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skills-'))
const home = join(root, 'home')
+11 -9
View File
@@ -11,7 +11,10 @@ import type { AgentType } from '../../shared/agent-status-types'
import type { Repo } from '../../shared/repo-types'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { SkillProviderRootOverrides } from './skill-provider-destinations'
import { resolveEnvironmentSkillProviderRoots } from './skill-provider-runtime-roots'
import {
resolveEnvironmentHermesSkillsRoot,
resolveEnvironmentSkillProviderRoots
} from './skill-provider-runtime-roots'
export type SkillScanRoot = Omit<SkillDiscoverySource, 'exists' | 'skippedReason'>
type SkillDiscoveryPathApi = Pick<typeof posix, 'basename' | 'join'>
@@ -77,6 +80,12 @@ export function buildSkillDiscoverySources(
const cwd = args.cwd ?? process.cwd()
const providerRootOverrides =
args.providerRootOverrides ?? (args.pathApi ? {} : resolveEnvironmentSkillProviderRoots())
// Why: HERMES_HOME moves the whole profile tree, so the default home path
// finds nothing for `hermes -p <profile>`. Only this process's own host can
// read it — a custom pathApi means the home belongs to another host.
const hermesSkillsRoot =
(args.pathApi ? null : resolveEnvironmentHermesSkillsRoot()) ??
pathApi.join(home, '.hermes', 'skills')
const roots: SkillScanRoot[] = [
source(
'home-codex',
@@ -144,14 +153,7 @@ export function buildSkillDiscoverySources(
['agent-skills'],
'omp'
),
source(
'home-hermes',
'Hermes home',
pathApi.join(home, '.hermes', 'skills'),
'home',
['agent-skills'],
'hermes'
),
source('home-hermes', 'Hermes home', hermesSkillsRoot, 'home', ['agent-skills'], 'hermes'),
source(
'home-prime-agent',
'Prime Agent home',
@@ -1,6 +1,7 @@
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
resolveEnvironmentHermesSkillsRoot,
resolveEnvironmentSkillProviderRoots,
resolveWslGrokSkillProviderRoot,
withClaudeSkillProviderRoot
@@ -30,6 +31,14 @@ describe('skill provider runtime roots', () => {
})
})
it('maps a relocated HERMES_HOME to its skill root and ignores relative ones', () => {
expect(resolveEnvironmentHermesSkillsRoot({ HERMES_HOME: join('/srv', 'hermes') })).toBe(
join('/srv', 'hermes', 'skills')
)
expect(resolveEnvironmentHermesSkillsRoot({ HERMES_HOME: '../hermes' })).toBeNull()
expect(resolveEnvironmentHermesSkillsRoot({})).toBeNull()
})
it('maps the WSL login shell GROK_HOME to a host-readable skill root', async () => {
await expect(
resolveWslGrokSkillProviderRoot('Ubuntu-24.04', async () => '/srv/grok\n')
@@ -37,6 +37,16 @@ export function resolveEnvironmentSkillProviderRoots(
}
}
// Why: `HERMES_HOME` relocates a whole Hermes profile tree (`hermes -p coder`),
// and the rest of Orca already treats it as authoritative. Hermes is not an
// install provider, so it stays out of `SkillProviderRootOverrides`.
export function resolveEnvironmentHermesSkillsRoot(
env: NodeJS.ProcessEnv = process.env
): string | null {
const hermesHome = normalizedRoot(env.HERMES_HOME)
return hermesHome ? join(hermesHome, 'skills') : null
}
export function withClaudeSkillProviderRoot(
roots: SkillProviderRootOverrides,
claudeConfigDirectory: string | null | undefined