{"repo":"NousResearch/hermes-agent","public_id":"18810","display_label":"Bump skill usage when cron jobs load skills","summary":"All three members have near-identical titles about incrementing skill usage when cron jobs load skills; two are scoped to 'cron' and one to 'curator'. The titles suggest duplicate or closely related changes, but no further detail is available from the titles alone.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19433,"merged_at":"2026-05-04T00:06:49+00:00"},"resolution":{"state":"resolved","by":"pr-19433"},"freshness":{"state":"verified","last_verified_at":"2026-05-04T00:06:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":18810,"pr_number":19433,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-04T00:06:49+00:00"}],"members":[{"kind":"issue","number":18810,"title":"fix(curator): bump skill usage when cron jobs load skills","url":null,"state":"closed"},{"kind":"pr","number":18811,"title":"fix(cron): bump skill usage when cron jobs load skills","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19433","number":19433}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19433,"title":"fix(cron): bump skill usage when cron jobs load skills","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cron/scheduler.py","tests/cron/test_scheduler.py"],"fix_hunks":[{"file":"cron/scheduler.py","hunks":["@@ -782,6 +782,7 @@ def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:\n         return prompt\n \n     from tools.skills_tool import skill_view\n+    from tools.skill_usage import bump_use\n \n     parts = []\n     skipped: list[str] = []","@@ -793,6 +794,12 @@ def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:\n             skipped.append(skill_name)\n             continue\n \n+        # Bump usage so the curator sees this skill as actively used.\n+        try:\n+            bump_use(skill_name)\n+        except Exception:\n+            logger.debug(\"Cron job: failed to bump skill usage for '%s'\", skill_name, exc_info=True)\n+\n         content = str(loaded.get(\"content\") or \"\").strip()\n         if parts:\n             parts.append(\"\")"]},{"file":"tests/cron/test_scheduler.py","hunks":["@@ -1857,6 +1857,54 @@ def _mixed_skill_view(name: str) -> str:\n         assert \"go\" in result\n \n \n+class TestBuildJobPromptBumpUse:\n+    \"\"\"Verify that cron jobs bump skill usage counters so the curator sees them as active.\"\"\"\n+\n+    def test_bump_use_called_for_loaded_skill(self):\n+        \"\"\"bump_use is called for each successfully loaded skill.\"\"\"\n+\n+        def _skill_view(name: str) -> str:\n+            return json.dumps({\"success\": True, \"content\": f\"Content for {name}.\"})\n+\n+        with patch(\"tools.skills_tool.skill_view\", side_effect=_skill_view), \\\n+             patch(\"tools.skill_usage.bump_use\") as mock_bump:\n+            _build_job_prompt({\"skills\": [\"alpha\", \"beta\"], \"prompt\": \"go\"})\n+\n+        assert mock_bump.call_count == 2\n+        calls = [c[0][0] for c in mock_bump.call_args_list]\n+        assert \"alpha\" in calls\n+        assert \"beta\" in calls\n+\n+    def test_bump_use_not_called_for_missing_skill(self):\n+        \"\"\"bump_use is NOT called when a skill fails to load.\"\"\"\n+\n+        def _missing_view(name: str) -> str:\n+            return json.dumps({\"success\": False, \"error\": \"not found\"})\n+\n+        with patch(\"tools.skills_tool.skill_view\", side_effect=_missing_view), \\\n+             patch(\"tools.skill_usage.bump_use\") as mock_bump:\n+            _build_job_prompt({\"skills\": [\"ghost\"], \"prompt\": \"go\"})\n+\n+        assert mock_bump.call_count == 0\n+\n+    def test_bump_failure_does_not_break_prompt(self, caplog):\n+        \"\"\"If bump_use raises, the prompt still builds — error is logged at DEBUG.\"\"\"\n+\n+        def _skill_view(name: str) -> str:\n+            return json.dumps({\"success\": True, \"content\": \"Works.\"})\n+\n+        with patch(\"tools.skills_tool.skill_view\", side_effect=_skill_view), \\\n+             patch(\"tools.skill_usage.bump_use\", side_effect=RuntimeError(\"boom\")), \\\n+             caplog.at_level(logging.DEBUG, logger=\"cron.scheduler\"):\n+            result = _build_job_prompt({\"skills\": [\"good-skill\"], \"prompt\": \"go\"})\n+\n+        # Prompt should still contain the skill content and original instruction\n+        assert \"Works.\" in result\n+        assert \"go\" in result\n+        # The error should be logged at DEBUG level, not crash\n+        assert any(\"failed to bump\" in r.message for r in caplog.records)\n+\n+\n class TestSendMediaViaAdapter:\n     \"\"\"Unit tests for _send_media_via_adapter — routes files to typed adapter methods.\"\"\"\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}