#!/usr/bin/env python3 """ recipe_engine.py — Deterministic Workflow Orchestrator for the Timmy Foundation. Executes "Recipes" — sequences of deterministic steps (shell commands, scripts) to reduce reliance on LLM reasoning for common, repetitive tasks. Usage: python3 scripts/recipe_engine.py recipes/deploy_beacon.json --vars REPO_PATH=/opt/the-beacon """ import os import sys import json import subprocess import argparse import time from datetime import datetime from typing import List, Dict, Any class RecipeEngine: def __init__(self, vars: Dict[str, str] = None): self.vars = vars or {} self.vars["TIMESTAMP"] = datetime.now().strftime("%Y%m%d-%H%M%S") def log(self, message: str): ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"[{ts}] {message}") def substitute_vars(self, text: str) -> str: for k, v in self.vars.items(): text = text.replace(f"{{{{{k}}}}}", v) return text def execute_step(self, step: Dict[str, Any]) -> bool: name = step.get("name", "Unnamed Step") command = step.get("command") if not command: self.log(f" [SKIP] {name}: No command provided.") return True command = self.substitute_vars(command) self.log(f" [EXEC] {name}: {command}") try: start_time = time.time() # Use shell=True to allow pipes and redirections in recipes process = subprocess.run(command, shell=True, capture_output=True, text=True) duration = time.time() - start_time if process.returncode == 0: self.log(f" [OK] {name} completed in {duration:.2f}s") if process.stdout.strip(): # Print first few lines of output lines = process.stdout.strip().split("\n") for line in lines[:3]: print(f" > {line}") if len(lines) > 3: print(f" > ... ({len(lines)-3} more lines)") return True else: self.log(f" [FAIL] {name} failed with exit code {process.returncode}") print(f" [ERR] {process.stderr.strip()}") return False except Exception as e: self.log(f" [ERROR] {name} encountered an exception: {e}") return False def run_recipe(self, recipe_path: str) -> bool: try: with open(recipe_path, "r") as f: recipe = json.load(f) except Exception as e: self.log(f"Failed to load recipe {recipe_path}: {e}") return False name = recipe.get("name", "Unknown Recipe") description = recipe.get("description", "") steps = recipe.get("steps", []) self.log(f"--- Starting Recipe: {name} ---") if description: self.log(f"Description: {description}") self.log(f"Steps to execute: {len(steps)}") for i, step in enumerate(steps, 1): self.log(f"Step {i}/{len(steps)}: {step.get('name')}") success = self.execute_step(step) if not success and not step.get("continue_on_failure", False): self.log(f"Recipe {name} aborted due to failure in step {i}.") return False self.log(f"--- Recipe {name} completed successfully ---") return True def main(): parser = argparse.ArgumentParser(description="Deterministic Recipe Engine") parser.add_argument("recipe", help="Path to the JSON recipe file") parser.add_argument("--vars", nargs="*", help="Variables in KEY=VALUE format") args = parser.parse_args() variables = {} if args.vars: for v in args.vars: if "=" in v: k, val = v.split("=", 1) variables[k] = val engine = RecipeEngine(vars=variables) success = engine.run_recipe(args.recipe) if not success: sys.exit(1) if __name__ == "__main__": main()