Summary
~/.hermes/skills/.usage.json accumulates stale entries and path-prefixed duplicates after skill reorganizations (flat → categorized moves, renames, symlink changes). This pollutes the curator telemetry and wastes the Journal's attention.
Reproduction
Current state on the VPS (~/.hermes/skills/.usage.json, 32 entries):
"productivity/knowledge-base": {"created_by": null, "use_count": 4, ...}
"social-media/xurl": {"created_by": null, "use_count": 33, ...}
These keys are directory-prefixed — the actual skill frontmatter says name: knowledge-base and name: xurl. But the usage.json key reflects the on-disk path (productivity/knowledge-base/SKILL.md, social-media/xurl/SKILL.md).
There is no entry for the canonical short names (knowledge-base, xurl), meaning telemetry is fractured across naming conventions.
Root Causes
1. _mutate() does not canonicalize skill names
skill_usage._mutate() (line 380) records entries under whatever name string callers pass — no normalization to the canonical frontmatter name: field:
def _mutate(skill_name: str, mutator) -> None:
data[skill_name] = rec # ← raw name, could be "productivity/knowledge-base"
save_usage(data)
Callers (skill_manage, skill_view bumps, skill_commands, skill_bundles) all pass through the name they receive without resolution. When a skill is loaded by directory path (e.g., skill_view("productivity/knowledge-base")), the path leaks into the usage key.
2. is_agent_created() is too broad
_mutate() gates on is_agent_created() (line 390):
if not is_agent_created(skill_name):
return # skip bundled + hub skills
This admits ALL non-bundled, non-hub skills — including project skills (shop, baguette, music-library), user-authored skills, and workflow skills. But the curator only manages skills with created_by == "agent" (_is_curator_managed_record, line 296). Result: 24 of 32 entries are ghost entries (created_by: null) that inflate the file with no curator value.
3. No cleanup on skill reorg
When skills move (e.g., flat sync-skills/ → devops/sync-skills/), old keys persist. The only cleanup path is forget() on explicit skill_manage(delete). Moves/renames/reorganizations leave orphaned keys.
Affected Code
tools/skill_usage.py: _mutate() (L380), is_agent_created() (L290), load_usage() (L323)
tools/skills_tool.py: _skill_view_with_bump() (L1535) — passes resolved but resolved can fall back to the raw name if frontmatter parse fails
tools/skill_manager_tool.py: L778-786 — passes raw args.get("name") directly
agent/skill_commands.py: _load_skill_payload() (L102) — skill_name = loaded_skill.get("name") or normalized, normalized can be a path
Suggested Fixes
-
Normalize in _mutate(): resolve the skill name to the canonical frontmatter name: before storing. If a path-prefixed name is passed (productivity/knowledge-base), find the SKILL.md, read the frontmatter, and use the canonical name.
-
Add a repair command to hermes curator: scan on-disk skills, match usage.json entries by frontmatter name, consolidate duplicates, and remove orphaned keys for skills that no longer exist on disk.
-
Consider narrowing _mutate(): only track skills where created_by == "agent" or add a separate flag for "telemetry opt-in" to avoid filling the file with project-skill noise.
Impact
- Journal entries waste attention on non-actionable ghost data
- Curator lifecycle timers (
last_used_at) are split across duplicate keys
- Disk is negligible (~3KB) but the file grows monotonically — never shrinks
Summary
~/.hermes/skills/.usage.jsonaccumulates stale entries and path-prefixed duplicates after skill reorganizations (flat → categorized moves, renames, symlink changes). This pollutes the curator telemetry and wastes the Journal's attention.Reproduction
Current state on the VPS (
~/.hermes/skills/.usage.json, 32 entries):These keys are directory-prefixed — the actual skill frontmatter says
name: knowledge-baseandname: xurl. But the usage.json key reflects the on-disk path (productivity/knowledge-base/SKILL.md,social-media/xurl/SKILL.md).There is no entry for the canonical short names (
knowledge-base,xurl), meaning telemetry is fractured across naming conventions.Root Causes
1.
_mutate()does not canonicalize skill namesskill_usage._mutate()(line 380) records entries under whatever name string callers pass — no normalization to the canonical frontmattername:field:Callers (
skill_manage,skill_viewbumps,skill_commands,skill_bundles) all pass through the name they receive without resolution. When a skill is loaded by directory path (e.g.,skill_view("productivity/knowledge-base")), the path leaks into the usage key.2.
is_agent_created()is too broad_mutate()gates onis_agent_created()(line 390):This admits ALL non-bundled, non-hub skills — including project skills (
shop,baguette,music-library), user-authored skills, and workflow skills. But the curator only manages skills withcreated_by == "agent"(_is_curator_managed_record, line 296). Result: 24 of 32 entries are ghost entries (created_by: null) that inflate the file with no curator value.3. No cleanup on skill reorg
When skills move (e.g., flat
sync-skills/→devops/sync-skills/), old keys persist. The only cleanup path isforget()on explicitskill_manage(delete). Moves/renames/reorganizations leave orphaned keys.Affected Code
tools/skill_usage.py:_mutate()(L380),is_agent_created()(L290),load_usage()(L323)tools/skills_tool.py:_skill_view_with_bump()(L1535) — passesresolvedbutresolvedcan fall back to the raw name if frontmatter parse failstools/skill_manager_tool.py: L778-786 — passes rawargs.get("name")directlyagent/skill_commands.py:_load_skill_payload()(L102) —skill_name = loaded_skill.get("name") or normalized, normalized can be a pathSuggested Fixes
Normalize in
_mutate(): resolve the skill name to the canonical frontmattername:before storing. If a path-prefixed name is passed (productivity/knowledge-base), find the SKILL.md, read the frontmatter, and use the canonical name.Add a
repaircommand tohermes curator: scan on-disk skills, match usage.json entries by frontmatter name, consolidate duplicates, and remove orphaned keys for skills that no longer exist on disk.Consider narrowing
_mutate(): only track skills wherecreated_by == "agent"or add a separate flag for "telemetry opt-in" to avoid filling the file with project-skill noise.Impact
last_used_at) are split across duplicate keys