Skip to content

feat: add Hermes Agent support with global skill installation#1350

Open
dev-v wants to merge 3 commits into
Fission-AI:mainfrom
dev-v:add-hermes-agent-support
Open

feat: add Hermes Agent support with global skill installation#1350
dev-v wants to merge 3 commits into
Fission-AI:mainfrom
dev-v:add-hermes-agent-support

Conversation

@dev-v

@dev-v dev-v commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Adds Hermes Agent (by Nous Research) as a supported tool, enabling openspec init --tools hermes and openspec update to install OpenSpec skills where Hermes discovers them.

Unlike all currently supported tools, Hermes discovers skills from a user-global directory (~/.hermes/skills/) rather than a project-local path. OpenSpec's skillsDir field served double duty (detection marker + installation target), which breaks for global-scope tools. This PR introduces a generic installDir field to separate the two concerns.

Approach

Pattern inspired by SpecKit's Hermes integration, which uses a detect_dir / dir separation for the same problem. This PR adapts that pattern to OpenSpec's architecture:

Concept SpecKit OpenSpec (this PR)
Detection path detect_dir (project-local) skillsDir (project-local, unchanged)
Installation path dir (global) installDir (new field, global)
Marker directory empty .hermes/skills/ empty .hermes/skills/

installDir is a generic mechanism — not Hermes-specific. Any future agent with global skill discovery can use it by setting the field.

What Changes

  • AIToolOption.installDir — new optional field; when set, skills install to the expanded path (supports ~) instead of <projectRoot>/<skillsDir>/skills
  • Hermes Agent added to AI_TOOLS: skillsDir: '.hermes', installDir: '~/.hermes/skills'
  • resolveSkillsDir() / resolveMarkerDir() — shared helpers in tool-detection.ts, replacing inline path joins across init, update, profile-sync-drift, and migration
  • getToolSkillStatus — for installDir tools, uses marker directory (not global path) to determine "configured", preventing global skills from polluting project-level detection
  • init / update — create project-local marker directory (.hermes/skills/) for installDir tools while writing skills to the global path
  • No command adapter — Hermes invokes skills via /skill:openspec-*, matching the Kimi/ForgeCode/Vibe pattern

Testing

  • pnpm run build ✅ (TypeScript compilation)
  • pnpm run lint ✅ (0 errors)
  • pnpm test ✅ (1887/1887 passed)

New test coverage:

  • test/core/available-tools.test.ts — Hermes detection via .hermes/ directory
  • test/core/shared/tool-detection.test.tsresolveSkillsDir, resolveMarkerDir, Hermes getToolSkillStatus/getToolVersionStatus with marker-based configured check

OpenSpec Change Artifacts

This PR includes a complete OpenSpec change proposal at openspec/changes/add-hermes-agent-support/:

  • proposal.md — motivation and impact
  • design.md — 5 design decisions with rationale and alternatives
  • specs/ai-tool-paths/spec.mdinstallDir field, Hermes paths, helper requirements
  • specs/cli-init/spec.md — global-install tool init behavior
  • specs/cli-update/spec.md — global-install tool update behavior
  • tasks.md — implementation task breakdown

openspec validate add-hermes-agent-support passes ✅

Related

Summary by CodeRabbit

  • New Features
    • Added Hermes Agent support with global skill installation under ~/.hermes/skills/.
    • Setup, update, drift checks, and migration now use a project-local marker for Hermes detection and manage global skills without generating command files.
    • Introduced install-vs-marker path handling via a new tool configuration option (installDir) to keep Hermes global skills isolated per project.
  • Bug Fixes
    • Prevented accidental deletion of globally installed Hermes skills during updates and profile/delivery changes.
  • Documentation
    • Updated supported-tools guidance with Hermes-specific installation and usage notes.
  • Tests
    • Added regression coverage for Hermes detection, migration, path resolution, and global-skill deletion safety.

