Add experimental Hermes agent target#1726
Conversation
Add the 'hermes' compile/install target (Nous Research Hermes agent), gated behind the new 'hermes' experimental flag. Skills reuse the agentskills.io SKILL.md format (.agents/skills/ at project scope, ~/.hermes/skills/ at user scope); instructions reuse the AGENTS.md compile family; MCP servers are written to the YAML mcp_servers: block of ~/.hermes/config.yaml via a new HermesClientAdapter. HERMES_HOME overrides the Hermes home directory. Spec-first: unit + integration tests precede the implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new experimental hermes target integrating the Hermes agent into APM’s target registry, compilation routing, and MCP server configuration.
Changes:
- Register
hermesas an experimental target (registry, CLI target detection/constants, factory wiring). - Implement Hermes MCP adapter writing
mcp_serversto~/.hermes/config.yaml(withHERMES_HOMEoverride) and gate runtime auto-discovery. - Add unit/integration tests and documentation/guide updates for Hermes behavior and flag-gating.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/integration/test_targets_registry_completeness.py | Adds Hermes adapter to registry completeness checks. |
| tests/unit/integration/test_targets.py | Adds hermes target invariants + scope/flag tests. |
| tests/unit/core/test_target_detection.py | Updates experimental target constant membership to include hermes. |
| tests/unit/core/test_scope.py | Updates KNOWN_TARGETS expected set to include hermes. |
| tests/unit/adapters/test_hermes_client_adapter.py | New unit tests for Hermes adapter conversion + YAML merge semantics. |
| tests/integration/test_hermes_target.py | New E2E coverage for hermes flag gating, deploy paths, and compile routing. |
| src/apm_cli/integration/targets.py | Adds hermes TargetProfile and resolve_hermes_root; extends user-scope env var logic. |
| src/apm_cli/integration/mcp_integrator_install.py | Refactors runtime discovery; adds hermes opt-in detection gating. |
| src/apm_cli/install/phases/targets.py | Adds hermes enable-hint logic via shared experimental-target check helper. |
| src/apm_cli/factory.py | Registers HermesClientAdapter in ClientFactory. |
| src/apm_cli/core/target_detection.py | Adds hermes to compile routing, descriptions, and EXPERIMENTAL_TARGETS. |
| src/apm_cli/core/experimental.py | Registers hermes experimental flag metadata. |
| src/apm_cli/bundle/lockfile_enrichment.py | Adds hermes path translation mapping for skills. |
| src/apm_cli/adapters/client/hermes.py | New Hermes MCP adapter writing YAML mcp_servers block. |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Documents hermes usage in apm install command reference. |
| docs/src/content/docs/producer/compile.md | Documents experimental targets incl. hermes; adds link to integration guide. |
| docs/src/content/docs/integrations/hermes.md | New Hermes integration documentation page. |
| docs/astro.config.mjs | Adds Hermes integration page to docs nav. |
| CHANGELOG.md | Notes new experimental Hermes target support and behavior. |
|
@microsoft-github-policy-service agree |
- resolve_hermes_root(): expanduser().resolve(strict=False) so traversal segments in $HERMES_HOME cannot create stray intermediate dirs on mkdir(parents=True); aligns with TargetProfile.for_scope normalization. - _to_hermes_format(): only emit url/command when truthy so a malformed entry never serializes as url: null / command: null into Hermes config; add regression tests for both remote-without-url and stdio-without-command. - target_detection: fix '~/.hermes/ skills' typo -> '~/.hermes/skills/'. - compile.md: fix relative link depth ../../integrations -> ../integrations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Status updateAll four Copilot review findings are addressed in 49799aa (path normalization, null-key guards for malformed MCP entries + regression tests, doc typo, and the relative-link depth fix). The earlier inline comments are on the first commit (bd4fa65) and predate the fix. Local CI mirror is green on the latest commit: The CI / CodeQL / Merge Gate / Spec conformance workflows currently show |
Live validation against a real Hermes deploymentValidated end to end against a real Hermes Agent v0.16.0 install (Docker), using this branch's APM build. The proof below is deterministic and secret-free, so a maintainer can reproduce it. No credentials are required for steps 1-7. Full agent round-trip (model + messaging gateway)Beyond the filesystem assertions, the APM-deployed skill was exercised through a live Hermes messaging gateway driving a real LLM turn. A task that matches the skill returned its exact, skill-defined marker, and Hermes' own trace shows it loaded the skill: That string is defined only inside the deployed Notes
|
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 3 | Clean registry-first extension following established adapter and target-profile patterns; one OCP violation in TargetProfile.for_scope name-dispatch is the only substantive finding. |
| CLI Logging Expert | 0 | 1 | 2 | ASCII-clean, credential-safe, enable-hint through CommandLogger; catch-all exception suppresses diagnostic type, and rich* call-sites omit symbol= prefix. |
| DevX UX Expert | 0 | 4 | 2 | Strong integration fundamentals, but three reference docs are incomplete and the post-enable hint misstates that apm install deploys AGENTS.md. |
| Supply Chain Security Expert | 1 | 1 | 1 | Literal credentials land in config.yaml without 0o600 protection -- every other adapter with the same threat model applies it; rework required. |
| OSS Growth Hacker | 0 | 4 | 1 | Genuine novel persona-expansion story (community-chat-native AI) and zero-format-migration proof point; guide is written for APM users rather than inbound Hermes users. |
| Auth Expert | 0 | 2 | 1 | No blocking auth issues; two recommended gaps: expanduser() absent in for_scope() and full-config loss on YAML parse error. |
| Doc Writer | 0 | 1 | 3 | New hermes.md is structurally sound and code-accurate; external product URL unverified, deprecated --target all referenced, sidebar.order duplicated. |
| Test Coverage Expert | 0 | 2 | 1 | Adapter unit and install-pipeline integration tests are strong; two gaps: apm compile -t hermes lacks a CLI-level test and _hermes_runtime_opted_in() double-gate is untested. |
| Performance Expert | 0 | 1 | 3 | Zero I/O overhead when flag is OFF; _hermes_runtime_opted_in() is one @lru_cache away from being permanently safe against future double-call. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Supply Chain Security Expert] (blocking-severity) Replace
dump_yamlcall inupdate_configwithatomic_write_text(path, payload, new_file_mode=0o600)-- Every peer adapter (Claude, Codex, Cursor, Gemini) enforces 0o600; HermesClientAdapter inherits OS-default umask (0o644). Literal credentials in a world-readable file is a concrete, verifiable exposure surface. The fix is one line and the pattern is already in the codebase. - [Auth Expert] Return
Falsefromupdate_configwhen_load_documentfails to parse, and addyaml.YAMLErrorto the_load_documentexcept clause -- Parse failure today silently overwritesconfig.yamlwith only the MCP block, discarding model-provider API keys and Telegram tokens. The docstring promises non-MCP key preservation; the implementation breaks it.load_yaml()raisesyaml.YAMLError, notValueErrororOSError, so the guard never fires for the most common real-world failure. - [Test Coverage Expert] Add CLI-level integration test for
apm compile -t hermesassertingAGENTS.mdis written to the project root -- Missing at integration-with-fixtures tier on a governed-by-policy surface. The current test is a static function-call assertion, not a CLI invocation; the compile routing promise is unverified end-to-end. - [Test Coverage Expert] Add integration test for
_hermes_runtime_opted_in()double-gate: flag-on + hermes-absent must not writeconfig.yaml-- Missing at integration-with-fixtures tier on a secure-by-default surface. Verifies the exact scenario a user triggers when enabling the experimental flag on a host without Hermes installed. - [DevX UX Expert] Fix
ExperimentalFlag.hintto distinguishapm install(skills only) fromapm compile -t hermes(AGENTS.md), and addhermestoinstall.md,experimental.md, andtargets-matrix.md-- The hint text today tells users thatapm installdeploys AGENTS.md; it does not. Three reference doc gaps mean a user who discovers hermes viaapm experimental listcannot verify it against any reference page.
Architecture
classDiagram
direction LR
class MCPClientAdapter {
<<Abstract>>
+_fetch_server_info(url, cache) dict
+_determine_config_key(url, name) str
+configure_mcp_server(url, name) bool
}
class CopilotClientAdapter {
+_client_label str
+mcp_servers_key str
+_format_server_config(info, env, vars) dict
+get_current_config() dict
+update_config(updates, enabled) bool
}
class HermesClientAdapter {
+target_name str
+mcp_servers_key str
+_supports_runtime_env_substitution bool
+_to_hermes_format(entry, enabled) dict
+_config_path() Path
+_load_document() dict
+get_current_config() dict
+update_config(updates, enabled) bool
+configure_mcp_server(url) bool
}
class ClaudeClientAdapter {
+target_name str
+mcp_servers_key str
}
class TargetProfile {
<<ValueObject>>
+name str
+root_dir str
+user_root_dir str
+requires_flag str
+compile_family str
+for_scope(user_scope) TargetProfile
}
class PrimitiveMapping {
<<ValueObject>>
+subdir str
+extension str
+format_id str
}
class ClientFactory {
<<Factory>>
+create_client(name) MCPClientAdapter
}
MCPClientAdapter <|-- CopilotClientAdapter
CopilotClientAdapter <|-- HermesClientAdapter
CopilotClientAdapter <|-- ClaudeClientAdapter
ClientFactory ..> HermesClientAdapter : creates
TargetProfile *-- PrimitiveMapping : primitives
class HermesClientAdapter:::touched
class TargetProfile:::touched
class ClientFactory:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["apm install --target hermes --global"] --> B["install/phases/targets.run()"]
B --> C["_resolve_targets_legacy\nintegration/targets.resolve_targets\nuser_scope=True, explicit=hermes"]
C --> D["KNOWN_TARGETS[hermes]\n_flag_gated -> is_enabled(hermes)\ncore/experimental.py"]
D -- "flag OFF" --> E["target not resolved, empty list"]
D -- "flag ON" --> F["TargetProfile.for_scope(user_scope=True)\nreads HERMES_HOME\nintegration/targets.py"]
F --> G["scope-resolved TargetProfile\nroot_dir = .hermes or abs(HERMES_HOME)"]
B --> H["_check_hermes_flag_gate\n-> _check_experimental_target_hint\ninstall/phases/targets.py"]
H -- "flag OFF + hermes in explicit" --> I["emit: apm experimental enable hermes"]
G --> J["SkillIntegrator.deploy\nwrite ~/.hermes/skills/name/SKILL.md"]
B --> K["mcp_integrator_install._hermes_runtime_opted_in\nis_enabled(hermes) AND resolve_hermes_root().is_dir\nprobe ~/.hermes/"]
K -- "opted in" --> L["HermesClientAdapter.configure_mcp_server\nadapters/client/hermes.py"]
L --> M["_fetch_server_info(url, cache)\nregistry HTTP\ninherited from MCPClientAdapter"]
M --> N["_format_server_config(info, env, vars)\ninherited from CopilotClientAdapter\nreturns Copilot-format dict"]
N --> O["update_config({key: copilot_dict})\nhermes.py:update_config"]
O --> P["_load_document\nload_yaml config.yaml\nutils/yaml_io.py"]
O --> Q["_to_hermes_format(copilot_dict)\ndrops type/tools/id, stamps enabled"]
Q --> R["dump_yaml config.yaml\nutils/yaml_io.py\nMISSING: 0o600 permission"]
Recommendation
Do not merge until the 0o600 permission fix and the parse-error abort path are both in place. The security gap is concrete, the fix pattern is established by every peer adapter, and there is no trade-off to weigh against fixing it. Once those two are green, fold in the hint-text correction (apm install vs. apm compile for AGENTS.md) and verify the external Nous Research URL, then ship. File the OCP refactor, the three reference doc gaps, and the two missing integration tests as issues in the same milestone. The Hermes integration is a well-architected addition with a genuine growth story and a zero-format-migration proof point that no existing target can match -- the only thing between it and merge is a permissions flag and an abort guard.
Full per-persona findings
Python Architect
-
[recommended] TargetProfile.for_scope name-dispatches on self.name instead of reading a dataclass field at
src/apm_cli/integration/targets.py:390
for_scope() containsif self.name in ('claude', 'hermes')to pick the correct env-var name (CLAUDE_CONFIG_DIR vs HERMES_HOME). Encoding identity-based branching inside a generic method violates OCP: a third tool with its own home env var requires editing for_scope again. The fix is a single optionalhome_env_var: str | None = Nonefield on TargetProfile.
Suggested: Addhome_env_var: str | None = Noneto TargetProfile. Sethome_env_var='CLAUDE_CONFIG_DIR'on claude andhome_env_var='HERMES_HOME'on hermes in KNOWN_TARGETS. In for_scope replace the name-dispatch withif self.home_env_var: env = os.environ.get(self.home_env_var, '').strip(). -
[nit] _client_label not overridden; HermesClientAdapter silently inherits 'Copilot CLI' at
src/apm_cli/adapters/client/hermes.py
Suggested: Add_client_label: str = 'Hermes'alongside the existing class attributes. -
[nit] _check_hermes_flag_gate and _check_openclaw_flag_gate are needless one-liner wrappers at
src/apm_cli/install/phases/targets.py
Suggested: Call_check_experimental_target_hintdirectly fromrun()with named arguments and remove the two wrapper functions.
CLI Logging Expert
-
[recommended] catch-all except Exception emits zero diagnostic signal even though exception type is credential-safe to log at
src/apm_cli/adapters/client/hermes.py:159
The exception TYPE (OSError, ValueError, ConnectionError) carries no credentials and would let users self-diagnose disk-full, permission-denied, or network failures.
Suggested:except Exception as exc: _rich_error(f'Error configuring MCP server ({type(exc).__name__})', symbol='error') -
[nit] All five rich* call-sites omit symbol= so messages render with no prefix on non-color terminals at
src/apm_cli/adapters/client/hermes.py
Suggested: Addsymbol='error',symbol='success',symbol='warning'to each call-site respectively. -
[nit] 'Successfully configured' wording is redundant -- _rich_success already signals success at
src/apm_cli/adapters/client/hermes.py:157
Suggested:_rich_success(f"Configured MCP server '{config_key}' for Hermes", symbol='success')
DevX UX Expert
-
[recommended]
--targethelp text in install.md and install.py omitshermesfrom the experimental target list atdocs/src/content/docs/reference/cli/install.md
Suggested: Extend the experimental target list to includehermes(andopenclaw). -
[recommended]
reference/experimental.mdAvailable Flags table does not listhermesatdocs/src/content/docs/reference/experimental.md
Suggested: Add a hermes row:| 'hermes' | Deploy skills, AGENTS.md, and MCP servers to the Hermes agent. | -
[recommended]
reference/targets-matrix.mdis missing ahermes (experimental)section atdocs/src/content/docs/reference/targets-matrix.md
Suggested: Add a section parallel to the openclaw section covering detection (none), enable command, deploy directories, and supported primitives. -
[recommended] ExperimentalFlag.hint for hermes conflates
apm installwithapm compilefor AGENTS.md atsrc/apm_cli/core/experimental.py
The hint says 'deploy skills + AGENTS.md' but install deploys only skills; AGENTS.md requiresapm compile -t hermes.
Suggested: 'Use --target hermes to deploy skills to your project (.agents/skills/), then run apm compile -t hermes to emit AGENTS.md. Use --target hermes --global for ~/.hermes/ skills + MCP servers in config.yaml.' -
[nit] MCP servers doc section doesn't call out the mcp_servers vs mcpServers key difference at
docs/src/content/docs/integrations/hermes.md -
[nit] Troubleshooting entry for skills.external_dirs tells users what to do but not how at
docs/src/content/docs/integrations/hermes.md
Supply Chain Security Expert
-
[blocking] update_config writes credential-bearing YAML with OS-default (world-readable) permissions at
src/apm_cli/adapters/client/hermes.py
ClaudeClientAdapter usesatomic_write_text(new_file_mode=0o600); Codex, Cursor, Gemini all callos.chmod(config_path, 0o600). HermesClientAdapter does neither. Literal secrets written with umask-default permissions (0o644) are readable by any process that can access $HOME.
Suggested: Useatomic_write_text(path, yaml_to_str(data), new_file_mode=0o600)inupdate_config. -
[recommended] _load_document catches (OSError, ValueError) but not yaml.YAMLError, so the 'rewrite from scratch' guard never fires for malformed YAML at
src/apm_cli/adapters/client/hermes.py
Suggested: Changeexcept (OSError, ValueError):toexcept (OSError, ValueError, yaml.YAMLError):in_load_document. -
[nit] _to_hermes_format returns non-dict input as-is without stamping enabled at
src/apm_cli/adapters/client/hermes.py
OSS Growth Hacker
-
[recommended] Integration guide opens by describing Hermes to APM users rather than answering 'why APM?' for an inbound Hermes user at
docs/src/content/docs/integrations/hermes.md
Suggested: Add a 'Why APM for Hermes users?' opener: APM gives Hermes a manifest, lockfile, and transitive dependency resolution for the SKILL.md / AGENTS.md content Hermes already reads. -
[recommended] Zero-new-format proof point is buried mid-paragraph rather than called out as the headline at
docs/src/content/docs/integrations/hermes.md
Suggested: Promote 'Hermes natively reads two open standards that APM already emits' to a:::tipcallout immediately below the caution block. -
[recommended] README hero line omits Hermes, missing a free ecosystem-momentum signal at
README.md
Suggested: AppendHermes (experimental)to the target list on the README hero line. -
[recommended] Hermes' Telegram/Discord-native persona is mentioned descriptively but not framed as a new community-native user segment APM is now reaching at
docs/src/content/docs/integrations/hermes.md -
[nit] CHANGELOG entry is technically accurate but doesn't surface the zero-format-migration story as a user-facing benefit at
CHANGELOG.md
Auth Expert
-
[recommended] expanduser() absent in for_scope() but present in resolve_hermes_root() -- tilde-style HERMES_HOME causes skills-path / MCP-write path divergence at
src/apm_cli/integration/targets.py
resolve_hermes_root()callsPath(env).expanduser().resolve();for_scope()callsPath(env).resolve()directly. If HERMES_HOME=~/foo, skills and MCP config land in different roots.
Suggested: ChangePath(env).resolve()toPath(env).expanduser().resolve()for the hermes branch infor_scope(). -
[recommended] _load_document() returns {} on parse error and update_config() silently overwrites full config.yaml, discarding non-MCP credentials at
src/apm_cli/adapters/client/hermes.py
The docstring promises 'all other top-level config keys are preserved'; the implementation breaks this guarantee on parse failure, discarding model-provider API keys, Telegram tokens, and any other native Hermes config.
Suggested: When_load_document()fails to parse, haveupdate_config()returnFalsewith error: 'config.yaml is malformed; refusing to overwrite -- fix or remove the file manually.' -
[nit] configure_mcp_server bare except Exception masks programming errors (AttributeError, TypeError from refactors) at
src/apm_cli/adapters/client/hermes.py
Doc Writer
-
[recommended] External product link (hermesagent.nousresearch.com/redacted) is unverified at
docs/src/content/docs/integrations/hermes.md
Suggested: Verify the URL resolves to the canonical Nous Research Hermes product page before merge; supplement with a GitHub repo URL if available. -
[nit] sidebar.order: 6 in hermes.md duplicates copilot-app.md's order: 6 at
docs/src/content/docs/integrations/hermes.md
Suggested: Set sidebar.order: 8 (next free slot after external-scanners.md at 7). -
[nit] Caution block says 'excluded from --target all' -- references a deprecated install flag at
docs/src/content/docs/integrations/hermes.md
Suggested: Replace with 'excluded fromapm compile --all'. -
[nit] Install code-block comment conflates both commands into one line, obscuring that AGENTS.md requires
apm compile, notapm installatdocs/src/content/docs/integrations/hermes.md
Test Coverage Expert
-
[recommended] apm compile -t hermes is not integration-tested; test_hermes_compiles_agents_md is a static function-call assertion, not a CLI invocation at
tests/integration/test_hermes_target.py
Per tier floor matrix, CLI command surface requires integration-with-fixtures.
Proof (missing at integration-with-fixtures):tests/integration/test_hermes_target.py::TestHermesCompileE2E::test_compile_hermes_emits_agents_md-- proves: apm compile -t hermes routes through the agents compile family and writes AGENTS.md to the project root [governed-by-policy] -
[recommended] _hermes_runtime_opted_in() double-gate (flag-enabled AND hermes-present) has no test at any tier at
tests/integration/test_mcp_install_flow.py
No test verifies: (a) flag-on + home-dir-absent + no-binary -> excluded from discovered runtimes; (b) flag-on + home-dir-exists -> included; (c) flag-off -> excluded regardless.
Proof (missing at integration-with-fixtures):tests/integration/test_mcp_install_flow.py::TestHermesMCPOptIn::test_flag_on_no_presence_skips_mcp_write-- proves: APM does not write ~/.hermes/config.yaml on a host where Hermes is absent even when the experimental flag is enabled [secure-by-default] -
[nit] _CROSS_TARGET_MAPS['hermes'] has no assertion; openclaw parallel entry has TestOpenclawCrossTargetMap at
tests/integration/test_hermes_target.py
Proof (missing at unit):tests/integration/test_hermes_target.py::TestHermesCrossTargetMap::test_cross_target_map_remaps_github_skills
Performance Expert
-
[recommended] _hermes_runtime_opted_in() is not memoized, leaving a latent shutil.which double-call risk at
src/apm_cli/integration/mcp_integrator_install.py
If a second call site is added with the flag ON and ~/.hermes/ absent,shutil.which('hermes')executes twice (5-200ms per call). One@lru_cachedecorator fixes this permanently.
Suggested: Add@functools.lru_cache(maxsize=None)to_hermes_runtime_opted_in(). -
[nit] Two-stage probe order (is_dir before find_runtime_binary) is correct -- stat before shutil.which at
src/apm_cli/integration/mcp_integrator_install.py
No change needed. -
[nit] Flag-OFF fast path costs exactly 2 dict lookups -- zero I/O for the 99%+ of users without hermes enabled at
src/apm_cli/integration/mcp_integrator_install.py
Correctly implemented.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by PR Review Panel for issue #1726 · sonnet46 23.7M · ◷
Address APM Review Panel needs_rework on PR microsoft#1726: - BLOCKING (supply-chain-security): write ~/.hermes/config.yaml atomically with 0o600 via atomic_write_text + os.chmod, so the credential-bearing config is never left group/world-readable (parity with claude/codex/ gemini/cursor). Tightens pre-existing loose files too. - auth-expert: _load_document now raises on a malformed/non-mapping existing config (incl. yaml.YAMLError, previously uncaught); update_config refuses to overwrite and returns False instead of silently discarding native Hermes credentials (model-provider keys, Telegram tokens). - docs: fix sidebar.order collision (6 -> 8) and stale --target all -> apm compile --all in the integration guide. Tests: +6 unit (0o600 new/loose-file, refuse-on-malformed, non-mapping, empty-file) and +9 integration (compile -t hermes emits AGENTS.md, _hermes_runtime_opted_in double-gate x4, cross-target map x2). Lint: ruff + pylint R0801 (10.00/10) + auth-signals all green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Head branch was pushed to by a user without write access
Addressed APM Review Panel
|
Sync the Stage 2 complexity/file-length refactor branch with main's 22 feature commits (Hermes #1726, Kiro IDE #1741, multi-host dep identity #1735, same-repo remote path deps #1732, git_file_transport #1740, revision pins #1738, marketplace sourceBase/source parity/inherit description #1736/#1739/#1755, pack --archive .zip #1720, mcp optional registry inputs #1734, and the v0.19.0/v0.20.0 releases). Conflict resolution preserved both sides: main's new features ported through the branch's extracted sibling modules, branch's tightened ruff thresholds (max-statements=120, max-branches=40, max-complexity=35, max-returns=8, max-args=12) and 800-line file limit retained. All 7 CI-mirror lint gates pass; full unit suite green (17099 passed). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TL;DR
Adds the Hermes Agent (Nous Research) as a new experimental APM compile/install target.
apm install --target hermesandapm compile -t hermesnow deploy APM primitives where Hermes natively reads them. Validated end-to-end against a real Hermes v0.16.0 deployment (Docker, z.ai GLM backend, live Telegram gateway).Problem (WHY)
Hermes is a terminal-native autonomous agent that lives in
~/.hermes/and talks to users over 20+ messaging platforms. It already consumes two open standards APM emits -- agentskills.ioSKILL.mdandAGENTS.md-- yet APM had nohermestarget, so users had to hand-wire skills, context, and MCP servers.Approach (WHAT)
Model
hermesas one data-drivenKNOWN_TARGETSentry plus a single new MCP adapter. No new skill/instruction transformers -- skills reuse the existingskill_standardformat and instructions reuse theagents(AGENTS.md) compile family. Only MCP needed a new writer because Hermes uses a YAMLmcp_servers:schema distinct from the JSONmcpServers/mcpof other clients.The target is gated behind a new
hermesexperimental flag (mirrorsopenclaw/copilot-cowork); it is invisible and excluded from--alluntilapm experimental enable hermes..agents/skills/<n>/SKILL.md(project) /~/.hermes/skills/<n>/SKILL.md(--global)AGENTS.mdat repo root (viaapm compile)mcp_servers:YAML block~/.hermes/config.yaml(user scope)HERMES_HOMEoverrides the Hermes home directory (mirrors Claude'sCLAUDE_CONFIG_DIR).Implementation (HOW)
adapters/client/hermes.py--HermesClientAdapter: writes the YAMLmcp_servers:block (stdiocommand/args/env, httpurl/headers, per-serverenabled), merges into existing config, and preserves all sibling top-level keys. Usesutils/yaml_io(no rawyaml.dump).integration/targets.py--hermesKNOWN_TARGETSentry,resolve_hermes_root(),HERMES_HOMEhandling infor_scope.core/experimental.py,core/target_detection.py,factory.py,install/phases/targets.py,integration/mcp_integrator_install.py,bundle/lockfile_enrichment.py-- flag registration, MCP-client registration, enable-hint, opt-in gating, cross-target skill-path map.integrations/hermes.md+ sidebar;producer/compile.md, apm-usagecommands.md,CHANGELOG.md.Spec-first ordering: unit + integration tests were written (and confirmed red) before the implementation.
Validation evidence
ruff check,ruff format --check, pylint R0801 (10.00/10), auth-signals -- all green.tests/unit/adapters/test_hermes_client_adapter.py+tests/integration/test_hermes_target.py, plus registry/scope/detection updates. Full targeted + broad suites pass.glm-4.6):apm install --target hermes --global-> Hermes lists the deployed skill natively (local / enabled) and the MCP server (uvx mcp-server-time ... enabled);~/.hermes/config.yamlgains themcp_serversentry with every sibling key preserved.apm compile -t hermes->AGENTS.mdemitted with the instruction content.How to test
Notes
.agents/skills/is shared at project scope;detect_by_dir=Falsekeeps hermes explicit---target-only so it never silently activates.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com