🔁
CodexCLI がすぐ諦める問題を無限ループで殴る
やりたいこと
CodexCLIに ToDoリストを渡したら、諦めずに最後まで実装させたい。1回で終わらないなら何回でもお願いしたい。WSL での動作確認です。
なぜ必要だったか
CodexCLI で todolist.md の実装を頼むと、途中までで満足して終わりがち。これはコンテキストの問題もあっていい面もあるんだけど、結局ただ /new todolist.md にしたがって実装して と繰り返すのがだるい。
→ 自動化してみた。
前提環境
- WSL2 (Windows Subsystem for Linux) で動作確認
- CodexCLI がインストール済み(
codexコマンドが使える状態) - bash が使える環境
Windows の通知機能も使ってるので、WSL からの PowerShell 呼び出しが前提になっている部分がある(消しても動きます)。
使い方
./codex-todo-loop.sh /path/to/project /path/to/todolist.md
環境変数で挙動を変えられる:
# 30回まで頑張ってもらう(デフォ20)
MAX_ITERS=30 ./codex-todo-loop.sh /path/to/project /path/to/todolist.md
# 進捗をGitコミット
COMMIT=true ./codex-todo-loop.sh /path/to/project /path/to/todolist.md
# 10秒ごとに残タスク数表示(生存確認)
STATUS_INTERVAL=10 ./codex-todo-loop.sh /path/to/project /path/to/todolist.md
スクリプト
#!/usr/bin/env bash
# codex-todo-loop.sh
# Usage:
# ./codex-todo-loop.sh <WORKDIR> <todolist.md> [MAX_ITERS]
# Example:
# ./codex-todo-loop.sh /home/fukuchi/dev/omrb /home/fukuchi/dev/omrb/docs/todolist.md
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <WORKDIR> <Todolist.md> [MAX_ITERS]" >&2
exit 1
fi
WORKDIR="$(realpath "$1")"
TODO_MD="$(realpath "$2")"
MAX_ITERS="${3:-20}"
PAUSE_SEC="${PAUSE_SEC:-2}"
TIMEOUT_SECS="${TIMEOUT_SECS:-420}"
COMMIT="${COMMIT:-false}" # 既定 false
STREAM="${STREAM:-on}" # on: codex出力をターミナルへも流す
STATUS_INTERVAL="${STATUS_INTERVAL:-0}" # >0ならN秒ごとに残数を表示(ハートビート)
need() { command -v "$1" >/dev/null 2>&1 || { echo "missing: $1" >&2; exit 2; }; }
need codex; need awk; need sed; need grep
command -v git >/dev/null 2>&1 || COMMIT=false
cd "$WORKDIR"
LOG_DIR="${LOG_DIR:-$WORKDIR/.codex-todo-logs}"
mkdir -p "$LOG_DIR"
notify_win() {
local msg="$1"
local ps_cli="/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"
local win_user="${WINDOWS_USER:-$USER}" # 環境変数で切り替え
local ps_script="/mnt/c/Users/${win_user}/Scripts/codex-notify.ps1"
if [[ -x "$ps_cli" && -f "$ps_script" ]]; then
"$ps_cli" -NoProfile -ExecutionPolicy Bypass -File "$ps_script" -Message "$msg" -Sound "Alarm5" >/dev/null 2>&1 || true
else
"$ps_cli" -NoProfile -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('$msg','codex-todo-loop') | Out-Null" >/dev/null 2>&1 || true
fi
}
unchecked_count() {
{ grep -E '^\s*[-*]\s*\[ \]' "$TODO_MD" || true; } | wc -l | tr -d ' '
}
has_unchecked() { [[ "$(unchecked_count)" -gt 0 ]]; }
progress_made() { local before="$1"; local after; after="$(unchecked_count)"; [[ "$after" -lt "$before" ]]; }
snapshot_hash() { sha1sum "$TODO_MD" | awk '{print $1}'; }
ensure_repo() {
if command -v git >/dev/null 2>&1 && ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git init -q
fi
}
$COMMIT && ensure_repo
# ハートビート(バックグラウンド)
heartbeat_pid=""
start_heartbeat() {
[[ "$STATUS_INTERVAL" -gt 0 ]] || return 0
(
while :; do
printf "[status] %(%Y-%m-%d %H:%M:%S)T remaining=%s\r" -1 "$(unchecked_count)"
sleep "$STATUS_INTERVAL"
done
) >&2 &
heartbeat_pid=$!
}
stop_heartbeat() {
[[ -n "$heartbeat_pid" ]] && kill "$heartbeat_pid" >/dev/null 2>&1 || true
heartbeat_pid=""
echo >&2
}
# --- preflight ---
if [[ ! -f "$TODO_MD" ]]; then
echo "[error] todo file not found: $TODO_MD" >&2
exit 2
fi
echo "[start] workdir=$WORKDIR todo=$TODO_MD max_iters=$MAX_ITERS"
if ! has_unchecked; then
echo "[preflight] no unchecked checklist items found in $TODO_MD (nothing to do)."
exit 0
fi
# --- main loop ---
iter=0
start_heartbeat
trap stop_heartbeat EXIT
while has_unchecked && [[ $iter -lt $MAX_ITERS ]]; do
iter=$((iter+1))
ts="$(date +%Y%m%d-%H%M%S)"
log="$LOG_DIR/run-$ts-i$(printf '%02d' "$iter").log"
before_unchecked="$(unchecked_count)"
before_hash="$(snapshot_hash)"
PROMPT=$(cat <<'EOF'
あなたは「与えられたリポジトリ内で、todolist.md のチェックボックスに従って実装・検証し、完了行のみ [x] に更新するエージェント」です。
【厳守】
1) 実際に動作確認できたタスクのみ、その行を - [ ] → - [x] に変更。未完了は [ ] のまま。
2) 実行/検証はリポ標準(make / npm scripts / docker compose 等)を優先。
3) 完了不可な場合は行末に <!-- BLOCKER: ... --> を追記して残す。
4) 変更は小さく安全に。テストがあるなら実行して結果を反映。
5) 保存を確実に行い、作業を終了する。
【作業ディレクトリ】<<WORKDIR>>
【ToDoファイル】<<TODO_MD>>
以下がToDoです。できるところまで実装し、完了した行だけ [x] に更新してください。
---- todolist begin ----
<<TODO_CONTENT>>
---- todolist end ----
EOF
)
PROMPT="${PROMPT//<<WORKDIR>>/$WORKDIR}"
PROMPT="${PROMPT//<<TODO_MD>>/$TODO_MD}"
TODO_CONTENT="$(cat "$TODO_MD")"
PROMPT="${PROMPT//<<TODO_CONTENT>>/$TODO_CONTENT}"
{
echo "------ [iter $iter] prompt begin ------"
echo "$PROMPT"
echo "------ [iter $iter] prompt end ------"
echo "[iter $iter] running codex..."
} > "$log"
# --- Codex 実行(リアルタイム表示: STREAM=onでtee)
set +e # codex exec のエラーを許容
if [[ "${STREAM}" == "on" ]]; then
if command -v timeout >/dev/null 2>&1; then
# pipefailだと左の失敗が伝播するので末尾に || true を付ける
timeout "$TIMEOUT_SECS" codex exec "$PROMPT" 2>&1 | tee -a "$log" || true
else
codex exec "$PROMPT" 2>&1 | tee -a "$log" || true
fi
else
if command -v timeout >/dev/null 2>&1; then
timeout "$TIMEOUT_SECS" codex exec "$PROMPT" >> "$log" 2>&1 || true
else
codex exec "$PROMPT" >> "$log" 2>&1 || true
fi
fi
set -e # エラーチェックを再開
sleep "$PAUSE_SEC"
after_unchecked="$(unchecked_count)"
echo "[iter $iter] unchecked: $before_unchecked -> $after_unchecked"
echo "[iter $iter] unchecked: $before_unchecked -> $after_unchecked" >> "$log"
if $COMMIT && command -v git >/dev/null 2>&1; then
git add -A || true
if ! git diff --cached --quiet; then
git commit -m "codex: iter $iter (unchecked $before_unchecked -> $after_unchecked)" >/dev/null 2>&1 || true
fi
fi
if ! progress_made "$before_unchecked"; then
echo "[iter $iter] no progress; stop."
echo "[iter $iter] no progress; stop." >> "$log"
break
fi
done
stop_heartbeat
# --- finish ---
if has_unchecked; then
left="$(unchecked_count)"
echo "[done] finished with $left unchecked remaining."
notify_win "ToDo 実行を終了(未完了 $left 件)"
else
echo "[done] all tasks completed."
notify_win "All tasks in $(basename "$TODO_MD") are completed ✅"
fi
カスタマイズ
細部は Codex などにカスタマイズ依頼いただけると!
Discussion