From 27da7b8ba8eeb27675419bf5585be63150014fb6 Mon Sep 17 00:00:00 2001 From: Labo UNP Date: Wed, 20 May 2026 22:18:13 -0400 Subject: [PATCH] Restore action: script support in wolf scheduler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fire_cron() was always calling dispatch_session() regardless of the action field, so every cron with "action": "script" silently spawned an empty Claude Code session with no prompt — dispatch_session bailed on the missing prompt, the session was reaped, and the bash command never ran. Adds dispatch_script() and routes fire_cron() by action type. Defaults to "session" so any cron without an explicit action keeps current behavior. Notifications stay quiet for script crons unless they declare notify: — no need to howl every two minutes for a heartbeat keepalive. Co-Authored-By: Claude Opus 4.7 (1M context) --- container/creatures/wolf.py | 41 +++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/container/creatures/wolf.py b/container/creatures/wolf.py index dbc0e5d..ea2db17 100644 --- a/container/creatures/wolf.py +++ b/container/creatures/wolf.py @@ -271,21 +271,44 @@ def _get_wolt_emoji(wolt_name: str) -> str: return "🐾" -def fire_cron(entry: dict): - """Execute a cron entry — dispatch session, then always notify with link.""" +def dispatch_script(entry: dict) -> bool: + """Run a cron entry's shell command fire-and-forget. Returns True on dispatch.""" + import subprocess name = entry.get("name", "unnamed") - owner = entry.get("_owner", "?") + command = entry.get("command", "") + if not command: + print(f"[wolf] {name}: script action has no command", file=sys.stderr) + return False + try: + subprocess.Popen(["bash", "-c", command], start_new_session=True) + print(f"[wolf] dispatched script for {name}: {command[:80]}") + return True + except Exception as e: + print(f"[wolf] script dispatch error for {name}: {e}", file=sys.stderr) + return False - _log_job(name, "session", event="started", owner=owner) - # Dispatch session for the owning wolt - link = dispatch_session(entry) +def fire_cron(entry: dict): + """Execute a cron entry — dispatch script or session, then notify.""" + name = entry.get("name", "unnamed") + owner = entry.get("_owner", "?") + action = entry.get("action", "session") - _log_job(name, "session", event="dispatched", owner=owner, link=link) + if action == "script": + _log_job(name, "script", event="started", owner=owner) + ok = dispatch_script(entry) + _log_job(name, "script", event="dispatched" if ok else "failed", owner=owner) + link = None + else: + _log_job(name, "session", event="started", owner=owner) + link = dispatch_session(entry) + _log_job(name, "session", event="dispatched", owner=owner, link=link) - # Build notification message - emoji = _get_wolt_emoji(owner) + # Notification (skip for script-action crons unless they have a custom notify) custom_msg = entry.get("notify") + if action == "script" and not custom_msg: + return + emoji = _get_wolt_emoji(owner) if custom_msg: notify_body = f'{emoji} {owner} has been notified: "{custom_msg}"' else: