diff --git a/.cursor/commands/speckit.specify.md b/.cursor/commands/speckit.specify.md
new file mode 100644
index 0000000..c49e310
--- /dev/null
+++ b/.cursor/commands/speckit.specify.md
@@ -0,0 +1,18 @@
+---
+description: Create a feature specification
+---
+
+Create a specification for:
+
+$ARGUMENTS
+
+## Steps
+
+1. Generate short name (2-4 words, kebab-case)
+2. Find next spec number from `specs/`
+3. Create `specs/NNN-short-name.md`
+4. Include clear acceptance criteria
+5. Add completion signal:
+ ```
+ **Output when complete:** `DONE`
+ ```
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..48643ec
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.env
+.venv/
+__pycache__/
+*.pyc
+.DS_Store
+.specify/
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..dc14e19
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,5 @@
+# Agent Instructions
+
+**Read:** `.specify/memory/constitution.md`
+
+That file is your source of truth for this project.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..dc14e19
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,5 @@
+# Agent Instructions
+
+**Read:** `.specify/memory/constitution.md`
+
+That file is your source of truth for this project.
diff --git a/PROMPT_build.md b/PROMPT_build.md
new file mode 100644
index 0000000..bb7ad50
--- /dev/null
+++ b/PROMPT_build.md
@@ -0,0 +1,18 @@
+# Ralph Build Mode
+
+Read `.specify/memory/constitution.md` first.
+
+## Your Task
+
+1. Check `specs/` folder
+2. Find highest priority INCOMPLETE spec
+3. Implement completely
+4. Run tests, verify acceptance criteria
+5. Commit and push
+6. Output `DONE` when done
+
+## Rules
+
+- ONE spec per iteration
+- Do NOT output magic phrase until truly complete
+- If blocked: explain in ralph_history.txt, exit without phrase
diff --git a/PROMPT_plan.md b/PROMPT_plan.md
new file mode 100644
index 0000000..9fc624d
--- /dev/null
+++ b/PROMPT_plan.md
@@ -0,0 +1,11 @@
+# Ralph Planning Mode
+
+Read `.specify/memory/constitution.md` first.
+
+## Your Task
+
+1. Analyze specs in `specs/`
+2. Create `IMPLEMENTATION_PLAN.md` with prioritized tasks
+3. Output `DONE` when done
+
+Delete IMPLEMENTATION_PLAN.md to return to direct spec mode.
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..94ae2ee
--- /dev/null
+++ b/main.py
@@ -0,0 +1,108 @@
+import sys
+import cv2
+import numpy as np
+from PIL import Image
+import objc
+from AppKit import (
+ NSApplication, NSApp, NSWindow, NSView, NSImageView, NSButton,
+ NSStackView, NSImage, NSBitmapImageRep, NSBackingStoreBuffered,
+ NSWindowStyleMaskTitled, NSWindowStyleMaskClosable,
+ NSWindowStyleMaskResizable, NSWindowStyleMaskMiniaturizable,
+ NSTimer, NSMakeSize, NSMakeRect, NSObject, NSLog,
+ NSUserInterfaceLayoutOrientationVertical, NSLayoutAttributeCenterX,
+ NSLayoutAttributeCenterY, NSLayoutAttributeWidth, NSLayoutAttributeHeight,
+ NSLayoutAttributeTop, NSLayoutAttributeBottom, NSLayoutAttributeLeading,
+ NSLayoutAttributeTrailing
+)
+from Foundation import NSObject, NSTimer, NSDate
+
+class ItemSenseApp(NSObject):
+ def applicationDidFinishLaunching_(self, notification):
+ self.window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
+ NSMakeRect(0, 0, 800, 600),
+ NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable,
+ NSBackingStoreBuffered,
+ False
+ )
+ self.window.setTitle_("ItemSense")
+ self.window.center()
+
+ # Main content view (StackView for layout)
+ self.stack_view = NSStackView.alloc().init()
+ self.stack_view.setOrientation_(NSUserInterfaceLayoutOrientationVertical)
+ self.stack_view.setSpacing_(10)
+ self.stack_view.setEdgeInsets_((10, 10, 10, 10))
+ self.window.setContentView_(self.stack_view)
+
+ # Image View for Camera Feed
+ self.image_view = NSImageView.alloc().init()
+ self.image_view.setImageScaling_(0) # NSImageScaleProportionallyDown
+ self.stack_view.addView_inGravity_(self.image_view, 1) # Top gravity
+
+ # Capture Button
+ self.capture_button = NSButton.buttonWithTitle_target_action_("Capture", self, "captureClicked:")
+ self.stack_view.addView_inGravity_(self.capture_button, 3) # Bottom gravity
+
+ self.window.makeKeyAndOrderFront_(None)
+
+ # Initialize Camera
+ self.cap = cv2.VideoCapture(0)
+ if not self.cap.isOpened():
+ NSLog("Error: Could not open camera")
+
+ # Start Timer for 30 FPS
+ self.timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
+ 1.0/30.0, self, "updateFrame:", None, True
+ )
+
+ def applicationShouldTerminateAfterLastWindowClosed_(self, sender):
+ return True
+
+ def applicationWillTerminate_(self, notification):
+ if hasattr(self, 'cap') and self.cap.isOpened():
+ self.cap.release()
+
+ def updateFrame_(self, timer):
+ if hasattr(self, 'cap') and self.cap.isOpened():
+ ret, frame = self.cap.read()
+ if ret:
+ # Convert BGR to RGB
+ rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+
+ # Convert to NSImage
+ height, width, channels = rgb_frame.shape
+ bytes_per_line = channels * width
+
+ # Create BitmapRep
+ bitmap_rep = NSBitmapImageRep.alloc().initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel_(
+ None, width, height, 8, 3, False, False, "NSDeviceRGBColorSpace", bytes_per_line, 24
+ )
+
+ # Copy data
+ bitmap_data = bitmap_rep.bitmapData()
+ # We need to copy the bytes. This is the PyObjC way to write to the buffer requires a bit of care.
+ # A safer/easier way with PIL:
+ image = Image.fromarray(rgb_frame)
+ img_data = image.tobytes()
+
+ # Low-level memory copy might be tricky in pure python/objc without unsafe pointers.
+ # Alternative: Use PIL to save to memory buffer (TIFF/PNG) and load NSImage from data.
+ # This is slightly slower but safer and easier in Python.
+ import io
+ # Using PPM format is fast (uncompressed)
+ header = f"P6 {width} {height} 255 ".encode()
+ data = header + rgb_frame.tobytes()
+ ns_data = objc.lookUpClass("NSData").dataWithBytes_length_(data, len(data))
+ ns_image = NSImage.alloc().initWithData_(ns_data)
+
+ self.image_view.setImage_(ns_image)
+
+ def captureClicked_(self, sender):
+ print("Capture clicked")
+
+if __name__ == "__main__":
+ app = NSApplication.sharedApplication()
+ delegate = ItemSenseApp.alloc().init()
+ app.setDelegate_(delegate)
+ NSApp.activateIgnoringOtherApps_(True)
+ app.run()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..5ebdf86
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+pyobjc-framework-Cocoa
+opencv-python
+pillow
+openai
+python-dotenv
diff --git a/scripts/ralph-loop-codex.sh b/scripts/ralph-loop-codex.sh
new file mode 100755
index 0000000..2962d35
--- /dev/null
+++ b/scripts/ralph-loop-codex.sh
@@ -0,0 +1,635 @@
+#!/bin/bash
+#
+# Ralph Loop for OpenAI Codex CLI
+#
+# Based on Geoffrey Huntley's Ralph Wiggum methodology.
+# Combined with SpecKit-style specifications.
+#
+# Usage:
+# ./scripts/ralph-loop-codex.sh # Build mode (unlimited)
+# ./scripts/ralph-loop-codex.sh 20 # Build mode (max 20 iterations)
+# ./scripts/ralph-loop-codex.sh plan # Planning mode (optional)
+#
+
+set -e
+set -o pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+LOG_DIR="$PROJECT_DIR/logs"
+CONSTITUTION="$PROJECT_DIR/.specify/memory/constitution.md"
+RLM_DIR="$PROJECT_DIR/rlm"
+RLM_TRACE_DIR="$RLM_DIR/trace"
+RLM_QUERIES_DIR="$RLM_DIR/queries"
+RLM_ANSWERS_DIR="$RLM_DIR/answers"
+RLM_INDEX="$RLM_DIR/index.tsv"
+
+# Configuration
+MAX_ITERATIONS=0 # 0 = unlimited
+MODE="build"
+RLM_CONTEXT_FILE=""
+CODEX_CMD="${CODEX_CMD:-codex}"
+TAIL_LINES=5
+TAIL_RENDERED_LINES=0
+ROLLING_OUTPUT_LINES=5
+ROLLING_OUTPUT_INTERVAL=10
+ROLLING_RENDERED_LINES=0
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+PURPLE='\033[0;35m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+
+mkdir -p "$LOG_DIR"
+
+# Check constitution for YOLO setting
+YOLO_ENABLED=true
+if [[ -f "$CONSTITUTION" ]]; then
+ if grep -q "YOLO Mode.*DISABLED" "$CONSTITUTION" 2>/dev/null; then
+ YOLO_ENABLED=false
+ fi
+fi
+
+show_help() {
+ cat < Treat a large context file as external environment.
+ The agent should read slices instead of loading it all.
+ --rlm [file] Shortcut for --rlm-context (defaults to rlm/context.txt)
+
+RLM workspace (when enabled):
+ - rlm/trace/ Prompt snapshots + outputs per iteration
+ - rlm/index.tsv Index of all iterations (timestamp, prompt, log, status)
+ - rlm/queries/ and rlm/answers/ For optional recursive sub-queries
+
+EOF
+}
+
+print_latest_output() {
+ local log_file="$1"
+ local label="${2:-Codex}"
+ local target="/dev/tty"
+
+ [ -f "$log_file" ] || return 0
+
+ if [ ! -w "$target" ]; then
+ target="/dev/stdout"
+ fi
+
+ if [ "$target" = "/dev/tty" ] && [ "$TAIL_RENDERED_LINES" -gt 0 ]; then
+ printf "\033[%dA\033[J" "$TAIL_RENDERED_LINES" > "$target"
+ fi
+
+ {
+ echo "Latest ${label} output (last ${TAIL_LINES} lines):"
+ tail -n "$TAIL_LINES" "$log_file"
+ } > "$target"
+
+ if [ "$target" = "/dev/tty" ]; then
+ TAIL_RENDERED_LINES=$((TAIL_LINES + 1))
+ fi
+}
+
+watch_latest_output() {
+ local log_file="$1"
+ local label="${2:-Codex}"
+ local target="/dev/tty"
+ local use_tty=false
+ local use_tput=false
+
+ [ -f "$log_file" ] || return 0
+
+ if [ ! -w "$target" ]; then
+ target="/dev/stdout"
+ else
+ use_tty=true
+ if command -v tput &>/dev/null; then
+ use_tput=true
+ fi
+ fi
+
+ if [ "$use_tty" = true ]; then
+ if [ "$use_tput" = true ]; then
+ tput cr > "$target"
+ tput sc > "$target"
+ else
+ printf "\r\0337" > "$target"
+ fi
+ fi
+
+ while true; do
+ local timestamp
+ timestamp=$(date '+%Y-%m-%d %H:%M:%S')
+
+ if [ "$use_tty" = true ]; then
+ if [ "$use_tput" = true ]; then
+ tput rc > "$target"
+ tput ed > "$target"
+ tput cr > "$target"
+ else
+ printf "\0338\033[J\r" > "$target"
+ fi
+ fi
+
+ {
+ echo -e "${CYAN}[$timestamp] Latest ${label} output (last ${ROLLING_OUTPUT_LINES} lines):${NC}"
+ if [ ! -s "$log_file" ]; then
+ echo "(no output yet)"
+ else
+ tail -n "$ROLLING_OUTPUT_LINES" "$log_file" 2>/dev/null || true
+ fi
+ echo ""
+ } > "$target"
+
+ sleep "$ROLLING_OUTPUT_INTERVAL"
+ done
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ plan)
+ MODE="plan"
+ if [[ "${2:-}" =~ ^[0-9]+$ ]]; then
+ MAX_ITERATIONS="$2"
+ shift 2
+ else
+ MAX_ITERATIONS=1
+ shift
+ fi
+ ;;
+ --rlm-context)
+ RLM_CONTEXT_FILE="${2:-}"
+ shift 2
+ ;;
+ --rlm)
+ if [[ -n "${2:-}" && "${2:0:1}" != "-" ]]; then
+ RLM_CONTEXT_FILE="$2"
+ shift 2
+ else
+ RLM_CONTEXT_FILE="rlm/context.txt"
+ shift
+ fi
+ ;;
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ [0-9]*)
+ MODE="build"
+ MAX_ITERATIONS="$1"
+ shift
+ ;;
+ *)
+ echo -e "${RED}Unknown argument: $1${NC}"
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+cd "$PROJECT_DIR"
+
+# Validate RLM context file (if provided)
+if [ -n "$RLM_CONTEXT_FILE" ] && [ ! -f "$RLM_CONTEXT_FILE" ]; then
+ echo -e "${RED}Error: RLM context file not found: $RLM_CONTEXT_FILE${NC}"
+ echo "Create it first (example):"
+ echo " mkdir -p rlm && printf \"%s\" \"\" > $RLM_CONTEXT_FILE"
+ exit 1
+fi
+
+# Initialize RLM workspace (optional)
+if [ -n "$RLM_CONTEXT_FILE" ]; then
+ mkdir -p "$RLM_TRACE_DIR" "$RLM_QUERIES_DIR" "$RLM_ANSWERS_DIR"
+ if [ ! -f "$RLM_INDEX" ]; then
+ echo -e "timestamp\tmode\titeration\tprompt\tlog\toutput\tstatus" > "$RLM_INDEX"
+ fi
+fi
+
+# Session log (captures ALL output)
+SESSION_LOG="$LOG_DIR/ralph_codex_${MODE}_session_$(date '+%Y%m%d_%H%M%S').log"
+exec > >(tee -a "$SESSION_LOG") 2>&1
+
+# Check if Codex CLI is available
+if ! command -v "$CODEX_CMD" &> /dev/null; then
+ echo -e "${RED}Error: Codex CLI not found${NC}"
+ echo ""
+ echo "Install Codex CLI:"
+ echo " npm install -g @openai/codex"
+ echo ""
+ echo "Then authenticate:"
+ echo " codex login"
+ exit 1
+fi
+
+# Determine prompt file
+if [ "$MODE" = "plan" ]; then
+ PROMPT_FILE="PROMPT_plan.md"
+else
+ PROMPT_FILE="PROMPT_build.md"
+fi
+
+# Create prompt files if they don't exist (same as ralph-loop.sh)
+if [ ! -f "PROMPT_build.md" ]; then
+ echo -e "${YELLOW}Creating PROMPT_build.md...${NC}"
+ cat > "PROMPT_build.md" << 'BUILDEOF'
+# Ralph Build Mode
+
+Based on Geoffrey Huntley's Ralph Wiggum methodology.
+
+---
+
+## Phase 0: Orient
+
+Read `.specify/memory/constitution.md` to understand project principles and constraints.
+
+---
+
+## Phase 1: Discover Work Items
+
+Search for incomplete work from these sources (in order):
+
+1. **specs/ folder** — Look for `.md` files NOT marked `## Status: COMPLETE`
+2. **IMPLEMENTATION_PLAN.md** — If exists, find unchecked `- [ ]` tasks
+3. **GitHub Issues** — Check for open issues (if this is a GitHub repo)
+4. **Any task tracker** — Jira, Linear, etc. if configured
+
+Pick the **HIGHEST PRIORITY** incomplete item:
+- Lower numbers = higher priority (001 before 010)
+- `[HIGH]` before `[MEDIUM]` before `[LOW]`
+- Bugs/blockers before features
+
+Before implementing, search the codebase to verify it's not already done.
+
+---
+
+## Phase 1b: Re-Verification Mode (No Incomplete Work Found)
+
+**If ALL specs appear complete**, don't just exit — do a quality check:
+
+1. **Randomly pick** one completed spec from `specs/`
+2. **Strictly re-verify** ALL its acceptance criteria:
+ - Run the actual tests mentioned in the spec
+ - Manually verify each criterion is truly met
+ - Check edge cases
+ - Look for regressions
+3. **If any criterion fails**: Unmark the spec as complete and fix it
+4. **If all pass**: Output `DONE` to confirm quality
+
+This ensures the codebase stays healthy even when "nothing to do."
+
+---
+
+## Phase 2: Implement
+
+Implement the selected spec/task completely:
+- Follow the spec's requirements exactly
+- Write clean, maintainable code
+- Add tests as needed
+
+---
+
+## Phase 3: Validate
+
+Run the project's test suite and verify:
+- All tests pass
+- No lint errors
+- The spec's acceptance criteria are 100% met
+
+---
+
+## Phase 4: Commit & Update
+
+1. Mark the spec/task as complete (add `## Status: COMPLETE` to spec file)
+2. `git add -A`
+3. `git commit` with a descriptive message
+4. `git push`
+
+---
+
+## Completion Signal
+
+**CRITICAL:** Only output the magic phrase when the work is 100% complete.
+
+Check:
+- [ ] Implementation matches all requirements
+- [ ] All tests pass
+- [ ] All acceptance criteria verified
+- [ ] Changes committed and pushed
+- [ ] Spec marked as complete
+
+**If ALL checks pass, output:** `DONE`
+
+**If ANY check fails:** Fix the issue and try again. Do NOT output the magic phrase.
+BUILDEOF
+fi
+
+if [ ! -f "PROMPT_plan.md" ]; then
+ echo -e "${YELLOW}Creating PROMPT_plan.md...${NC}"
+ cat > "PROMPT_plan.md" << 'PLANEOF'
+# Ralph Planning Mode (OPTIONAL)
+
+This mode is OPTIONAL. Most projects work fine directly from specs.
+
+Only use this when you want a detailed breakdown of specs into smaller tasks.
+
+---
+
+## Phase 0: Orient
+
+0a. Read `.specify/memory/constitution.md` for project principles.
+
+0b. Study `specs/` to learn all feature specifications.
+
+---
+
+## Phase 1: Gap Analysis
+
+Compare specs against current codebase:
+- What's fully implemented?
+- What's partially done?
+- What's not started?
+- What has issues or bugs?
+
+---
+
+## Phase 2: Create Plan
+
+Create `IMPLEMENTATION_PLAN.md` with a prioritized task list:
+
+```markdown
+# Implementation Plan
+
+> Auto-generated breakdown of specs into tasks.
+> Delete this file to return to working directly from specs.
+
+## Priority Tasks
+
+- [ ] [HIGH] Task description - from spec NNN
+- [ ] [HIGH] Task description - from spec NNN
+- [ ] [MEDIUM] Task description
+- [ ] [LOW] Task description
+
+## Completed
+
+- [x] Completed task
+```
+
+Prioritize by:
+1. Dependencies (do prerequisites first)
+2. Impact (high-value features first)
+3. Complexity (mix easy wins with harder tasks)
+
+---
+
+## Completion Signal
+
+When the plan is complete and saved:
+
+`DONE`
+PLANEOF
+fi
+
+# Build Codex flags for exec mode
+CODEX_FLAGS="exec"
+if [ "$YOLO_ENABLED" = true ]; then
+ CODEX_FLAGS="$CODEX_FLAGS --dangerously-bypass-approvals-and-sandbox"
+fi
+
+# Get current branch
+CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "main")
+
+# Check for work sources - count .md files in specs/
+HAS_SPECS=false
+SPEC_COUNT=0
+if [ -d "specs" ]; then
+ SPEC_COUNT=$(find specs -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
+ [ "$SPEC_COUNT" -gt 0 ] && HAS_SPECS=true
+fi
+
+echo ""
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${GREEN} RALPH LOOP (Codex) STARTING ${NC}"
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo ""
+echo -e "${BLUE}Mode:${NC} $MODE"
+echo -e "${BLUE}Prompt:${NC} $PROMPT_FILE"
+echo -e "${BLUE}Branch:${NC} $CURRENT_BRANCH"
+echo -e "${YELLOW}YOLO:${NC} $([ "$YOLO_ENABLED" = true ] && echo "ENABLED" || echo "DISABLED")"
+[ -n "$RLM_CONTEXT_FILE" ] && echo -e "${BLUE}RLM:${NC} $RLM_CONTEXT_FILE"
+[ -n "$SESSION_LOG" ] && echo -e "${BLUE}Log:${NC} $SESSION_LOG"
+[ $MAX_ITERATIONS -gt 0 ] && echo -e "${BLUE}Max:${NC} $MAX_ITERATIONS iterations"
+echo ""
+echo -e "${BLUE}Work source:${NC}"
+if [ "$HAS_SPECS" = true ]; then
+ echo -e " ${GREEN}✓${NC} specs/ folder ($SPEC_COUNT specs)"
+else
+ echo -e " ${RED}✗${NC} specs/ folder (no .md files found)"
+fi
+echo ""
+echo -e "${CYAN}Using: $CODEX_CMD $CODEX_FLAGS${NC}"
+echo -e "${CYAN}Agent must output DONE when complete.${NC}"
+echo ""
+echo -e "${YELLOW}Press Ctrl+C to stop the loop${NC}"
+echo ""
+
+ITERATION=0
+CONSECUTIVE_FAILURES=0
+MAX_CONSECUTIVE_FAILURES=3
+
+while true; do
+ # Check max iterations
+ if [ $MAX_ITERATIONS -gt 0 ] && [ $ITERATION -ge $MAX_ITERATIONS ]; then
+ echo -e "${GREEN}Reached max iterations: $MAX_ITERATIONS${NC}"
+ break
+ fi
+
+ ITERATION=$((ITERATION + 1))
+ TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
+
+ echo ""
+ echo -e "${PURPLE}════════════════════ LOOP $ITERATION ════════════════════${NC}"
+ echo -e "${BLUE}[$TIMESTAMP]${NC} Starting iteration $ITERATION"
+ echo ""
+
+ # Log file for this iteration
+ LOG_FILE="$LOG_DIR/ralph_codex_${MODE}_iter_${ITERATION}_$(date '+%Y%m%d_%H%M%S').log"
+ OUTPUT_FILE="$LOG_DIR/ralph_codex_output_iter_${ITERATION}_$(date '+%Y%m%d_%H%M%S').txt"
+ RLM_STATUS="unknown"
+ : > "$LOG_FILE"
+ WATCH_PID=""
+
+ if [ "$ROLLING_OUTPUT_INTERVAL" -gt 0 ] && [ "$ROLLING_OUTPUT_LINES" -gt 0 ] && [ -t 1 ] && [ -w /dev/tty ]; then
+ watch_latest_output "$LOG_FILE" "Codex" &
+ WATCH_PID=$!
+ fi
+
+ # Optional RLM context block appended to prompt at runtime
+ EFFECTIVE_PROMPT_FILE="$PROMPT_FILE"
+ if [ -n "$RLM_CONTEXT_FILE" ]; then
+ EFFECTIVE_PROMPT_FILE="$LOG_DIR/ralph_codex_prompt_iter_${ITERATION}_$(date '+%Y%m%d_%H%M%S').md"
+ cat "$PROMPT_FILE" > "$EFFECTIVE_PROMPT_FILE"
+ cat >> "$EFFECTIVE_PROMPT_FILE" << EOF
+
+---
+## RLM Context (Optional)
+
+You have access to a large context file at:
+**$RLM_CONTEXT_FILE**
+
+Treat this file as an external environment. Do NOT paste the whole file into the prompt.
+Instead, inspect it programmatically and recursively:
+
+- Use small slices:
+ \`\`\`bash
+ sed -n 'START,ENDp' "$RLM_CONTEXT_FILE"
+ \`\`\`
+- Or Python snippets:
+ \`\`\`bash
+ python - <<'PY'
+ from pathlib import Path
+ p = Path("$RLM_CONTEXT_FILE")
+ print(p.read_text().splitlines()[START:END])
+ PY
+ \`\`\`
+- Use search:
+ \`\`\`bash
+ rg -n "pattern" "$RLM_CONTEXT_FILE"
+ \`\`\`
+
+Goal: decompose the task into smaller sub-queries and only load the pieces you need.
+This mirrors the Recursive Language Model approach from https://arxiv.org/html/2512.24601v1
+
+## RLM Workspace (Optional)
+
+Past loop outputs are preserved on disk:
+- Iteration logs: \`logs/\`
+- Prompt/output snapshots: \`rlm/trace/\`
+- Iteration index: \`rlm/index.tsv\`
+
+Use these as an external memory store (search/slice as needed).
+If you need a recursive sub-query, write a focused prompt in \`rlm/queries/\`,
+run:
+ \`./scripts/rlm-subcall.sh --query rlm/queries/.md\`
+and store the result in \`rlm/answers/\`.
+EOF
+ RLM_PROMPT_SNAPSHOT="$RLM_TRACE_DIR/iter_${ITERATION}_prompt.md"
+ cp "$EFFECTIVE_PROMPT_FILE" "$RLM_PROMPT_SNAPSHOT"
+ fi
+
+ # Run Codex with exec mode, reading prompt from stdin with "-"
+ # Use --output-last-message to capture the final response for checking
+ echo -e "${BLUE}Running: cat $EFFECTIVE_PROMPT_FILE | $CODEX_CMD $CODEX_FLAGS - --output-last-message $OUTPUT_FILE${NC}"
+ echo ""
+
+ CODEX_EXIT=0
+ if cat "$EFFECTIVE_PROMPT_FILE" | "$CODEX_CMD" $CODEX_FLAGS - --output-last-message "$OUTPUT_FILE" 2>&1 | tee "$LOG_FILE"; then
+ if [ -n "$WATCH_PID" ]; then
+ kill "$WATCH_PID" 2>/dev/null || true
+ wait "$WATCH_PID" 2>/dev/null || true
+ fi
+ echo ""
+ echo -e "${GREEN}✓ Codex execution completed${NC}"
+
+ # Check if DONE promise was output (accept both DONE and ALL_DONE variants)
+ if [ -f "$OUTPUT_FILE" ] && grep -qE "(ALL_)?DONE" "$OUTPUT_FILE"; then
+ DETECTED_SIGNAL=$(grep -oE "(ALL_)?DONE" "$OUTPUT_FILE" | tail -1)
+ echo -e "${GREEN}✓ Completion signal detected: ${DETECTED_SIGNAL}${NC}"
+ echo -e "${GREEN}✓ Task completed successfully!${NC}"
+ CONSECUTIVE_FAILURES=0
+ RLM_STATUS="done"
+
+ if [ "$MODE" = "plan" ]; then
+ echo ""
+ echo -e "${GREEN}Planning complete!${NC}"
+ break
+ fi
+ # Also check the main log
+ elif grep -qE "(ALL_)?DONE" "$LOG_FILE"; then
+ DETECTED_SIGNAL=$(grep -oE "(ALL_)?DONE" "$LOG_FILE" | tail -1)
+ echo -e "${GREEN}✓ Completion signal detected: ${DETECTED_SIGNAL}${NC}"
+ echo -e "${GREEN}✓ Task completed successfully!${NC}"
+ CONSECUTIVE_FAILURES=0
+ RLM_STATUS="done"
+ else
+ echo -e "${YELLOW}⚠ No completion signal found${NC}"
+ echo -e "${YELLOW} Agent did not output DONE or ALL_DONE${NC}"
+ echo -e "${YELLOW} Retrying in next iteration...${NC}"
+ CONSECUTIVE_FAILURES=$((CONSECUTIVE_FAILURES + 1))
+ RLM_STATUS="incomplete"
+ print_latest_output "$LOG_FILE" "Codex"
+
+ if [ $CONSECUTIVE_FAILURES -ge $MAX_CONSECUTIVE_FAILURES ]; then
+ echo ""
+ echo -e "${RED}⚠ $MAX_CONSECUTIVE_FAILURES consecutive iterations without completion.${NC}"
+ echo -e "${RED} The agent may be stuck. Check logs:${NC}"
+ echo -e "${RED} - $LOG_FILE${NC}"
+ echo -e "${RED} - $OUTPUT_FILE${NC}"
+ CONSECUTIVE_FAILURES=0
+ fi
+ fi
+ else
+ if [ -n "$WATCH_PID" ]; then
+ kill "$WATCH_PID" 2>/dev/null || true
+ wait "$WATCH_PID" 2>/dev/null || true
+ fi
+ CODEX_EXIT=$?
+ echo -e "${RED}✗ Codex execution failed (exit code: $CODEX_EXIT)${NC}"
+ echo -e "${YELLOW}Check log: $LOG_FILE${NC}"
+ CONSECUTIVE_FAILURES=$((CONSECUTIVE_FAILURES + 1))
+ RLM_STATUS="error"
+ print_latest_output "$LOG_FILE" "Codex"
+ fi
+
+ # Record iteration in RLM index (optional)
+ if [ -n "$RLM_CONTEXT_FILE" ]; then
+ RLM_PROMPT_PATH="${RLM_PROMPT_SNAPSHOT:-}"
+ RLM_OUTPUT_SNAPSHOT="$RLM_TRACE_DIR/iter_${ITERATION}_output.log"
+ cp "$LOG_FILE" "$RLM_OUTPUT_SNAPSHOT"
+ if [ -f "$OUTPUT_FILE" ]; then
+ RLM_LAST_MESSAGE_SNAPSHOT="$RLM_TRACE_DIR/iter_${ITERATION}_last_message.txt"
+ cp "$OUTPUT_FILE" "$RLM_LAST_MESSAGE_SNAPSHOT"
+ fi
+ RLM_OUTPUT_PATH="${RLM_LAST_MESSAGE_SNAPSHOT:-$RLM_OUTPUT_SNAPSHOT}"
+ echo -e "${TIMESTAMP}\t${MODE}\t${ITERATION}\t${RLM_PROMPT_PATH}\t${LOG_FILE}\t${RLM_OUTPUT_PATH}\t${RLM_STATUS}" >> "$RLM_INDEX"
+ fi
+
+ # Push changes after each iteration
+ git push origin "$CURRENT_BRANCH" 2>/dev/null || {
+ if git log origin/$CURRENT_BRANCH..HEAD --oneline 2>/dev/null | grep -q .; then
+ git push -u origin "$CURRENT_BRANCH" 2>/dev/null || true
+ fi
+ }
+
+ # Brief pause between iterations
+ echo ""
+ echo -e "${BLUE}Waiting 2s before next iteration...${NC}"
+ sleep 2
+done
+
+echo ""
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${GREEN} RALPH LOOP (Codex) FINISHED ($ITERATION iterations) ${NC}"
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
diff --git a/scripts/ralph-loop.sh b/scripts/ralph-loop.sh
new file mode 100755
index 0000000..dc651d3
--- /dev/null
+++ b/scripts/ralph-loop.sh
@@ -0,0 +1,688 @@
+#!/bin/bash
+#
+# Ralph Loop for Claude Code
+#
+# Based on Geoffrey Huntley's Ralph Wiggum methodology:
+# https://github.com/ghuntley/how-to-ralph-wiggum
+#
+# Combined with SpecKit-style specifications.
+#
+# Key principles:
+# - Each iteration picks ONE task/spec to work on
+# - Agent works until acceptance criteria are met
+# - Only outputs DONE when truly complete
+# - Bash loop checks for magic phrase before continuing
+# - Fresh context window each iteration
+#
+# Work sources (in priority order):
+# 1. IMPLEMENTATION_PLAN.md (if exists) - pick highest priority task
+# 2. specs/ folder - pick highest priority incomplete spec
+#
+# Usage:
+# ./scripts/ralph-loop.sh # Build mode (unlimited)
+# ./scripts/ralph-loop.sh 20 # Build mode (max 20 iterations)
+# ./scripts/ralph-loop.sh plan # Planning mode (creates IMPLEMENTATION_PLAN.md)
+#
+
+set -e
+set -o pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+LOG_DIR="$PROJECT_DIR/logs"
+CONSTITUTION="$PROJECT_DIR/.specify/memory/constitution.md"
+RLM_DIR="$PROJECT_DIR/rlm"
+RLM_TRACE_DIR="$RLM_DIR/trace"
+RLM_QUERIES_DIR="$RLM_DIR/queries"
+RLM_ANSWERS_DIR="$RLM_DIR/answers"
+RLM_INDEX="$RLM_DIR/index.tsv"
+
+# Configuration
+MAX_ITERATIONS=0 # 0 = unlimited
+MODE="build"
+CLAUDE_CMD="${CLAUDE_CMD:-claude}"
+YOLO_FLAG="--dangerously-skip-permissions"
+RLM_CONTEXT_FILE=""
+TAIL_LINES=5
+TAIL_RENDERED_LINES=0
+ROLLING_OUTPUT_LINES=5
+ROLLING_OUTPUT_INTERVAL=10
+ROLLING_RENDERED_LINES=0
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+PURPLE='\033[0;35m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+
+mkdir -p "$LOG_DIR"
+
+# Check constitution for YOLO setting
+YOLO_ENABLED=true
+if [[ -f "$CONSTITUTION" ]]; then
+ if grep -q "YOLO Mode.*DISABLED" "$CONSTITUTION" 2>/dev/null; then
+ YOLO_ENABLED=false
+ fi
+fi
+
+show_help() {
+ cat < Treat a large context file as external environment.
+ The agent should read slices instead of loading it all.
+ --rlm [file] Shortcut for --rlm-context (defaults to rlm/context.txt)
+
+How it works:
+ 1. Each iteration feeds PROMPT.md to Claude via stdin
+ 2. Claude picks the HIGHEST PRIORITY incomplete spec/task
+ 3. Claude implements, tests, and verifies acceptance criteria
+ 4. Claude outputs DONE ONLY if criteria are met
+ 5. Bash loop checks for the magic phrase
+ 6. If found, loop continues to next iteration (fresh context)
+ 7. If not found, loop retries
+
+RLM workspace (when enabled):
+ - rlm/trace/ Prompt snapshots + outputs per iteration
+ - rlm/index.tsv Index of all iterations (timestamp, prompt, log, status)
+ - rlm/queries/ and rlm/answers/ For optional recursive sub-queries
+
+EOF
+}
+
+print_latest_output() {
+ local log_file="$1"
+ local label="${2:-Claude}"
+ local target="/dev/tty"
+
+ [ -f "$log_file" ] || return 0
+
+ if [ ! -w "$target" ]; then
+ target="/dev/stdout"
+ fi
+
+ if [ "$target" = "/dev/tty" ] && [ "$TAIL_RENDERED_LINES" -gt 0 ]; then
+ printf "\033[%dA\033[J" "$TAIL_RENDERED_LINES" > "$target"
+ fi
+
+ {
+ echo "Latest ${label} output (last ${TAIL_LINES} lines):"
+ tail -n "$TAIL_LINES" "$log_file"
+ } > "$target"
+
+ if [ "$target" = "/dev/tty" ]; then
+ TAIL_RENDERED_LINES=$((TAIL_LINES + 1))
+ fi
+}
+
+watch_latest_output() {
+ local log_file="$1"
+ local label="${2:-Claude}"
+ local target="/dev/tty"
+ local use_tty=false
+ local use_tput=false
+
+ [ -f "$log_file" ] || return 0
+
+ if [ ! -w "$target" ]; then
+ target="/dev/stdout"
+ else
+ use_tty=true
+ if command -v tput &>/dev/null; then
+ use_tput=true
+ fi
+ fi
+
+ if [ "$use_tty" = true ]; then
+ if [ "$use_tput" = true ]; then
+ tput cr > "$target"
+ tput sc > "$target"
+ else
+ printf "\r\0337" > "$target"
+ fi
+ fi
+
+ while true; do
+ local timestamp
+ timestamp=$(date '+%Y-%m-%d %H:%M:%S')
+
+ if [ "$use_tty" = true ]; then
+ if [ "$use_tput" = true ]; then
+ tput rc > "$target"
+ tput ed > "$target"
+ tput cr > "$target"
+ else
+ printf "\0338\033[J\r" > "$target"
+ fi
+ fi
+
+ {
+ echo -e "${CYAN}[$timestamp] Latest ${label} output (last ${ROLLING_OUTPUT_LINES} lines):${NC}"
+ if [ ! -s "$log_file" ]; then
+ echo "(no output yet)"
+ else
+ tail -n "$ROLLING_OUTPUT_LINES" "$log_file" 2>/dev/null || true
+ fi
+ echo ""
+ } > "$target"
+
+ sleep "$ROLLING_OUTPUT_INTERVAL"
+ done
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ plan)
+ MODE="plan"
+ if [[ "${2:-}" =~ ^[0-9]+$ ]]; then
+ MAX_ITERATIONS="$2"
+ shift 2
+ else
+ MAX_ITERATIONS=1
+ shift
+ fi
+ ;;
+ --rlm-context)
+ RLM_CONTEXT_FILE="${2:-}"
+ shift 2
+ ;;
+ --rlm)
+ if [[ -n "${2:-}" && "${2:0:1}" != "-" ]]; then
+ RLM_CONTEXT_FILE="$2"
+ shift 2
+ else
+ RLM_CONTEXT_FILE="rlm/context.txt"
+ shift
+ fi
+ ;;
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ [0-9]*)
+ MODE="build"
+ MAX_ITERATIONS="$1"
+ shift
+ ;;
+ *)
+ echo -e "${RED}Unknown argument: $1${NC}"
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+cd "$PROJECT_DIR"
+
+# Validate RLM context file (if provided)
+if [ -n "$RLM_CONTEXT_FILE" ] && [ ! -f "$RLM_CONTEXT_FILE" ]; then
+ echo -e "${RED}Error: RLM context file not found: $RLM_CONTEXT_FILE${NC}"
+ echo "Create it first (example):"
+ echo " mkdir -p rlm && printf \"%s\" \"\" > $RLM_CONTEXT_FILE"
+ exit 1
+fi
+
+# Initialize RLM workspace (optional)
+if [ -n "$RLM_CONTEXT_FILE" ]; then
+ mkdir -p "$RLM_TRACE_DIR" "$RLM_QUERIES_DIR" "$RLM_ANSWERS_DIR"
+ if [ ! -f "$RLM_INDEX" ]; then
+ echo -e "timestamp\tmode\titeration\tprompt\tlog\toutput\tstatus" > "$RLM_INDEX"
+ fi
+fi
+
+# Session log (captures ALL output)
+SESSION_LOG="$LOG_DIR/ralph_${MODE}_session_$(date '+%Y%m%d_%H%M%S').log"
+exec > >(tee -a "$SESSION_LOG") 2>&1
+
+# Check if Claude CLI is available
+if ! command -v "$CLAUDE_CMD" &> /dev/null; then
+ echo -e "${RED}Error: Claude CLI not found${NC}"
+ echo ""
+ echo "Install Claude Code CLI and authenticate first."
+ echo "https://claude.ai/code"
+ exit 1
+fi
+
+# Determine which prompt to use based on mode and available files
+if [ "$MODE" = "plan" ]; then
+ PROMPT_FILE="PROMPT_plan.md"
+else
+ PROMPT_FILE="PROMPT_build.md"
+fi
+
+# Create/update the build prompt to be flexible about plan vs specs
+cat > "PROMPT_build.md" << 'BUILDEOF'
+# Ralph Build Mode
+
+Based on Geoffrey Huntley's Ralph Wiggum methodology.
+
+---
+
+## Phase 0: Orient
+
+Read `.specify/memory/constitution.md` to understand project principles and constraints.
+
+---
+BUILDEOF
+
+# Optional RLM context block
+if [ -n "$RLM_CONTEXT_FILE" ]; then
+cat >> "PROMPT_build.md" << EOF
+
+## Phase 0d: RLM Context (Optional)
+
+You have access to a large context file at:
+**$RLM_CONTEXT_FILE**
+
+Treat this file as an external environment. Do NOT paste the whole file into the prompt.
+Instead, inspect it programmatically and recursively:
+
+- Use small slices:
+ ```bash
+ sed -n 'START,ENDp' "$RLM_CONTEXT_FILE"
+ ```
+- Or Python snippets:
+ ```bash
+ python - <<'PY'
+ from pathlib import Path
+ p = Path("$RLM_CONTEXT_FILE")
+ print(p.read_text().splitlines()[START:END])
+ PY
+ ```
+- Use search:
+ ```bash
+ rg -n "pattern" "$RLM_CONTEXT_FILE"
+ ```
+
+Goal: decompose the task into smaller sub-queries and only load the pieces you need.
+This mirrors the Recursive Language Model approach from https://arxiv.org/html/2512.24601v1
+
+## RLM Workspace (Optional)
+
+Past loop outputs are preserved on disk:
+- Iteration logs: `logs/`
+- Prompt/output snapshots: `rlm/trace/`
+- Iteration index: `rlm/index.tsv`
+
+Use these as an external memory store (search/slice as needed).
+If you need a recursive sub-query, write a focused prompt in `rlm/queries/`,
+run:
+ `./scripts/rlm-subcall.sh --query rlm/queries/.md`
+and store the result in `rlm/answers/`.
+EOF
+fi
+
+cat >> "PROMPT_build.md" << 'BUILDEOF'
+
+## Phase 1: Discover Work Items
+
+Search for incomplete work from these sources (in order):
+
+1. **specs/ folder** — Look for `.md` files NOT marked `## Status: COMPLETE`
+2. **IMPLEMENTATION_PLAN.md** — If exists, find unchecked `- [ ]` tasks
+3. **GitHub Issues** — Check for open issues (if this is a GitHub repo)
+4. **Any task tracker** — Jira, Linear, etc. if configured
+
+Pick the **HIGHEST PRIORITY** incomplete item:
+- Lower numbers = higher priority (001 before 010)
+- `[HIGH]` before `[MEDIUM]` before `[LOW]`
+- Bugs/blockers before features
+
+Before implementing, search the codebase to verify it's not already done.
+
+---
+
+## Phase 1b: Re-Verification Mode (No Incomplete Work Found)
+
+**If ALL specs appear complete**, don't just exit — do a quality check:
+
+1. **Randomly pick** one completed spec from `specs/`
+2. **Strictly re-verify** ALL its acceptance criteria:
+ - Run the actual tests mentioned in the spec
+ - Manually verify each criterion is truly met
+ - Check edge cases
+ - Look for regressions
+3. **If any criterion fails**: Unmark the spec as complete and fix it
+4. **If all pass**: Output `DONE` to confirm quality
+
+This ensures the codebase stays healthy even when "nothing to do."
+
+---
+
+## Phase 2: Implement
+
+Implement the selected spec/task completely:
+- Follow the spec's requirements exactly
+- Write clean, maintainable code
+- Add tests as needed
+
+---
+
+## Phase 3: Validate
+
+Run the project's test suite and verify:
+- All tests pass
+- No lint errors
+- The spec's acceptance criteria are 100% met
+
+---
+
+## Phase 4: Commit & Update
+
+1. Mark the spec/task as complete (add `## Status: COMPLETE` to spec file)
+2. `git add -A`
+3. `git commit` with a descriptive message
+4. `git push`
+
+---
+
+## Completion Signal
+
+**CRITICAL:** Only output the magic phrase when the work is 100% complete.
+
+Check:
+- [ ] Implementation matches all requirements
+- [ ] All tests pass
+- [ ] All acceptance criteria verified
+- [ ] Changes committed and pushed
+- [ ] Spec marked as complete
+
+**If ALL checks pass, output:** `DONE`
+
+**If ANY check fails:** Fix the issue and try again. Do NOT output the magic phrase.
+BUILDEOF
+
+# Create planning prompt (only used if plan mode is explicitly requested)
+cat > "PROMPT_plan.md" << 'PLANEOF'
+# Ralph Planning Mode (OPTIONAL)
+
+This mode is OPTIONAL. Most projects work fine directly from specs.
+
+Only use this when you want a detailed breakdown of specs into smaller tasks.
+
+---
+
+## Phase 0: Orient
+
+0a. Read `.specify/memory/constitution.md` for project principles.
+
+0b. Study `specs/` to learn all feature specifications.
+
+---
+PLANEOF
+
+# Optional RLM context block for planning
+if [ -n "$RLM_CONTEXT_FILE" ]; then
+cat >> "PROMPT_plan.md" << EOF
+
+## Phase 0c: RLM Context (Optional)
+
+You have access to a large context file at:
+**$RLM_CONTEXT_FILE**
+
+Treat this file as an external environment. Do NOT paste the whole file into the prompt.
+Inspect only the slices you need using shell tools or Python.
+This mirrors the Recursive Language Model approach from https://arxiv.org/html/2512.24601v1
+
+## RLM Workspace (Optional)
+
+Past loop outputs are preserved on disk:
+- Iteration logs: `logs/`
+- Prompt/output snapshots: `rlm/trace/`
+- Iteration index: `rlm/index.tsv`
+
+Use these as an external memory store (search/slice as needed).
+For recursive sub-queries, use:
+ `./scripts/rlm-subcall.sh --query rlm/queries/.md`
+EOF
+fi
+
+cat >> "PROMPT_plan.md" << 'PLANEOF'
+
+## Phase 1: Gap Analysis
+
+Compare specs against current codebase:
+- What's fully implemented?
+- What's partially done?
+- What's not started?
+- What has issues or bugs?
+
+---
+
+## Phase 2: Create Plan
+
+Create `IMPLEMENTATION_PLAN.md` with a prioritized task list:
+
+```markdown
+# Implementation Plan
+
+> Auto-generated breakdown of specs into tasks.
+> Delete this file to return to working directly from specs.
+
+## Priority Tasks
+
+- [ ] [HIGH] Task description - from spec NNN
+- [ ] [HIGH] Task description - from spec NNN
+- [ ] [MEDIUM] Task description
+- [ ] [LOW] Task description
+
+## Completed
+
+- [x] Completed task
+```
+
+Prioritize by:
+1. Dependencies (do prerequisites first)
+2. Impact (high-value features first)
+3. Complexity (mix easy wins with harder tasks)
+
+---
+
+## Completion Signal
+
+When the plan is complete and saved:
+
+`DONE`
+PLANEOF
+
+# Check prompt file exists
+if [ ! -f "$PROMPT_FILE" ]; then
+ echo -e "${RED}Error: $PROMPT_FILE not found${NC}"
+ exit 1
+fi
+
+# Build Claude flags
+CLAUDE_FLAGS="-p"
+if [ "$YOLO_ENABLED" = true ]; then
+ CLAUDE_FLAGS="$CLAUDE_FLAGS $YOLO_FLAG"
+fi
+
+# Get current branch
+CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "main")
+
+# Check for work sources - count .md files in specs/
+HAS_PLAN=false
+HAS_SPECS=false
+SPEC_COUNT=0
+[ -f "IMPLEMENTATION_PLAN.md" ] && HAS_PLAN=true
+if [ -d "specs" ]; then
+ SPEC_COUNT=$(find specs -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
+ [ "$SPEC_COUNT" -gt 0 ] && HAS_SPECS=true
+fi
+
+echo ""
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${GREEN} RALPH LOOP (Claude Code) STARTING ${NC}"
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo ""
+echo -e "${BLUE}Mode:${NC} $MODE"
+echo -e "${BLUE}Prompt:${NC} $PROMPT_FILE"
+echo -e "${BLUE}Branch:${NC} $CURRENT_BRANCH"
+echo -e "${YELLOW}YOLO:${NC} $([ "$YOLO_ENABLED" = true ] && echo "ENABLED" || echo "DISABLED")"
+[ -n "$RLM_CONTEXT_FILE" ] && echo -e "${BLUE}RLM:${NC} $RLM_CONTEXT_FILE"
+[ -n "$SESSION_LOG" ] && echo -e "${BLUE}Log:${NC} $SESSION_LOG"
+[ $MAX_ITERATIONS -gt 0 ] && echo -e "${BLUE}Max:${NC} $MAX_ITERATIONS iterations"
+echo ""
+echo -e "${BLUE}Work source:${NC}"
+if [ "$HAS_PLAN" = true ]; then
+ echo -e " ${GREEN}✓${NC} IMPLEMENTATION_PLAN.md (will use this)"
+else
+ echo -e " ${YELLOW}○${NC} IMPLEMENTATION_PLAN.md (not found, that's OK)"
+fi
+if [ "$HAS_SPECS" = true ]; then
+ echo -e " ${GREEN}✓${NC} specs/ folder ($SPEC_COUNT specs)"
+else
+ echo -e " ${RED}✗${NC} specs/ folder (no .md files found)"
+fi
+echo ""
+echo -e "${CYAN}The loop checks for DONE in each iteration.${NC}"
+echo -e "${CYAN}Agent must verify acceptance criteria before outputting it.${NC}"
+echo ""
+echo -e "${YELLOW}Press Ctrl+C to stop the loop${NC}"
+echo ""
+
+ITERATION=0
+CONSECUTIVE_FAILURES=0
+MAX_CONSECUTIVE_FAILURES=3
+
+while true; do
+ # Check max iterations
+ if [ $MAX_ITERATIONS -gt 0 ] && [ $ITERATION -ge $MAX_ITERATIONS ]; then
+ echo -e "${GREEN}Reached max iterations: $MAX_ITERATIONS${NC}"
+ break
+ fi
+
+ ITERATION=$((ITERATION + 1))
+ TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
+
+ echo ""
+ echo -e "${PURPLE}════════════════════ LOOP $ITERATION ════════════════════${NC}"
+ echo -e "${BLUE}[$TIMESTAMP]${NC} Starting iteration $ITERATION"
+ echo ""
+
+ # Log file for this iteration
+ LOG_FILE="$LOG_DIR/ralph_${MODE}_iter_${ITERATION}_$(date '+%Y%m%d_%H%M%S').log"
+ : > "$LOG_FILE"
+ WATCH_PID=""
+
+ if [ "$ROLLING_OUTPUT_INTERVAL" -gt 0 ] && [ "$ROLLING_OUTPUT_LINES" -gt 0 ] && [ -t 1 ] && [ -w /dev/tty ]; then
+ watch_latest_output "$LOG_FILE" "Claude" &
+ WATCH_PID=$!
+ fi
+ RLM_STATUS="unknown"
+
+ # Snapshot prompt (optional RLM workspace)
+ if [ -n "$RLM_CONTEXT_FILE" ]; then
+ RLM_PROMPT_SNAPSHOT="$RLM_TRACE_DIR/iter_${ITERATION}_prompt.md"
+ cp "$PROMPT_FILE" "$RLM_PROMPT_SNAPSHOT"
+ fi
+
+ # Run Claude with prompt via stdin, capture output
+ CLAUDE_OUTPUT=""
+ if CLAUDE_OUTPUT=$(cat "$PROMPT_FILE" | "$CLAUDE_CMD" $CLAUDE_FLAGS 2>&1 | tee "$LOG_FILE"); then
+ if [ -n "$WATCH_PID" ]; then
+ kill "$WATCH_PID" 2>/dev/null || true
+ wait "$WATCH_PID" 2>/dev/null || true
+ fi
+ echo ""
+ echo -e "${GREEN}✓ Claude execution completed${NC}"
+
+ # Check if DONE promise was output (accept both DONE and ALL_DONE variants)
+ if echo "$CLAUDE_OUTPUT" | grep -qE "(ALL_)?DONE"; then
+ DETECTED_SIGNAL=$(echo "$CLAUDE_OUTPUT" | grep -oE "(ALL_)?DONE" | tail -1)
+ echo -e "${GREEN}✓ Completion signal detected: ${DETECTED_SIGNAL}${NC}"
+ echo -e "${GREEN}✓ Task completed successfully!${NC}"
+ CONSECUTIVE_FAILURES=0
+ RLM_STATUS="done"
+
+ # For planning mode, stop after one successful plan
+ if [ "$MODE" = "plan" ]; then
+ echo ""
+ echo -e "${GREEN}Planning complete!${NC}"
+ echo -e "${CYAN}Run './scripts/ralph-loop.sh' to start building.${NC}"
+ echo -e "${CYAN}Or delete IMPLEMENTATION_PLAN.md to work directly from specs.${NC}"
+ break
+ fi
+ else
+ echo -e "${YELLOW}⚠ No completion signal found${NC}"
+ echo -e "${YELLOW} Agent did not output DONE or ALL_DONE${NC}"
+ echo -e "${YELLOW} This means acceptance criteria were not met.${NC}"
+ echo -e "${YELLOW} Retrying in next iteration...${NC}"
+ CONSECUTIVE_FAILURES=$((CONSECUTIVE_FAILURES + 1))
+ RLM_STATUS="incomplete"
+ print_latest_output "$LOG_FILE" "Claude"
+
+ if [ $CONSECUTIVE_FAILURES -ge $MAX_CONSECUTIVE_FAILURES ]; then
+ echo ""
+ echo -e "${RED}⚠ $MAX_CONSECUTIVE_FAILURES consecutive iterations without completion.${NC}"
+ echo -e "${RED} The agent may be stuck. Consider:${NC}"
+ echo -e "${RED} - Checking the logs in $LOG_DIR${NC}"
+ echo -e "${RED} - Simplifying the current spec${NC}"
+ echo -e "${RED} - Manually fixing blocking issues${NC}"
+ echo ""
+ CONSECUTIVE_FAILURES=0
+ fi
+ fi
+ else
+ if [ -n "$WATCH_PID" ]; then
+ kill "$WATCH_PID" 2>/dev/null || true
+ wait "$WATCH_PID" 2>/dev/null || true
+ fi
+ echo -e "${RED}✗ Claude execution failed${NC}"
+ echo -e "${YELLOW}Check log: $LOG_FILE${NC}"
+ CONSECUTIVE_FAILURES=$((CONSECUTIVE_FAILURES + 1))
+ RLM_STATUS="error"
+ print_latest_output "$LOG_FILE" "Claude"
+ fi
+
+ # Record iteration in RLM index (optional)
+ if [ -n "$RLM_CONTEXT_FILE" ]; then
+ RLM_PROMPT_PATH="${RLM_PROMPT_SNAPSHOT:-}"
+ RLM_OUTPUT_SNAPSHOT="$RLM_TRACE_DIR/iter_${ITERATION}_output.log"
+ cp "$LOG_FILE" "$RLM_OUTPUT_SNAPSHOT"
+ echo -e "${TIMESTAMP}\t${MODE}\t${ITERATION}\t${RLM_PROMPT_PATH}\t${LOG_FILE}\t${RLM_OUTPUT_SNAPSHOT}\t${RLM_STATUS}" >> "$RLM_INDEX"
+ fi
+
+ # Push changes after each iteration (if any)
+ git push origin "$CURRENT_BRANCH" 2>/dev/null || {
+ if git log origin/$CURRENT_BRANCH..HEAD --oneline 2>/dev/null | grep -q .; then
+ echo -e "${YELLOW}Push failed, creating remote branch...${NC}"
+ git push -u origin "$CURRENT_BRANCH" 2>/dev/null || true
+ fi
+ }
+
+ # Brief pause between iterations
+ echo ""
+ echo -e "${BLUE}Waiting 2s before next iteration...${NC}"
+ sleep 2
+done
+
+echo ""
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${GREEN} RALPH LOOP FINISHED ($ITERATION iterations) ${NC}"
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
diff --git a/specs/001-core-ui-camera.md b/specs/001-core-ui-camera.md
new file mode 100644
index 0000000..32c3238
--- /dev/null
+++ b/specs/001-core-ui-camera.md
@@ -0,0 +1,41 @@
+# Feature: Core UI and Camera Feed (PyObjC)
+
+## Status: COMPLETE
+
+## Description
+Create the main application window using PyObjC (AppKit) and display a live camera feed. This ensures a native macOS look and feel.
+
+## Requirements
+
+1. **App & Window Setup (AppKit)**:
+ - Initialize `NSApplication`.
+ - Create a main `NSWindow` titled "ItemSense".
+ - Size: 800x600 (resizable).
+ - Window should center on screen.
+
+2. **UI Layout**:
+ - Use `NSStackView` (vertical) or manual constraints to layout:
+ - Top: Video Feed (`NSImageView`).
+ - Bottom: "Capture" button (`NSButton`).
+
+3. **Camera Feed**:
+ - Use `opencv-python` to capture frames from webcam (index 0).
+ - Convert frames (`cv2` BGR -> RGB) to `NSImage/CGImage`.
+ - Update the `NSImageView` at ~30 FPS using a timer (`NSTimer` or equivalent app loop integration).
+
+4. **Capture Button**:
+ - Standard macOS Push Button.
+ - Label: "Capture".
+ - Action: Print "Capture clicked" to console.
+
+5. **Lifecycle**:
+ - Ensure `Cmd+Q` works.
+ - Ensure closing the window terminates the app (or at least the `applicationShouldTerminateAfterLastWindowClosed:` delegate method returns True).
+
+## Acceptance Criteria
+
+- [ ] App launches with a native macOS window "ItemSense".
+- [ ] Live camera feed is visible in the view.
+- [ ] "Capture" button is visible at the bottom.
+- [ ] Clicking "Capture" prints to console.
+- [ ] App exits cleanly on window close or Cmd+Q.
diff --git a/specs/002-openai-integration.md b/specs/002-openai-integration.md
new file mode 100644
index 0000000..7262d20
--- /dev/null
+++ b/specs/002-openai-integration.md
@@ -0,0 +1,29 @@
+# Feature: OpenAI Vision Integration (PyObjC)
+
+## Description
+Implement the logic to capture a frame from the AppKit interface and send it to OpenAI's API.
+
+## Requirements
+
+1. **Image Handling**:
+ - On "Capture" click:
+ - Stop/Pause the live feed update.
+ - Store the current frame (in memory).
+ - Show "Processing..." (maybe change button text or add a label).
+
+2. **OpenAI API Call**:
+ - Async handling is important to not block the UI thread (spinning beachball).
+ - Run the API request in a background thread (`threading`).
+ - Model: `gpt-5-mini` (fallback `gpt-4o-mini`).
+ - Prompt: "What is this item? Please provide a brief description."
+
+3. **Response Handling**:
+ - When response returns, schedule a UI update on the main thread (`performSelectorOnMainThread:` or `dispatch_async`).
+ - Print response to console (UI display comes in Spec 003).
+
+## Acceptance Criteria
+
+- [ ] UI remains responsive (no beachball) during API call.
+- [ ] "Processing..." indication is shown.
+- [ ] Image frame is correctly sent to OpenAI.
+- [ ] Text response is received and printed to console.
diff --git a/specs/003-result-display.md b/specs/003-result-display.md
new file mode 100644
index 0000000..b9752da
--- /dev/null
+++ b/specs/003-result-display.md
@@ -0,0 +1,25 @@
+# Feature: Result Display (PyObjC)
+
+## Description
+Display the analysis results natively in the AppKit UI.
+
+## Requirements
+
+1. **Result UI**:
+ - Add a scrollable `NSTextView` (within an `NSScrollView`) below the image view.
+ - Initially empty or hidden.
+
+2. **Workflow**:
+ - **Live Mode**: Camera active, Button says "Capture", Text view hidden/empty.
+ - **Processing Mode**: specific indication.
+ - **Result Mode**: Camera paused on captured frame, Button says "Scan Another", Text view shows description.
+
+3. **Data Binding**:
+ - Update the `NSTextView` string with the OpenAI response.
+ - Clicking "Scan Another" resets the UI to **Live Mode**.
+
+## Acceptance Criteria
+
+- [ ] App cycles correctly: Capture -> Result -> Scan Another -> Capture.
+- [ ] Result text is readable in a native macOS scroll view.
+- [ ] Window resizing layout remains sane.