Skip to content

Commit 6b9f714

Browse files
LeonSGP43teknium1
authored andcommitted
fix(curator): make manual runs synchronous
1 parent bda7b24 commit 6b9f714

3 files changed

Lines changed: 134 additions & 8 deletions

File tree

hermes_cli/curator.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import argparse
1313
import sys
1414
from datetime import datetime, timezone
15+
from pathlib import Path
1516
from typing import Optional
1617

1718

@@ -57,7 +58,8 @@ def _cmd_status(args) -> int:
5758
print(f" last summary: {summary}")
5859
_report = state.get("last_report_path")
5960
if _report:
60-
print(f" last report: {_report}")
61+
suffix = "" if Path(_report).exists() else " (missing)"
62+
print(f" last report: {_report}{suffix}")
6163
_ih = curator.get_interval_hours()
6264
_interval_label = (
6365
f"{_ih // 24}d" if _ih % 24 == 0 and _ih >= 24
@@ -161,6 +163,8 @@ def _cmd_run(args) -> int:
161163
return 1
162164

163165
dry = bool(getattr(args, "dry_run", False))
166+
background = bool(getattr(args, "background", False))
167+
synchronous = bool(getattr(args, "synchronous", False)) or not background
164168
if dry:
165169
print("curator: running DRY-RUN (report only, no mutations)...")
166170
else:
@@ -171,7 +175,7 @@ def _on_summary(msg: str) -> None:
171175

172176
result = curator.run_curator_review(
173177
on_summary=_on_summary,
174-
synchronous=bool(args.synchronous),
178+
synchronous=synchronous,
175179
dry_run=dry,
176180
)
177181
auto = result.get("auto_transitions", {})
@@ -188,13 +192,19 @@ def _on_summary(msg: str) -> None:
188192
f"archived={auto.get('archived', 0)} "
189193
f"reactivated={auto.get('reactivated', 0)}"
190194
)
191-
if not args.synchronous:
195+
if not synchronous:
192196
print("llm pass running in background — check `hermes curator status` later")
193197
if dry:
194-
print(
195-
"dry-run: no changes applied. When the report lands, read it with "
196-
"`hermes curator status` and run `hermes curator run` (no flag) to apply."
197-
)
198+
if synchronous:
199+
print(
200+
"dry-run: no changes applied. Read the report with "
201+
"`hermes curator status` and run `hermes curator run` (no flag) to apply."
202+
)
203+
else:
204+
print(
205+
"dry-run: no changes applied. When the report lands, read it with "
206+
"`hermes curator status` and run `hermes curator run` (no flag) to apply."
207+
)
198208
return 0
199209

200210