Hermes Agent discovers skills from ~/.hermes/skills/ (user-global)
rather than a project-local directory. This introduces a generic
installDir field on AIToolOption to separate installation path from
detection path, enabling Hermes and any future global-scope agent.

Key changes:
- Add installDir optional field to AIToolOption (config.ts)
- Add Hermes Agent to AI_TOOLS with skillsDir='.hermes',
  installDir='~/.hermes/skills'
- Add resolveSkillsDir() and resolveMarkerDir() shared helpers
- getToolSkillStatus uses marker directory for installDir tools
  to prevent global skills from polluting project-level detection
- init/update create project-local marker directory for
  installDir tools while writing skills to the global path
- No command adapter — Hermes invokes skills via /skill:openspec-*
- Includes OpenSpec change artifacts (proposal, design, specs, tasks)

Tested: build ✅ | lint ✅ (0 errors) | test ✅ (1887/1887 passed)

Pattern inspired by SpecKit's detect_dir/dir separation for Hermes
Agent integration.
@dev-v dev-v requested a review from TabishB as a code owner July 11, 2026 13:12
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66679570-ec64-436c-a55e-696ea55cd67f

📥 Commits

Reviewing files that changed from the base of the PR and between a2c3e56 and 488cf28.

📒 Files selected for processing (8)
  • openspec/changes/add-hermes-agent-support/design.md
  • openspec/changes/add-hermes-agent-support/proposal.md
  • openspec/changes/add-hermes-agent-support/specs/cli-init/spec.md
  • openspec/changes/add-hermes-agent-support/specs/cli-update/spec.md
  • openspec/changes/add-hermes-agent-support/tasks.md
  • src/core/init.ts
  • src/core/update.ts
  • test/core/update.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • openspec/changes/add-hermes-agent-support/proposal.md
  • openspec/changes/add-hermes-agent-support/tasks.md
  • src/core/update.ts

📝 Walkthrough

Walkthrough

Hermes Agent support adds global skill installation at ~/.hermes/skills, project-local marker directories for detection, shared path-resolution helpers, lifecycle integration, documentation, and test coverage.

Changes

Hermes Agent support

Layer / File(s) Summary
Support contracts and tool registration
openspec/changes/add-hermes-agent-support/..., src/core/config.ts
Defines installDir, Hermes marker behavior, adapterless command handling, and registers Hermes with ~/.hermes/skills.
Path resolution and status detection
src/core/shared/..., test/core/shared/tool-detection.test.ts
Adds shared installation and marker path helpers and updates skill/version status logic for globally installed tools.
Initialization, update, and tracking integration
src/core/init.ts, src/core/update.ts, src/core/migration.ts, src/core/profile-sync-drift.ts
Uses resolved installation paths, creates project-local markers, preserves global skill directories, and updates migration and drift tracking.
User documentation and validation
docs/supported-tools.md, test/core/...
Documents Hermes installation and invocation behavior and verifies discovery, migration isolation, status handling, and deletion safety.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant InitOrUpdate
  participant PathResolvers
  participant HermesSkills
  participant ProjectMarker
  User->>InitOrUpdate: initialize or update Hermes
  InitOrUpdate->>PathResolvers: resolve install and marker directories
  PathResolvers-->>InitOrUpdate: global install path and project marker path
  InitOrUpdate->>HermesSkills: generate skills globally
  InitOrUpdate->>ProjectMarker: create marker directory
  ProjectMarker-->>User: Hermes configuration detected for project
Loading

Possibly related PRs

Suggested reviewers: tabishb

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Hermes Agent support with global skill installation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/core/available-tools.test.ts (1)

181-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test correctly validates Hermes detection; consider also asserting installDir.

The test properly creates .hermes, verifies detection, and checks value, name, and skillsDir. Since this PR introduces the installDir field, adding an assertion for it would guard against accidental removal of the global installation path configuration.

💡 Suggested addition
       expect(hermesTool?.skillsDir).toBe('.hermes');
