256 lines
7.9 KiB
Bash
Executable File
256 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: loop.sh (--plan PROMPT | --plan-file PATH) --name NAME [options]
|
|
|
|
Required:
|
|
--plan PROMPT The implementation plan (given to both worker and judge)
|
|
--plan-file PATH Read the plan from a file
|
|
--name NAME Name prefix for agents (e.g. "fix-bug" → "fix-bug-work-1", "fix-bug-verify-1")
|
|
|
|
Options:
|
|
--max-iterations N Maximum loop iterations (default: 10)
|
|
--worktree NAME Run all agents in this worktree (created on first use, reused after)
|
|
--worker-provider P Worker provider: claude or codex (default: codex)
|
|
--worker-model M Worker model (default: provider's default)
|
|
--judge-provider P Judge provider: claude or codex (default: claude)
|
|
--judge-model M Judge model (default: provider's default)
|
|
--thinking LEVEL Thinking level for worker (default: medium)
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
# Defaults
|
|
max_iterations=10
|
|
worker_provider="codex"
|
|
worker_model=""
|
|
judge_provider="claude"
|
|
judge_model="sonnet"
|
|
plan=""
|
|
plan_file_input=""
|
|
name=""
|
|
thinking="medium"
|
|
worktree=""
|
|
state_root="${HOME}/.paseo/loops"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--plan) plan="$2"; shift 2 ;;
|
|
--plan-file) plan_file_input="$2"; shift 2 ;;
|
|
--name) name="$2"; shift 2 ;;
|
|
--max-iterations) max_iterations="$2"; shift 2 ;;
|
|
--worktree) worktree="$2"; shift 2 ;;
|
|
--worker-provider) worker_provider="$2"; shift 2 ;;
|
|
--worker-model) worker_model="$2"; shift 2 ;;
|
|
--judge-provider) judge_provider="$2"; shift 2 ;;
|
|
--judge-model) judge_model="$2"; shift 2 ;;
|
|
--thinking) thinking="$2"; shift 2 ;;
|
|
--help|-h) usage ;;
|
|
*) echo "Unknown option: $1"; usage ;;
|
|
esac
|
|
done
|
|
|
|
load_prompt() {
|
|
local inline_value="$1"
|
|
local file_value="$2"
|
|
local label="$3"
|
|
|
|
if [[ -n "$inline_value" && -n "$file_value" ]]; then
|
|
echo "Error: use either --${label} or --${label}-file, not both"
|
|
usage
|
|
fi
|
|
|
|
if [[ -n "$file_value" ]]; then
|
|
[[ -f "$file_value" ]] || { echo "Error: --${label}-file not found: $file_value"; exit 1; }
|
|
local file_content
|
|
file_content="$(cat "$file_value")"
|
|
[[ -n "$file_content" ]] || { echo "Error: --${label}-file is empty: $file_value"; exit 1; }
|
|
printf '%s' "$file_content"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "$inline_value" ]]; then
|
|
printf '%s' "$inline_value"
|
|
return 0
|
|
fi
|
|
|
|
echo "Error: either --${label} or --${label}-file is required"
|
|
usage
|
|
}
|
|
|
|
plan="$(load_prompt "$plan" "$plan_file_input" "plan")"
|
|
[[ -z "$name" ]] && { echo "Error: --name is required"; usage; }
|
|
|
|
mkdir -p "$state_root"
|
|
|
|
generate_loop_id() {
|
|
uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-' | cut -c1-6
|
|
}
|
|
|
|
loop_id="$(generate_loop_id)"
|
|
state_dir="${state_root}/${loop_id}"
|
|
while [[ -e "$state_dir" ]]; do
|
|
loop_id="$(generate_loop_id)"
|
|
state_dir="${state_root}/${loop_id}"
|
|
done
|
|
|
|
mkdir -p "$state_dir"
|
|
|
|
plan_file="${state_dir}/plan.md"
|
|
last_reason_file="${state_dir}/last_reason.md"
|
|
history_log="${state_dir}/history.log"
|
|
|
|
printf '%s\n' "$plan" > "$plan_file"
|
|
printf '' > "$last_reason_file"
|
|
printf '' > "$history_log"
|
|
|
|
# Build worker flags
|
|
worker_flags=()
|
|
if [[ "$worker_provider" == "codex" ]]; then
|
|
worker_flags+=(--mode full-access --provider codex)
|
|
elif [[ "$worker_provider" == "claude" ]]; then
|
|
worker_flags+=(--mode bypass)
|
|
fi
|
|
[[ -n "$worker_model" ]] && worker_flags+=(--model "$worker_model")
|
|
[[ -n "$thinking" ]] && worker_flags+=(--thinking "$thinking")
|
|
|
|
# Build judge flags
|
|
judge_flags=()
|
|
if [[ "$judge_provider" == "codex" ]]; then
|
|
judge_flags+=(--mode full-access --provider codex)
|
|
elif [[ "$judge_provider" == "claude" ]]; then
|
|
judge_flags+=(--mode bypass)
|
|
fi
|
|
[[ -n "$judge_model" ]] && judge_flags+=(--model "$judge_model")
|
|
|
|
# Worktree flags — passed to every paseo run call
|
|
worktree_flags=()
|
|
if [[ -n "$worktree" ]]; then
|
|
base_branch="$(git branch --show-current 2>/dev/null || echo "main")"
|
|
worktree_flags+=(--worktree "$worktree" --base "$base_branch")
|
|
fi
|
|
|
|
# Judge output schema
|
|
judge_schema='{"type":"object","properties":{"criteria_met":{"type":"boolean"},"reason":{"type":"string"}},"required":["criteria_met","reason"],"additionalProperties":false}'
|
|
|
|
iteration=0
|
|
|
|
echo "=== Loop started: $name ==="
|
|
echo " Loop ID: $loop_id"
|
|
echo " State dir: $state_dir"
|
|
echo " Plan file: $plan_file"
|
|
echo " Last reason file: $last_reason_file"
|
|
echo " History log: $history_log"
|
|
echo " Steering: edits to plan.md are picked up on the next worker/judge launch."
|
|
echo " Worker: $worker_provider ${worker_model:-(default)}"
|
|
echo " Judge: $judge_provider ${judge_model:-(default)}"
|
|
if [[ -n "$worktree" ]]; then
|
|
echo " Worktree: $worktree (base: $base_branch)"
|
|
fi
|
|
echo " Max iterations: $max_iterations"
|
|
echo ""
|
|
|
|
while [[ $iteration -lt $max_iterations ]]; do
|
|
iteration=$((iteration + 1))
|
|
echo "--- Iteration $iteration/$max_iterations ---"
|
|
|
|
if [[ ! -s "$plan_file" ]]; then
|
|
echo "Error: plan file is missing or empty: $plan_file"
|
|
exit 1
|
|
fi
|
|
|
|
current_plan="$(cat "$plan_file")"
|
|
last_reason="$(cat "$last_reason_file")"
|
|
|
|
# Build worker prompt with plan in XML tags
|
|
worker_prompt="Implement the following plan exactly as specified.
|
|
|
|
<plan>
|
|
$current_plan
|
|
</plan>"
|
|
|
|
if [[ -n "$last_reason" ]]; then
|
|
worker_prompt="$worker_prompt
|
|
|
|
<previous-verification-failure>
|
|
The previous attempt was verified and did NOT pass. Address the following issues before anything else:
|
|
|
|
$last_reason
|
|
</previous-verification-failure>"
|
|
fi
|
|
|
|
# Launch worker
|
|
worker_name="${name}-work-${iteration}"
|
|
echo "Launching worker: $worker_name"
|
|
worker_id=$(paseo run -d "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" "$worker_prompt" -q)
|
|
echo "Worker [$worker_name] launched. ID: $worker_id"
|
|
echo " Stream logs: paseo logs $worker_id -f"
|
|
echo " Inspect: paseo inspect $worker_id"
|
|
echo " Wait: paseo wait $worker_id"
|
|
|
|
# Wait for worker
|
|
echo ""
|
|
echo "Waiting for worker to complete..."
|
|
paseo wait "$worker_id"
|
|
echo "Worker done."
|
|
|
|
# Build judge prompt with plan in XML tags
|
|
judge_prompt="You are a fact checker. Your ONLY job is to verify whether the plan below was implemented correctly and completely.
|
|
|
|
Inspect the codebase independently. Do NOT fix anything. Do NOT suggest fixes. Do NOT provide guidance.
|
|
|
|
<plan>
|
|
$current_plan
|
|
</plan>
|
|
|
|
<instructions>
|
|
Verify that every step of the plan has been implemented to the letter. If the plan contains acceptance criteria, check each one individually.
|
|
|
|
Report ONLY facts:
|
|
- criteria_met: true if the ENTIRE plan is implemented correctly, false if ANY part is missing or wrong
|
|
- reason: for each item in the plan, state whether it was implemented correctly with evidence (file contents, test output, etc.). Be specific about what passed and what failed.
|
|
</instructions>"
|
|
|
|
# Launch judge (blocking, structured output)
|
|
judge_name="${name}-verify-${iteration}"
|
|
echo ""
|
|
echo "Launching judge: $judge_name (synchronous, blocks until verdict)"
|
|
verdict=$(paseo run "${judge_flags[@]}" "${worktree_flags[@]}" --name "$judge_name" --output-schema "$judge_schema" "$judge_prompt")
|
|
echo "Verdict: $verdict"
|
|
|
|
# Parse verdict
|
|
criteria_met=$(echo "$verdict" | jq -r '.criteria_met')
|
|
reason=$(echo "$verdict" | jq -r '.reason')
|
|
printf '[%s] iteration=%s worker_id=%s judge=%s criteria_met=%s reason=%s\n' \
|
|
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
|
|
"$iteration" \
|
|
"$worker_id" \
|
|
"$judge_name" \
|
|
"$criteria_met" \
|
|
"$(echo "$reason" | tr '\n' ' ')" >> "$history_log"
|
|
|
|
if [[ "$criteria_met" == "true" ]]; then
|
|
echo ""
|
|
echo "=== Loop complete: criteria met on iteration $iteration ==="
|
|
echo "Reason: $reason"
|
|
echo ""
|
|
echo "Review the final worker's changes:"
|
|
echo " paseo logs $worker_id"
|
|
echo " paseo inspect $worker_id"
|
|
exit 0
|
|
fi
|
|
|
|
echo "Criteria not met: $reason"
|
|
echo "Starting next iteration..."
|
|
|
|
# Keep only the latest judge reason for the next worker iteration.
|
|
printf '%s\n' "$reason" > "$last_reason_file"
|
|
|
|
echo ""
|
|
done
|
|
|
|
echo "=== Loop exhausted: $max_iterations iterations without meeting criteria ==="
|
|
exit 1
|