feat(#2091): migrate hermes onto EoS imperative adapter + extensionEvents dialect (ADR-1239)

- Fold 7 hardcoded isHermes/runtime === 'hermes' branches in bin/install.js into
  descriptor-driven _hostBehaviors lookups (skillFrontmatterVersion,
  skillsManifestPrefix, trackCategoryDescription, writeCategoryDescription,
  reportSkillsCount, legacyCommandsGsdCleanup, brandingRewrites)
- Add runtime.hostBehaviors block to capabilities/hermes/capability.json
- Register EXTENSION_EVENT_SURFACES.hermes (13 real plugin hook events) in
  src/host-integration.cts — replaces the borrowed hookEvents:'claude' 6-event
  surface that silently never fired on Hermes
- Add extensionEvents:'hermes' to the descriptor
- Add 'hermes' to VALID_EXTENSION_EVENTS in capability-validator.cjs
- Regenerate capability-registry.cjs
- Tests: hermes-imperative-reference (negotiation, axes, fail-closed, source-guard),
  hermes-dispatch-upgrade (dispatch posture, degradation, fail-closed)
- Changeset + docs update
This commit is contained in:
Tom Boucher
2026-07-09 20:47:35 -04:00
parent 2abc8ec9e4
commit f152e9a069
9 changed files with 282 additions and 27 deletions

View File

@@ -1851,7 +1851,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
// Hermes' SKILL.md spec lists `version` as a required frontmatter field.
// Track GSD's package version so Hermes' skill_view() reports a stable
// identifier per install.
if (runtime === 'hermes') fm += `version: ${yamlQuote(pkg.version)}\n`;
if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
// #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
// so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
// we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
@@ -7067,7 +7067,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
// removes the directory; we must preserve/restore user artifacts before that path.
// This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory
// was already removed and skip if so (idempotent).
if (isQwen || isHermes) {
if (isQwen || _hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
// dev-preferences may have survived in skills/ as SKILL.md — nothing to do for
// that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed),
// attempt migration. In practice _runLegacyUninstallCleanup removes it first,
@@ -7821,7 +7821,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
// resolves destSubpath (which includes hermes's 'skills/gsd' nesting) — do not
// re-append 'gsd' or the hermes dir gets double-nested to skills/gsd/gsd.
const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, options.scope === 'local' ? 'local' : 'global');
const codexSkillsManifestPrefix = isHermes ? 'skills/gsd/' : 'skills/';
const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
const agentsDir = path.join(configDir, 'agents');
const manifest = {
version: pkg.version,
@@ -7871,8 +7871,8 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
manifest.files[`${codexSkillsManifestPrefix}${skillName}/${rel}`] = hash;
}
}
// For Hermes, also hash the category DESCRIPTION.md so reinstall detects drift.
if (isHermes) {
// Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift.
if (_hostBehaviors(runtime).trackCategoryDescription) {
const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md');
if (fs.existsSync(descPath)) {
manifest.files['skills/gsd/DESCRIPTION.md'] = fileHash(descPath);
@@ -8822,13 +8822,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
}
}
// Hermes only: write DESCRIPTION.md for the gsd/ category after layout install
if (isHermes) {
// Descriptor-driven (#2090): write DESCRIPTION.md for the gsd/ category after layout install
if (_hostBehaviors(runtime).writeCategoryDescription) {
writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
}
// Verify installed artifacts and report
if (isHermes) {
if (_hostBehaviors(runtime).reportSkillsCount) {
const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
if (fs.existsSync(hermesSkillsDir)) {
// Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
@@ -9234,7 +9234,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
content = content.replace(/\.claude\//g, '.qwen/');
} else if (isHermes) {
} else if (_hostBehaviors(runtime).brandingRewrites) {
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
content = content.replace(/\.claude\//g, '.hermes/');
@@ -9333,7 +9333,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
}
if (isHermes) {
if (_hostBehaviors(runtime).brandingRewrites) {
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
}