Files
plan-tools/plan-refine
Taylor Eernisse 102d15cda7 Fix sed portability: use temp file instead of sed -i
BSD sed (macOS) and GNU sed (Linux) interpret `sed -i ''`
differently — GNU treats the empty string as the script
argument, not the backup suffix. Replace with portable
sed-to-tmpfile-and-mv pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 15:23:18 -05:00

468 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# plan-refine — Automated plan iteration: Codex (ChatGPT) review + Claude integration
# Usage: plan-refine <plan-file> [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."
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 <status> Filter to plans with this status
--root <path> 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 <file.md> --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 <plan-file> [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 <m> Claude model for integration (default: your default)
--codex-model <m> 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 <plan-file>" >&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 <prompt> -o $FEEDBACK_FILE --skip-git-repo-check"
echo ""
if [[ "$NO_INTEGRATE" != "true" ]]; then
echo "Step 2: Claude CLI integration"
echo " claude -p <integration-prompt> --allowedTools Read,Edit,Write --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 and Edit tools. 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."
local CLAUDE_STREAM_ARGS=(claude -p "$CLAUDE_PROMPT"
--allowedTools "Read,Edit,Write"
--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