diff --git a/scripts/codex-run.sh b/scripts/codex-run.sh new file mode 100755 index 0000000..ebbf294 --- /dev/null +++ b/scripts/codex-run.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# codex-crew: deterministic wrapper around `codex exec` with stall watchdog. +# +# Usage: +# codex-run.sh exec [codex-flags...] [prompt] +# codex-run.sh resume [codex-flags...] [prompt] +# codex-run.sh review [--base X|--commit Y|--uncommitted] [codex-flags...] [prompt] +# +# If /prompt.md exists and no positional prompt given, it is fed via stdin. +# Review runs against the git repo at the CURRENT WORKING DIRECTORY (no -C support). +# +# Files written to : events.jsonl last.md err.log thread_id status +# status: ok | stalled | error: . Exit code 0 only on ok. +# +# Env knobs: +# CODEX_STALL_TIMEOUT seconds of event silence before kill (default 600) +# CODEX_WATCH_INTERVAL watchdog poll interval seconds (default 15) +# CODEX_BIN codex binary (default codex; tests use a stub) +set -u + +JOB_DIR=$1; MODE=$2; shift 2 +mkdir -p "$JOB_DIR" +JOB_DIR=$(cd "$JOB_DIR" && pwd) +EV=$JOB_DIR/events.jsonl LAST=$JOB_DIR/last.md ERR=$JOB_DIR/err.log +STALL=${CODEX_STALL_TIMEOUT:-600} +TICK=${CODEX_WATCH_INTERVAL:-15} +BIN=${CODEX_BIN:-codex} + +case $MODE in + exec) CMD=("$BIN" exec --json --skip-git-repo-check -o "$LAST") ;; + resume) TID=$1; shift; printf '%s\n' "$TID" > "$JOB_DIR/thread_id" + CMD=("$BIN" exec resume "$TID" --json --skip-git-repo-check -o "$LAST") ;; + review) CMD=("$BIN" exec review --json -o "$LAST") ;; + *) echo "usage: codex-run.sh exec|resume|review ..." >&2; exit 2 ;; +esac + +rm -f "$JOB_DIR/status" "$LAST"; : > "$EV"; : > "$ERR" + +if [[ $# -eq 0 && -s $JOB_DIR/prompt.md ]]; then + "${CMD[@]}" - < "$JOB_DIR/prompt.md" > "$EV" 2> "$ERR" & +else + "${CMD[@]}" "$@" < /dev/null > "$EV" 2> "$ERR" & +fi +PID=$! + +( while kill -0 "$PID" 2>/dev/null; do + sleep "$TICK" + now=$(date +%s); mt=$(stat -c %Y "$EV" 2>/dev/null || echo "$now") + if (( now - mt > STALL )); then + echo stalled > "$JOB_DIR/status" + kill "$PID" 2>/dev/null; sleep 3; kill -9 "$PID" 2>/dev/null + break + fi + done ) & +WATCH=$! + +wait "$PID"; RC=$? +kill "$WATCH" 2>/dev/null; wait "$WATCH" 2>/dev/null + +TID_FOUND=$(grep -m1 -o '"thread_id":"[^"]*"' "$EV" | cut -d'"' -f4) +[[ -n $TID_FOUND ]] && printf '%s\n' "$TID_FOUND" > "$JOB_DIR/thread_id" + +if [[ -s $JOB_DIR/status ]]; then + : # stalled, written by watchdog +elif [[ $RC -eq 0 && -s $LAST ]]; then + echo ok > "$JOB_DIR/status" +else + echo "error: exit=$RC $(tail -c 300 "$ERR" | tr '\n' ' ')" > "$JOB_DIR/status" +fi + +ST=$(head -n1 "$JOB_DIR/status") +echo "status: $ST" +echo "thread_id: $(cat "$JOB_DIR/thread_id" 2>/dev/null || echo none)" +[[ $ST == ok ]] diff --git a/tests/stub-hang/codex b/tests/stub-hang/codex new file mode 100755 index 0000000..f5b302d --- /dev/null +++ b/tests/stub-hang/codex @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# fake codex: emits one event then hangs silently +echo '{"type":"thread.started","thread_id":"hang-thread-456"}' +sleep 300 diff --git a/tests/stub-ok/codex b/tests/stub-ok/codex new file mode 100755 index 0000000..a4bfc59 --- /dev/null +++ b/tests/stub-ok/codex @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# fake codex: emits thread.started, writes last message file, exits 0 +out="" +prev="" +for a in "$@"; do [[ $prev == "-o" ]] && out=$a; prev=$a; done +echo '{"type":"thread.started","thread_id":"test-thread-123"}' +echo '{"type":"turn.completed"}' +echo "stub answer" > "$out" +exit 0 diff --git a/tests/test-run.sh b/tests/test-run.sh new file mode 100755 index 0000000..c65e794 --- /dev/null +++ b/tests/test-run.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +RUN=$HERE/../scripts/codex-run.sh +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +# 1. happy path -> status ok, thread_id captured, exit 0 +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/ok" exec "hello" +[[ $(cat "$TMP/ok/status") == ok ]] +[[ $(cat "$TMP/ok/thread_id") == test-thread-123 ]] +[[ $(cat "$TMP/ok/last.md") == "stub answer" ]] + +# 2. stall -> watchdog kills, status stalled, exit nonzero +if CODEX_BIN=$HERE/stub-hang/codex CODEX_STALL_TIMEOUT=4 CODEX_WATCH_INTERVAL=2 \ + "$RUN" "$TMP/hang" exec "hello"; then + echo "FAIL: stall run must exit nonzero"; exit 1 +fi +[[ $(cat "$TMP/hang/status") == stalled ]] +[[ $(cat "$TMP/hang/thread_id") == hang-thread-456 ]] + +# 3. prompt.md via stdin path +mkdir -p "$TMP/stdin"; echo "prompt from file" > "$TMP/stdin/prompt.md" +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/stdin" exec +[[ $(cat "$TMP/stdin/status") == ok ]] + +echo "ALL PASS"