diff --git a/hermes_cli/curator.py b/hermes_cli/curator.py index 50c297217c..ed86a92c26 100644 --- a/hermes_cli/curator.py +++ b/hermes_cli/curator.py @@ -12,6 +12,7 @@ from __future__ import annotations import argparse import sys from datetime import datetime, timezone +from pathlib import Path from typing import Optional @@ -57,7 +58,8 @@ def _cmd_status(args) -> int: print(f" last summary: {summary}") _report = state.get("last_report_path") if _report: - print(f" last report: {_report}") + suffix = "" if Path(_report).exists() else " (missing)" + print(f" last report: {_report}{suffix}") _ih = curator.get_interval_hours() _interval_label = ( f"{_ih // 24}d" if _ih % 24 == 0 and _ih >= 24 @@ -161,6 +163,8 @@ def _cmd_run(args) -> int: return 1 dry = bool(getattr(args, "dry_run", False)) + background = bool(getattr(args, "background", False)) + synchronous = bool(getattr(args, "synchronous", False)) or not background if dry: print("curator: running DRY-RUN (report only, no mutations)...") else: @@ -171,7 +175,7 @@ def _cmd_run(args) -> int: result = curator.run_curator_review( on_summary=_on_summary, - synchronous=bool(args.synchronous), + synchronous=synchronous, dry_run=dry, ) auto = result.get("auto_transitions", {}) @@ -188,13 +192,19 @@ def _cmd_run(args) -> int: f"archived={auto.get('archived', 0)} " f"reactivated={auto.get('reactivated', 0)}" ) - if not args.synchronous: + if not synchronous: print("llm pass running in background — check `hermes curator status` later") if dry: - print( - "dry-run: no changes applied. When the report lands, read it with " - "`hermes curator status` and run `hermes curator run` (no flag) to apply." - ) + if synchronous: + print( + "dry-run: no changes applied. Read the report with " + "`hermes curator status` and run `hermes curator run` (no flag) to apply." + ) + else: + print( + "dry-run: no changes applied. When the report lands, read it with " + "`hermes curator status` and run `hermes curator run` (no flag) to apply." + ) return 0 @@ -461,7 +471,11 @@ def register_cli(parent: argparse.ArgumentParser) -> None: p_run = subs.add_parser("run", help="Trigger a curator review now") p_run.add_argument( "--sync", "--synchronous", dest="synchronous", action="store_true", - help="Wait for the LLM review pass to finish (default: background thread)", + help="Wait for the LLM review pass to finish (default for manual runs)", + ) + p_run.add_argument( + "--background", dest="background", action="store_true", + help="Start the LLM review pass in a background thread and return immediately", ) p_run.add_argument( "--dry-run", dest="dry_run", action="store_true", diff --git a/tests/hermes_cli/test_curator_run.py b/tests/hermes_cli/test_curator_run.py new file mode 100644 index 0000000000..2e0b3fbd93 --- /dev/null +++ b/tests/hermes_cli/test_curator_run.py @@ -0,0 +1,87 @@ +"""Tests for `hermes curator run` CLI behavior.""" + +from __future__ import annotations + +from types import SimpleNamespace + + +def _args(**kwargs): + values = { + "dry_run": False, + "synchronous": False, + "background": False, + } + values.update(kwargs) + return SimpleNamespace(**values) + + +def test_run_defaults_to_synchronous(monkeypatch, capsys): + import agent.curator as curator_state + import hermes_cli.curator as curator_cli + + calls = [] + monkeypatch.setattr(curator_state, "is_enabled", lambda: True) + monkeypatch.setattr( + curator_state, + "run_curator_review", + lambda **kwargs: calls.append(kwargs) or {"auto_transitions": {}}, + ) + + assert curator_cli._cmd_run(_args()) == 0 + + assert calls[0]["synchronous"] is True + assert calls[0]["dry_run"] is False + assert "background" not in capsys.readouterr().out + + +def test_run_background_opts_into_async(monkeypatch, capsys): + import agent.curator as curator_state + import hermes_cli.curator as curator_cli + + calls = [] + monkeypatch.setattr(curator_state, "is_enabled", lambda: True) + monkeypatch.setattr( + curator_state, + "run_curator_review", + lambda **kwargs: calls.append(kwargs) or {"auto_transitions": {}}, + ) + + assert curator_cli._cmd_run(_args(background=True)) == 0 + + assert calls[0]["synchronous"] is False + assert "llm pass running in background" in capsys.readouterr().out + + +def test_run_sync_wins_over_background(monkeypatch): + import agent.curator as curator_state + import hermes_cli.curator as curator_cli + + calls = [] + monkeypatch.setattr(curator_state, "is_enabled", lambda: True) + monkeypatch.setattr( + curator_state, + "run_curator_review", + lambda **kwargs: calls.append(kwargs) or {"auto_transitions": {}}, + ) + + assert curator_cli._cmd_run(_args(synchronous=True, background=True)) == 0 + + assert calls[0]["synchronous"] is True + + +def test_dry_run_default_reports_synchronous_wording(monkeypatch, capsys): + import agent.curator as curator_state + import hermes_cli.curator as curator_cli + + monkeypatch.setattr(curator_state, "is_enabled", lambda: True) + monkeypatch.setattr( + curator_state, + "run_curator_review", + lambda **kwargs: {"auto_transitions": {}}, + ) + + assert curator_cli._cmd_run(_args(dry_run=True)) == 0 + + out = capsys.readouterr().out + assert "When the report lands" not in out + assert "Read the report with `hermes curator status`" in out diff --git a/tests/hermes_cli/test_curator_status.py b/tests/hermes_cli/test_curator_status.py index b4c3548c42..2075ebc2b6 100644 --- a/tests/hermes_cli/test_curator_status.py +++ b/tests/hermes_cli/test_curator_status.py @@ -175,3 +175,28 @@ def test_status_no_skills_produces_clean_empty_output(curator_status_env): # None of the ranking sections render assert "most active" not in out assert "least active" not in out + + +def test_status_marks_missing_last_report_path(monkeypatch, capsys, tmp_path): + import agent.curator as curator_state + import hermes_cli.curator as curator_cli + import tools.skill_usage as skill_usage + + missing_report = tmp_path / "stale-report" + monkeypatch.setattr(curator_state, "load_state", lambda: { + "paused": False, + "last_run_at": None, + "last_run_summary": "auto: no changes", + "run_count": 1, + "last_report_path": str(missing_report), + }) + monkeypatch.setattr(curator_state, "is_enabled", lambda: True) + 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: []) + + assert curator_cli._cmd_status(SimpleNamespace()) == 0 + + out = capsys.readouterr().out + assert f"last report: {missing_report} (missing)" in out