@@ -461,7 +471,11 @@ def register_cli(parent: argparse.ArgumentParser) -> None:
461471
p_run = subs.add_parser("run", help="Trigger a curator review now")
462472
p_run.add_argument(
463473
"--sync", "--synchronous", dest="synchronous", action="store_true",
464-
help="Wait for the LLM review pass to finish (default: background thread)",
474+
help="Wait for the LLM review pass to finish (default for manual runs)",
475+
)
476+
p_run.add_argument(
477+
"--background", dest="background", action="store_true",
478+
help="Start the LLM review pass in a background thread and return immediately",
465479
)
466480
p_run.add_argument(
467481
"--dry-run", dest="dry_run", action="store_true",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Tests for `hermes curator run` CLI behavior."""
2+
3+
from __future__ import annotations
4+
5+
from types import SimpleNamespace
6+
7+
8+
def _args(**kwargs):
9+
values = {
10+
"dry_run": False,
11+
"synchronous": False,
12+
"background": False,
13+
}
14+
values.update(kwargs)
15+
return SimpleNamespace(**values)
16+
17+
18+
def test_run_defaults_to_synchronous(monkeypatch, capsys):
19+
import agent.curator as curator_state
20+
import hermes_cli.curator as curator_cli
21+
22+
calls = []
23+
monkeypatch.setattr(curator_state, "is_enabled", lambda: True)
24+
monkeypatch.setattr(
25+
curator_state,
26+
"run_curator_review",
27+
lambda **kwargs: calls.append(kwargs) or {"auto_transitions": {}},
28+
)
29+
30+
assert curator_cli._cmd_run(_args()) == 0
31+
32+
assert calls[0]["synchronous"] is True
33+
assert calls[0]["dry_run"] is False
34+
assert "background" not in capsys.readouterr().out
35+
36+
37+
def test_run_background_opts_into_async(monkeypatch, capsys):
38+
import agent.curator as curator_state
39+
import hermes_cli.curator as curator_cli
40+
41+
calls = []
42+
monkeypatch.setattr(curator_state, "is_enabled", lambda: True)
43+
monkeypatch.setattr(
44+
curator_state,
45+
"run_curator_review",
46+
lambda **kwargs: calls.append(kwargs) or {"auto_transitions": {}},
47+
)
48+
49+
assert curator_cli._cmd_run(_args(background=True)) == 0
50+
51+
assert calls[0]["synchronous"] is False
52+
assert "llm pass running in background" in capsys.readouterr().out
53+
54+
55+
def test_run_sync_wins_over_background(monkeypatch):
56+
import agent.curator as curator_state
57+
import hermes_cli.curator as curator_cli
58+
59+
calls = []
60+
monkeypatch.setattr(curator_state, "is_enabled", lambda: True)
61+
monkeypatch.setattr(
62+
curator_state,
63+
"run_curator_review",
64+
lambda **kwargs: calls.append(kwargs) or {"auto_transitions": {}},
65+
)
66+
67+
assert curator_cli._cmd_run(_args(synchronous=True, background=True)) == 0
68+
69+
assert calls[0]["synchronous"] is True
70+
71+
72+
def test_dry_run_default_reports_synchronous_wording(monkeypatch, capsys):
73+
import agent.curator as curator_state
74+
import hermes_cli.curator as curator_cli
75+
76+
monkeypatch.setattr(curator_state, "is_enabled", lambda: True)
77+
monkeypatch.setattr(
78+
curator_state,
79+
"run_curator_review",
80+
lambda **kwargs: {"auto_transitions": {}},
81+
)
82+
83+
assert curator_cli._cmd_run(_args(dry_run=True)) == 0
84+
85+
out = capsys.readouterr().out
86+
assert "When the report lands" not in out
87+
assert "Read the report with `hermes curator status`" in out

tests/hermes_cli/test_curator_status.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,28 @@ def test_status_no_skills_produces_clean_empty_output(curator_status_env):
175175
# None of the ranking sections render
176176
assert "most active" not in out
177177
assert "least active" not in out
178+
179+
180+
def test_status_marks_missing_last_report_path(monkeypatch, capsys, tmp_path):
181+
import agent.curator as curator_state
182+
import hermes_cli.curator as curator_cli
183+
import tools.skill_usage as skill_usage
184+
185+
missing_report = tmp_path / "stale-report"
186+
monkeypatch.setattr(curator_state, "load_state", lambda: {
187+
"paused": False,
188+
"last_run_at": None,
189+
"last_run_summary": "auto: no changes",
190+
"run_count": 1,
191+
"last_report_path": str(missing_report),
192+
})
193+
monkeypatch.setattr(curator_state, "is_enabled", lambda: True)
194+
monkeypatch.setattr(curator_state, "get_interval_hours", lambda: 168)
195+
monkeypatch.setattr(curator_state, "get_stale_after_days", lambda: 30)
196+
monkeypatch.setattr(curator_state, "get_archive_after_days", lambda: 90)
197+
monkeypatch.setattr(skill_usage, "agent_created_report", lambda: [])
198+
199+
assert curator_cli._cmd_status(SimpleNamespace()) == 0
200+
201+
out = capsys.readouterr().out
202+
assert f"last report: {missing_report} (missing)" in out

0 commit comments

Comments
 (0)