#!/bin/bash # Prompt assembly — composes the final prompt from base + mode overlay. # Injects runtime variables (scope budgets, current story, iteration count). # Build the complete prompt for a given agent role and mode. # Usage: build_prompt "generator" "implement" # build_prompt "evaluator" "implement" build_prompt() { local role="$1" # generator | evaluator local mode="$2" # implement | explore | fix local base_file="$LOOP_DIR/prompts/${role}/_base.md" local mode_file="$LOOP_DIR/prompts/${role}/${mode}.md" local prompt="" # Start with base prompt if [ -f "$base_file" ]; then prompt=$(cat "$base_file") else log "WARNING: Missing base prompt: $base_file" return 1 fi # Append mode-specific overlay if [ -f "$mode_file" ]; then prompt="${prompt} --- $(cat "$mode_file")" else log "WARNING: Missing mode prompt: $mode_file" fi # Inject runtime variables prompt=$(inject_variables "$prompt" "$mode") printf '%s\n' "$prompt" } # Replace template variables in prompt text inject_variables() { local text="$1" local mode="$2" # Scope budgets from config local max_read max_write max_modify max_read=$(get_config_value ".scopeBudgets.${mode}.maxFilesToRead" "50") max_write=$(get_config_value ".scopeBudgets.${mode}.maxLinesToWrite" "500") max_modify=$(get_config_value ".scopeBudgets.${mode}.maxFilesToModify" "10") text="${text//\{\{MAX_FILES_TO_READ\}\}/$max_read}" text="${text//\{\{MAX_LINES_TO_WRITE\}\}/$max_write}" text="${text//\{\{MAX_FILES_TO_MODIFY\}\}/$max_modify}" text="${text//\{\{MODE\}\}/$mode}" text="${text//\{\{ITERATION\}\}/$ITERATION}" text="${text//\{\{MAX_ITERATIONS\}\}/$MAX_ITERATIONS}" text="${text//\{\{LOOP_DIR\}\}/$LOOP_DIR}" text="${text//\{\{PROJECT_ROOT\}\}/$PROJECT_ROOT}" text="${text//\{\{CURRENT_STORY_ID\}\}/${CURRENT_STORY_ID:-unknown}}" text="${text//\{\{PRE_GENERATOR_SHA\}\}/${PRE_GENERATOR_SHA:-HEAD~1}}" printf '%s\n' "$text" } # Read a value from config.json with a default fallback get_config_value() { local path="$1" local default="$2" local config="$LOOP_DIR/config.json" [ -f "$config" ] || { echo "$default"; return; } if command -v jq &>/dev/null; then local val val=$(jq -r "$path // empty" "$config" 2>/dev/null) echo "${val:-$default}" else LOOP_CONFIG="$config" LOOP_PATH="$path" LOOP_DEFAULT="$default" python3 -c " import json, os d = json.load(open(os.environ['LOOP_CONFIG'])) keys = os.environ['LOOP_PATH'].lstrip('.').split('.') for k in keys: d = d.get(k) if isinstance(d, dict) else None if d is None: break val = d if d is not None and d != {} else os.environ['LOOP_DEFAULT'] # Normalize Python booleans to lowercase for shell compatibility if isinstance(val, bool): val = str(val).lower() print(val, end='') " fi }