fix: curator labels bundled skills as agent-created (#64393)

This commit is contained in:
webtecnica 2026-07-14 11:25:14 -03:00 committed by Teknium
parent b6accee0d7
commit b9fedab47a
6 changed files with 56 additions and 31 deletions

View file

@ -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:

View file

@ -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,

View file

@ -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":

View file

@ -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

View file

@ -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,
)

View file

@ -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