#!/usr/bin/env bash # plan-refine — Automated plan iteration: Codex (ChatGPT) review + Claude integration # Usage: plan-refine [options] # plan-refine status [options] set -euo pipefail # ────────────────────────────────────────────── # Prompts (inlined) # ────────────────────────────────────────────── EVAL_PROMPT="Carefully review this entire plan for me and come up with your best revisions in terms of better architecture, new features, changed features, etc. to make it better, more robust/reliable, more performant, more compelling/useful, etc. For each proposed change, give me your detailed analysis and rationale/justification for why it would make the project better along with the git-diff style change versus the original plan shown below. IMPORTANT: If the plan contains a '## Rejected Recommendations' section at the bottom, these are suggestions that were already considered and deliberately rejected in previous iterations. Do NOT re-propose anything listed there. Focus your energy on genuinely new improvements instead." INTEGRATE_PROMPT="I asked ChatGPT to review your plan. I want you to REALLY carefully analyze their plan with an open mind and be intellectually honest about what they did that's better than your plan. Then I want you to come up with the best possible revisions to your plan (you should simply update your existing document for your original plan with the revisions) that artfully and skillfully blends the \"best of all worlds\" to create a true, ultimate, superior hybrid version of the plan that best achieves our stated goals and will work the best in real-world practice to solve the problems we are facing and our overarching goals while ensuring the extreme success of the enterprise as best as possible; you should provide me with a complete series of git-diff style changes to your original plan to turn it into the new, enhanced, much longer and detailed plan that integrates the best of all the plans with every good idea included." # ────────────────────────────────────────────── # Frontmatter helpers # ────────────────────────────────────────────── get_frontmatter() { local file="$1" key="$2" default="${3:-}" if ! head -1 "$file" | grep -q '^---$'; then echo "$default"; return fi local value value=$(awk '/^---$/{n++; next} n==1{print}' "$file" \ | grep "^${key}:" | head -1 \ | sed "s/^${key}: *//; s/^ *//; s/ *$//; s/^[\"']//; s/[\"']$//") echo "${value:-$default}" } set_frontmatter() { local file="$1" key="$2" value="$3" if ! head -1 "$file" | grep -q '^---$'; then local tmp; tmp=$(mktemp) { echo "---"; echo "${key}: ${value}"; echo "---"; cat "$file"; } > "$tmp" mv "$tmp" "$file"; return fi if grep -q "^${key}:" "$file"; then sed "s|^${key}:.*|${key}: ${value}|" "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" else awk -v key="$key" -v val="$value" ' BEGIN { count=0; inserted=0 } /^---$/ { count++ } count == 2 && !inserted { print key ": " val; inserted=1 } { print } ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" fi } is_plan_file() { local file="$1" [[ -f "$file" ]] && head -20 "$file" | grep -q "^plan: true" } init_frontmatter() { local file="$1" title="${2:-$(basename "$file" .md)}" local today; today=$(date +%Y-%m-%d) if head -1 "$file" | grep -q '^---$'; then set_frontmatter "$file" "plan" "true" [[ -z "$(get_frontmatter "$file" "status")" ]] && set_frontmatter "$file" "status" "drafting" [[ -z "$(get_frontmatter "$file" "iteration")" ]] && set_frontmatter "$file" "iteration" "0" [[ -z "$(get_frontmatter "$file" "target_iterations")" ]] && set_frontmatter "$file" "target_iterations" "8" [[ -z "$(get_frontmatter "$file" "created")" ]] && set_frontmatter "$file" "created" "$today" set_frontmatter "$file" "updated" "$today" else local tmp; tmp=$(mktemp) { echo "---" echo "plan: true" echo "title: \"${title}\"" echo "status: drafting" echo "iteration: 0" echo "target_iterations: 8" echo "beads_revision: 0" echo "related_plans: []" echo "created: ${today}" echo "updated: ${today}" echo "---" echo "" cat "$file" } > "$tmp" mv "$tmp" "$file" fi } # ────────────────────────────────────────────── # Claude stream-json progress display # ────────────────────────────────────────────── claude_progress() { node --input-type=module -e ' import { createInterface } from "readline"; const rl = createInterface({ input: process.stdin }); let toolCount = 0; rl.on("line", (line) => { try { const event = JSON.parse(line); if (event.type === "assistant" && event.message?.content) { for (const block of event.message.content) { if (block.type === "text" && block.text) { process.stderr.write(block.text); if (!block.text.endsWith("\n")) process.stderr.write("\n"); } if (block.type === "tool_use") { toolCount++; const input = block.input || {}; const file = input.file_path || input.path || input.pattern || ""; const short = file ? file.split("/").pop() : ""; process.stderr.write(" [" + block.name + (short ? ": " + short : "") + "]\n"); } } } if (event.type === "result") { const cost = event.cost_usd ? " ($" + event.cost_usd.toFixed(4) + ")" : ""; process.stderr.write("\n [Done — " + (event.num_turns || "?") + " turns, " + toolCount + " tool calls" + cost + "]\n"); } } catch {} });' } # ────────────────────────────────────────────── # Subcommand: status # ────────────────────────────────────────────── cmd_status() { local SEARCH_ROOT="${HOME}/projects" local JSON_OUTPUT=false local FILTER_STATUS="" while [[ $# -gt 0 ]]; do case "$1" in --json) JSON_OUTPUT=true; shift ;; --status) FILTER_STATUS="$2"; shift 2 ;; --root) SEARCH_ROOT="$2"; shift 2 ;; -h|--help) cat <<'EOF' plan-refine status — Dashboard for plan files across projects Usage: plan-refine status [options] Options: --json Machine-readable JSON output --status Filter to plans with this status --root Search root (default: ~/projects) -h, --help Show this help EOF exit 0 ;; -*) echo "Unknown option: $1" >&2; exit 2 ;; *) SEARCH_ROOT="$1"; shift ;; esac done if [[ ! -d "$SEARCH_ROOT" ]]; then echo "Error: Search root not found: $SEARCH_ROOT" >&2; exit 1 fi declare -a PLAN_FILES=() while IFS= read -r -d '' file; do if is_plan_file "$file"; then PLAN_FILES+=("$file") fi done < <(find "$SEARCH_ROOT" \ -name "*.md" \ -not -path "*/node_modules/*" \ -not -path "*/.git/*" \ -not -path "*/.jj/*" \ -not -path "*/target/*" \ -not -name "*.feedback-*" \ -print0 2>/dev/null) if [[ ${#PLAN_FILES[@]} -eq 0 ]]; then if [[ "$JSON_OUTPUT" == "true" ]]; then echo '{"plans":[],"summary":{"total":0}}' else echo "No plan files found under $SEARCH_ROOT" echo "Initialize a plan: plan-refine --init" fi exit 0 fi if [[ "$JSON_OUTPUT" == "true" ]]; then echo '{"plans":[' local first=true for file in "${PLAN_FILES[@]}"; do local status; status=$(get_frontmatter "$file" "status" "unknown") [[ -n "$FILTER_STATUS" && "$status" != "$FILTER_STATUS" ]] && continue local title; title=$(get_frontmatter "$file" "title" "$(basename "$file" .md)") local iteration; iteration=$(get_frontmatter "$file" "iteration" "0") local target; target=$(get_frontmatter "$file" "target_iterations" "") local beads_rev; beads_rev=$(get_frontmatter "$file" "beads_revision" "0") local updated; updated=$(get_frontmatter "$file" "updated" "") local project; project=$(echo "$file" | sed "s|${SEARCH_ROOT}/||" | cut -d/ -f1) local feedback_count; feedback_count=$(ls "${file%.md}".feedback-*.md 2>/dev/null | wc -l | tr -d ' ') [[ "$first" != "true" ]] && echo "," first=false echo " {\"path\":\"$file\",\"project\":\"$project\",\"title\":\"$title\",\"status\":\"$status\",\"iteration\":$iteration,\"target_iterations\":${target:-null},\"beads_revision\":$beads_rev,\"feedback_files\":$feedback_count,\"updated\":\"$updated\"}" done echo ']}' else echo "" echo " Plan Status Dashboard" echo " $(date '+%Y-%m-%d %H:%M')" echo " ════════════════════════════════════════════════════════════════════════" printf " %-18s %-28s %-12s %-14s %s\n" "Project" "Plan" "Status" "Iterations" "Updated" echo " ────────────────────────────────────────────────────────────────────────" local total=0 local by_status_drafting=0 by_status_iterating=0 by_status_splitting=0 local by_status_refining=0 by_status_ready=0 by_status_implementing=0 by_status_completed=0 for file in "${PLAN_FILES[@]}"; do local status; status=$(get_frontmatter "$file" "status" "unknown") [[ -n "$FILTER_STATUS" && "$status" != "$FILTER_STATUS" ]] && continue local title; title=$(get_frontmatter "$file" "title" "") local iteration; iteration=$(get_frontmatter "$file" "iteration" "0") local target; target=$(get_frontmatter "$file" "target_iterations" "") local beads_rev; beads_rev=$(get_frontmatter "$file" "beads_revision" "0") local updated; updated=$(get_frontmatter "$file" "updated" "") local project; project=$(echo "$file" | sed "s|${SEARCH_ROOT}/||" | cut -d/ -f1) local plan_name; plan_name=$(basename "$file" .md) [[ ${#project} -gt 18 ]] && project="${project:0:15}..." [[ ${#plan_name} -gt 28 ]] && plan_name="${plan_name:0:25}..." local iter_display if [[ -n "$target" && "$target" != "0" ]]; then iter_display="${iteration}/${target}" else iter_display="${iteration}" fi [[ "$beads_rev" != "0" ]] && iter_display="${iter_display} br:${beads_rev}" printf " %-18s %-28s %-12s %-14s %s\n" \ "$project" "$plan_name" "$status" "$iter_display" "$updated" total=$((total + 1)) case "$status" in drafting) by_status_drafting=$((by_status_drafting + 1)) ;; iterating) by_status_iterating=$((by_status_iterating + 1)) ;; splitting) by_status_splitting=$((by_status_splitting + 1)) ;; refining) by_status_refining=$((by_status_refining + 1)) ;; ready) by_status_ready=$((by_status_ready + 1)) ;; implementing) by_status_implementing=$((by_status_implementing + 1)) ;; completed) by_status_completed=$((by_status_completed + 1)) ;; esac done echo " ════════════════════════════════════════════════════════════════════════" echo "" local summary_parts=() [[ $by_status_drafting -gt 0 ]] && summary_parts+=("${by_status_drafting} drafting") [[ $by_status_iterating -gt 0 ]] && summary_parts+=("${by_status_iterating} iterating") [[ $by_status_splitting -gt 0 ]] && summary_parts+=("${by_status_splitting} splitting") [[ $by_status_refining -gt 0 ]] && summary_parts+=("${by_status_refining} refining") [[ $by_status_ready -gt 0 ]] && summary_parts+=("${by_status_ready} ready") [[ $by_status_implementing -gt 0 ]] && summary_parts+=("${by_status_implementing} implementing") [[ $by_status_completed -gt 0 ]] && summary_parts+=("${by_status_completed} completed") IFS=", " echo " ${total} plans: ${summary_parts[*]}" echo "" echo " Pipeline: drafting -> iterating -> splitting -> refining -> ready -> implementing -> completed" echo "" fi } # ────────────────────────────────────────────── # Subcommand: refine (default) # ────────────────────────────────────────────── cmd_refine() { local CLAUDE_MODEL="" CODEX_MODEL="" DRY_RUN=false NO_INTEGRATE=false PLAN_FILE="" INIT_ONLY=false while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true; shift ;; --no-integrate) NO_INTEGRATE=true; shift ;; --claude-model) CLAUDE_MODEL="$2"; shift 2 ;; --codex-model) CODEX_MODEL="$2"; shift 2 ;; --init) INIT_ONLY=true; shift ;; -h|--help) cat <<'EOF' plan-refine — Automated plan iteration via Codex CLI + Claude CLI Usage: plan-refine [options] plan-refine status [options] Runs the full cycle automatically: 1. Sends plan to ChatGPT via Codex CLI 2. Captures ChatGPT's response 3. Claude CLI integrates feedback back into the plan file Requires: codex CLI (codex login) and claude CLI Options: --dry-run Preview what would happen --no-integrate Get ChatGPT feedback only, skip Claude integration --claude-model Claude model for integration (default: your default) --codex-model Codex/ChatGPT model (default: codex default) --init Add plan frontmatter to file without running anything -h, --help Show this help Subcommands: status Show dashboard of all plan files EOF exit 0 ;; -*) echo "Unknown option: $1" >&2; exit 2 ;; *) PLAN_FILE="$1"; shift ;; esac done if [[ -z "$PLAN_FILE" ]]; then echo "Error: No plan file specified" >&2 echo "Usage: plan-refine " >&2 exit 2 fi PLAN_FILE="$(cd "$(dirname "$PLAN_FILE")" && pwd)/$(basename "$PLAN_FILE")" if [[ ! -f "$PLAN_FILE" ]]; then echo "Error: Plan file not found: $PLAN_FILE" >&2; exit 1 fi if ! head -1 "$PLAN_FILE" | grep -q '^---$'; then echo "No frontmatter found. Initializing..." init_frontmatter "$PLAN_FILE" fi if ! is_plan_file "$PLAN_FILE"; then set_frontmatter "$PLAN_FILE" "plan" "true" fi if [[ "$INIT_ONLY" == "true" ]]; then init_frontmatter "$PLAN_FILE" echo "Frontmatter initialized for: $PLAN_FILE" exit 0 fi local ITERATION TARGET STATUS NEXT_ITERATION PLAN_DIR FEEDBACK_FILE ITERATION=$(get_frontmatter "$PLAN_FILE" "iteration" "0") TARGET=$(get_frontmatter "$PLAN_FILE" "target_iterations" "8") STATUS=$(get_frontmatter "$PLAN_FILE" "status" "drafting") NEXT_ITERATION=$((ITERATION + 1)) PLAN_DIR=$(dirname "$PLAN_FILE") FEEDBACK_FILE="${PLAN_FILE%.md}.feedback-${NEXT_ITERATION}.md" if [[ "$STATUS" == "ready" || "$STATUS" == "implementing" || "$STATUS" == "completed" ]]; then echo "Warning: Plan status is '$STATUS' -- already past iteration phase." echo "Continue anyway? (y/N)" read -r confirm [[ "$confirm" != "y" && "$confirm" != "Y" ]] && exit 0 fi echo "=== plan-refine ===" echo " Plan: $(basename "$PLAN_FILE")" echo " Status: $STATUS" echo " Iteration: $ITERATION -> $NEXT_ITERATION (target: $TARGET)" echo " Feedback: $(basename "$FEEDBACK_FILE")" if [[ "$NO_INTEGRATE" == "true" ]]; then echo " Mode: ChatGPT review only" else echo " Mode: Full cycle (ChatGPT + Claude integration)" fi echo "" if [[ "$DRY_RUN" == "true" ]]; then echo "=== DRY RUN ===" echo "" echo "Step 1: ChatGPT via Codex CLI" echo " codex exec -o $FEEDBACK_FILE --skip-git-repo-check" echo "" if [[ "$NO_INTEGRATE" != "true" ]]; then echo "Step 2: Claude CLI integration" echo " claude -p --allowedTools Read,Edit,Write,mcp__morph-fast-tools__edit_file --permission-mode acceptEdits" fi exit 0 fi # Step 1: ChatGPT via Codex CLI if [[ -f "$FEEDBACK_FILE" && -s "$FEEDBACK_FILE" ]]; then echo "[Step 1] Feedback file already exists, skipping ChatGPT step." echo " $(basename "$FEEDBACK_FILE") ($(wc -c < "$FEEDBACK_FILE" | tr -d ' ') bytes)" echo " Delete it to re-run ChatGPT: rm $(basename "$FEEDBACK_FILE")" else echo "[Step 1] Sending plan to ChatGPT via Codex CLI..." local FULL_PROMPT="${EVAL_PROMPT} --- $(cat "$PLAN_FILE")" local CODEX_ARGS=(codex exec -o "$FEEDBACK_FILE" --skip-git-repo-check -s read-only) [[ -n "$CODEX_MODEL" ]] && CODEX_ARGS+=(-m "$CODEX_MODEL") local CODEX_EXIT=0 echo "$FULL_PROMPT" | "${CODEX_ARGS[@]}" - || CODEX_EXIT=$? if [[ $CODEX_EXIT -ne 0 ]]; then echo "Error: Codex send failed (exit $CODEX_EXIT)" >&2; exit $CODEX_EXIT fi if [[ ! -s "$FEEDBACK_FILE" ]]; then echo "Error: Codex produced empty output" >&2; exit 1 fi echo "ChatGPT feedback saved to: $FEEDBACK_FILE ($(wc -c < "$FEEDBACK_FILE" | tr -d ' ') bytes)" fi # Step 2: Claude integration if [[ "$NO_INTEGRATE" == "true" ]]; then echo "" echo "Skipping integration (--no-integrate)." echo "Run manually: plan-refine $(basename "$PLAN_FILE")" else echo "" echo "[Step 2] Claude integrating feedback into plan..." local CLAUDE_PROMPT="${INTEGRATE_PROMPT} The original plan is at: ${PLAN_FILE} ChatGPT's feedback is at: ${FEEDBACK_FILE} You have Read, Edit, and the Morph edit_file MCP tool. Prefer the Morph edit_file tool (mcp__morph-fast-tools__edit_file) for edits — it's faster and handles large changes better than legacy Edit. Edit the plan file directly — do NOT output the plan content to stdout. - Preserve the YAML frontmatter block (between the --- delimiters) at the top unchanged. - Only modify the content below the frontmatter. - Print a brief summary to stdout as you work: which changes you're accepting, rejecting, or modifying, and why. Keep it concise — bullet points, not essays. - CRITICAL: For any recommendations you REJECT, append them to a '## Rejected Recommendations' section at the very bottom of the plan file (create it if it doesn't exist, or append to it if it does). Each entry should be a bullet with a brief reason for rejection. This prevents future reviewers from re-proposing the same changes. Format: '- **** — rejected because '. Keep the section cumulative across iterations — never remove previous entries." local CLAUDE_STREAM_ARGS=(claude -p "$CLAUDE_PROMPT" --allowedTools "Read,Edit,Write,mcp__morph-fast-tools__edit_file,mcp__morph-fast-tools__warpgrep_codebase_search" --permission-mode acceptEdits --add-dir "$PLAN_DIR" --output-format stream-json --verbose) [[ -n "$CLAUDE_MODEL" ]] && CLAUDE_STREAM_ARGS+=(--model "$CLAUDE_MODEL") set +o pipefail "${CLAUDE_STREAM_ARGS[@]}" | claude_progress local CLAUDE_EXIT=${PIPESTATUS[0]} set -o pipefail if [[ $CLAUDE_EXIT -ne 0 ]]; then echo "Error: Claude integration failed (exit $CLAUDE_EXIT)" >&2 echo "Feedback still available at: $FEEDBACK_FILE" >&2 exit $CLAUDE_EXIT fi echo "Integration complete. Plan updated." fi # Update frontmatter set_frontmatter "$PLAN_FILE" "iteration" "$NEXT_ITERATION" set_frontmatter "$PLAN_FILE" "updated" "$(date +%Y-%m-%d)" [[ "$STATUS" == "drafting" ]] && set_frontmatter "$PLAN_FILE" "status" "iterating" echo "" echo "=== Iteration $NEXT_ITERATION/$TARGET complete ===" [[ $NEXT_ITERATION -ge $TARGET ]] && echo "" && echo "Target iterations reached. Plan may be ready for bead splitting." } # ────────────────────────────────────────────── # Dispatch # ────────────────────────────────────────────── case "${1:-}" in status) shift; cmd_status "$@" ;; -h|--help) cmd_refine --help ;; *) cmd_refine "$@" ;; esac