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.
This commit is contained in:
95
lib/prompt.sh
Normal file
95
lib/prompt.sh
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/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
|
||||
}
|
||||
Reference in New Issue
Block a user