+      expect(hermesTool?.installDir).toBe('~/.hermes/skills');
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/available-tools.test.ts` around lines 181 - 195, Extend the Hermes
detection test around getAvailableTools to assert that the detected hermesTool
also has the expected global installDir value introduced by this change. Keep
the existing value, name, and skillsDir assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/supported-tools.md`:
- Around line 62-63: Update the Hermes Agent footnote to state that `.hermes` is
the project-local marker directory used for auto-detection, while
`.hermes/skills/` may be created as a subdirectory during initialization; do not
describe `.hermes/skills/` itself as the detection directory.

In `@test/core/shared/tool-detection.test.ts`:
- Around line 383-422: Make the Hermes global-install tests deterministic by
mocking or injecting the global install root used by getToolSkillStatus and
getToolVersionStatus, then create controlled SKILL.md fixtures and assert exact
skill counts instead of accepting environment-dependent values. Extend the
getToolVersionStatus tests to verify generatedByVersion and needsUpdate for
matching and differing versions, while preserving the existing marker-directory
setup. Run the focused test with pnpm exec vitest run
test/core/shared/tool-detection.test.ts.

---

Nitpick comments:
In `@test/core/available-tools.test.ts`:
- Around line 181-195: Extend the Hermes detection test around getAvailableTools
to assert that the detected hermesTool also has the expected global installDir
value introduced by this change. Keep the existing value, name, and skillsDir
assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fab203cf-e81f-4ca3-848a-cb3095548e4e

📥 Commits

Reviewing files that changed from the base of the PR and between 0a99f41 and 837bc4a.

📒 Files selected for processing (18)
  • docs/supported-tools.md
  • openspec/changes/add-hermes-agent-support/.openspec.yaml
  • openspec/changes/add-hermes-agent-support/README.md
  • openspec/changes/add-hermes-agent-support/design.md
  • openspec/changes/add-hermes-agent-support/proposal.md
  • openspec/changes/add-hermes-agent-support/specs/ai-tool-paths/spec.md
  • openspec/changes/add-hermes-agent-support/specs/cli-init/spec.md
  • openspec/changes/add-hermes-agent-support/specs/cli-update/spec.md
  • openspec/changes/add-hermes-agent-support/tasks.md
  • src/core/config.ts
  • src/core/init.ts
  • src/core/migration.ts
  • src/core/profile-sync-drift.ts
  • src/core/shared/index.ts
  • src/core/shared/tool-detection.ts
  • src/core/update.ts
  • test/core/available-tools.test.ts
  • test/core/shared/tool-detection.test.ts

Comment thread docs/supported-tools.md Outdated
Comment thread test/core/shared/tool-detection.test.ts Outdated

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is close, but I think the migration/drift path still leaks global Hermes state into project-local decisions.

scanInstalledWorkflowArtifacts() now reads resolveSkillsDir() for Hermes, which is ~/.hermes/skills, without first checking the project-local marker. During one-time profile migration, a project that merely has .hermes/ detected can inherit workflows and delivery=skills from another project global Hermes install. That violates the PR core invariant that global skills must not make a project look configured. Please gate global-install tools on resolveMarkerDir() in migration/drift scanning, and add a regression with global Hermes skills present but no project marker.

Address PR Fission-AI#1350 review feedback:

- migration.ts: scanInstalledWorkflowArtifacts() now checks the project-local
  marker directory before scanning the global skills directory for
  installDir tools (e.g. Hermes), preventing cross-project state leakage
- migration.test.ts: add regression tests for global skills with and without
  project-local marker
- tool-detection.test.ts: make Hermes tests deterministic by mocking
  os.homedir(), assert exact skillCount and version behavior
- available-tools.test.ts: assert installDir field on Hermes detection
- supported-tools.md: clarify .hermes (detection) vs .hermes/skills/ (marker)
@dev-v

dev-v commented Jul 12, 2026

Copy link
Copy Markdown
Author

