#!/usr/bin/env bash # # Template loading functions for RLCR loop hooks # # This library provides functions to load and render prompt templates. # # Template Variable Syntax # ======================== # Templates use {{VARIABLE_NAME}} syntax for placeholders. # - Variable names: uppercase letters, numbers, underscores only # - Example: {{PLAN_FILE}}, {{CURRENT_ROUND}}, {{GOAL_TRACKER_FILE}} # - Single-pass substitution: {{VAR}} in a value will NOT be expanded # - Missing variables: placeholder is kept as-is (e.g., {{UNDEFINED}}) # # Available functions: # - get_template_dir: Get path to template directory # - load_template: Load a template file by name # - render_template: Replace {{VAR}} placeholders with values # - load_and_render: Load and render in one call # - load_and_render_safe: Same as above but with fallback for missing templates # - validate_template_dir: Check if template directory is valid # # Get the template directory path # This is relative to the hooks/lib directory (goes up 2 levels to plugin root) get_template_dir() { local script_dir="$1" local plugin_root plugin_root="$(cd "$script_dir/../.." && pwd)" echo "$plugin_root/prompt-template" } # Load a template file and output its contents # Usage: load_template "$TEMPLATE_DIR" "codex/full-alignment-review.md" # Returns empty string if file not found load_template() { local template_dir="$1" local template_name="$2" local template_path="$template_dir/$template_name" if [[ -f "$template_path" ]]; then cat "$template_path" else echo "Warning: Template not found: $template_path" >&2 fi } # Render a template with multiple variable substitutions (single-pass) # Usage: render_template "$template_content" "VAR1=value1" "VAR2=value2" ... # Variables should be passed as VAR=value pairs # # IMPORTANT: This uses a single-pass approach to prevent placeholder injection. # If a variable value contains {{OTHER_VAR}}, it will NOT be replaced. # This prevents content like REVIEW_CONTENT from having its {{...}} patterns # accidentally substituted, which could corrupt prompts. render_template() { local content="$1" shift # Build environment variables for all substitutions # Using TMPL_VAR_ prefix to avoid conflicts local -a env_vars=() for var_assignment in "$@"; do local var_name="${var_assignment%%=*}" local var_value="${var_assignment#*=}" env_vars+=("TMPL_VAR_${var_name}=${var_value}") done # Single-pass replacement using awk # Scans for {{VAR}} patterns and replaces them with values from environment # Replaced content goes directly to output without re-scanning local awk_exit=0 content=$(env "${env_vars[@]}" awk ' BEGIN { # Build lookup table from environment variables with TMPL_VAR_ prefix for (name in ENVIRON) { if (substr(name, 1, 9) == "TMPL_VAR_") { var_name = substr(name, 10) # Remove prefix vars[var_name] = ENVIRON[name] } } } { line = $0 result = "" # Process line character by character, looking for {{ patterns while (length(line) > 0) { # Find next {{ open_idx = index(line, "{{") if (open_idx == 0) { # No more placeholders, append rest of line result = result line break } # Append everything before {{ result = result substr(line, 1, open_idx - 1) line = substr(line, open_idx) # line now starts with {{ # Find closing }} close_idx = index(substr(line, 3), "}}") if (close_idx == 0) { # No closing }}, treat {{ as literal result = result substr(line, 1, 2) line = substr(line, 3) continue } # Extract variable name (between {{ and }}) var_name = substr(line, 3, close_idx - 1) placeholder = "{{" var_name "}}" # Look up in our variables table if (var_name in vars) { # Replace with value (value goes to output, not re-scanned) result = result vars[var_name] } else { # Keep original placeholder if not found result = result placeholder } # Move past the placeholder line = substr(line, length(placeholder) + 1) } print result }' <<< "$content") || awk_exit=$? if [[ $awk_exit -ne 0 ]]; then echo "Error: Template rendering failed (awk exit code: $awk_exit)" >&2 return 1 fi echo "$content" } # Load and render a template in one step # Usage: load_and_render "$TEMPLATE_DIR" "block/git-not-clean.md" "GIT_ISSUES=uncommitted changes" load_and_render() { local template_dir="$1" local template_name="$2" shift 2 local content content=$(load_template "$template_dir" "$template_name") if [[ -n "$content" ]]; then render_template "$content" "$@" fi } # Append content from another template file # Usage: append_template "$base_content" "$TEMPLATE_DIR" "claude/post-alignment.md" # Only appends if the template exists and is non-empty. append_template() { local base_content="$1" local template_dir="$2" local template_name="$3" local additional_content additional_content=$(load_template "$template_dir" "$template_name" 2>/dev/null) || true echo "$base_content" if [[ -n "$additional_content" ]]; then echo "$additional_content" fi } # ======================================== # Safe versions with fallback messages # ======================================== # Emit a fallback message, optionally rendering template variables. _emit_fallback() { local fallback_msg="$1" shift if [[ $# -gt 0 ]]; then render_template "$fallback_msg" "$@" else echo "$fallback_msg" fi } # Load and render with a fallback message if template fails # Usage: load_and_render_safe "$TEMPLATE_DIR" "block/message.md" "fallback message" "VAR=value" ... # Returns fallback message if template is missing or empty load_and_render_safe() { local template_dir="$1" local template_name="$2" local fallback_msg="$3" shift 3 local content content=$(load_template "$template_dir" "$template_name" 2>/dev/null) || true if [[ -z "$content" ]]; then _emit_fallback "$fallback_msg" "$@" return fi local result result=$(render_template "$content" "$@") || true if [[ -z "$result" ]]; then _emit_fallback "$fallback_msg" "$@" return fi echo "$result" } # Validate that TEMPLATE_DIR exists and contains templates # Usage: validate_template_dir "$TEMPLATE_DIR" # Returns 0 if valid, 1 if not validate_template_dir() { local template_dir="$1" if [[ ! -d "$template_dir" ]]; then echo "ERROR: Template directory not found: $template_dir" >&2 return 1 fi local required_subdirs=("block" "codex" "claude" "plan") local missing=() local subdir for subdir in "${required_subdirs[@]}"; do if [[ ! -d "$template_dir/$subdir" ]]; then missing+=("$subdir") fi done if [[ ${#missing[@]} -gt 0 ]]; then echo "ERROR: Template directory missing subdirectories (${missing[*]}): $template_dir" >&2 return 1 fi return 0 }