Skip to content

fix(gateway): match disabled/optional skills by frontmatter slug, not dir name#18753

Merged
teknium1 merged 1 commit into
mainfrom
fix/unavailable-skill-slug-match
May 2, 2026
Merged

fix(gateway): match disabled/optional skills by frontmatter slug, not dir name#18753
teknium1 merged 1 commit into
mainfrom
fix/unavailable-skill-slug-match

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

@teknium1 teknium1 commented May 2, 2026

Summary

_check_unavailable_skill (gateway/run.py) turns a failed "/foo" lookup into a pointed hint — "disabled, enable with hermes skills config" or "available but not installed, install with hermes skills install …" — instead of the bare "unknown command". It was doing the match by comparing skill_md.parent.name (the directory name, lowercased with underscores swapped to hyphens) against the typed command, which silently misses every skill whose directory name drifted from the declared frontmatter name:.

On a current install 19 skills hit that drift. Examples:

dir registered slug (what users type)
mlops/stable-diffusion /stable-diffusion-image-generation
mlops/qdrant /qdrant-vector-search
mlops/saelens /sparse-autoencoder-training
mlops/flash-attention /optimizing-attention-flash
mlops/modal /modal-serverless-gpu

In every one of those cases, _check_unavailable_skill would compare stable-diffusion to stable-diffusion-image-generation and return None, so the user got the generic unknown-command reply even though the disabled/optional hint was exactly what the function is there to produce.

Fix

