|
# Claude Code Loop Functions |
|
# Automated looping functions for running Claude Code in batch mode |
|
# |
|
# Functions: |
|
# xloop - Loop with a fixed prompt (explore → research → implement → verify → commit) |
|
# ximp - Loop with random improvement prompts from a curated list |
|
# xpolish - Loop with UX/UI polish focus using a system prompt file |
|
# xz - Single-shot interactive session with required phases |
|
# xconv - Query recent conversation history in plain English |
|
# |
|
# Features: |
|
# - File locking for parallel execution (prevents conflicts) |
|
# - Session history tracking (avoids repeating work) |
|
# - Auto-retry on errors with 5-minute cooldown |
|
# - Short-run detection (stops after 3 consecutive <2min sessions) |
|
# - Verbose error reporting with countdown timer |
|
# |
|
# Requirements: |
|
# - Claude Code CLI (claude) |
|
# - jq (for file locking hook) |
|
# - Place claude_file_lock.sh in ~/.config/scripts/ |
|
|
|
# --- Helper: cleanup locks owned by a specific loop ID --- |
|
_claude_loop_cleanup() { |
|
local id="$1" dir="$2" |
|
for f in "$dir"/*(.N); do |
|
read -r owner _ < "$f" 2>/dev/null && [[ "$owner" == "$id" ]] && rm -f "$f" |
|
done |
|
} |
|
|
|
# --- Core loop engine --- |
|
# Usage: _claude_loop <tag> <log_dir> <max_iter> <prompt_fn> [extra_claude_flags...] |
|
# prompt_fn is called each iteration and should echo the prompt string. |
|
_claude_loop() { |
|
local tag="$1" log_dir="$2" max_iter="$3" prompt_fn="$4" |
|
shift 4 |
|
local -a extra_flags=("$@") |
|
mkdir -p "$log_dir" |
|
local lock_dir=".claude-locks" |
|
mkdir -p "$lock_dir" |
|
# unique id: tag + unique suffix (RANDOM x2 to avoid collisions across parallel jobs) |
|
local loop_id="${tag}-${RANDOM}-${RANDOM}" |
|
local lock_settings |
|
lock_settings=$(cat <<EOSETTINGS |
|
{"hooks":{"PreToolUse":[{"matcher":"Edit|Write","hooks":[{"type":"command","command":"$HOME/.config/scripts/claude_file_lock.sh"}]}]}} |
|
EOSETTINGS |
|
) |
|
# clean up our locks on any exit |
|
trap "_claude_loop_cleanup '${loop_id}' '${lock_dir}'" EXIT INT TERM HUP |
|
local short_runs=0 iteration=0 |
|
local max_history=5 |
|
while true; do |
|
# purge stale locks from any agent (older than TTL) |
|
local _now=$(date +%s) |
|
for _lf in "$lock_dir"/*(.N); do |
|
read -r _owner _ts < "$_lf" 2>/dev/null || { rm -f "$_lf"; continue; } |
|
if (( _now - _ts > 180 )); then |
|
rm -f "$_lf" |
|
fi |
|
done |
|
if (( max_iter > 0 && iteration >= max_iter )); then |
|
echo "--- $tag: reached max iterations ($max_iter), stopping ---" |
|
_claude_loop_cleanup "$loop_id" "$lock_dir" |
|
return 0 |
|
fi |
|
(( iteration++ )) |
|
local _cl_prompt |
|
_cl_prompt=$("$prompt_fn") |
|
local session_file="$log_dir/$(date +%Y-%m-%d-%H%M%S)-session.md" |
|
# only feed the most recent N session files as history |
|
local previous="" |
|
local prev_files=("$log_dir"/*-session.md(N)) |
|
if (( ${#prev_files[@]} )); then |
|
local -a recent_files=("${prev_files[@]: -$max_history}") |
|
previous="<previous-sessions> |
|
$(cat "${recent_files[@]}") |
|
</previous-sessions> |
|
|
|
DO NOT repeat work already covered above. |
|
|
|
" |
|
fi |
|
echo "--- $tag [$loop_id]: iteration $iteration${max_iter:+/$max_iter} — $(date '+%H:%M:%S') — $_cl_prompt ---" |
|
local start=$SECONDS |
|
local _err_file=$(mktemp) |
|
CLAUDE_LOOP_ID="$loop_id" \ |
|
CLAUDE_LOCK_DIR="$lock_dir" \ |
|
claude --dangerously-skip-permissions --print --verbose \ |
|
--settings "$lock_settings" \ |
|
"${extra_flags[@]}" \ |
|
"${previous}Spawn subagent swarms as needed in any phase to parallelize work. |
|
|
|
A file locking system is active. If a tool call is rejected with LOCKED, skip that file and work on other files instead. Do not retry locked files. |
|
|
|
## Phases |
|
1. Explore the codebase |
|
2. Research online |
|
3. Implement |
|
4. Verify |
|
5. Commit with message prefixed by [$tag] e.g. \"[$tag] feat: ...\" |
|
6. Write a terse summary of what you did to $session_file (files changed, what was improved, what to avoid repeating) |
|
|
|
--- |
|
|
|
## Goal |
|
|
|
${_cl_prompt}" 2> >(tee "$_err_file" >&2) |
|
local exit_code=$? |
|
local elapsed=$(( SECONDS - start )) |
|
# release locks owned by this session after each iteration |
|
_claude_loop_cleanup "$loop_id" "$lock_dir" |
|
if [[ $exit_code -ne 0 ]]; then |
|
echo "" |
|
echo "╔══════════════════════════════════════════════════════════════╗" |
|
echo "║ $tag ERROR — $(date '+%Y-%m-%d %H:%M:%S') " |
|
echo "║ Exit code: $exit_code | Session lasted: ${elapsed}s " |
|
echo "║ Stderr output: " |
|
if [[ -s "$_err_file" ]]; then |
|
sed 's/^/║ /' "$_err_file" | tail -5 |
|
else |
|
echo "║ (no stderr captured) " |
|
fi |
|
echo "╚══════════════════════════════════════════════════════════════╝" |
|
rm -f "$_err_file" |
|
echo "" |
|
local _wait=300 |
|
while (( _wait > 0 )); do |
|
printf "\r--- $tag: retrying in %d:%02d ---" $((_wait/60)) $((_wait%60)) |
|
sleep 10 |
|
(( _wait -= 10 )) |
|
done |
|
printf "\r--- $tag: retrying now \n" |
|
continue |
|
fi |
|
rm -f "$_err_file" |
|
if (( elapsed < 120 )); then |
|
(( short_runs++ )) |
|
echo "--- $tag: session lasted ${elapsed}s (<2min), short run $short_runs/3 ---" |
|
if (( short_runs >= 3 )); then |
|
echo "--- $tag: 3 consecutive short sessions, stopping ---" |
|
return 1 |
|
fi |
|
else |
|
short_runs=0 |
|
fi |
|
echo "--- $tag: latest commit ---" |
|
git log -1 --stat --format="%h %s%n%b" |
|
echo "--- $tag: session complete ---" |
|
done |
|
} |
|
|
|
# --- Improvement prompts for ximp --- |
|
_ximp_prompts=( |
|
"Make it faster" |
|
"Make it simpler" |
|
"Make it more readable" |
|
"Make it more robust" |
|
"Make it more maintainable" |
|
"Make it more elegant" |
|
"Make it more consistent" |
|
"Make it more modular" |
|
"Make it more testable" |
|
"Make it more secure" |
|
"Make it more accessible" |
|
"Make it more intuitive" |
|
"Make it more resilient" |
|
"Make it more observable" |
|
"Make it more portable" |
|
"Make it more scalable" |
|
"Make it more composable" |
|
"Make it more idiomatic" |
|
"Make it more declarative" |
|
"Make it more efficient" |
|
"Improve the architecture" |
|
"Improve the user experience" |
|
"Improve the developer experience" |
|
"Improve the error experience" |
|
"Improve the data flow" |
|
"Improve the abstractions" |
|
"Improve the performance" |
|
"Improve the reliability" |
|
"Improve the test quality" |
|
"Improve the documentation" |
|
"Reduce complexity" |
|
"Reduce coupling" |
|
"Reduce friction" |
|
"Reduce cognitive load" |
|
"Reduce surface area for bugs" |
|
"Tighten up the design" |
|
"Clean up the messiest part" |
|
"Rethink the weakest abstraction" |
|
"Strengthen the boundaries" |
|
"Polish the rough edges" |
|
"Find and fix the biggest code smell" |
|
"Find and fix the biggest bottleneck" |
|
"Find and fix the biggest risk" |
|
"Modernize the oldest pattern" |
|
"Raise the overall code quality" |
|
"Raise the bar on error handling" |
|
"Raise the bar on testing" |
|
"Make the happy path clearer" |
|
"Make the codebase more fun to work in" |
|
"Make the next developer's life easier" |
|
) |
|
_ximp_pick() { echo "${_ximp_prompts[$(( RANDOM % ${#_ximp_prompts[@]} + 1 ))]}"; } |
|
|
|
# --- ximp: random improvement loop --- |
|
ximp() { |
|
local -A opts |
|
zparseopts -D -E -A opts n: |
|
local max_iter="${opts[-n]:-0}" |
|
_claude_loop "ximp" ".ximp-sessions/${RANDOM}-${RANDOM}" "$max_iter" "_ximp_pick" |
|
} |
|
|
|
# --- xloop: fixed prompt loop --- |
|
xloop() { |
|
local -A opts |
|
zparseopts -D -E -A opts n: |
|
local max_iter="${opts[-n]:-0}" |
|
[[ $# -eq 0 ]] && { echo "Usage: xloop [-n max] <prompt>"; return 1; } |
|
_xloop_prompt="$*" |
|
_xloop_pick() { echo "$_xloop_prompt"; } |
|
_claude_loop "xloop" ".xloop-sessions/${RANDOM}-${RANDOM}" "$max_iter" "_xloop_pick" |
|
unfunction _xloop_pick 2>/dev/null |
|
unset _xloop_prompt |
|
} |
|
|
|
# --- xpolish: UX/UI polish loop (requires UX.md system prompt file) --- |
|
xpolish() { |
|
local -A opts |
|
zparseopts -D -E -A opts n: |
|
local max_iter="${opts[-n]:-0}" |
|
[[ $# -eq 0 ]] && { echo "Usage: xpolish [-n max] <prompt>"; return 1; } |
|
_xpolish_prompt="$*" |
|
_xpolish_pick() { echo "$_xpolish_prompt"; } |
|
_claude_loop "xpolish" ".xpolish-sessions/${RANDOM}-${RANDOM}" "$max_iter" "_xpolish_pick" \ |
|
--system-prompt-file "$HOME/.config/zsh/conf.d/UX.md" |
|
unfunction _xpolish_pick 2>/dev/null |
|
unset _xpolish_prompt |
|
} |
|
|
|
# --- xconv: query recent conversation history --- |
|
xconv() { |
|
[[ $# -eq 0 ]] && { echo "Usage: xconv <plain english question>"; return 1; } |
|
claude --dangerously-skip-permissions --print \ |
|
"Use the Skill(recent-conversations) to answer this question about my recent conversations: $*" |
|
} |
|
|
|
# --- xz: single-shot with required phases --- |
|
xz() { |
|
local _xz_system="You MUST follow these phases in order. Do not skip phases. Spawn subagent swarms as needed to parallelize work within each phase. |
|
|
|
## Required Phases (follow strictly) |
|
1. EXPLORE — Thoroughly explore the codebase to understand the architecture and relevant files |
|
2. RESEARCH — Search online for best practices, documentation, or solutions |
|
3. IMPLEMENT — Make the necessary code changes |
|
4. VERIFY — Test and validate your changes work correctly |
|
5. COMMIT — Commit with message prefixed by [xz] e.g. \"[xz] feat: ...\"" |
|
|
|
if [[ $# -eq 0 ]]; then |
|
claude --dangerously-skip-permissions --verbose \ |
|
--append-system-prompt "$_xz_system" |
|
else |
|
claude --dangerously-skip-permissions --verbose \ |
|
--append-system-prompt "$_xz_system" \ |
|
"$*" |
|
fi |
|
} |