109 lines
4.7 KiB
Bash
Executable File
109 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# codex-crew: deterministic wrapper around `codex exec` with stall watchdog.
|
|
#
|
|
# Usage:
|
|
# codex-run.sh <job_dir> exec [codex-flags...]
|
|
# codex-run.sh <job_dir> resume <thread_id> [codex-flags...]
|
|
# codex-run.sh <job_dir> review [--base X|--commit Y|--uncommitted] [codex-flags...]
|
|
#
|
|
# If <job_dir>/prompt.md is non-empty it is always fed via stdin ('-'); codex-flags
|
|
# stay positional, so options and a file prompt combine freely.
|
|
# Review runs against the git repo at the CURRENT WORKING DIRECTORY (no -C support).
|
|
# Review mode: codex CLI forbids combining scope flags (--base/--commit/--uncommitted)
|
|
# with a positional [PROMPT] — and stdin '-' counts as PROMPT. So with a non-empty
|
|
# prompt.md the scope flag is translated into prompt text and review runs with custom
|
|
# instructions only; with an empty prompt.md the native `review <scope>` is used.
|
|
#
|
|
# Files written to <job_dir>: events.jsonl last.md err.log thread_id status
|
|
# status: ok | stalled | error: <reason>. 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
|
|
PF=$JOB_DIR/prompt.md
|
|
STALL=${CODEX_STALL_TIMEOUT:-600}
|
|
TICK=${CODEX_WATCH_INTERVAL:-15}
|
|
BIN=${CODEX_BIN:-codex}
|
|
FLAGS_FILE="$JOB_DIR/codex-flags"
|
|
build_cmd() { local -a f=("$@"); CMD=("$BIN" "${f[@]}" exec --json --skip-git-repo-check -o "$LAST"); }
|
|
|
|
case $MODE in
|
|
exec) if (( $# )); then printf '%s\0' "$@" > "$FLAGS_FILE"; else : > "$FLAGS_FILE"; fi
|
|
build_cmd "$@" ;;
|
|
review)
|
|
if [[ -s $JOB_DIR/prompt.md ]]; then
|
|
SCOPE_DESC=""; REST=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--base) SCOPE_DESC="Review the changes on the current branch against base ref '$2' — run \`git diff $2...HEAD\` (merge-base) to see the full change set."; shift 2 ;;
|
|
--commit) SCOPE_DESC="Review the changes introduced by commit '$2' — run \`git show $2\` to see them."; shift 2 ;;
|
|
--uncommitted) SCOPE_DESC="Review all uncommitted changes (staged, unstaged, untracked) — run \`git status\` and \`git diff HEAD\` to see them."; shift ;;
|
|
*) REST+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
if (( ${#REST[@]} )); then printf '%s\0' "${REST[@]}" > "$FLAGS_FILE"; else : > "$FLAGS_FILE"; fi
|
|
set -- "${REST[@]}"
|
|
if [[ -n $SCOPE_DESC ]]; then
|
|
PF=$JOB_DIR/prompt.final.md
|
|
{ printf 'This is a code review task. %s\n\n' "$SCOPE_DESC"; cat "$JOB_DIR/prompt.md"; } > "$PF"
|
|
fi
|
|
CMD=("$BIN" "$@" exec review --json --skip-git-repo-check -o "$LAST")
|
|
else
|
|
SCOPE_ARGS=(); REST=()
|
|
while [[ $# -gt 0 ]]; do case $1 in --base) SCOPE_ARGS=(--base "$2"); shift 2;; --commit) SCOPE_ARGS=(--commit "$2"); shift 2;; --uncommitted) SCOPE_ARGS=(--uncommitted); shift;; *) REST+=("$1"); shift;; esac; done
|
|
if (( ${#REST[@]} )); then printf '%s\0' "${REST[@]}" > "$FLAGS_FILE"; else : > "$FLAGS_FILE"; fi
|
|
CMD=("$BIN" "${REST[@]}" exec review "${SCOPE_ARGS[@]}" --json --skip-git-repo-check -o "$LAST")
|
|
fi ;;
|
|
resume) TID=$1; shift
|
|
printf '%s\n' "$TID" > "$JOB_DIR/thread_id"
|
|
if [[ -s $FLAGS_FILE ]]; then mapfile -d '' -t SAVED < "$FLAGS_FILE"; set -- "${SAVED[@]}" "$@"; fi
|
|
CMD=("$BIN" "$@" exec resume "$TID" --json --skip-git-repo-check -o "$LAST") ;;
|
|
*) echo "usage: codex-run.sh <job_dir> exec|resume|review ..." >&2; exit 2 ;;
|
|
esac
|
|
|
|
rm -f "$JOB_DIR/status" "$LAST"; : > "$EV"; : > "$ERR"
|
|
|
|
if [[ -s $PF ]]; then
|
|
"${CMD[@]}" - < "$PF" > "$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 ]]
|