Compare commits

...

1 Commits

Author SHA1 Message Date
teknium1
762b681423 feat(curator): add hermes curator usage — all-skills usage view
Surfaces the usage_report()/provenance() data layer added in #36701 as a
user-facing CLI command. Unlike `hermes curator status` (scoped to
curator-managed agent-created candidates), `usage` lists every skill on disk
— bundled built-ins and hub-installed included — with per-skill use/view/patch
counts and an agent/bundled/hub provenance tag.

Flags: --sort {activity,recent,name}, --provenance {agent,bundled,hub} filter,
--json for machine-readable output.
2026-06-01 03:05:41 -07:00
2 changed files with 191 additions and 0 deletions

View File

@@ -473,6 +473,66 @@ def _cmd_list_archived(args) -> int:
return 0
def _cmd_usage(args) -> int:
"""Show usage telemetry for ALL skills, with provenance.
Unlike `status` (curator-scoped to agent-created candidates), this lists
every skill on disk — bundled built-ins and hub-installed included — so you
can see how often each is actually used regardless of curation.
"""
import json as _json
from tools import skill_usage
rows = skill_usage.usage_report()
prov_filter = getattr(args, "provenance", None)
if prov_filter:
rows = [r for r in rows if r.get("provenance") == prov_filter]
sort_key = getattr(args, "sort", "activity")
if sort_key == "name":
rows.sort(key=lambda r: r["name"])
elif sort_key == "recent":
# Most-recently-active first; never-active sinks to the bottom.
rows.sort(key=lambda r: r.get("last_activity_at") or "", reverse=True)
else: # "activity" (default): most-used first
rows.sort(key=lambda r: r.get("activity_count", 0), reverse=True)
if getattr(args, "json", False):
print(_json.dumps(rows, indent=2, ensure_ascii=False))
return 0
if not rows:
print("curator: no skills found")
return 0
# Provenance tallies for a quick header.
counts = {"agent": 0, "bundled": 0, "hub": 0}
for r in rows:
counts[r.get("provenance", "agent")] = counts.get(r.get("provenance", "agent"), 0) + 1
print(
f"skills: {len(rows)} total "
f"(agent={counts['agent']} bundled={counts['bundled']} hub={counts['hub']})"
)
print()
print(
f" {'skill':40s} {'origin':8s} "
f"{'use':>4s} {'view':>4s} {'patch':>5s} {'act':>4s} last_activity"
)
for r in rows:
last = _fmt_ts(r.get("last_activity_at"))
print(
f" {r['name'][:40]:40s} "
f"{r.get('provenance', 'agent'):8s} "
f"{r.get('use_count', 0):>4d} "
f"{r.get('view_count', 0):>4d} "
f"{r.get('patch_count', 0):>5d} "
f"{r.get('activity_count', 0):>4d} "
f"{last}"
)
return 0
# ---------------------------------------------------------------------------
# argparse wiring (called from hermes_cli.main)
# ---------------------------------------------------------------------------
@@ -489,6 +549,25 @@ def register_cli(parent: argparse.ArgumentParser) -> None:
p_status = subs.add_parser("status", help="Show curator status and skill stats")
p_status.set_defaults(func=_cmd_status)
p_usage = subs.add_parser(
"usage",
help="Show usage telemetry for ALL skills (built-in, hub, agent) with provenance",
)
p_usage.add_argument(
"--sort", choices=("activity", "recent", "name"), default="activity",
help="Sort order: activity (most-used first, default), recent "
"(most-recently-active first), or name (alphabetical)",
)
p_usage.add_argument(
"--provenance", choices=("agent", "bundled", "hub"), default=None,
help="Only show skills of this origin",
)
p_usage.add_argument(
"--json", action="store_true",
help="Emit the full report as JSON instead of a table",
)
p_usage.set_defaults(func=_cmd_usage)
p_run = subs.add_parser("run", help="Trigger a curator review now")
p_run.add_argument(
"--sync", "--synchronous", dest="synchronous", action="store_true",

View File

@@ -0,0 +1,112 @@
"""Tests for `hermes curator usage` — the all-skills usage view.
Covers:
- Lists every skill regardless of provenance (agent / bundled / hub), unlike
`status` which is scoped to curator-managed candidates.
- --provenance filter, --sort ordering, and --json output.
"""
from __future__ import annotations
import json
from types import SimpleNamespace
def _fake_rows():
return [
{
"name": "agent-skill", "provenance": "agent", "state": "active",
"use_count": 2, "view_count": 1, "patch_count": 0,
"activity_count": 3, "last_activity_at": "2026-05-01T10:00:00+00:00",
"created_at": "2026-01-01T00:00:00+00:00", "_persisted": True,
},
{
"name": "bundled-skill", "provenance": "bundled", "state": "active",
"use_count": 9, "view_count": 4, "patch_count": 0,
"activity_count": 13, "last_activity_at": "2026-05-10T10:00:00+00:00",
"created_at": "2026-01-01T00:00:00+00:00", "_persisted": True,
},
{
"name": "hub-skill", "provenance": "hub", "state": "active",
"use_count": 0, "view_count": 0, "patch_count": 0,
"activity_count": 0, "last_activity_at": None,
"created_at": "2026-01-01T00:00:00+00:00", "_persisted": False,
},
]
def test_usage_lists_all_provenances(monkeypatch, capsys):
import hermes_cli.curator as curator_cli
import tools.skill_usage as skill_usage
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
args = SimpleNamespace(sort="activity", provenance=None, json=False)
assert curator_cli._cmd_usage(args) == 0
out = capsys.readouterr().out
# Header tally and all three skills present.
assert "agent=1" in out and "bundled=1" in out and "hub=1" in out
assert "agent-skill" in out
assert "bundled-skill" in out
assert "hub-skill" in out
def test_usage_sort_activity_orders_most_used_first(monkeypatch, capsys):
import hermes_cli.curator as curator_cli
import tools.skill_usage as skill_usage
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
args = SimpleNamespace(sort="activity", provenance=None, json=False)
assert curator_cli._cmd_usage(args) == 0
out = capsys.readouterr().out
# bundled-skill (act=13) must appear before agent-skill (act=3).
assert out.index("bundled-skill") < out.index("agent-skill")
def test_usage_provenance_filter(monkeypatch, capsys):
import hermes_cli.curator as curator_cli
import tools.skill_usage as skill_usage
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
args = SimpleNamespace(sort="activity", provenance="bundled", json=False)
assert curator_cli._cmd_usage(args) == 0
out = capsys.readouterr().out
assert "bundled-skill" in out
assert "agent-skill" not in out
assert "hub-skill" not in out
def test_usage_json_output(monkeypatch, capsys):
import hermes_cli.curator as curator_cli
import tools.skill_usage as skill_usage
monkeypatch.setattr(skill_usage, "usage_report", _fake_rows)
args = SimpleNamespace(sort="name", provenance=None, json=True)
assert curator_cli._cmd_usage(args) == 0
out = capsys.readouterr().out
data = json.loads(out)
assert {r["name"] for r in data} == {"agent-skill", "bundled-skill", "hub-skill"}
assert {r["provenance"] for r in data} == {"agent", "bundled", "hub"}
def test_usage_empty(monkeypatch, capsys):
import hermes_cli.curator as curator_cli
import tools.skill_usage as skill_usage
monkeypatch.setattr(skill_usage, "usage_report", lambda: [])
args = SimpleNamespace(sort="activity", provenance=None, json=False)
assert curator_cli._cmd_usage(args) == 0
assert "no skills found" in capsys.readouterr().out
def test_usage_command_is_registered():
"""The `usage` subcommand must be wired into the curator argparse tree."""
import argparse
import hermes_cli.curator as curator_cli
parser = argparse.ArgumentParser(prog="hermes curator")
curator_cli.register_cli(parser)
args = parser.parse_args(["usage", "--sort", "recent", "--provenance", "hub", "--json"])
assert args.func is curator_cli._cmd_usage
assert args.sort == "recent"
assert args.provenance == "hub"
assert args.json is True