Last active
April 11, 2026 18:17
-
-
Save anaimi/21876bc315a7cfd99fea2a0627b9ef15 to your computer and use it in GitHub Desktop.
Code Kibitzer
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
| #!/usr/bin/env bash | |
| # | |
| # kibitzer.sh | |
| # Silent second-opinion code reviewer for Claude Code, powered by Codex. | |
| # | |
| # ── Objective ──────────────────────────────────────────────────────────────── | |
| # | |
| # A 'kibitzer' is someone who watches others (especially at chess | |
| # or cards) and offers unsolicited advice or commentary from the sidelines. | |
| # | |
| # This script is a kibitzer for Claude Code (CC). | |
| # | |
| # It watches a Claude Code (CC) tmux pane, when idle, sends latest activity | |
| # to Codex for review, and injects findings back into CC. | |
| # | |
| # | |
| # ── How it works ───────────────────────────────────────────────────────────── | |
| # | |
| # You are expected to be using tmux, with a Claude Code (CC) pane named 'cc'. | |
| # | |
| # 1. Poll CC's tmux pane via capture-pane | |
| # 2. Detect idle (output stable for IDLE_SECS) | |
| # 3. Diff against previous capture to extract new activity | |
| # 4. Send the last 50 lines to Codex via `codex exec resume --full-auto` | |
| # 5. Classify Codex's one-line reply: | |
| # SILENT → do nothing (default) | |
| # NOTE: <msg> → soft-inject "# kibitzer: <msg>" into CC | |
| # STOP: <msg> → Escape + hard-inject "# kibitzer STOP: <msg>" | |
| # 6. STOP hard-interrupts CC (Escape + inject); NOTE soft-injects | |
| # | |
| # ── Usage ──────────────────────────────────────────────────────────────────── | |
| # | |
| # ./kibitzer.sh seed Create initial Codex session | |
| # ./kibitzer.sh run Main loop (background this) | |
| # ./kibitzer.sh once Single tick, for debugging | |
| # ./kibitzer.sh test Run unit tests (no tmux needed) | |
| # | |
| # ── Examples ───────────────────────────────────────────────────────────────── | |
| # | |
| # # First time — seed Codex with the kibitzer role: | |
| # REPO_DIR=/var/www/myapp ./kibitzer.sh seed | |
| # | |
| # # Start watching (find your CC pane with: tmux list-panes -a): | |
| # CC_PANE=mysession:1.0 REPO_DIR=/var/www/myapp ./kibitzer.sh run & | |
| # | |
| # # Stop: | |
| # kill $(cat /tmp/kibitzer.pid) | |
| # | |
| # ── Customization ──────────────────────────────────────────────────────────── | |
| # | |
| # CC_PANE tmux pane for Claude Code (default: cc) | |
| # IDLE_SECS seconds of idle before reviewing (default: 5) | |
| # POLL_SECS polling interval (default: 3) | |
| # REPO_DIR working directory for codex exec (default: .) | |
| # CODEX_SESSION session id to resume (default: --last) | |
| # PIDFILE where to write the PID (default: /tmp/kibitzer.pid) | |
| # | |
| # ── Requirements ───────────────────────────────────────────────────────────── | |
| # | |
| # - tmux (running, with CC in a pane) | |
| # - codex CLI (npm i -g @openai/codex) | |
| # - REPO_DIR must be a git repo (codex exec requirement) | |
| # | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| set -uo pipefail | |
| # ── Configuration ──────────────────────────────────────────────────────────── | |
| CC_PANE="${CC_PANE:-cc}" | |
| IDLE_SECS="${IDLE_SECS:-5}" | |
| POLL_SECS="${POLL_SECS:-3}" | |
| REPO_DIR="${REPO_DIR:-.}" | |
| CODEX_SESSION="${CODEX_SESSION:-}" | |
| PIDFILE="${PIDFILE:-/tmp/kibitzer.pid}" | |
| # ── Prompts ────────────────────────────────────────────────────────────────── | |
| SEED_PROMPT='You are a silent code review kibitzer. | |
| When asked to review Claude Code (CC) activity, respond with exactly one line: | |
| SILENT, NOTE: <msg>, or STOP: <msg>. Default SILENT. Max 40 words. | |
| Study the code base carefully in order to be useful and relevant. You comments | |
| should be concise and to the point. Focus on: | |
| - architecure (opportunity for simplification or better petterns or alternative approaches) | |
| - logical bugs, race conditions | |
| - security (writing into unowned record, reading unowned record, raw error msg leak, etc) | |
| - code smell | |
| etc | |
| Pay attention to instructions in AGENTS.md and CLAUDE.md. | |
| Do not make code edits. | |
| Acknowledge.' | |
| REVIEW_PROMPT='Review this CC activity for critical issues. | |
| Respond with exactly one line: SILENT, NOTE: <msg>, or STOP: <msg>. | |
| Default SILENT. Max 40 words.' | |
| # ── IO (overridable for tests via CAPTURE_CMD, SEND_CMD, CODEX_CMD) ───────── | |
| : "${CAPTURE_CMD:=tmux capture-pane -p -S -400 -t}" | |
| : "${SEND_CMD:=tmux send-keys -t}" | |
| capture() { $CAPTURE_CMD "$1"; } | |
| send_escape() { $SEND_CMD "$1" Escape; } | |
| send_line() { | |
| local pane="$1" text="$2" | |
| if [ -n "${SEND_LITERAL_CMD:-}" ]; then | |
| $SEND_LITERAL_CMD "$pane" "$text" | |
| else | |
| tmux send-keys -t "$pane" -l "$text" | |
| sleep 0.2 | |
| tmux send-keys -t "$pane" Enter | |
| fi | |
| } | |
| # ── Pure functions ─────────────────────────────────────────────────────────── | |
| strip_ansi() { | |
| sed -E $'s/\x1b\\[[0-9;?]*[a-zA-Z]//g; s/\r$//' | |
| } | |
| diff_new_lines() { | |
| local prev="$1" curr="$2" | |
| awk -v prev="$prev" ' | |
| BEGIN { n=split(prev, a, "\n"); for (i=1;i<=n;i++) seen[a[i]]=1 } | |
| { if (!($0 in seen)) print } | |
| ' <<<"$curr" | |
| } | |
| classify_reply() { | |
| local r="$1" | |
| [ -z "$r" ] && { echo "NONE"; return; } | |
| local msg | |
| case "$r" in | |
| SILENT|silent) echo "SILENT" ;; | |
| STOP:*) msg="${r#STOP:}"; msg="${msg# }"; echo "STOP $msg" ;; | |
| NOTE:*) msg="${r#NOTE:}"; msg="${msg# }"; echo "NOTE $msg" ;; | |
| WHISPER:*) msg="${r#WHISPER:}"; msg="${msg# }"; echo "NOTE $msg" ;; | |
| *) echo "NOTE $r" ;; | |
| esac | |
| } | |
| # ── Orchestration ──────────────────────────────────────────────────────────── | |
| consult_codex() { | |
| local chunk="$1" | |
| local resume_flag="${CODEX_SESSION:-}" | |
| [ -z "$resume_flag" ] && resume_flag="--last" | |
| local prompt | |
| printf -v prompt '%s\n\n<cc-activity>\n%s\n</cc-activity>' \ | |
| "$REVIEW_PROMPT" "$(tail -50 <<<"$chunk")" | |
| local reply_file="/tmp/kibitzer-reply.$$" | |
| if [ -n "${CODEX_CMD:-}" ]; then | |
| $CODEX_CMD "$prompt" > "$reply_file" 2>/dev/null | |
| else | |
| (cd "$REPO_DIR" && codex exec resume $resume_flag --full-auto \ | |
| -o "$reply_file" "$prompt") >/dev/null 2>&1 | |
| fi | |
| local reply="" | |
| [ -f "$reply_file" ] && reply="$(cat "$reply_file")" | |
| rm -f "$reply_file" | |
| echo "$reply" | |
| } | |
| inject() { | |
| local cls="$1" | |
| local msg="${cls#* }" | |
| if [[ "$cls" == STOP* ]]; then | |
| send_escape "$CC_PANE"; sleep 0.2 | |
| send_line "$CC_PANE" "# kibitzer STOP: $msg" | |
| else | |
| send_line "$CC_PANE" "# kibitzer: $msg" | |
| fi | |
| } | |
| tick() { | |
| local prev_file="${PREV_FILE:-/tmp/kibitzer.prev.$$}" | |
| local prev="" | |
| [ -f "$prev_file" ] && prev="$(cat "$prev_file")" | |
| local curr; curr="$(capture "$CC_PANE" 2>&1 | strip_ansi)" | |
| echo "$curr" >"$prev_file" | |
| local new; new="$(diff_new_lines "$prev" "$curr")" | |
| TICK_NEW_LINES="$([ -n "$new" ] && wc -l <<<"$new" || echo 0)" | |
| if [ -z "$new" ]; then | |
| TICK_RESULT="no-change" | |
| return 0 | |
| fi | |
| local reply; reply="$(consult_codex "$new")" | |
| local cls; cls="$(classify_reply "$reply")" | |
| case "$cls" in | |
| NONE|SILENT) TICK_RESULT="SILENT" ; return 0 ;; | |
| STOP*) TICK_RESULT="STOP" ; inject "$cls" ;; | |
| *) TICK_RESULT="NOTE" ; inject "$cls" ;; | |
| esac | |
| } | |
| # ── Commands ───────────────────────────────────────────────────────────────── | |
| check_cc_pane() { | |
| if capture "$CC_PANE" >/dev/null 2>&1; then | |
| local lines; lines="$(capture "$CC_PANE" 2>/dev/null | wc -l)" | |
| echo " CC pane '$CC_PANE': reachable ($lines lines visible)" | |
| return 0 | |
| else | |
| echo " CC pane '$CC_PANE': NOT FOUND — check tmux pane name" | |
| return 1 | |
| fi | |
| } | |
| check_codex() { | |
| if command -v codex >/dev/null 2>&1; then | |
| echo " codex CLI: found ($(command -v codex))" | |
| return 0 | |
| else | |
| echo " codex CLI: NOT FOUND — install with: npm i -g @openai/codex" | |
| return 1 | |
| fi | |
| } | |
| run_loop() { | |
| # kill previous instance if still running | |
| if [ -f "$PIDFILE" ]; then | |
| local old_pid; old_pid="$(cat "$PIDFILE")" | |
| if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then | |
| echo "kibitzer: killing previous instance (pid $old_pid)" | |
| kill "$old_pid" 2>/dev/null | |
| sleep 0.5 | |
| fi | |
| fi | |
| local last_curr="" stable_since=0 | |
| export PREV_FILE="/tmp/kibitzer.prev.$$" | |
| : >"$PREV_FILE" | |
| echo $$ > "$PIDFILE" | |
| trap 'rm -f "$PREV_FILE" "$PIDFILE"' EXIT | |
| # ── Startup banner ── | |
| echo "┌─────────────────────────────────────────────┐" | |
| echo "│ kibitzer — silent code review watcher │" | |
| echo "└─────────────────────────────────────────────┘" | |
| echo " pid: $$ pidfile: $PIDFILE" | |
| echo " repo: $REPO_DIR" | |
| echo " CC_PANE=$CC_PANE poll=${POLL_SECS}s idle=${IDLE_SECS}s" | |
| echo "" | |
| echo "── Preflight checks ──" | |
| local checks_ok=true | |
| check_cc_pane || checks_ok=false | |
| check_codex || checks_ok=false | |
| if [ -d "$REPO_DIR/.git" ]; then | |
| echo " repo git: ok" | |
| else | |
| echo " repo git: WARNING — $REPO_DIR is not a git repo (codex requires git)" | |
| checks_ok=false | |
| fi | |
| echo "" | |
| if [ "$checks_ok" = false ]; then | |
| echo "── WARNING: some checks failed — will attempt to run anyway ──" | |
| echo "" | |
| fi | |
| echo "── Watching ($(date +%Y-%m-%d\ %H:%M:%S)) ──" | |
| local tick_count=0 silent_count=0 note_count=0 stop_count=0 | |
| TICK_RESULT="" TICK_NEW_LINES=0 | |
| while true; do | |
| local curr; curr="$(capture "$CC_PANE" 2>/dev/null | strip_ansi)" | |
| if [ -z "$curr" ]; then | |
| echo "[$(date +%H:%M:%S)] ⚠ CC pane empty or unreachable" | |
| sleep "$POLL_SECS" | |
| continue | |
| fi | |
| if [ "$curr" = "$last_curr" ]; then | |
| if (( stable_since == 0 )); then stable_since=$SECONDS; fi | |
| local idle_elapsed=$(( SECONDS - stable_since )) | |
| if (( idle_elapsed >= IDLE_SECS )); then | |
| local t0=$SECONDS | |
| TICK_RESULT="" TICK_NEW_LINES=0 | |
| tick | |
| local elapsed=$(( SECONDS - t0 )) | |
| ((tick_count++)) | |
| case "$TICK_RESULT" in | |
| SILENT) ((silent_count++)) ;; | |
| NOTE) ((note_count++)) ;; | |
| STOP) ((stop_count++)) ;; | |
| esac | |
| printf '[%s] tick #%-3d %2ds %3d new lines result=%-9s totals: %d silent / %d note / %d stop\n' \ | |
| "$(date +%H:%M:%S)" "$tick_count" "$elapsed" "$TICK_NEW_LINES" \ | |
| "$TICK_RESULT" "$silent_count" "$note_count" "$stop_count" | |
| stable_since=0 | |
| fi | |
| else | |
| last_curr="$curr"; stable_since=0 | |
| fi | |
| sleep "$POLL_SECS" | |
| done | |
| } | |
| seed_session() { | |
| echo "Seeding kibitzer Codex session..." | |
| (cd "$REPO_DIR" && codex exec --full-auto "$SEED_PROMPT") 2>&1 | |
| echo | |
| echo "Done. Now run: CC_PANE=<pane> REPO_DIR=<repo> $0 run" | |
| } | |
| # ── Tests ──────────────────────────────────────────────────────────────────── | |
| assert_eq() { | |
| local name="$1" want="$2" got="$3" | |
| if [ "$want" = "$got" ]; then | |
| echo "ok - $name" | |
| else | |
| echo "FAIL - $name" | |
| echo " want: $(printf %q "$want")" | |
| echo " got: $(printf %q "$got")" | |
| return 1 | |
| fi | |
| } | |
| run_tests() { | |
| local fails=0 | |
| assert_eq "strip_ansi" "red plain" \ | |
| "$(printf '\x1b[31mred\x1b[0m plain' | strip_ansi)" || ((fails++)) | |
| assert_eq "diff adds" "c" \ | |
| "$(diff_new_lines $'a\nb' $'a\nb\nc')" || ((fails++)) | |
| assert_eq "diff same" "" \ | |
| "$(diff_new_lines $'a\nb' $'a\nb')" || ((fails++)) | |
| assert_eq "cls SILENT" "SILENT" "$(classify_reply "SILENT")" || ((fails++)) | |
| assert_eq "cls STOP" "STOP danger" "$(classify_reply "STOP: danger")" || ((fails++)) | |
| assert_eq "cls NOTE" "NOTE fyi" "$(classify_reply "NOTE: fyi")" || ((fails++)) | |
| assert_eq "cls WHISPER" "NOTE hey" "$(classify_reply "WHISPER: hey")" || ((fails++)) | |
| assert_eq "cls bare" "NOTE raw" "$(classify_reply "raw")" || ((fails++)) | |
| assert_eq "cls empty" "NONE" "$(classify_reply "")" || ((fails++)) | |
| local TMPDIR; TMPDIR="$(mktemp -d)" | |
| local sent_log="$TMPDIR/sent.log" | |
| export CAPTURE_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_capture" | |
| export SEND_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_send $sent_log" | |
| export SEND_LITERAL_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_send $sent_log" | |
| export CODEX_CMD="$(cd "$(dirname "$0")" && pwd)/$(basename "$0") _fake_codex" | |
| export FAKE_CODEX_REPLY="STOP: migration will drop column" | |
| export FAKE_CC_OUT=$'$ edit migrations/0003.sql\nALTER TABLE users DROP email;' | |
| export PREV_FILE="$TMPDIR/prev" | |
| : >"$PREV_FILE" | |
| tick >/dev/null | |
| local sent; sent="$(cat "$sent_log")" | |
| grep -q "Escape" <<<"$sent" \ | |
| && assert_eq "tick Escape on STOP" "1" "1" \ | |
| || { assert_eq "tick Escape on STOP" "1" "0"; ((fails++)); } | |
| grep -q "kibitzer STOP: migration will drop column" <<<"$sent" \ | |
| && assert_eq "tick STOP msg" "1" "1" \ | |
| || { assert_eq "tick STOP msg" "1" "0"; ((fails++)); } | |
| : >"$sent_log"; : >"$PREV_FILE" | |
| export FAKE_CODEX_REPLY="SILENT" | |
| tick >/dev/null | |
| [ ! -s "$sent_log" ] \ | |
| && assert_eq "tick SILENT no inject" "1" "1" \ | |
| || { assert_eq "tick SILENT no inject" "1" "0"; ((fails++)); } | |
| rm -rf "$TMPDIR" | |
| echo | |
| (( fails == 0 )) && echo "All tests passed." || echo "$fails test(s) failed." | |
| return "$fails" | |
| } | |
| # ── Test fakes (spawned as subprocesses) ───────────────────────────────────── | |
| _fake_capture() { printf '%s\n' "${FAKE_CC_OUT:-}"; } | |
| _fake_send() { local log="$1"; shift; printf 'SEND pane=%s args=%s\n' "$1" "${*:2}" >>"$log"; } | |
| _fake_codex() { echo "${FAKE_CODEX_REPLY:-SILENT}"; } | |
| # ── Entry ──────────────────────────────────────────────────────────────────── | |
| case "${1:-run}" in | |
| run) run_loop ;; | |
| once) tick ;; | |
| test) run_tests ;; | |
| seed) seed_session ;; | |
| _fake_capture) shift; _fake_capture "$@" ;; | |
| _fake_send) shift; _fake_send "$@" ;; | |
| _fake_codex) shift; _fake_codex "$@" ;; | |
| *) echo "usage: $0 {run|once|test|seed}" >&2; exit 2 ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment