From b9fedab47a7dc7bb09bbaf18c12ce7af3ddf7929 Mon Sep 17 00:00:00 2001 From: webtecnica <75556242+webtecnica@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:25:14 -0300 Subject: [PATCH] fix: curator labels bundled skills as agent-created (#64393) --- agent/curator.py | 21 +++++++++--------- hermes_cli/curator.py | 22 +++++++++++++------ .../hermes_cli/test_curator_archive_prune.py | 14 ++++++------ tests/hermes_cli/test_curator_status.py | 6 ++--- tests/tools/test_skill_usage.py | 2 +- tools/skill_usage.py | 22 ++++++++++++++++--- 6 files changed, 56 insertions(+), 31 deletions(-) diff --git a/agent/curator.py b/agent/curator.py index dc908fc35939..975ed102de0b 100644 --- a/agent/curator.py +++ b/agent/curator.py @@ -325,7 +325,7 @@ def apply_automatic_transitions(now: Optional[datetime] = None) -> Dict[str, int counts = {"marked_stale": 0, "archived": 0, "reactivated": 0, "checked": 0, "seeded": 0} - for row in _u.agent_created_report(): + for row in _u.curated_report(): counts["checked"] += 1 name = row["name"] if row.get("pinned"): @@ -1472,15 +1472,16 @@ def _render_report_markdown(p: Dict[str, Any]) -> str: # --------------------------------------------------------------------------- def _render_candidate_list() -> str: - """Human/agent-readable list of agent-created skills with usage stats.""" - rows = skill_usage.agent_created_report() + """Human/agent-readable list of curator-managed skills with usage stats.""" + rows = skill_usage.curated_report() if not rows: - return "No agent-created skills to review." + return "No curator-managed skills to review." cron_referenced = _cron_referenced_skills() - lines = [f"Agent-created skills ({len(rows)}):\n"] + lines = [f"Curator-managed skills ({len(rows)}):\n"] for r in rows: lines.append( f"- {r['name']} " + f"provenance={r.get('provenance', 'agent')} " f"state={r['state']} " f"pinned={'yes' if r.get('pinned') else 'no'} " f"cron={'yes' if r['name'] in cron_referenced else 'no'} " @@ -1533,7 +1534,7 @@ def run_curator_review( if dry_run: # Count candidates without mutating state. try: - report = skill_usage.agent_created_report() + report = skill_usage.curated_report() counts = { "checked": len(report), "marked_stale": 0, @@ -1586,7 +1587,7 @@ def run_curator_review( nonlocal auto_summary # Snapshot skill state BEFORE the LLM pass so the report can diff. try: - before_report = skill_usage.agent_created_report() + before_report = skill_usage.curated_report() except Exception: before_report = [] before_names = {r.get("name") for r in before_report if isinstance(r, dict)} @@ -1612,7 +1613,7 @@ def run_curator_review( state2["last_run_duration_seconds"] = elapsed state2["last_run_summary"] = final_summary try: - after_report = skill_usage.agent_created_report() + after_report = skill_usage.curated_report() except Exception: after_report = [] try: @@ -1699,7 +1700,7 @@ def run_curator_review( try: rename_lines = _build_rename_summary( before_names=before_names, - after_report=skill_usage.agent_created_report(), + after_report=skill_usage.curated_report(), tool_calls=llm_meta.get("tool_calls", []) or [], model_final=llm_meta.get("final", "") or "", ) @@ -1717,7 +1718,7 @@ def run_curator_review( # reporting bug never breaks the curator itself. Report path is # recorded in state so `hermes curator status` can point at it. try: - after_report = skill_usage.agent_created_report() + after_report = skill_usage.curated_report() except Exception: after_report = [] try: diff --git a/hermes_cli/curator.py b/hermes_cli/curator.py index c64fd5afef8c..4d5bc70f39ec 100644 --- a/hermes_cli/curator.py +++ b/hermes_cli/curator.py @@ -82,20 +82,28 @@ def _cmd_status(args) -> int: f"{'' if curator.get_consolidate() else ' (prune-only; LLM merge pass opt-in)'}" ) - rows = skill_usage.agent_created_report() + rows = skill_usage.curated_report() if not rows: - print("\nno agent-created skills") + print("\nno curator-managed skills") return 0 by_state = {"active": [], "stale": [], "archived": []} pinned = [] + agent_count = 0 + bundled_count = 0 for r in rows: state_name = r.get("state", "active") by_state.setdefault(state_name, []).append(r) if r.get("pinned"): pinned.append(r["name"]) + prov = r.get("provenance", "agent") + if prov == "agent": + agent_count += 1 + elif prov == "bundled": + bundled_count += 1 - print(f"\nagent-created skills: {len(rows)} total") + print(f"\ncurator-managed skills: {len(rows)} total " + f"(agent-created={agent_count} bundled={bundled_count})") for state_name in ("active", "stale", "archived"): bucket = by_state.get(state_name, []) print(f" {state_name:10s} {len(bucket)}") @@ -317,7 +325,7 @@ def _idle_days(record: dict) -> Optional[int]: def _cmd_prune(args) -> int: - """Bulk-archive agent-created skills idle for >= N days. + """Bulk-archive curator-managed skills idle for >= N days. Pinned skills are exempt. Already-archived skills are skipped. Default ``--days 90`` matches a conservative read of the curator's own archive @@ -333,7 +341,7 @@ def _cmd_prune(args) -> int: skip_confirm = bool(getattr(args, "yes", False)) candidates = [] - for r in skill_usage.agent_created_report(): + for r in skill_usage.curated_report(): if r.get("pinned"): continue if r.get("state") == skill_usage.STATE_ARCHIVED: @@ -491,7 +499,7 @@ def _cmd_list_archived(args) -> int: def _cmd_usage(args) -> int: """Show usage telemetry for ALL skills, with provenance. - Unlike `status` (curator-scoped to agent-created candidates), this lists + Unlike `status` (curator-scoped to curated 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. """ @@ -635,7 +643,7 @@ def register_cli(parent: argparse.ArgumentParser) -> None: p_prune = subs.add_parser( "prune", - help="Bulk-archive agent-created skills idle for >= N days (default 90)", + help="Bulk-archive curator-managed skills idle for >= N days (default 90)", ) p_prune.add_argument( "--days", type=int, default=90, diff --git a/tests/hermes_cli/test_curator_archive_prune.py b/tests/hermes_cli/test_curator_archive_prune.py index ef8b914a8bf4..eecb75ee0beb 100644 --- a/tests/hermes_cli/test_curator_archive_prune.py +++ b/tests/hermes_cli/test_curator_archive_prune.py @@ -101,7 +101,7 @@ def test_prune_nothing_to_do(monkeypatch, capsys): import hermes_cli.curator as curator_cli import tools.skill_usage as skill_usage - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: []) + monkeypatch.setattr(skill_usage, "curated_report", lambda: []) rc = curator_cli._cmd_prune(_ns(days=30, yes=True, dry_run=False)) assert rc == 0 assert "nothing to prune" in capsys.readouterr().out @@ -117,7 +117,7 @@ def test_prune_filters_pinned_and_archived(monkeypatch, capsys): _mk_record("recent", idle_days=10), _mk_record("old-active", idle_days=200), ] - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: rows) + monkeypatch.setattr(skill_usage, "curated_report", lambda: rows) archived = [] monkeypatch.setattr( skill_usage, "archive_skill", @@ -144,7 +144,7 @@ def test_prune_falls_back_to_created_at_when_never_used(monkeypatch, capsys): # Force last_activity_at to None explicitly rows[0]["last_activity_at"] = None - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: rows) + monkeypatch.setattr(skill_usage, "curated_report", lambda: rows) archived = [] monkeypatch.setattr( skill_usage, "archive_skill", @@ -160,7 +160,7 @@ def test_prune_dry_run_makes_no_changes(monkeypatch, capsys): import tools.skill_usage as skill_usage rows = [_mk_record("old-skill", idle_days=200)] - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: rows) + monkeypatch.setattr(skill_usage, "curated_report", lambda: rows) archived = [] monkeypatch.setattr( skill_usage, "archive_skill", @@ -179,7 +179,7 @@ def test_prune_prompts_without_yes(monkeypatch, capsys): import tools.skill_usage as skill_usage rows = [_mk_record("old-skill", idle_days=200)] - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: rows) + monkeypatch.setattr(skill_usage, "curated_report", lambda: rows) archived = [] monkeypatch.setattr( skill_usage, "archive_skill", @@ -197,7 +197,7 @@ def test_prune_confirms_with_y(monkeypatch, capsys): import tools.skill_usage as skill_usage rows = [_mk_record("old-skill", idle_days=200)] - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: rows) + monkeypatch.setattr(skill_usage, "curated_report", lambda: rows) archived = [] monkeypatch.setattr( skill_usage, "archive_skill", @@ -217,7 +217,7 @@ def test_prune_reports_partial_failure(monkeypatch, capsys): _mk_record("ok-skill", idle_days=200), _mk_record("bad-skill", idle_days=200), ] - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: rows) + monkeypatch.setattr(skill_usage, "curated_report", lambda: rows) def fake_archive(name): if name == "bad-skill": diff --git a/tests/hermes_cli/test_curator_status.py b/tests/hermes_cli/test_curator_status.py index 2075ebc2b690..46d3b9fb8d93 100644 --- a/tests/hermes_cli/test_curator_status.py +++ b/tests/hermes_cli/test_curator_status.py @@ -32,7 +32,7 @@ def test_status_uses_last_activity_not_only_last_used(monkeypatch, capsys): monkeypatch.setattr(curator_state, "get_interval_hours", lambda: 168) monkeypatch.setattr(curator_state, "get_stale_after_days", lambda: 30) monkeypatch.setattr(curator_state, "get_archive_after_days", lambda: 90) - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: [ + monkeypatch.setattr(skill_usage, "curated_report", lambda: [ { "name": "recently-viewed", "state": "active", @@ -171,7 +171,7 @@ def test_status_hides_most_active_when_all_zero(curator_status_env): def test_status_no_skills_produces_clean_empty_output(curator_status_env): env = curator_status_env out = _capture_status(env["curator_cli"]) - assert "no agent-created skills" in out + assert "no curator-managed skills" in out # None of the ranking sections render assert "most active" not in out assert "least active" not in out @@ -194,7 +194,7 @@ def test_status_marks_missing_last_report_path(monkeypatch, capsys, tmp_path): monkeypatch.setattr(curator_state, "get_interval_hours", lambda: 168) monkeypatch.setattr(curator_state, "get_stale_after_days", lambda: 30) monkeypatch.setattr(curator_state, "get_archive_after_days", lambda: 90) - monkeypatch.setattr(skill_usage, "agent_created_report", lambda: []) + monkeypatch.setattr(skill_usage, "curated_report", lambda: []) assert curator_cli._cmd_status(SimpleNamespace()) == 0 diff --git a/tests/tools/test_skill_usage.py b/tests/tools/test_skill_usage.py index d167fa559169..fa5b9b4e89f1 100644 --- a/tests/tools/test_skill_usage.py +++ b/tests/tools/test_skill_usage.py @@ -786,7 +786,7 @@ def test_end_to_end_telemetry_tracked_but_lifecycle_refused(skills_home): def test_usage_report_covers_all_provenance(skills_home): """usage_report() surfaces every skill with provenance, unlike the - curator-scoped agent_created_report().""" + curator-scoped curated_report().""" from tools.skill_usage import ( bump_use, usage_report, mark_agent_created, ) diff --git a/tools/skill_usage.py b/tools/skill_usage.py index ee5a35d2ce79..e62fbefbffe3 100644 --- a/tools/skill_usage.py +++ b/tools/skill_usage.py @@ -875,11 +875,15 @@ def _find_external_skill_dir(skill_name: str) -> Optional[Path]: # Reporting — for the curator CLI / slash command # --------------------------------------------------------------------------- -def agent_created_report() -> List[Dict[str, Any]]: - """Return a list of {name, state, pinned, last_activity_at, ...} +def curated_report() -> List[Dict[str, Any]]: + """Return a list of {name, provenance, state, pinned, last_activity_at, ...} records for every curator-managed skill. Missing usage records are backfilled with defaults so callers can always index fields. + ``provenance`` is 'agent', 'bundled', or 'hub' (see :func:`provenance`). + Bundled skills are only included when ``curator.prune_builtins`` is enabled. + Hub-installed skills are never included. + Each row carries ``_persisted``: True when a real record exists in ``.usage.json``, False when the row is a fresh backfill (e.g. a built-in seen for the first time). The curator uses this to seed the inactivity @@ -897,10 +901,22 @@ def agent_created_report() -> List[Dict[str, Any]]: row = {"name": name, **rec, "_persisted": persisted} row["last_activity_at"] = latest_activity_at(row) row["activity_count"] = activity_count(row) + row["provenance"] = provenance(name) rows.append(row) return rows +def agent_created_report() -> List[Dict[str, Any]]: + """DEPRECATED — use :func:`curated_report` instead. + + Used to return everything :func:`curated_report` returns (including bundled + skills when ``curator.prune_builtins`` is enabled), which made the + "agent-created" name misleading. Kept as a compatibility alias for + external callers; new code should call ``curated_report()``. + """ + return curated_report() + + def provenance(skill_name: str) -> str: """Classify a skill's origin: 'hub', 'bundled', or 'agent'. @@ -917,7 +933,7 @@ def provenance(skill_name: str) -> str: def usage_report() -> List[Dict[str, Any]]: """Return usage telemetry for EVERY skill on disk, with provenance. - Unlike ``agent_created_report()`` (which is scoped to curator-managed + Unlike ``curated_report()`` (which is scoped to curator-managed candidates), this surfaces all skills — bundled built-ins and hub-installed included — so callers can answer "how often is this skill used" independent of whether it's ever curated. Rows carry a