Created
February 13, 2026 06:39
-
-
Save alextangson/345b4a5d69df364751a394e347e5993a to your computer and use it in GitHub Desktop.
OpenClaw + Codex CLI 零轮询脚本 — 邪修大法第三弹
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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