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.
96 lines
2.9 KiB
Bash
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
|
|
}
|