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 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",

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