feat(cron): Automation Blueprints — parameterized automation templates across every surface - #41309
Merged
Conversation
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
10 |
not-subscriptable |
3 |
unresolved-attribute |
3 |
unresolved-import |
3 |
unsupported-operator |
1 |
First entries
tests/cron/test_suggestions.py:198: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
cron/scripts/classify_items.py:215: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `dict[str, Any] | None`
tests/cron/test_suggestions.py:198: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["monitor"]` on object of type `str`
tests/tools/test_blueprints.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/cli_commands_mixin.py:1302: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_blueprint_command` has no attribute `_console_print`
tests/cron/test_suggestions.py:198: [not-subscriptable] not-subscriptable: Cannot subscript object of type `float` with no `__getitem__` method
tests/cron/test_suggestions.py:198: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["monitor"]` on object of type `list[Unknown]`
tests/cron/test_blueprint_catalog.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/cron/test_suggestions.py:197: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["monitor"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/cron/test_suggestions.py:198: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["monitor"]` on object of type `list[str]`
tests/cron/test_suggestions.py:198: [not-subscriptable] not-subscriptable: Cannot subscript object of type `int` with no `__getitem__` method
tests/cron/test_suggestions.py:198: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["provider"]` on object of type `str`
tests/cron/test_blueprint_catalog.py:145: [invalid-argument-type] invalid-argument-type: Argument to function `blueprint_deeplink` is incorrect: Expected `AutomationBlueprint`, found `AutomationBlueprint | None`
tests/cron/test_blueprint_catalog.py:150: [invalid-argument-type] invalid-argument-type: Argument to function `blueprint_catalog_entry` is incorrect: Expected `AutomationBlueprint`, found `AutomationBlueprint | None`
hermes_cli/cli_commands_mixin.py:1277: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_suggestions_command` has no attribute `_console_print`
tests/cron/test_suggestions.py:13: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/cron/test_blueprint_catalog.py:140: [invalid-argument-type] invalid-argument-type: Argument to function `blueprint_slash_command` is incorrect: Expected `AutomationBlueprint`, found `AutomationBlueprint | None`
tests/cron/test_suggestions.py:198: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["provider"]` on object of type `list[Unknown]`
tests/cron/test_blueprint_catalog.py:121: [invalid-argument-type] invalid-argument-type: Argument to function `fill_blueprint` is incorrect: Expected `AutomationBlueprint`, found `AutomationBlueprint | None`
tests/cron/test_blueprint_catalog.py:128: [invalid-argument-type] invalid-argument-type: Argument to function `blueprint_form_schema` is incorrect: Expected `AutomationBlueprint`, found `AutomationBlueprint | None`
✅ Fixed issues: none
Unchanged: 5611 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
teknium1
force-pushed
the
feat/suggested-cron-jobs
branch
from
June 8, 2026 01:51
7b49a62 to
57b9901
Compare
teknium1
force-pushed
the
feat/suggested-cron-jobs
branch
4 times, most recently
from
June 8, 2026 17:02
f5e2859 to
9b08c71
Compare
teknium1
enabled auto-merge (rebase)
June 11, 2026 16:50
OutThisLife
previously approved these changes
Jun 11, 2026
auto-merge was automatically disabled
June 11, 2026 16:52
Rebase failed
Hermes can propose automations and let the user accept them with one tap via /suggestions, instead of making them assemble cron jobs by hand. Every proposal — wherever it originates — flows through one surface. Sources (the 'where suggestions come from'): - catalog: curated starter automations (daily briefing, important-mail monitor, weekly review, workday-start reminder) via /suggestions catalog - recipe: installing a skill that carries a metadata.hermes.recipe block registers a suggestion instead of auto-scheduling - usage / integration: reserved for the background-review detector and account-connect triggers (sources defined; emitters land next) Pieces: - cron/suggestions.py — the store. add/list/accept/dismiss, dedup+latch by key (dismissed proposals never re-offered), pending cap so it can't become a nag wall. Accepting calls the existing cron.jobs.create_job — there is NO second job engine. Mirrors jobs.py storage (atomic writes, lock, 0600). - cron/suggestion_catalog.py — the curated set. The important-mail monitor entry is where the old proactive-monitor poll->classify->surface engine lives now (cron/scripts/classify_items.py + the 'monitor' aux task), as ONE catalog automation rather than a standalone feature. - tools/recipes.py — recipe<->job bridge; register_recipe_suggestion() makes a recipe source 'recipe' of this surface. recipe_to_job_spec() is the single translation both the direct and suggestion paths share. - hermes_cli/suggestions_cmd.py — shared /suggestions handler (CLI + gateway never drift); /suggestions [accept N|dismiss N|catalog|clear]. - Wired: CommandDef + CLI dispatch (cli.py) + gateway dispatch (gateway/run.py) + aux 'monitor' task (config.py) + recipe-install hook (skills_hub.py). Consent-first throughout: nothing auto-schedules; acceptance is always explicit; dismissals latch. Supersedes #41122 (proactive-monitor) and #41127 (recipes): both fold in here as a catalog entry and a suggestion source respectively. Tests: store (dedup/cap/accept/dismiss/latch), catalog seeding+idempotency, recipe->suggestion bridge, command handler, aux config. E2E: recipe SKILL.md -> parsed -> suggested -> accepted -> real cron job persisted to jobs.json.
…every surface A 'recipe' is a one-place definition of an automation that every surface renders natively. The slot schema (cron/recipe_catalog.py) is the single source of truth; four renderers consume it, and all paths end at the same cron.jobs.create_job — no second job engine. Form where there's a screen, conversation where there's a chat line: - Dashboard / GUI app: a Recipes sub-tab on the Cron page renders each recipe's typed slots as a form (time-picker, enum dropdown, free-text); submit POSTs /api/cron/recipes/instantiate which fills + creates the job. - CLI / TUI / messengers: /cron-recipe lists the catalog, shows a recipe's fields, or fills + creates from a pasted 'key slot=val' command. The shared handler (hermes_cli/cron_recipe_cmd.py) names any missing/invalid slot so the agent can ask a targeted follow-up. - Docs: a generated Cron Recipes catalog page (website, .mdx + React cards) shows each recipe with a copy-paste command and a 'Send to App' button. - Desktop: a hermes:// URL scheme (Electron single-instance lock + setAsDefaultProtocolClient + open-url/second-instance) routes hermes://cron-recipe/<key>?slot=val into the chat composer pre-filled. Typed slots (time/enum/text/weekdays) with defaults: users never type raw cron — recipes parameterize time-of-day and weekday sets and translate to cron expressions; a free-text 'schedule' slot is the full-flexibility escape hatch. Consent-first throughout: nothing schedules without an explicit submit or send. Core: - cron/recipe_catalog.py — CronRecipe + RecipeSlot, 5 curated recipes, recipe_form_schema / recipe_slash_command / recipe_deeplink / recipe_catalog_entry renderers, fill_recipe (validate + translate to create_job kwargs). - hermes_cli/cron_recipe_cmd.py — shared /cron-recipe handler (CLI + TUI + gateway never drift). CommandDef + dispatch in commands.py / cli.py / gateway/run.py. Dashboard: GET /api/cron/recipes + POST /api/cron/recipes/instantiate (web_server.py), CronRecipes.tsx gallery+form, Segmented sub-tab on CronPage, api.ts methods + types. Desktop: hermes:// scheme end to end (main.cjs deep-link router + ready-queue, preload onDeepLink/signalDeepLinkReady, global.d.ts types, desktop-controller composer prefill, electron-builder protocols key). Docs: extract-cron-recipes.py generator wired into prebuild.mjs, cron-recipes-catalog.mdx + CronRecipesCatalog React component, sidebar entry. Generated index json gitignored like skills.json. Tests: 23 core (catalog/slots/schedule-resolution/validation/renderers/command handler/generator) + 5 web_server endpoint tests. E2E verified end to end: slot fill -> create_job -> persisted job with correct schedule/deliver/origin.
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.
…ot names, surface-aware UX Review fixes for the Cron Recipes stack before release: - hydration-move: */90 in the cron minute field silently wraps to hourly (croniter-verified) — 90/120-minute options never fired at their stated cadence. Replaced with an hour-field step (0 9-17/2 * * 1-5) and an interval_hours slot whose options (1/2/3h) all fire as labeled. - fill_recipe: reject unknown slot names. A typo'd 'tiem=07:15' used to silently create the job at the 08:00 default; now it 422s on the dashboard form and errors on the slash/deep-link paths with the valid slot list. - deliver slot: non-strict enum (options are suggestions, scheduler validates downstream) so slack/whatsapp/etc. users aren't locked out; GET /api/cron/recipes rewrites its options from cron_delivery_targets() so the dashboard form only offers configured platforms; help text no longer claims dashboard-created jobs deliver to 'the chat you set this up from' (the endpoint strips origin — they go to the home channel). - gateway: success/accept messages no longer point at /cron (cli_only); surface-aware hint instead. Conversational fill now sends the 'Setting up X — I'll ask you a couple of things…' ack before the agent turn, matching the CLI experience. - important-mail catalog entry: reference the urgency classifier by module path (python3 -m cron.scripts.classify_items) instead of baking an absolute host path into the job prompt — stale after relocation and nonexistent on remote terminal backends. cron/scripts is now a real package and ships in the wheel (pyproject packages.find). - export_recipe: interval schedules round-trip again — parse_schedule stores 'minutes' but the renderer only read 'seconds', so every interval job exported as the silent '0 9 * * *' fallback. - skills_hub install: say so when a recipe suggestion is dropped (latched dedup or pending cap) instead of printing nothing. Targeted tests: 58 cron/recipe + 261 web_server pass; E2E-validated all 14 recipes fill+parse, hydration cadences via croniter, typo rejection on slash + endpoint paths, surface-aware hints, and interval export round-trip.
…t the 50-cap CI tests the PR merged with current main, where the new /memory canonical command filled Slack's 50-slash cap: with btw/bg/reset all pinned ahead of canonicals, the last canonical (/debug) got clamped and the Telegram-parity test failed. Canonical commands must win slots over alias spellings — /new keeps its native slot and 'reset' stays reachable via /hermes reset. Also updates test_includes_aliases_as_first_class_slashes to assert the pinned-alias contract (_SLACK_PRIORITY_ALIASES survive) instead of a specific unpinned alias's survival, which was the same change-detector pattern the docstring already warned about.
teknium1
force-pushed
the
feat/suggested-cron-jobs
branch
from
June 11, 2026 17:07
80123fd to
9c07e06
Compare
Product rename across every surface: module/file names (blueprint_catalog, tools/blueprints, blueprint_cmd), slash command /cron-recipe -> /blueprint (alias /bp), dashboard API /api/cron/blueprints, desktop deep-link hermes://blueprint/<key>, docs catalog page + extract script, and the skill frontmatter block metadata.hermes.blueprint. No behavior change.
austinpickett
self-requested a review
June 11, 2026 17:39
austinpickett
approved these changes
Jun 11, 2026
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pick an automation by name and Hermes asks you for what it needs — no cron syntax, no
slot=valuetyping. One blueprint definition renders natively on every surface (dashboard form, CLI/TUI/messenger slash command, agent conversation, docs catalog), and every path ends at the samecron.jobs.create_job— there is no second job engine.This PR has three layers:
Suggested Cron Jobs — Hermes can propose automations and you accept them with one tap via
/suggestions, instead of assembling cron jobs by hand. Sources: a curated catalog, blueprint-carrying skills, and (reserved) usage/integration detectors. Consent-first: nothing schedules without explicit acceptance; dismissals latch.Automation Blueprints — parameterized templates with typed slots (
time/enum/text/weekdays). The slot schema (cron/blueprint_catalog.py) is the single source of truth; four renderers consume it. 14 curated blueprints spanning daily/weekly/general (morning brief, important-mail monitor, weekly review, news digest, meal plan, habit check-in, hydration nudge, learning drip, and more).POST /api/cron/blueprints/instantiatefills + creates the job (no agent turn).hermes://blueprint/<key>?slot=valdeep-link routes into the chat composer pre-filled.Conversational fill —
/blueprint <name>(alias/bp) name-matches a blueprint (exact → prefix → substring → fuzzy; ambiguous lists candidates) and seeds the agent to ask you for each value one at a time, then calls the existingcronjobtool. Theslot=valueone-liner remains as the deterministic dashboard/docs/power-user shortcut. No new tool.How the conversational fill stays invariant-safe
The seed enters as a normal user turn — never a synthetic injection.
BlueprintCommandResult{text, agent_seed};match_blueprint()+build_blueprint_seed()are the shared pieces.event.textto the seed and falls through to the agent (the same pattern/steeruses).self._pending_agent_seed; the interactive loop consumes it right afterprocess_command()and runs it as the next turn.Changes (by surface)
cron/suggestions.py(store: add/list/accept/dismiss, dedup+latch, pending cap),cron/suggestion_catalog.py,cron/blueprint_catalog.py(AutomationBlueprint + BlueprintSlot, 14 blueprints, 4 renderers,fill_blueprint),tools/blueprints.py(blueprint↔job bridge),hermes_cli/suggestions_cmd.py+hermes_cli/blueprint_cmd.py(shared handlers); CommandDef + CLI dispatch + gateway dispatch.AutomationBlueprints.tsxgallery+form,CronPagesub-tab,api.ts.hermes://deep-link end to end.creating-skills.mdblueprint section.Validation
tests/cron/(blueprints, suggestions, schedule, validation, renderers, handlers)tests/tools/test_blueprints.py+ blueprint web endpointsslot=val→fill_blueprint→create_job→ persisted job (schedule/deliver/origin correct)Rebased onto current
main; linear history, contributor authorship preserved.Infographic