Skip to content

skill_usage.py: ghost entries and duplicate prefixes in .usage.json after skill reorgs #1

Description

@Seven74AI

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

  1. 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.

  2. 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.

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions