Files
loop-loop/lib/prompt.sh
Sheldon Finlay 17e5eb707f feat: agent loop harness with Claude Code plugin support
Generator-evaluator architecture with iterative context-reset for
long-running coding tasks. Ships as a Claude Code plugin — install
with /plugin and use /agent-loop:init, /agent-loop:plan, /agent-loop:run.
2026-03-27 08:03:18 -04:00

96 lines
2.9 KiB
Bash

#!/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
}