@alfred-openspec Good catch — you're right, the migration path was violating the PR's own invariant. Fixed in a2c3e56.

Root cause: scanInstalledWorkflowArtifacts() received tools from getAvailableTools() (which checks .hermes/) and then unconditionally called resolveSkillsDir() for Hermes, reading the global ~/.hermes/skills/ without first checking the project-local marker. A project with merely .hermes/ present would inherit workflows + delivery=skills from another project's global install.

Fix: Added a marker gate in scanInstalledWorkflowArtifacts() — for installDir tools, it now checks resolveMarkerDir() (project-local .hermes/skills/) before scanning the global skills directory. This aligns the migration path with the same marker-gating pattern already used in getToolSkillStatus().

if (tool.installDir) {
  const markerDir = resolveMarkerDir(tool, projectPath);
  if (!fs.existsSync(markerDir)) continue;
}

Regression test (migration.test.tsHermes global-install isolation):

  • Global ~/.hermes/skills/ has 2 SKILL.md fixtures + project has .hermes/ but no marker → asserts workflows === [] and no migration writes to config.
  • Same global skills + marker present → asserts correct workflow detection and migration.

Also addressed the CodeRabbit comments in the same commit:

  • tool-detection.test.ts: tests now mock os.homedir() to a temp dir with controlled SKILL.md fixtures, asserting exact skillCount and generatedByVersion/needsUpdate for matching, differing, and missing versions.
  • docs/supported-tools.md: footnote now distinguishes .hermes (detection directory) from .hermes/skills/ (marker created during init).
  • available-tools.test.ts: added installDir assertion.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/core/shared/tool-detection.test.ts (1)

376-506: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hermes global-install tests are now deterministic and well-structured.

The past review concern about non-deterministic Hermes tests has been fully addressed: os.homedir() is mocked to a temp directory, SKILL.md fixtures are created with controlled generatedBy versions, exact skillCount values are asserted, and generatedByVersion/needsUpdate are verified for matching, differing, and missing version scenarios.

