feat(cron-recipes): /cron-recipe <name> seeds a conversational fill

Reworks the chat-line UX: pick a recipe by name and the agent asks you for
what it needs, one question at a time, instead of forcing you to hand-type a
slot=val command line.

- /cron-recipe                  -> lists the catalog
- /cron-recipe <name>           -> forgiving name match (exact/prefix/substring/
                                   fuzzy; ambiguous lists candidates), then seeds
                                   the agent with a natural-language fill request
                                   built from the recipe's typed slots + schedule
                                   and prompt templates. The agent asks for each
                                   value one at a time and calls the EXISTING
                                   cronjob tool. No new tool.
- /cron-recipe <name> slot=val  -> unchanged deterministic path (fill_recipe ->
                                   create_job) for the dashboard/docs/power user.

Mechanism (no new plumbing, invariant-safe — the seed enters as a normal user
turn, never a synthetic injection):
- shared handler returns RecipeCommandResult{text, agent_seed}; match_recipe()
  and build_recipe_seed() are the new shared pieces.
- gateway: dispatch rewrites event.text to the seed and falls through to the
  agent (the same pattern /steer uses).
- CLI: handler sets a one-shot self._pending_agent_seed; the interactive loop
  consumes it right after process_command() and runs it as the next turn.

The typed-slot schema stays the single source of truth (still validates the
form/inline path via fill_recipe); the agent path just renders those slots into
the questions to ask. Docs updated to lead with the name-then-ask flow.
This commit is contained in:
teknium1
2026-06-11 10:49:47 -07:00
committed by Teknium
parent 1593ca5406
commit e976faac7a
7 changed files with 299 additions and 75 deletions
+34 -11
View File
@@ -143,20 +143,41 @@ class TestCommandHandler:
def test_bare_lists_catalog(self, isolated_home):
from hermes_cli.cron_recipe_cmd import handle_cron_recipe_command
out = handle_cron_recipe_command("")
assert "morning-brief" in out and "Cron Recipes" in out
res = handle_cron_recipe_command("")
assert "morning-brief" in res.text and "Cron Recipes" in res.text
assert res.agent_seed is None
def test_show_recipe_fields(self, isolated_home):
def test_name_seeds_agent(self, isolated_home):
from hermes_cli.cron_recipe_cmd import handle_cron_recipe_command
out = handle_cron_recipe_command("morning-brief")
assert "Fields:" in out and "time" in out
# `/cron-recipe <name>` (no inline slots) now seeds the agent to ask
# the user for each value conversationally instead of dumping fields.
res = handle_cron_recipe_command("morning-brief")
assert res.agent_seed is not None
assert "morning-brief" in res.agent_seed
assert "cronjob tool" in res.agent_seed
# the schedule template is handed to the agent to build the cron expr
assert "* * *" in res.agent_seed
def test_name_match_is_forgiving(self, isolated_home):
from hermes_cli.cron_recipe_cmd import handle_cron_recipe_command, match_recipe
# prefix match
r, cands = match_recipe("morning")
assert r is not None and r.key == "morning-brief"
# fuzzy / typo
r2, _ = match_recipe("mornning-brief")
assert r2 is not None and r2.key == "morning-brief"
# a forgiving name still seeds the agent
res = handle_cron_recipe_command("morning")
assert res.agent_seed is not None
def test_fill_creates_job(self, isolated_home):
from hermes_cli.cron_recipe_cmd import handle_cron_recipe_command
out = handle_cron_recipe_command("morning-brief time=07:30 deliver=telegram")
assert "Scheduled" in out
res = handle_cron_recipe_command("morning-brief time=07:30 deliver=telegram")
assert "Scheduled" in res.text
assert res.agent_seed is None
jobs = isolated_home.load_jobs()
assert len(jobs) == 1
assert (jobs[0].get("schedule_display") or jobs[0].get("schedule")) == "30 7 * * *"
@@ -165,14 +186,16 @@ class TestCommandHandler:
def test_unknown_recipe(self, isolated_home):
from hermes_cli.cron_recipe_cmd import handle_cron_recipe_command
out = handle_cron_recipe_command("does-not-exist")
assert "No cron recipe" in out
res = handle_cron_recipe_command("zzz-nope-nothing")
assert "No cron recipe" in res.text
assert res.agent_seed is None
def test_bad_value_names_slot(self, isolated_home):
from hermes_cli.cron_recipe_cmd import handle_cron_recipe_command
out = handle_cron_recipe_command("morning-brief time=99:99")
assert "Can't set up" in out and "time" in out
res = handle_cron_recipe_command("morning-brief time=99:99")
assert "Can't set up" in res.text and "time" in res.text
assert res.agent_seed is None
class TestDocsGenerator: