fix(curator): make manual runs synchronous

This commit is contained in:
LeonSGP43 2026-05-06 12:25:46 +08:00 committed by Teknium
parent bda7b240b4
commit 6b9f7140bb
3 changed files with 134 additions and 8 deletions

View file

@ -12,6 +12,7 @@ from __future__ import annotations
import argparse import argparse
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path
from typing import Optional from typing import Optional
@ -57,7 +58,8 @@ def _cmd_status(args) -> int:
print(f" last summary: {summary}") print(f" last summary: {summary}")
_report = state.get("last_report_path") _report = state.get("last_report_path")
if _report: 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() _ih = curator.get_interval_hours()
_interval_label = ( _interval_label = (
f"{_ih // 24}d" if _ih % 24 == 0 and _ih >= 24 f"{_ih // 24}d" if _ih % 24 == 0 and _ih >= 24
@ -161,6 +163,8 @@ def _cmd_run(args) -> int:
return 1 return 1
dry = bool(getattr(args, "dry_run", False)) dry = bool(getattr(args, "dry_run", False))
background = bool(getattr(args, "background", False))
synchronous = bool(getattr(args, "synchronous", False)) or not background
if dry: if dry:
print("curator: running DRY-RUN (report only, no mutations)...") print("curator: running DRY-RUN (report only, no mutations)...")
else: else:
@ -171,7 +175,7 @@ def _cmd_run(args) -> int:
result = curator.run_curator_review( result = curator.run_curator_review(
on_summary=_on_summary, on_summary=_on_summary,
synchronous=bool(args.synchronous), synchronous=synchronous,
dry_run=dry, dry_run=dry,
) )
auto = result.get("auto_transitions", {}) auto = result.get("auto_transitions", {})
@ -188,13 +192,19 @@ def _cmd_run(args) -> int:
f"archived={auto.get('archived', 0)} " f"archived={auto.get('archived', 0)} "
f"reactivated={auto.get('reactivated', 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") print("llm pass running in background — check `hermes curator status` later")
if dry: if dry:
print( if synchronous:
"dry-run: no changes applied. When the report lands, read it with " print(
"`hermes curator status` and run `hermes curator run` (no flag) to apply." "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 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 = subs.add_parser("run", help="Trigger a curator review now")
p_run.add_argument( p_run.add_argument(
"--sync", "--synchronous", dest="synchronous", action="store_true", "--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( p_run.add_argument(
"--dry-run", dest="dry_run", action="store_true", "--dry-run", dest="dry_run", action="store_true",

View file

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

View file

@ -175,3 +175,28 @@ def test_status_no_skills_produces_clean_empty_output(curator_status_env):
# None of the ranking sections render # None of the ranking sections render
assert "most active" not in out assert "most active" not in out
assert "least 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