One optional improvement: the createGlobalSkill helper and the beforeEach/afterEach setup are duplicated across the getToolSkillStatus and getToolVersionStatus describe blocks (lines 396-403 / 462-469 and 379-390 / 451-460). Extracting these into a shared helper or a parent describe block would reduce duplication if either suite's setup logic needs to change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/shared/tool-detection.test.ts` around lines 376 - 506, Optionally
consolidate the duplicated Hermes test setup shared by getToolSkillStatus and
getToolVersionStatus: move the fake-home beforeEach/afterEach lifecycle and
createGlobalSkill helper into a common parent describe or shared helper.
Preserve the existing isolated temporary home behavior and all test assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/core/shared/tool-detection.test.ts`:
- Around line 376-506: Optionally consolidate the duplicated Hermes test setup
shared by getToolSkillStatus and getToolVersionStatus: move the fake-home
beforeEach/afterEach lifecycle and createGlobalSkill helper into a common parent
describe or shared helper. Preserve the existing isolated temporary home
behavior and all test assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 339df69e-a428-4b9d-8316-de0fbda4f564

📥 Commits

Reviewing files that changed from the base of the PR and between 837bc4a and a2c3e56.

📒 Files selected for processing (5)
  • docs/supported-tools.md
  • src/core/migration.ts
  • test/core/available-tools.test.ts
  • test/core/migration.test.ts
  • test/core/shared/tool-detection.test.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/supported-tools.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/core/available-tools.test.ts
  • src/core/migration.ts

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the follow-up commit. The migration marker gate looks like the right direction, but I still see a blocker in update cleanup: Hermes resolves skillsDir to ~/.hermes/skills, so removeUnselectedSkillDirs/removeSkillDirs can delete user-global OpenSpec skills from a project-local update.\n\nConcrete case: Project A installs Hermes with a custom profile, Project B has a core-profile Hermes marker, then running openspec update in Project B prunes non-core workflow directories out of ~/.hermes/skills and breaks Project A. The commands-only path has the same global-delete issue, and Hermes has no command adapter to replace those skills.\n\nI think global-install tools should be additive/overwrite-only during update, or cleanup should only touch project-local marker state. Please add a Hermes regression for custom-to-core update and delivery=commands so a project-local update cannot prune ~/.hermes/skills.

Global-install tools (installDir set, e.g. Hermes) must not delete skill
directories from the shared global path during per-project update or init.
A project-local profile or delivery change would otherwise destroy skills
that other projects on the machine depend on. Hermes has no command adapter,
so delivery=commands cannot substitute skills with commands — deletion
leaves the user with nothing.

Added installDir guards on all skill-deletion call sites:
- update.ts: removeUnselectedSkillDirs (profile change)
- update.ts: removeSkillDirs (delivery=commands)
- init.ts: removeSkillDirs (delivery=commands)

Global-install tools are now additive/overwrite-only: existing skills are
overwritten, new skills are created, but no skill directories are deleted
from the global path. Project-local tools are unaffected.

Added regression tests:
- custom→core profile switch preserves non-core skill dirs in ~/.hermes/skills/
- delivery=commands preserves skill dirs in ~/.hermes/skills/

Addresses second round of CHANGES_REQUESTED from @alfred-openspec on PR Fission-AI#1350.
@dev-v

dev-v commented Jul 14, 2026

Copy link
Copy Markdown
Author

@alfred-openspec You're right again — the cleanup path was the remaining hole. Fixed in 488cf28.

Root cause: removeUnselectedSkillDirs and removeSkillDirs in update.ts (and removeSkillDirs in init.ts) operated on whatever resolveSkillsDir() returned — for Hermes that's ~/.hermes/skills/, the shared global directory. A per-project profile or delivery change would prune global skill directories that other projects on the machine depend on.

Fix: Added installDir guards on all skill-deletion call sites:

// update.ts — inside the per-tool update loop

// Skills delivery: skip deselected-workflow cleanup for global-install tools
if (!tool.installDir) {
  removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, desiredWorkflows);
}

// Commands-only delivery: skip all-skills cleanup for global-install tools
if (!shouldGenerateSkills && !tool.installDir) {
  removedSkillCount += await this.removeSkillDirs(skillsDir);
}
// init.ts — same guard on the commands-only path
if (!shouldGenerateSkills && !toolConfig?.installDir) {
  removedSkillCount += await this.removeSkillDirs(skillsDir);
}

Global-install tools are now additive/overwrite-only during update and init:

  • Overwrite existing skill files with new version content ✓
  • Create newly-selected skill directories ✓
  • Create/maintain the project-local marker directory ✓
  • Delete skill directories from the global path ✗ (skipped)

For project-local tools (no installDir), deletion behavior is unchanged.

Design rationale: Global skills are shared across all projects on the machine. A per-project profile or delivery change must not delete skills that other projects depend on. Hermes has no command adapter, so delivery=commands cannot substitute skills with commands — deletion would leave the user with nothing. Residual skill directories not in the current profile are harmless: Hermes displays them as available and their content remains valid.

Regression tests (update.test.tsglobal-install skill deletion safety):

  1. should not remove non-profile skill directories from global path on profile change — Sets up Hermes with marker + global skills for both core (openspec-explore) and non-core (openspec-propose) workflows. Runs openspec update --force with core profile. Asserts openspec-propose/SKILL.md still exists in ~/.hermes/skills/. Also asserts core skill was overwritten with new content.

  2. should not delete global skills when delivery is commands-only — Sets up Hermes with marker + global skill. Runs openspec update --force with delivery=commands. Asserts skill file still exists in ~/.hermes/skills/.

Both tests mock os.homedir() to a temp directory for deterministic isolation.

Artifacts updated: design.md (Decision #6), cli-update/spec.md (+2 scenarios), cli-init/spec.md (+1 scenario), proposal.md, tasks.md (Section 9).

All 1894 tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants