From 7312f7f849e892cbe6534e5e6cc32ae5b6d7a474 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 10 May 2026 06:44:53 -0700 Subject: [PATCH] feat(curator): hint at `hermes curator pin` in the rename block (#23212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces the pin command at the moment users care about it: when a consolidation just landed against their skill library and they're looking at the umbrella name in the curator output. Previously `hermes curator pin` existed but had no discovery surface — users only learned it existed by reading docs or stumbling onto `hermes curator --help`. The hint: archived 3 skill(s): • docx-extraction → document-tools • pdf-extraction → document-tools • old-stale — pruned (stale) full report: hermes curator status keep an umbrella stable: hermes curator pin document-tools Gated on having at least one consolidation that produced an umbrella. Pruned-only runs (nothing surviving to pin) skip the hint. When multiple umbrellas were produced, picks alphabetically first as a concrete example rather than listing them all. 3 new tests in tests/agent/test_curator_classification.py covering: consolidation produces hint with real umbrella name, pruned-only run omits it, multi-umbrella picks one example. --- agent/curator.py | 16 +++- tests/agent/test_curator_classification.py | 103 +++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/agent/curator.py b/agent/curator.py index f9c10d05656..d0147d4c4fb 100644 --- a/agent/curator.py +++ b/agent/curator.py @@ -899,9 +899,12 @@ def _build_rename_summary( • flaky-thing — pruned (stale) • old-utility → spreadsheet-ops full report: hermes curator status + keep an umbrella stable: hermes curator pin document-tools Cap is 10 entries so a 50-skill consolidation doesn't blow up - agent.log; the full list is always in REPORT.md. + agent.log; the full list is always in REPORT.md. The pin hint only + appears when at least one consolidation produced an umbrella worth + pinning (pruned-only runs skip it). """ after_by_name = {r.get("name"): r for r in after_report if isinstance(r, dict)} after_names = set(after_by_name.keys()) @@ -950,6 +953,17 @@ def _build_rename_summary( if total > SHOW: lines.append(f" … and {total - SHOW} more") lines.append("full report: hermes curator status") + # Pin hint — only surface it when there's actually a destination skill + # worth pinning. The umbrella skills that absorbed content are the natural + # candidates: pinning one tells future curator runs to leave it alone. + # Pruned-only runs don't get this hint (nothing surviving to pin). + if consolidated: + umbrellas = sorted({e.get("into") for e in consolidated if e.get("into")}) + if umbrellas: + example = umbrellas[0] + lines.append( + f"keep an umbrella stable: hermes curator pin {example}" + ) return "\n".join(lines) diff --git a/tests/agent/test_curator_classification.py b/tests/agent/test_curator_classification.py index 29187c5a641..804e5a65ecc 100644 --- a/tests/agent/test_curator_classification.py +++ b/tests/agent/test_curator_classification.py @@ -1020,3 +1020,106 @@ def test_rename_summary_mixed_consolidation_and_pruning(curator_env): assert merge_idx < drop_idx, "consolidated should render before pruned" assert "merge-me → umbrella" in lines[merge_idx] assert "drop-me — pruned (stale)" in lines[drop_idx] + + +# --------------------------------------------------------------------------- +# Pin hint — surfaces `hermes curator pin ` in the rename block so +# users learn the command exists at the moment they care (a consolidation +# just landed against their library). The hint is gated on having at least +# one umbrella destination — pruned-only runs skip it. +# --------------------------------------------------------------------------- + + +def test_rename_summary_pin_hint_appears_when_consolidation_produced_umbrella(curator_env): + """When at least one skill was absorbed into an umbrella, hint at pinning it.""" + result = curator_env._build_rename_summary( + before_names={"pdf-extraction", "docx-extraction", "document-tools"}, + after_report=[{"name": "document-tools", "state": "active"}], + tool_calls=[ + { + "name": "skill_manage", + "arguments": json.dumps({ + "action": "delete", + "name": "pdf-extraction", + "absorbed_into": "document-tools", + }), + }, + { + "name": "skill_manage", + "arguments": json.dumps({ + "action": "delete", + "name": "docx-extraction", + "absorbed_into": "document-tools", + }), + }, + ], + model_final="", + ) + assert "hermes curator pin document-tools" in result + assert "keep an umbrella stable" in result + + +def test_rename_summary_pin_hint_skipped_for_pruned_only_runs(curator_env): + """Pruned-only runs have nothing surviving to pin — hint should not appear.""" + result = curator_env._build_rename_summary( + before_names={"old-flaky-thing", "another-stale", "keeper"}, + after_report=[{"name": "keeper", "state": "active"}], + tool_calls=[ + { + "name": "skill_manage", + "arguments": json.dumps({ + "action": "delete", + "name": "old-flaky-thing", + "absorbed_into": "", + }), + }, + { + "name": "skill_manage", + "arguments": json.dumps({ + "action": "delete", + "name": "another-stale", + "absorbed_into": "", + }), + }, + ], + model_final="", + ) + # Block still renders (skills were archived) but no pin hint. + assert "archived 2 skill(s):" in result + assert "hermes curator pin" not in result + assert "keep an umbrella stable" not in result + + +def test_rename_summary_pin_hint_picks_one_umbrella_when_multiple_absorbed(curator_env): + """Multiple umbrellas → hint shows one example (alphabetically first), not a list.""" + result = curator_env._build_rename_summary( + before_names={"a-skill", "b-skill", "umbrella-zeta", "umbrella-alpha"}, + after_report=[ + {"name": "umbrella-zeta", "state": "active"}, + {"name": "umbrella-alpha", "state": "active"}, + ], + tool_calls=[ + { + "name": "skill_manage", + "arguments": json.dumps({ + "action": "delete", + "name": "a-skill", + "absorbed_into": "umbrella-zeta", + }), + }, + { + "name": "skill_manage", + "arguments": json.dumps({ + "action": "delete", + "name": "b-skill", + "absorbed_into": "umbrella-alpha", + }), + }, + ], + model_final="", + ) + # Sorted picks alphabetically first. + assert "hermes curator pin umbrella-alpha" in result + # Exactly one hint line, not one per umbrella. + pin_lines = [ln for ln in result.splitlines() if "hermes curator pin" in ln] + assert len(pin_lines) == 1