Extract a small _skill_slug_from_frontmatter(skill_md) helper that reads the SKILL.md frontmatter and normalizes exactly like agent.skill_commands.scan_skill_commands (lowercase, spaces/underscores → hyphens, strip anything outside [a-z0-9-], collapse runs of hyphens, strip edges). Use it in both branches of _check_unavailable_skill:

  • disabled-skills branch: slug == normalized and declared_name in disabled — the disabled set is keyed by the declared frontmatter name (that's what hermes skills config / save_disabled_skills writes), which is independent from the slug.
  • optional-skills branch: match on slug alone.

Tests

Five new tests in tests/gateway/test_unavailable_skill_hint.py, all failing on main and passing with the fix:

  1. Drift case, disabled branch — dir=stable-diffusion + name: Stable Diffusion Image Generation → typing stable-diffusion-image-generation yields the disabled hint.
  2. Unknown command still returns None.
  3. Matched-but-not-disabled still returns None.
  4. Non-alnum chars are stripped (C++ Code Reviewc-code-review).
  5. Drift case, optional-skills branch — same directory/name shape yields the "not installed" hint with the correct official/mlops/stable-diffusion install path.

Related

… dir name

_check_unavailable_skill is meant to turn a typed "/foo" command that
doesn't resolve into a specific hint — "disabled, enable with hermes
skills config" or "available but not installed, install with hermes
skills install …" — instead of the generic "unknown command" reply.

It was doing the match with `skill_md.parent.name.lower().replace("_", "-")`,
comparing that to the typed command. For every skill whose directory name
drifted from its declared frontmatter `name:`, that comparison failed and
the user got the unhelpful generic path. On a standard install today 19
skills have this drift, e.g.:

  dir: mlops/stable-diffusion
  frontmatter: name: Stable Diffusion Image Generation
  registered slug (what the user types): /stable-diffusion-image-generation

  dir: mlops/qdrant
  frontmatter: name: Qdrant Vector Search
  registered slug: /qdrant-vector-search

  dir: mlops/flash-attention
  frontmatter: name: Optimizing Attention Flash
  registered slug: /optimizing-attention-flash

In every case, _check_unavailable_skill would fall through because
"stable-diffusion" != "stable-diffusion-image-generation", even with the
skill sitting right there on disk.

Fix: extract a small `_skill_slug_from_frontmatter` helper that reads the
SKILL.md frontmatter and normalizes exactly like scan_skill_commands
(lower, spaces/underscores → hyphens, strip non-[a-z0-9-], collapse
runs of hyphens, strip edges). Use it in both the
disabled-skills branch and the optional-skills branch. The disabled-set
membership check now uses the declared frontmatter name (which is what
`hermes skills config` writes into skills.disabled / platform_disabled),
not the slug.

Tests: five cases in tests/gateway/test_unavailable_skill_hint.py —
the drift case for the disabled branch, unknown-command negative,
matched-but-not-disabled negative, non-alnum stripping, and the drift
case for the optional-skills branch. All five fail against main and
pass with the fix.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery tool/skills Skills system (list, view, manage) labels May 2, 2026
@teknium1 teknium1 merged commit 6ec74ae into main May 2, 2026
10 of 11 checks passed
@teknium1 teknium1 deleted the fix/unavailable-skill-slug-match branch May 2, 2026 09:00
teknium1 added a commit that referenced this pull request May 2, 2026
#18759)

Discord's per-command name limit is 32 chars. When two skill slugs
share the same first 32 chars (or a skill slug clamps onto a reserved
gateway command name), only the first seen wins — the second is
dropped from the /skill autocomplete. The old behavior incremented a
``hidden`` counter silently, so skill authors had no way to discover
the drop short of noticing their skill was missing from the picker.

Not an actively-biting bug today (no collisions on the default catalog
as of 2026-05), but a landmine the moment someone ships a skill with a
long name. The earlier series in #18745 / #18753 / #18754 dropped the
other silent data-loss paths in the Discord /skill collector; this one
lights up the last remaining one.

Fix: promote ``_names_used`` from a set to a dict keyed by the clamped
name, mapping to the source cmd_key (or a ``"<reserved>"`` sentinel
for names inherited via ``reserved_names``). On collision, log a
WARNING naming both sides — the winner, the loser, the clamped name,
and what to rename.

Two phrasings:

* skill-vs-skill — "both clamp to X on Discord's 32-char command-name
  limit; only the winner appears in /skill. Rename one skill's
  frontmatter ``name:`` to differ in its first 32 chars."
* skill-vs-reserved — "collides with a reserved gateway command name;
  the skill will not appear in /skill. Rename the skill's frontmatter
  ``name:``."

Tests: three cases in
``tests/hermes_cli/test_discord_skill_clamp_warning.py`` —
skill-vs-skill collision (warning names both cmd_keys + clamped prefix),
skill-vs-reserved collision (warning uses the distinct phrasing), and a
no-collision negative (zero warnings emitted).
nickdlkk pushed a commit to nickdlkk/hermes-agent that referenced this pull request May 11, 2026
… dir name (NousResearch#18753)

_check_unavailable_skill is meant to turn a typed "/foo" command that
doesn't resolve into a specific hint — "disabled, enable with hermes
skills config" or "available but not installed, install with hermes
skills install …" — instead of the generic "unknown command" reply.

It was doing the match with `skill_md.parent.name.lower().replace("_", "-")`,
comparing that to the typed command. For every skill whose directory name
drifted from its declared frontmatter `name:`, that comparison failed and
the user got the unhelpful generic path. On a standard install today 19
skills have this drift, e.g.:

  dir: mlops/stable-diffusion
  frontmatter: name: Stable Diffusion Image Generation
  registered slug (what the user types): /stable-diffusion-image-generation

  dir: mlops/qdrant
  frontmatter: name: Qdrant Vector Search
  registered slug: /qdrant-vector-search

  dir: mlops/flash-attention
  frontmatter: name: Optimizing Attention Flash
  registered slug: /optimizing-attention-flash

In every case, _check_unavailable_skill would fall through because
"stable-diffusion" != "stable-diffusion-image-generation", even with the
skill sitting right there on disk.

Fix: extract a small `_skill_slug_from_frontmatter` helper that reads the
SKILL.md frontmatter and normalizes exactly like scan_skill_commands
(lower, spaces/underscores → hyphens, strip non-[a-z0-9-], collapse
runs of hyphens, strip edges). Use it in both the
disabled-skills branch and the optional-skills branch. The disabled-set
membership check now uses the declared frontmatter name (which is what
`hermes skills config` writes into skills.disabled / platform_disabled),
not the slug.

Tests: five cases in tests/gateway/test_unavailable_skill_hint.py —
the drift case for the disabled branch, unknown-command negative,
matched-but-not-disabled negative, non-alnum stripping, and the drift
case for the optional-skills branch. All five fail against main and
pass with the fix.
nickdlkk pushed a commit to nickdlkk/hermes-agent that referenced this pull request May 11, 2026
NousResearch#18759)

Discord's per-command name limit is 32 chars. When two skill slugs
share the same first 32 chars (or a skill slug clamps onto a reserved
gateway command name), only the first seen wins — the second is
dropped from the /skill autocomplete. The old behavior incremented a
``hidden`` counter silently, so skill authors had no way to discover
the drop short of noticing their skill was missing from the picker.

Not an actively-biting bug today (no collisions on the default catalog
as of 2026-05), but a landmine the moment someone ships a skill with a
long name. The earlier series in NousResearch#18745 / NousResearch#18753 / NousResearch#18754 dropped the
other silent data-loss paths in the Discord /skill collector; this one
lights up the last remaining one.

Fix: promote ``_names_used`` from a set to a dict keyed by the clamped
name, mapping to the source cmd_key (or a ``"<reserved>"`` sentinel
for names inherited via ``reserved_names``). On collision, log a
WARNING naming both sides — the winner, the loser, the clamped name,
and what to rename.

Two phrasings:

* skill-vs-skill — "both clamp to X on Discord's 32-char command-name
  limit; only the winner appears in /skill. Rename one skill's
  frontmatter ``name:`` to differ in its first 32 chars."
* skill-vs-reserved — "collides with a reserved gateway command name;
  the skill will not appear in /skill. Rename the skill's frontmatter
  ``name:``."

Tests: three cases in
``tests/hermes_cli/test_discord_skill_clamp_warning.py`` —
skill-vs-skill collision (warning names both cmd_keys + clamped prefix),
skill-vs-reserved collision (warning uses the distinct phrasing), and a
no-collision negative (zero warnings emitted).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants