Skip to content

Instantly share code, notes, and snippets.

@alextangson
Created February 13, 2026 06:39
Show Gist options
  • Select an option

  • Save alextangson/345b4a5d69df364751a394e347e5993a to your computer and use it in GitHub Desktop.

Select an option

Save alextangson/345b4a5d69df364751a394e347e5993a to your computer and use it in GitHub Desktop.
OpenClaw + Codex CLI 零轮询脚本 — 邪修大法第三弹
#!/bin/bash
# =============================================================
# dispatch-codex.sh — Dispatch a task to Codex CLI (background)
# =============================================================
# Called by OpenClaw/Forge to launch Codex CLI in the background.
# Writes task info to state file, starts Codex in non-interactive
# exec mode, and returns immediately (non-blocking).
#
# On completion, on-codex-complete.sh is called automatically
# to write results and notify OpenClaw.
#
# Usage:
# dispatch-codex.sh "task description" [work_dir] [--review] [--base BRANCH]
#
# Modes:
# Default (exec): codex exec --full-auto — audit + fix + commit
# Review (--review): codex review --base main — read-only code review
#
# Examples:
# dispatch-codex.sh "audit backend/app/services/ for security issues" ~/repos/stello
# dispatch-codex.sh "review latest changes" ~/repos/stello --review --base main~5
# =============================================================
set -euo pipefail
STATE_DIR="/Users/macmini/.openclaw/workspace/state"
SCRIPTS_DIR="/Users/macmini/.openclaw/workspace/scripts"
TASK_FILE="${STATE_DIR}/codex-task.json"
PID_FILE="${STATE_DIR}/codex-pid"
LOG_FILE="${STATE_DIR}/codex-dispatch.log"
CODEX_OUTPUT="${STATE_DIR}/codex-output.log"
RESULT_FILE="${STATE_DIR}/codex-result.json"
CODEX_BIN="/Users/macmini/.volta/bin/codex"
# Ensure state dir exists
mkdir -p "$STATE_DIR"
# ---- Parse arguments ----
TASK="${1:-}"
WORK_DIR="${2:-$(pwd)}"
MODE="exec"
REVIEW_BASE="main"
shift 2 2>/dev/null || true
while [[ $# -gt 0 ]]; do
case "$1" in
--review)
MODE="review"
shift
;;
--base)
REVIEW_BASE="${2:-main}"
shift 2
;;
*)
shift
;;
esac
done
if [ -z "$TASK" ]; then
echo "ERROR: No task provided"
echo "Usage: dispatch-codex.sh \"task description\" [work_dir] [--review] [--base BRANCH]"
exit 1
fi
# ---- Logging helper ----
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
log "=== New Codex dispatch ==="
log "Task: $TASK"
log "Work dir: $WORK_DIR"
log "Mode: $MODE"
[ "$MODE" = "review" ] && log "Review base: $REVIEW_BASE"
# ---- Check if a Codex task is already running ----
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
EXISTING_PID=$(cat "$PID_FILE")
echo "ERROR: Codex is already running (PID: $EXISTING_PID)"
echo "Wait for it to finish or kill it: kill $EXISTING_PID"
log "Aborted: existing Codex process running (PID: $EXISTING_PID)"
exit 1
fi
# ---- Clean up previous state ----
rm -f "$RESULT_FILE" 2>/dev/null
rm -f "${STATE_DIR}/codex-complete.lock" 2>/dev/null
# ---- Write task file ----
DISPATCHED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
jq -n \
--arg task "$TASK" \
--arg work_dir "$WORK_DIR" \
--arg dispatched_at "$DISPATCHED_AT" \
--arg mode "$MODE" \
--arg review_base "$REVIEW_BASE" \
'{
task: $task,
work_dir: $work_dir,
dispatched_at: $dispatched_at,
mode: $mode,
review_base: $review_base
}' > "$TASK_FILE"
log "Task file written to $TASK_FILE"
# ---- Ensure work directory exists ----
if [ ! -d "$WORK_DIR" ]; then
echo "ERROR: Work directory does not exist: $WORK_DIR"
log "Aborted: work directory not found: $WORK_DIR"
exit 1
fi
# ---- Sync repo before audit ----
log "Syncing repo: git pull origin main"
cd "$WORK_DIR" && git pull origin main --ff-only >> "$LOG_FILE" 2>&1 || log "WARNING: git pull failed, proceeding with current state"
# ---- Write launch script to avoid quoting hell in nohup bash -c ----
LAUNCH_SCRIPT="${STATE_DIR}/codex-launch.sh"
if [ "$MODE" = "review" ]; then
# codex review --base BRANCH cannot take a positional PROMPT argument
# Use --title for context instead
cat > "$LAUNCH_SCRIPT" <<LAUNCH_EOF
#!/bin/bash
cd "$WORK_DIR"
"$CODEX_BIN" review --base "$REVIEW_BASE" --title "$TASK" > "$CODEX_OUTPUT" 2>&1
EXIT_CODE=\$?
echo "{\"exit_code\": \$EXIT_CODE, \"mode\": \"$MODE\"}" | "$SCRIPTS_DIR/on-codex-complete.sh"
LAUNCH_EOF
else
cat > "$LAUNCH_SCRIPT" <<LAUNCH_EOF
#!/bin/bash
cd "$WORK_DIR"
"$CODEX_BIN" exec --full-auto -C "$WORK_DIR" -o "${STATE_DIR}/codex-last-message.txt" "$TASK" > "$CODEX_OUTPUT" 2>&1
EXIT_CODE=\$?
echo "{\"exit_code\": \$EXIT_CODE, \"mode\": \"$MODE\"}" | "$SCRIPTS_DIR/on-codex-complete.sh"
LAUNCH_EOF
fi
chmod +x "$LAUNCH_SCRIPT"
log "Launch script written to $LAUNCH_SCRIPT"
# ---- Launch Codex in background ----
nohup bash "$LAUNCH_SCRIPT" > /dev/null 2>&1 &
CODEX_PID=$!
echo "$CODEX_PID" > "$PID_FILE"
log "Codex launched with PID: $CODEX_PID"
log "Output logging to: $CODEX_OUTPUT"
# ---- Output for OpenClaw ----
echo "Codex task dispatched successfully!"
echo " Task: $TASK"
echo " Work dir: $WORK_DIR"
echo " Mode: $MODE"
[ "$MODE" = "review" ] && echo " Review base: $REVIEW_BASE"
echo " PID: $CODEX_PID"
echo " Dispatched at: $DISPATCHED_AT"
echo ""
echo "Codex is running in the background."
echo "Result will be written to: $RESULT_FILE"
exit 0
#!/bin/bash
# =============================================================
# on-codex-complete.sh — Codex CLI Completion Callback
# =============================================================
# Called by dispatch-codex.sh wrapper after Codex CLI finishes.
# Reads exit code from stdin, collects results, writes state
# file, and notifies OpenClaw via wake event.
#
# Input (stdin): JSON with exit_code and mode
# {"exit_code": 0, "mode": "exec"}
#
# Mirrors the behavior of on-cc-complete.sh for Claude Code.
# =============================================================
set -euo pipefail
STATE_DIR="/Users/macmini/.openclaw/workspace/state"
TASK_FILE="${STATE_DIR}/codex-task.json"
RESULT_FILE="${STATE_DIR}/codex-result.json"
LOCK_FILE="${STATE_DIR}/codex-complete.lock"
LOG_FILE="${STATE_DIR}/codex-hook.log"
PID_FILE="${STATE_DIR}/codex-pid"
CODEX_OUTPUT="${STATE_DIR}/codex-output.log"
LAST_MSG_FILE="${STATE_DIR}/codex-last-message.txt"
# Ensure state dir exists
mkdir -p "$STATE_DIR"
# ---- Logging helper ----
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
log "=== Codex completion callback ==="
# ---- Read completion input from stdin ----
INPUT=$(cat)
log "Input received: $INPUT"
EXIT_CODE=$(echo "$INPUT" | jq -r '.exit_code // "unknown"')
MODE=$(echo "$INPUT" | jq -r '.mode // "exec"')
log "Exit code: $EXIT_CODE, Mode: $MODE"
# ---- Deduplication: skip if result was written in last 60s ----
if [ -f "$RESULT_FILE" ]; then
RESULT_AGE=$(( $(date +%s) - $(stat -f %m "$RESULT_FILE" 2>/dev/null || echo 0) ))
if [ "$RESULT_AGE" -lt 60 ]; then
log "Dedup: result file is ${RESULT_AGE}s old (< 60s), skipping"
exit 0
fi
fi
# ---- Acquire simple lock ----
if [ -f "$LOCK_FILE" ]; then
LOCK_AGE=$(( $(date +%s) - $(stat -f %m "$LOCK_FILE" 2>/dev/null || echo 0) ))
if [ "$LOCK_AGE" -lt 30 ]; then
log "Lock exists and is ${LOCK_AGE}s old, skipping"
exit 0
fi
fi
echo $$ > "$LOCK_FILE"
# ---- Read task info ----
TASK_NAME="unknown"
TASK_DISPATCHED=""
WORK_DIR="$(pwd)"
if [ -f "$TASK_FILE" ]; then
TASK_NAME=$(jq -r '.task // "unknown"' "$TASK_FILE")
TASK_DISPATCHED=$(jq -r '.dispatched_at // ""' "$TASK_FILE")
WORK_DIR=$(jq -r '.work_dir // "'"$(pwd)"'"' "$TASK_FILE")
log "Task file found: $TASK_NAME"
else
log "No task file found, using defaults"
fi
# ---- Determine status ----
if [ "$EXIT_CODE" = "0" ]; then
STATUS="completed"
else
STATUS="failed"
fi
# ---- Calculate duration ----
COMPLETED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
DURATION="unknown"
if [ -n "$TASK_DISPATCHED" ]; then
START_TS=$(python3 -c "
from datetime import datetime, timezone
try:
dt = datetime.fromisoformat('${TASK_DISPATCHED}'.replace('Z','+00:00'))
print(int(dt.timestamp()))
except:
print(0)
" 2>/dev/null || echo 0)
END_TS=$(date +%s)
if [ "$START_TS" -gt 0 ]; then
ELAPSED=$(( END_TS - START_TS ))
if [ "$ELAPSED" -ge 3600 ]; then
HOURS=$(( ELAPSED / 3600 ))
MINUTES=$(( (ELAPSED % 3600) / 60 ))
SECONDS=$(( ELAPSED % 60 ))
DURATION="${HOURS}h${MINUTES}m${SECONDS}s"
else
MINUTES=$(( ELAPSED / 60 ))
SECONDS=$(( ELAPSED % 60 ))
DURATION="${MINUTES}m${SECONDS}s"
fi
fi
fi
# ---- Read last message from Codex (if exec mode with -o) ----
LAST_MESSAGE=""
if [ -f "$LAST_MSG_FILE" ]; then
LAST_MESSAGE=$(head -c 2000 "$LAST_MSG_FILE" 2>/dev/null || echo "")
log "Last message file found (${#LAST_MESSAGE} chars)"
fi
# ---- Read tail of output log ----
OUTPUT_TAIL=""
if [ -f "$CODEX_OUTPUT" ]; then
OUTPUT_TAIL=$(tail -50 "$CODEX_OUTPUT" 2>/dev/null | head -c 3000 || echo "")
log "Output log tail captured"
fi
# ---- Collect git changes (if any commits were made) ----
GIT_CHANGES=""
if [ -d "$WORK_DIR/.git" ]; then
GIT_CHANGES=$(cd "$WORK_DIR" && git log --oneline -5 2>/dev/null | head -c 500 || echo "")
fi
# ---- Write result JSON ----
jq -n \
--arg task "$TASK_NAME" \
--arg work_dir "$WORK_DIR" \
--arg mode "$MODE" \
--arg status "$STATUS" \
--argjson exit_code "${EXIT_CODE:-1}" \
--arg completed_at "$COMPLETED_AT" \
--arg duration "$DURATION" \
--arg last_message "$LAST_MESSAGE" \
--arg output_tail "$OUTPUT_TAIL" \
--arg git_changes "$GIT_CHANGES" \
'{
task: $task,
work_dir: $work_dir,
mode: $mode,
status: $status,
exit_code: $exit_code,
completed_at: $completed_at,
duration: $duration,
last_message: $last_message,
output_tail: $output_tail,
git_changes: $git_changes
}' > "$RESULT_FILE"
log "Result written to $RESULT_FILE (status: $STATUS)"
# ---- Clean up PID file ----
rm -f "$PID_FILE" 2>/dev/null
# ---- Notify OpenClaw via wake event ----
if command -v openclaw &>/dev/null; then
if [ "$STATUS" = "completed" ]; then
openclaw send "🔍 Codex 审计完成通知:任务「${TASK_NAME}」已完成,耗时 ${DURATION},模式: ${MODE},工作目录: ${WORK_DIR}。请读取 ${RESULT_FILE} 查看详细结果。" 2>/dev/null &
else
openclaw send "❌ Codex 审计失败通知:任务「${TASK_NAME}」失败(exit code: ${EXIT_CODE}),耗时 ${DURATION},模式: ${MODE}。请检查 ${CODEX_OUTPUT} 查看错误日志。" 2>/dev/null &
fi
log "Wake event sent to OpenClaw (status: $STATUS)"
else
log "WARNING: openclaw CLI not found, skipping wake event"
fi
# ---- Cleanup lock ----
rm -f "$LOCK_FILE"
log "Callback completed successfully"
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment