mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-07-25 03:22:43 -04:00
fb0a458d4f
# Description of Changes AI app generation benchmark comparing SpacetimeDB vs PostgreSQL (Express + Socket.io + Drizzle ORM). Same AI model (Claude Sonnet 4.6), same prompts, same chat app, two backends. Upgraded through 12 feature levels, manually graded at each level, bugs fixed, all costs measured via OpenTelemetry. Results viewable at: https://spacetimedb.com/llms-benchmark-sequential-upgrade ## Benchmark harness (`tools/llm-sequential-upgrade/`) - `run.sh`: orchestrates headless Claude Code sessions for code generation, sequential upgrades, and bug fixes. Tracks all API costs via OTel. Supports `--upgrade`, `--fix`, `--composed-prompt`, `--resume-session` modes. - `grade.sh` / `grade-agents.sh`: grading harnesses for manual testing of generated apps. - `docker-compose.otel.yaml`: OTel collector + PostgreSQL services. - `generate-report.mjs` / `parse-telemetry.mjs`: aggregate per-session telemetry into cost reports. - Backend guidelines in `backends/`: SpacetimeDB SDK reference, config templates, server setup docs, PostgreSQL setup with Drizzle/Socket.io guidance. **After https://github.com/clockworklabs/SpacetimeDB/pull/4740 merges, we will likely want to update this so that it reads backend and SDK guidance from SKILLS** ## Two complete benchmark runs **Run 1 (20260403):** Original methodology. **Run 2 (20260406):** Refined methodology with domain bias removed from SpacetimeDB SDK docs and PostgreSQL instructions made feature-spec-neutral. **Note: no meaningful changes in results were observed with these changes. Domain familiarity biases were very small and almost certainly not the cause of STDB's major gains over PG stack.** Each run contains full L1-L12 app source for both backends, level snapshots preserving state before each upgrade, and per-session OTel cost summaries. ## 12 feature levels | Level | Feature | |---|---| | L1 | Basic Chat + Typing + Read Receipts + Unread Counts | | L2 | Scheduled Messages | | L3 | Ephemeral Messages | | L4 | Message Reactions | | L5 | Message Editing with History | | L6 | Real-Time Permissions (kick, ban, promote) | | L7 | Rich User Presence | | L8 | Message Threading | | L9 | Private Rooms + Direct Messages | | L10 | Room Activity Indicators | | L11 | Draft Sync | | L12 | Anonymous to Registered Migration | ## Results | | Run 1 (20260403) | Run 2 (20260406) | |---|---|---| | **SpacetimeDB total cost** | $13.33 | $12.62 | | **PostgreSQL total cost** | $17.80 | $19.68 | | **SpacetimeDB bugs** | 5 | 2 | | **PostgreSQL bugs** | 19 | 8 | | **SpacetimeDB fix sessions** | 4 | 1 | | **PostgreSQL fix sessions** | 17 | 10 | Both runs agree: SpacetimeDB apps are cheaper to build, have fewer bugs, and require fewer fix iterations. The refined methodology (Run 2) widened the cost gap and **confirmed the advantage is structural, not an artifact of domain-biased SDK docs.** ## Performance benchmark (`perf-benchmark/`) Stress throughput tool that fires concurrent writers at peak saturation against the AI-generated `send_message` handlers. | Tier | SpacetimeDB (avg) | PostgreSQL (avg) | Ratio | |---|---|---|---| | AI-generated (as-shipped) | 5,267 msgs/sec | 694 msgs/sec | 7.6x | | PG rate limit removed | 5,267 msgs/sec | 1,070 msgs/sec | 4.9x | | Optimized (same features kept) | 25,278 msgs/sec | 1,139 msgs/sec | 22x | The gap widens with optimization because SpacetimeDB's bottleneck is fixable code patterns in the reducer while PostgreSQL's bottleneck is architectural (sequential network round-trips to an external database). Optimized reference code with all features preserved is in `perf-benchmark/results/optimized-reference/`. ## Data handling Per-session cost summaries (`cost-summary.json`, `COST_REPORT.md`, `metadata.json`) are committed. Raw OTel telemetry (`raw-telemetry.jsonl`) containing PII is excluded via `.gitignore` and stored privately. # API and ABI breaking changes None. All changes are in `tools/llm-sequential-upgrade/`. No production code, library, or SDK changes. # Expected complexity level and risk **1 - Trivial.** Self-contained benchmarking tooling and data. No interaction with production code. # Testing - [x] L1-L12 upgrades completed on all 4 apps (2 backends x 2 runs) with OTel cost capture - [x] All levels manually graded after each upgrade; bugs filed and fixed via the harness - [x] Methodology refinement between runs validated (domain bias removal, feature-neutral instructions) - [x] Stress benchmarks run across both runs x 3 tiers (as-shipped, rate-limit-removed, optimized) - [x] Optimized benchmarks verified to preserve all original features - [x] Sensitive data (PII in raw telemetry) removed from repo and gitignored - [ ] Reviewer: spot-check that METRICS_DATA.json / METRICS_REPORT.json numbers match the telemetry cost-summary.json files --------- Co-authored-by: Tyler Cloutier <cloutiertyler@users.noreply.github.com> Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
954 lines
38 KiB
Bash
954 lines
38 KiB
Bash
#!/bin/bash -l
|
|
# Sequential Upgrade Launcher — Phase 1: Generate & Deploy
|
|
#
|
|
# Runs code generation and deployment in headless Claude Code with OTel tracking.
|
|
# After this completes, run grade.sh to do browser testing and grading interactively.
|
|
#
|
|
# Usage:
|
|
# ./run.sh # defaults: level=1, backend=spacetime, variant=sequential-upgrade
|
|
# ./run.sh --level 5 --backend postgres # generate from scratch at level 5
|
|
# ./run.sh --variant one-shot --backend spacetime # one-shot: all features in one prompt
|
|
# ./run.sh --rules standard --backend spacetime # standard: SDK rules only, no templates
|
|
# ./run.sh --run-index 1 --backend spacetime # parallel run with offset ports
|
|
# ./run.sh --fix <app-dir> # fix bugs in existing app (reads BUG_REPORT.md)
|
|
# ./run.sh --upgrade <app-dir> --level 3 # add level 3 features to existing level 2 app (incremental feature file)
|
|
# ./run.sh --upgrade <app-dir> --level 3 --composed-prompt # use the full cumulative composed spec instead
|
|
# ./run.sh --upgrade <app-dir> --level 3 --resume-session # same, but resume prior session for cache
|
|
#
|
|
# Prerequisites:
|
|
# - Claude Code CLI installed (claude or npx @anthropic-ai/claude-code)
|
|
# - Docker running (for OTel Collector)
|
|
# - SpacetimeDB running (spacetime start)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Configurable container name for PostgreSQL backend
|
|
POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-llm-sequential-upgrade-postgres-1}"
|
|
|
|
# ─── Parse arguments ─────────────────────────────────────────────────────────
|
|
|
|
LEVEL=1
|
|
LEVEL_EXPLICIT=""
|
|
BACKEND="spacetime"
|
|
VARIANT="sequential-upgrade"
|
|
RULES="guided"
|
|
TEST_MODE="" # playwright | chrome-mcp | (empty = no automated testing)
|
|
RUN_INDEX=0
|
|
FIX_MODE=""
|
|
FIX_APP_DIR=""
|
|
UPGRADE_MODE=""
|
|
UPGRADE_APP_DIR=""
|
|
RESUME_SESSION=""
|
|
COMPOSED_UPGRADE_PROMPT=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--level) LEVEL="$2"; LEVEL_EXPLICIT=1; shift 2 ;;
|
|
--backend) BACKEND="$2"; shift 2 ;;
|
|
--variant) VARIANT="$2"; shift 2 ;;
|
|
--rules) RULES="$2"; shift 2 ;;
|
|
--test) TEST_MODE="$2"; shift 2 ;;
|
|
--run-index) RUN_INDEX="$2"; shift 2 ;;
|
|
--fix) FIX_MODE=1; FIX_APP_DIR="$2"; shift 2 ;;
|
|
--upgrade) UPGRADE_MODE=1; UPGRADE_APP_DIR="$2"; shift 2 ;;
|
|
--composed-prompt) COMPOSED_UPGRADE_PROMPT=1; shift ;;
|
|
--resume-session) RESUME_SESSION=1; shift ;;
|
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# Validate rules level
|
|
case "$RULES" in
|
|
guided|standard|minimal) ;;
|
|
*) echo "ERROR: --rules must be guided, standard, or minimal"; exit 1 ;;
|
|
esac
|
|
|
|
# ─── Port allocation ──────────────────────────────────────────────────────────
|
|
# Each backend has a 100-port range. Run-index offsets within that range.
|
|
# SpacetimeDB: 6173 + run-index (6173, 6174, 6175, ...)
|
|
# PostgreSQL: 6273 + run-index (6273, 6274, 6275, ...)
|
|
# Express: 6001 + run-index (6001, 6002, 6003, ...)
|
|
VITE_PORT_STDB=$((6173 + RUN_INDEX))
|
|
VITE_PORT_PG=$((6273 + RUN_INDEX))
|
|
EXPRESS_PORT=$((6001 + RUN_INDEX))
|
|
PG_PORT=6432 # Shared container, isolation via per-run database names
|
|
STDB_PORT=3000 # SpacetimeDB server is shared, modules are isolated by name
|
|
|
|
if [[ "$BACKEND" == "spacetime" ]]; then
|
|
VITE_PORT=$VITE_PORT_STDB
|
|
else
|
|
VITE_PORT=$VITE_PORT_PG
|
|
fi
|
|
|
|
# Variant-specific defaults
|
|
if [[ "$VARIANT" == "one-shot" ]]; then
|
|
if [[ -z "$LEVEL_EXPLICIT" ]]; then
|
|
LEVEL=12 # one-shot defaults to all features
|
|
fi
|
|
if [[ -n "$UPGRADE_MODE" ]]; then
|
|
echo "WARNING: --upgrade is not meaningful with --variant one-shot"
|
|
echo "One-shot generates all features in a single session."
|
|
UPGRADE_MODE=""
|
|
UPGRADE_APP_DIR=""
|
|
fi
|
|
fi
|
|
|
|
# Determine mode label early (used in metadata and output)
|
|
if [[ -n "$FIX_MODE" ]]; then
|
|
MODE_LABEL="fix"
|
|
elif [[ -n "$UPGRADE_MODE" ]]; then
|
|
MODE_LABEL="upgrade"
|
|
else
|
|
MODE_LABEL="generate"
|
|
fi
|
|
|
|
# ─── Find Claude CLI ─────────────────────────────────────────────────────────
|
|
|
|
# Add Claude Code desktop install to PATH if not already findable
|
|
_APPDATA_UNIX="${APPDATA:-$HOME/AppData/Roaming}"
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
_APPDATA_UNIX=$(cygpath "$_APPDATA_UNIX" 2>/dev/null || echo "$_APPDATA_UNIX")
|
|
fi
|
|
CLAUDE_DESKTOP_DIR="$_APPDATA_UNIX/Claude/claude-code"
|
|
if [[ -d "$CLAUDE_DESKTOP_DIR" ]]; then
|
|
CLAUDE_LATEST=$(ls -d "$CLAUDE_DESKTOP_DIR"/*/ 2>/dev/null | sort -V | tail -1)
|
|
if [[ -n "$CLAUDE_LATEST" ]]; then
|
|
export PATH="$PATH:$CLAUDE_LATEST"
|
|
fi
|
|
fi
|
|
|
|
CLAUDE_CMD=""
|
|
if command -v claude &>/dev/null; then
|
|
CLAUDE_CMD="claude"
|
|
elif command -v claude.exe &>/dev/null; then
|
|
CLAUDE_CMD="claude.exe"
|
|
else
|
|
if command -v npx &>/dev/null; then
|
|
if npx @anthropic-ai/claude-code --version &>/dev/null; then
|
|
CLAUDE_CMD="npx @anthropic-ai/claude-code"
|
|
else
|
|
echo "ERROR: Claude Code CLI not found via npx."
|
|
echo "Install it with: npm install -g @anthropic-ai/claude-code"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "ERROR: Claude Code CLI not found (tried: claude, claude.exe, npx)."
|
|
echo "Install it with: npm install -g @anthropic-ai/claude-code"
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "Using Claude CLI: $CLAUDE_CMD"
|
|
|
|
# ─── Pre-flight checks ──────────────────────────────────────────────────────
|
|
|
|
echo ""
|
|
echo "=== Pre-flight Checks ==="
|
|
|
|
# Ensure spacetime is in PATH (Windows installs to AppData/Local/SpacetimeDB)
|
|
SPACETIME_DIR="${USERPROFILE:-$HOME}/AppData/Local/SpacetimeDB"
|
|
if [[ -d "$SPACETIME_DIR" ]]; then
|
|
export PATH="$PATH:$SPACETIME_DIR"
|
|
fi
|
|
# Also try the cygpath-resolved home
|
|
_USER="${USER:-${USERNAME:-$(whoami)}}"
|
|
if [[ -d "/c/Users/$_USER/AppData/Local/SpacetimeDB" ]]; then
|
|
export PATH="$PATH:/c/Users/$_USER/AppData/Local/SpacetimeDB"
|
|
fi
|
|
|
|
PG_DATABASE="spacetime"
|
|
PG_CONNECTION_URL="postgresql://spacetime:spacetime@localhost:6432/spacetime"
|
|
|
|
if [[ "$BACKEND" == "spacetime" ]]; then
|
|
if spacetime server ping local &>/dev/null; then
|
|
echo "[OK] SpacetimeDB is running"
|
|
else
|
|
echo "[FAIL] SpacetimeDB is not running. Start it with: spacetime start"
|
|
exit 1
|
|
fi
|
|
elif [[ "$BACKEND" == "postgres" ]]; then
|
|
if docker exec "$POSTGRES_CONTAINER" psql -U spacetime -d spacetime -c "SELECT 1" &>/dev/null; then
|
|
echo "[OK] PostgreSQL container is running"
|
|
else
|
|
echo "[FAIL] PostgreSQL is not reachable. Check Docker container $POSTGRES_CONTAINER."
|
|
exit 1
|
|
fi
|
|
|
|
# Per-run database isolation: each run-index gets its own database
|
|
# Run 0 uses "spacetime" (default), Run N uses "spacetime_runN"
|
|
if [[ $RUN_INDEX -gt 0 ]]; then
|
|
PG_DATABASE="spacetime_run${RUN_INDEX}"
|
|
# Create the database if it doesn't exist
|
|
docker exec "$POSTGRES_CONTAINER" psql -U spacetime -d spacetime -c \
|
|
"SELECT 1 FROM pg_database WHERE datname = '$PG_DATABASE'" | grep -q 1 || \
|
|
docker exec "$POSTGRES_CONTAINER" psql -U spacetime -d spacetime -c \
|
|
"CREATE DATABASE $PG_DATABASE OWNER spacetime;" 2>/dev/null
|
|
echo "[OK] PostgreSQL database: $PG_DATABASE (run-index $RUN_INDEX)"
|
|
else
|
|
PG_DATABASE="spacetime"
|
|
echo "[OK] PostgreSQL database: $PG_DATABASE (default)"
|
|
fi
|
|
PG_CONNECTION_URL="postgresql://spacetime:spacetime@localhost:6432/$PG_DATABASE"
|
|
fi
|
|
|
|
if ! docker info &>/dev/null; then
|
|
echo "[FAIL] Docker is not running."
|
|
exit 1
|
|
fi
|
|
|
|
# Shared telemetry directory (OTel Collector writes here)
|
|
SHARED_TELEMETRY_DIR="$SCRIPT_DIR/telemetry"
|
|
mkdir -p "$SHARED_TELEMETRY_DIR"
|
|
|
|
# Rotate telemetry log if over 10MB to prevent unbounded growth
|
|
LOGS_FILE="$SHARED_TELEMETRY_DIR/logs.jsonl"
|
|
if [[ -f "$LOGS_FILE" ]]; then
|
|
SIZE=$(wc -c < "$LOGS_FILE")
|
|
if [[ $SIZE -gt 10485760 ]]; then
|
|
ARCHIVE="$SHARED_TELEMETRY_DIR/logs-$(date +%Y%m%d-%H%M%S).jsonl.bak"
|
|
mv "$LOGS_FILE" "$ARCHIVE"
|
|
echo "[INFO] Rotated logs.jsonl ($SIZE bytes) to $(basename "$ARCHIVE")"
|
|
fi
|
|
fi
|
|
|
|
if docker compose -f "$SCRIPT_DIR/docker-compose.otel.yaml" ps --status running 2>/dev/null | grep -q otel-collector; then
|
|
echo "[OK] OTel Collector is running"
|
|
else
|
|
echo "[...] Starting OTel Collector..."
|
|
docker compose -f "$SCRIPT_DIR/docker-compose.otel.yaml" up -d
|
|
echo "[OK] OTel Collector started"
|
|
fi
|
|
|
|
if command -v node &>/dev/null; then
|
|
echo "[OK] Node.js $(node --version)"
|
|
else
|
|
echo "[FAIL] Node.js not found."
|
|
exit 1
|
|
fi
|
|
|
|
COMPOSED_PROMPT="$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/composed/$(printf '%02d' "$LEVEL")_"*".md"
|
|
# shellcheck disable=SC2086
|
|
if ls $COMPOSED_PROMPT &>/dev/null; then
|
|
PROMPT_FILE=$(ls $COMPOSED_PROMPT 2>/dev/null | head -1)
|
|
echo "[OK] Prompt file: $(basename "$PROMPT_FILE")"
|
|
else
|
|
echo "[FAIL] No composed prompt found for level $LEVEL"
|
|
exit 1
|
|
fi
|
|
|
|
# Strip UI contracts from prompt if not using Playwright testing
|
|
if [[ "$TEST_MODE" != "playwright" ]]; then
|
|
STRIPPED_PROMPT="/tmp/seq-upgrade-prompt-${RUN_INDEX}-$(basename "$PROMPT_FILE")"
|
|
# Remove **UI contract:** blocks (from the line through the next blank line or next ###)
|
|
sed '/^\*\*UI contract:\*\*/,/^$/d; /^\*\*Important:\*\* Each feature below includes/d' "$PROMPT_FILE" > "$STRIPPED_PROMPT"
|
|
PROMPT_FILE="$STRIPPED_PROMPT"
|
|
echo "[OK] UI contracts stripped (test=$TEST_MODE)"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# ─── Create run directories ─────────────────────────────────────────────────
|
|
|
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
DATE_STAMP=$(date +%Y%m%d)
|
|
START_TIME=$(date +%Y-%m-%dT%H:%M:%S%z)
|
|
START_TIME_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
# Variant-based directory structure:
|
|
# llm-sequential-upgrade/<variant>/<variant>-YYYYMMDD/ ← shared comparison run
|
|
# <backend>/ ← per-backend (spacetime|postgres)
|
|
# results/chat-app-<timestamp>/
|
|
# telemetry/<run-id>/
|
|
# inputs/
|
|
VARIANT_DIR="$SCRIPT_DIR/$VARIANT"
|
|
|
|
# For upgrade/fix, reuse the existing RUN_BASE_DIR from the app's parent structure.
|
|
# For generate, create a new dated run directory.
|
|
if [[ -n "$UPGRADE_MODE" || -n "$FIX_MODE" ]]; then
|
|
# Derive RUN_BASE_DIR from existing app directory structure:
|
|
# <variant>/<variant>-DATE/<backend>/results/chat-app-*/
|
|
if [[ -n "$UPGRADE_MODE" ]]; then
|
|
APP_DIR="$UPGRADE_APP_DIR"
|
|
else
|
|
APP_DIR="$FIX_APP_DIR"
|
|
fi
|
|
# Detect backend from app directory structure BEFORE deriving paths.
|
|
# Must happen here so $BACKEND is correct for TELEMETRY_DIR assignment below.
|
|
if [[ -d "$APP_DIR/backend/spacetimedb" ]]; then
|
|
BACKEND="spacetime"
|
|
elif [[ -d "$APP_DIR/server" ]]; then
|
|
BACKEND="postgres"
|
|
fi
|
|
# Walk up from app dir: chat-app-* → results → <backend> → <variant>-DATE
|
|
RUN_BASE_DIR="$(cd "$APP_DIR/../../.." 2>/dev/null && pwd)"
|
|
# Validate it looks like a run base dir (has a backend subdirectory)
|
|
if [[ ! -d "$RUN_BASE_DIR/$BACKEND" ]]; then
|
|
# Fallback: create new run base dir (legacy app dir not under variant structure)
|
|
RUN_BASE_DIR="$VARIANT_DIR/$VARIANT-$DATE_STAMP"
|
|
fi
|
|
TELEMETRY_DIR="$RUN_BASE_DIR/$BACKEND/telemetry"
|
|
RESULTS_DIR="$RUN_BASE_DIR/$BACKEND/results"
|
|
else
|
|
# Generate mode: create/reuse a shared dated comparison run directory.
|
|
# Both backends (spacetime + postgres) share the same parent folder.
|
|
# Dedup only triggers if THIS backend already has a subdirectory
|
|
# (i.e. a second generate for the same backend on the same day).
|
|
RUN_BASE_DIR="$VARIANT_DIR/$VARIANT-$DATE_STAMP"
|
|
# Dedup: only increment if a COMPLETED run exists for this backend
|
|
# (has telemetry with cost data). Bare/abandoned stubs don't count.
|
|
_backend_has_completed_run() {
|
|
ls "$1/$BACKEND/telemetry/"*/cost-summary.json &>/dev/null 2>&1
|
|
}
|
|
if _backend_has_completed_run "$RUN_BASE_DIR"; then
|
|
SEQ=2
|
|
while _backend_has_completed_run "$RUN_BASE_DIR-$SEQ"; do ((SEQ++)); done
|
|
RUN_BASE_DIR="$RUN_BASE_DIR-$SEQ"
|
|
fi
|
|
TELEMETRY_DIR="$RUN_BASE_DIR/$BACKEND/telemetry"
|
|
RESULTS_DIR="$RUN_BASE_DIR/$BACKEND/results"
|
|
fi
|
|
|
|
# Backend detection for fix/upgrade mode is done earlier (before TELEMETRY_DIR assignment).
|
|
|
|
if [[ -n "$UPGRADE_MODE" ]]; then
|
|
RUN_ID="$BACKEND-upgrade-to-level$LEVEL-$TIMESTAMP"
|
|
elif [[ -n "$FIX_MODE" ]]; then
|
|
RUN_ID="$BACKEND-fix-level$LEVEL-$TIMESTAMP"
|
|
else
|
|
RUN_ID="$BACKEND-level$LEVEL-$TIMESTAMP"
|
|
APP_DIR="$RESULTS_DIR/chat-app-$TIMESTAMP"
|
|
mkdir -p "$APP_DIR"
|
|
fi
|
|
|
|
RUN_DIR="$TELEMETRY_DIR/$RUN_ID"
|
|
mkdir -p "$RUN_DIR"
|
|
|
|
# On Windows (Git Bash/MSYS2), convert paths to native format for Node.js
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
RUN_DIR_NATIVE=$(cygpath -w "$RUN_DIR")
|
|
APP_DIR_NATIVE=$(cygpath -w "$APP_DIR")
|
|
SCRIPT_DIR_NATIVE=$(cygpath -w "$SCRIPT_DIR")
|
|
else
|
|
RUN_DIR_NATIVE="$RUN_DIR"
|
|
APP_DIR_NATIVE="$APP_DIR"
|
|
SCRIPT_DIR_NATIVE="$SCRIPT_DIR"
|
|
fi
|
|
|
|
echo "=== Sequential Upgrade: ${MODE_LABEL^} ==="
|
|
echo " Variant: $VARIANT"
|
|
echo " Rules: $RULES"
|
|
echo " Level: $LEVEL"
|
|
echo " Backend: $BACKEND"
|
|
echo " Run index: $RUN_INDEX (Vite=$VITE_PORT)"
|
|
echo " Run ID: $RUN_ID"
|
|
echo " Run base: $RUN_BASE_DIR"
|
|
echo " App dir: $APP_DIR_NATIVE"
|
|
echo " Telemetry: $RUN_DIR"
|
|
echo ""
|
|
|
|
# ─── Enable OpenTelemetry ────────────────────────────────────────────────────
|
|
# Unset Claude Desktop host-management vars — they suppress OTEL telemetry when
|
|
# run.sh is invoked from within a Claude Desktop agent session (Bash tool).
|
|
unset CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST
|
|
unset CLAUDE_CODE_ENTRYPOINT
|
|
|
|
export CLAUDE_CODE_ENABLE_TELEMETRY=1
|
|
export OTEL_LOGS_EXPORTER=otlp
|
|
export OTEL_METRICS_EXPORTER=otlp
|
|
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
|
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
|
export OTEL_LOGS_EXPORT_INTERVAL=1000
|
|
export OTEL_METRIC_EXPORT_INTERVAL=5000
|
|
|
|
# ─── Generate session ID ───────────────────────────────────────────────────
|
|
# NOTE: OTEL_RESOURCE_ATTRIBUTES is set AFTER SESSION_ID is generated (below)
|
|
# Pre-generate a UUID so we can pass --session-id to Claude and save it in
|
|
# metadata for future --resume-session use.
|
|
|
|
SESSION_ID=$(python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null || node -e "const c=require('crypto');console.log([c.randomBytes(4),c.randomBytes(2),c.randomBytes(2),c.randomBytes(2),c.randomBytes(6)].map(b=>b.toString('hex')).join('-'))")
|
|
|
|
# Tag all OTel records with run.id and session.id so parse-telemetry.mjs can
|
|
# filter by session even when multiple backends run in parallel on the same collector.
|
|
export OTEL_RESOURCE_ATTRIBUTES="run.id=$RUN_ID,session.id=$SESSION_ID"
|
|
|
|
# ─── Save run metadata ──────────────────────────────────────────────────────
|
|
|
|
# Escape backslashes for JSON (Windows paths have backslashes)
|
|
APP_DIR_JSON="${APP_DIR_NATIVE//\\/\\\\}"
|
|
|
|
cat > "$RUN_DIR/metadata.json" <<EOF
|
|
{
|
|
"level": $LEVEL,
|
|
"backend": "$BACKEND",
|
|
"timestamp": "$TIMESTAMP",
|
|
"startedAt": "$START_TIME",
|
|
"startedAtUtc": "$START_TIME_UTC",
|
|
"runId": "$RUN_ID",
|
|
"appDir": "$APP_DIR_JSON",
|
|
"promptFile": "$(basename "$PROMPT_FILE")",
|
|
"phase": "$MODE_LABEL",
|
|
"variant": "$VARIANT",
|
|
"rules": "$RULES",
|
|
"testMode": "${TEST_MODE:-none}",
|
|
"runIndex": $RUN_INDEX,
|
|
"vitePort": $VITE_PORT,
|
|
"expressPort": $EXPRESS_PORT,
|
|
"pgDatabase": "${PG_DATABASE:-}",
|
|
"sessionId": "$SESSION_ID"
|
|
}
|
|
EOF
|
|
|
|
# ─── Snapshot inputs ───────────────────────────────────────────────────────
|
|
# Copy all inputs (prompts, backend specs, tooling, etc.) into the run directory
|
|
# so each run is self-contained and reproducible even if the tooling changes.
|
|
|
|
snapshot_inputs() {
|
|
local INPUTS_DIR="$RUN_BASE_DIR/$BACKEND/inputs"
|
|
if [[ -d "$INPUTS_DIR" ]]; then
|
|
return # already snapshotted (upgrade/fix into existing run)
|
|
fi
|
|
mkdir -p "$INPUTS_DIR/backends" "$INPUTS_DIR/test-plans" \
|
|
"$INPUTS_DIR/prompts/composed" "$INPUTS_DIR/prompts/language"
|
|
|
|
# Shared tooling
|
|
for f in CLAUDE.md run.sh grade.sh parse-telemetry.mjs \
|
|
docker-compose.otel.yaml otel-collector-config.yaml \
|
|
DEVELOP.md .gitignore; do
|
|
cp "$SCRIPT_DIR/$f" "$INPUTS_DIR/" 2>/dev/null || true
|
|
done
|
|
|
|
# Backend specs (only relevant backend)
|
|
cp "$SCRIPT_DIR/backends/$BACKEND.md" "$INPUTS_DIR/backends/" 2>/dev/null || true
|
|
if [[ "$BACKEND" == "spacetime" ]]; then
|
|
cp "$SCRIPT_DIR/backends/spacetime-sdk-rules.md" "$INPUTS_DIR/backends/" 2>/dev/null || true
|
|
cp "$SCRIPT_DIR/backends/spacetime-templates.md" "$INPUTS_DIR/backends/" 2>/dev/null || true
|
|
fi
|
|
|
|
# Test plans
|
|
cp "$SCRIPT_DIR/test-plans/"*.md "$INPUTS_DIR/test-plans/" 2>/dev/null || true
|
|
|
|
# Prompts (only relevant language file, all composed levels)
|
|
local PROMPTS_SRC="$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts"
|
|
cp "$PROMPTS_SRC/composed/"*.md "$INPUTS_DIR/prompts/composed/" 2>/dev/null || true
|
|
cp "$PROMPTS_SRC/language/typescript-$BACKEND.md" "$INPUTS_DIR/prompts/language/" 2>/dev/null || true
|
|
|
|
echo " Inputs snapshotted to $INPUTS_DIR"
|
|
}
|
|
|
|
snapshot_inputs
|
|
|
|
# Write app-dir.txt so benchmark.sh can find the app directory without racing
|
|
echo "$APP_DIR" > "$RUN_DIR/app-dir.txt"
|
|
|
|
# ─── Build the prompt ────────────────────────────────────────────────────────
|
|
|
|
if [[ -n "$FIX_MODE" ]]; then
|
|
# ─── FIX MODE: Read bug report, fix code, redeploy ──────────────────────
|
|
|
|
# In fix mode, APP_DIR is the existing app dir
|
|
APP_DIR="$FIX_APP_DIR"
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
APP_DIR_NATIVE=$(cygpath -w "$APP_DIR")
|
|
else
|
|
APP_DIR_NATIVE="$APP_DIR"
|
|
fi
|
|
|
|
if [[ ! -f "$APP_DIR/BUG_REPORT.md" ]]; then
|
|
echo "ERROR: No BUG_REPORT.md found in $APP_DIR"
|
|
echo "Run the grading session first to produce a bug report."
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== Sequential Upgrade: Fix Iteration ==="
|
|
echo " App dir: $APP_DIR_NATIVE"
|
|
echo " Bug report: $APP_DIR_NATIVE/BUG_REPORT.md"
|
|
echo ""
|
|
|
|
# Detect backend from existing app directory structure
|
|
if [[ -d "$APP_DIR/backend/spacetimedb" ]]; then
|
|
FIX_BACKEND="spacetime"
|
|
elif [[ -d "$APP_DIR/server" ]]; then
|
|
FIX_BACKEND="postgres"
|
|
else
|
|
FIX_BACKEND="unknown"
|
|
fi
|
|
|
|
PROMPT=$(cat <<PROMPT_EOF
|
|
Fix the bugs in the sequential upgrade app.
|
|
|
|
**App directory:** $APP_DIR_NATIVE
|
|
**Backend:** $FIX_BACKEND
|
|
|
|
**Instructions:**
|
|
1. Read the CLAUDE.md in this directory for backend-specific architecture and deploy instructions
|
|
2. Read BUG_REPORT.md in the app directory — it describes what's broken
|
|
3. Read the relevant source code files mentioned in the bug report
|
|
4. Fix each bug described in the report
|
|
5. Rebuild and redeploy ALL servers:
|
|
- For PostgreSQL: restart the Express server (npm run dev in server/) AND the Vite client
|
|
- For SpacetimeDB: run spacetime publish, then restart the Vite client
|
|
6. Verify the fix by testing the endpoint/behavior described in the bug report
|
|
7. Make sure ALL servers are running:
|
|
- Client dev server on port $VITE_PORT
|
|
- For PostgreSQL: Express API server on port $EXPRESS_PORT (test with curl)
|
|
8. Append this fix iteration to ITERATION_LOG.md in the app directory
|
|
|
|
CRITICAL: After fixing code, you MUST verify the servers are running and the bug is fixed.
|
|
Do NOT just edit files and say "done" — actually restart the servers and test.
|
|
|
|
Do NOT do browser testing — that happens in the grading session.
|
|
Cost tracking is automatic via OpenTelemetry — do NOT estimate tokens.
|
|
|
|
When done, output: FIX_COMPLETE
|
|
PROMPT_EOF
|
|
)
|
|
|
|
elif [[ -n "$UPGRADE_MODE" ]]; then
|
|
# ─── UPGRADE MODE: Add new features from a higher level prompt ─────────
|
|
|
|
APP_DIR="$UPGRADE_APP_DIR"
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
APP_DIR_NATIVE=$(cygpath -w "$APP_DIR")
|
|
else
|
|
APP_DIR_NATIVE="$APP_DIR"
|
|
fi
|
|
|
|
# ─── Snapshot previous level before upgrading ─────────────────────────
|
|
PREV_LEVEL=$((LEVEL - 1))
|
|
SNAPSHOT_DIR="$APP_DIR/level-$PREV_LEVEL"
|
|
if [[ -d "$SNAPSHOT_DIR" ]]; then
|
|
echo "Snapshot level-$PREV_LEVEL already exists — skipping snapshot"
|
|
else
|
|
echo "Snapshotting current app state to level-$PREV_LEVEL..."
|
|
mkdir -p "$SNAPSHOT_DIR"
|
|
# Copy app source dirs (exclude node_modules, dist, snapshots)
|
|
for item in "$APP_DIR"/*; do
|
|
base=$(basename "$item")
|
|
case "$base" in
|
|
level-*|node_modules|dist|.vite|drizzle|dev-server.log) continue ;;
|
|
*) cp -r "$item" "$SNAPSHOT_DIR/" 2>/dev/null ;;
|
|
esac
|
|
done
|
|
echo " Saved to $SNAPSHOT_DIR"
|
|
fi
|
|
|
|
# Detect backend from existing app directory structure
|
|
if [[ -d "$APP_DIR/backend/spacetimedb" ]]; then
|
|
UPGRADE_BACKEND="spacetime"
|
|
elif [[ -d "$APP_DIR/server" ]]; then
|
|
UPGRADE_BACKEND="postgres"
|
|
else
|
|
UPGRADE_BACKEND="unknown"
|
|
fi
|
|
|
|
# Resolve prompt file path
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
PROMPT_FILE_NATIVE=$(cygpath -w "$PROMPT_FILE")
|
|
LANG_PROMPT_NATIVE=$(cygpath -w "$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/language/typescript-$UPGRADE_BACKEND.md")
|
|
else
|
|
PROMPT_FILE_NATIVE="$PROMPT_FILE"
|
|
LANG_PROMPT_NATIVE="$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/language/typescript-$UPGRADE_BACKEND.md"
|
|
fi
|
|
|
|
PREV_LEVEL=$((LEVEL - 1))
|
|
|
|
echo "=== Sequential Upgrade: Upgrade to Level $LEVEL ==="
|
|
echo " App dir: $APP_DIR_NATIVE"
|
|
echo " Backend: $UPGRADE_BACKEND"
|
|
echo " From level: $PREV_LEVEL → $LEVEL"
|
|
echo " Prompt: $(basename "$PROMPT_FILE")"
|
|
echo ""
|
|
|
|
# In upgrade mode, default to the incremental feature file (only the new
|
|
# feature). Pass --composed-prompt to use the full cumulative composed spec
|
|
# for this level, matching how the original L1-L11 benchmark was prompted.
|
|
if [[ -n "$COMPOSED_UPGRADE_PROMPT" ]]; then
|
|
FEATURE_FILE="$PROMPT_FILE"
|
|
echo " Using composed (cumulative) feature file: $(basename "$FEATURE_FILE")"
|
|
else
|
|
FEATURE_PROMPT="$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/features/$(printf '%02d' "$LEVEL")_"*".md"
|
|
# shellcheck disable=SC2086
|
|
FEATURE_FILE=$(ls $FEATURE_PROMPT 2>/dev/null | head -1)
|
|
if [[ -n "$FEATURE_FILE" ]]; then
|
|
echo " Using incremental feature file: $(basename "$FEATURE_FILE")"
|
|
else
|
|
echo " WARNING: No incremental feature file for level $LEVEL, falling back to composed prompt"
|
|
FEATURE_FILE="$PROMPT_FILE"
|
|
fi
|
|
fi
|
|
|
|
# Read language and feature files to inline into the prompt
|
|
LANG_CONTENT=$(cat "$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/language/typescript-$UPGRADE_BACKEND.md" 2>/dev/null || echo "")
|
|
FEATURE_CONTENT=$(cat "$FEATURE_FILE" 2>/dev/null || echo "")
|
|
|
|
PROMPT=$(cat <<PROMPT_EOF
|
|
Upgrade the existing chat app to add the new feature(s) from level $LEVEL.
|
|
|
|
**App directory:** $APP_DIR_NATIVE
|
|
**Backend:** $UPGRADE_BACKEND
|
|
**Current level:** $PREV_LEVEL (all features from level $PREV_LEVEL are already implemented and working)
|
|
**Target level:** $LEVEL
|
|
|
|
**Instructions:**
|
|
1. Read the CLAUDE.md in this directory for backend-specific architecture and SDK reference
|
|
2. Read the existing source code to understand the current architecture
|
|
3. Add the new feature(s) to both backend and frontend, integrating with the existing code
|
|
4. Rebuild and redeploy (see CLAUDE.md for backend-specific steps)
|
|
5. Verify the build succeeds: npx tsc --noEmit && npm run build (if applicable)
|
|
6. Make sure the dev server is running on port $VITE_PORT
|
|
|
|
Features from level $PREV_LEVEL and below are ALREADY IMPLEMENTED — do NOT rewrite them.
|
|
Only add the NEW feature(s) that appear in the feature spec below but not in level $PREV_LEVEL.
|
|
|
|
Do NOT do browser testing — that happens in a separate grading session.
|
|
Cost tracking is automatic via OpenTelemetry — do NOT estimate tokens.
|
|
|
|
When done, output: UPGRADE_COMPLETE
|
|
|
|
---
|
|
|
|
$LANG_CONTENT
|
|
|
|
---
|
|
|
|
$FEATURE_CONTENT
|
|
PROMPT_EOF
|
|
)
|
|
|
|
else
|
|
# ─── GENERATE MODE: Initial code generation and deploy ──────────────────
|
|
|
|
# Resolve absolute paths for prompt references
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
PROMPT_FILE_NATIVE=$(cygpath -w "$PROMPT_FILE")
|
|
LANG_PROMPT_NATIVE=$(cygpath -w "$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/language/typescript-$BACKEND.md")
|
|
else
|
|
PROMPT_FILE_NATIVE="$PROMPT_FILE"
|
|
LANG_PROMPT_NATIVE="$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/language/typescript-$BACKEND.md"
|
|
fi
|
|
|
|
# Read language and feature files to inline into the prompt
|
|
LANG_CONTENT=$(cat "$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/language/typescript-$BACKEND.md" 2>/dev/null || echo "")
|
|
FEATURE_CONTENT=$(cat "$PROMPT_FILE" 2>/dev/null || echo "")
|
|
|
|
PROMPT=$(cat <<PROMPT_EOF
|
|
Run the sequential upgrade benchmark — GENERATE AND DEPLOY ONLY.
|
|
|
|
**Configuration:**
|
|
- Level: $LEVEL
|
|
- Backend: $BACKEND
|
|
- App output directory: $APP_DIR_NATIVE (this is also your working directory)
|
|
- Run ID: $RUN_ID
|
|
|
|
**Instructions:**
|
|
1. Read the CLAUDE.md in this directory — it has backend-specific setup, architecture, and SDK reference
|
|
2. Follow the phases in CLAUDE.md to generate, build, and deploy the app
|
|
3. Write all code in the current directory
|
|
|
|
If the build fails, fix and retry (up to 3 times per phase).
|
|
Write an ITERATION_LOG.md tracking any build reprompts.
|
|
|
|
Do NOT do browser testing — that happens in a separate grading session.
|
|
Cost tracking is automatic via OpenTelemetry — do NOT estimate tokens.
|
|
|
|
When done, output: DEPLOY_COMPLETE
|
|
|
|
---
|
|
|
|
$LANG_CONTENT
|
|
|
|
---
|
|
|
|
$FEATURE_CONTENT
|
|
PROMPT_EOF
|
|
)
|
|
fi
|
|
|
|
echo "Starting Claude Code session ($MODE_LABEL)..."
|
|
echo "─────────────────────────────────────────────"
|
|
|
|
# ─── Assemble backend-specific CLAUDE.md into app directory ─────────────────
|
|
# Build CLAUDE.md at runtime by concatenating the workflow, SDK rules, and
|
|
# templates. This ensures Claude always gets the latest rules inlined directly
|
|
# (no "go find and read this other file" that it might skip).
|
|
|
|
if [[ -z "$FIX_MODE" && -z "$UPGRADE_MODE" ]]; then
|
|
# Assemble CLAUDE.md based on --rules level:
|
|
# guided: full phases + SDK rules + code templates (most prescriptive)
|
|
# standard: SDK rules only (no templates, no step-by-step phases)
|
|
# minimal: just the tech stack name (least prescriptive)
|
|
if [[ "$RULES" == "minimal" ]]; then
|
|
if [[ "$BACKEND" == "spacetime" ]]; then
|
|
echo "Build this app using the SpacetimeDB TypeScript SDK (npm package: spacetimedb)." > "$APP_DIR/CLAUDE.md"
|
|
echo "Server module in backend/spacetimedb/, React client in client/." >> "$APP_DIR/CLAUDE.md"
|
|
echo "Vite dev server port: $VITE_PORT" >> "$APP_DIR/CLAUDE.md"
|
|
else
|
|
echo "Build this app using PostgreSQL + Express + Socket.io + Drizzle ORM." > "$APP_DIR/CLAUDE.md"
|
|
echo "Express server in server/, React client in client/." >> "$APP_DIR/CLAUDE.md"
|
|
echo "PostgreSQL connection: $PG_CONNECTION_URL" >> "$APP_DIR/CLAUDE.md"
|
|
echo "Express port: $EXPRESS_PORT | Vite port: $VITE_PORT" >> "$APP_DIR/CLAUDE.md"
|
|
fi
|
|
echo "Assembled minimal CLAUDE.md (rules=$RULES)"
|
|
elif [[ "$RULES" == "standard" ]]; then
|
|
if [[ "$BACKEND" == "spacetime" ]]; then
|
|
cat "$SCRIPT_DIR/backends/spacetime-sdk-rules.md" > "$APP_DIR/CLAUDE.md"
|
|
else
|
|
echo "# PostgreSQL Backend" > "$APP_DIR/CLAUDE.md"
|
|
echo "" >> "$APP_DIR/CLAUDE.md"
|
|
echo "PostgreSQL connection: \`$PG_CONNECTION_URL\`" >> "$APP_DIR/CLAUDE.md"
|
|
echo "" >> "$APP_DIR/CLAUDE.md"
|
|
echo "Use Express (port $EXPRESS_PORT) + Socket.io + Drizzle ORM. Server in \`server/\`, client in \`client/\`." >> "$APP_DIR/CLAUDE.md"
|
|
echo "Vite dev server port: $VITE_PORT" >> "$APP_DIR/CLAUDE.md"
|
|
fi
|
|
echo "Assembled standard CLAUDE.md (rules=$RULES)"
|
|
else
|
|
# guided (default) — full phases + SDK rules + templates
|
|
if [[ "$BACKEND" == "spacetime" ]]; then
|
|
{
|
|
cat "$SCRIPT_DIR/backends/spacetime.md"
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
cat "$SCRIPT_DIR/backends/spacetime-sdk-rules.md"
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
cat "$SCRIPT_DIR/backends/spacetime-templates.md"
|
|
} > "$APP_DIR/CLAUDE.md"
|
|
echo "Assembled guided CLAUDE.md from spacetime.md + sdk-rules + templates"
|
|
else
|
|
cp "$SCRIPT_DIR/backends/$BACKEND.md" "$APP_DIR/CLAUDE.md"
|
|
echo "Copied backends/$BACKEND.md → app CLAUDE.md"
|
|
fi
|
|
fi
|
|
|
|
# Prepend unique run ID to bust Anthropic's server-side prompt cache.
|
|
# Cache is keyed on content — a unique prefix guarantees a cold run every time.
|
|
sed -i "1s|^|<!-- run-id: $RUN_ID -->\n\n|" "$APP_DIR/CLAUDE.md"
|
|
|
|
# Patch ports and database names in CLAUDE.md for parallel runs (run-index > 0)
|
|
if [[ $RUN_INDEX -gt 0 ]]; then
|
|
sed -i \
|
|
-e "s/6173/$VITE_PORT_STDB/g" \
|
|
-e "s/6273/$VITE_PORT_PG/g" \
|
|
-e "s/:6001/:$EXPRESS_PORT/g" \
|
|
-e "s/localhost:6001/localhost:$EXPRESS_PORT/g" \
|
|
-e "s|localhost:6432/spacetime|localhost:6432/$PG_DATABASE|g" \
|
|
-e "s|spacetime:spacetime@localhost:6432/spacetime|spacetime:spacetime@localhost:6432/$PG_DATABASE|g" \
|
|
"$APP_DIR/CLAUDE.md"
|
|
echo " Patched for run-index=$RUN_INDEX (Vite=$VITE_PORT, Express=$EXPRESS_PORT, DB=$PG_DATABASE)"
|
|
fi
|
|
fi
|
|
|
|
# ─── Run Claude Code ─────────────────────────────────────────────────────────
|
|
# Run from the APP directory so CLAUDE.md auto-discovery picks up the
|
|
# backend-specific file, not the parent llm-sequential-upgrade/CLAUDE.md.
|
|
|
|
cd "$APP_DIR"
|
|
|
|
# NOTE: Git isolation disabled — it breaks --resume-session because Claude Code
|
|
# ties sessions to the project root (.git location). Without isolation, Claude
|
|
# may see parent repo files, but session continuity is more important for
|
|
# sequential upgrades. Use cleanup.sh after testing to remove any artifacts.
|
|
|
|
# Build resume flag if --resume-session was passed and a prior session ID exists
|
|
RESUME_FLAG=""
|
|
if [[ -n "$RESUME_SESSION" && -n "$UPGRADE_MODE" ]]; then
|
|
# Find the most recent telemetry dir for this app to get its session ID.
|
|
# Search variant structure: <variant>/<variant>-DATE/telemetry/*/
|
|
# Sort by modification time (newest first), break on first match.
|
|
PREV_SESSION_ID=""
|
|
SEARCH_DIRS=$(find "$VARIANT_DIR" -path "*/telemetry/*" -name "metadata.json" -exec dirname {} \; 2>/dev/null | sort -r)
|
|
for tdir in $SEARCH_DIRS; do
|
|
if [[ -f "$tdir/metadata.json" ]]; then
|
|
META_PATH="$(cygpath -w "$tdir/metadata.json" 2>/dev/null || echo "$tdir/metadata.json")"
|
|
TDIR_APP=$(node -e "const m=JSON.parse(require('fs').readFileSync(process.argv[1],'utf-8')); process.stdout.write(m.appDir||'')" -- "$META_PATH" 2>/dev/null)
|
|
if [[ "$TDIR_APP" == "$APP_DIR_NATIVE" || "$TDIR_APP" == "$APP_DIR_JSON" ]]; then
|
|
SID=$(node -e "const m=JSON.parse(require('fs').readFileSync(process.argv[1],'utf-8')); process.stdout.write(m.sessionId||'')" -- "$META_PATH" 2>/dev/null)
|
|
if [[ -n "$SID" ]]; then
|
|
PREV_SESSION_ID="$SID"
|
|
break # newest match found, stop searching
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
if [[ -n "$PREV_SESSION_ID" ]]; then
|
|
RESUME_FLAG="--resume $PREV_SESSION_ID --fork-session"
|
|
echo "Forking prior session: $PREV_SESSION_ID"
|
|
else
|
|
echo "No prior session ID found for this app — starting fresh"
|
|
fi
|
|
fi
|
|
|
|
# --fork-session creates a new session branched from the prior one (keeps context)
|
|
$CLAUDE_CMD --print --verbose --output-format text --dangerously-skip-permissions \
|
|
--add-dir "$APP_DIR" \
|
|
--add-dir "$SCRIPT_DIR" \
|
|
--add-dir "$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts" \
|
|
--session-id "$SESSION_ID" $RESUME_FLAG -p "$PROMPT"
|
|
EXIT_CODE=$?
|
|
|
|
echo ""
|
|
echo "─────────────────────────────────────────────"
|
|
|
|
# ─── Record end time ─────────────────────────────────────────────────────────
|
|
|
|
END_TIME=$(date +%Y-%m-%dT%H:%M:%S%z)
|
|
END_TIME_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
# Update metadata with end time — use native path for Node.js on Windows
|
|
METADATA_FILE_NATIVE="$RUN_DIR_NATIVE/metadata.json"
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
METADATA_FILE_NATIVE=$(cygpath -w "$RUN_DIR/metadata.json")
|
|
fi
|
|
node -e "
|
|
const fs = require('fs');
|
|
const f = process.argv[1];
|
|
const m = JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
m.endedAt = '$END_TIME';
|
|
m.endedAtUtc = '$END_TIME_UTC';
|
|
m.exitCode = $EXIT_CODE;
|
|
m.mode = '$MODE_LABEL';
|
|
m.sessionId = '$SESSION_ID';
|
|
fs.writeFileSync(f, JSON.stringify(m, null, 2));
|
|
" -- "$METADATA_FILE_NATIVE" || echo "WARNING: Failed to update metadata with end time"
|
|
|
|
# ─── Snapshot completed level (upgrade mode) ─────────────────────────────────
|
|
|
|
if [[ -n "$UPGRADE_MODE" && $EXIT_CODE -eq 0 ]]; then
|
|
LEVEL_SNAPSHOT="$APP_DIR/level-$LEVEL"
|
|
if [[ ! -d "$LEVEL_SNAPSHOT" ]]; then
|
|
echo "Snapshotting upgraded app state to level-$LEVEL..."
|
|
mkdir -p "$LEVEL_SNAPSHOT"
|
|
for item in "$APP_DIR"/*; do
|
|
base=$(basename "$item")
|
|
case "$base" in
|
|
level-*|node_modules|dist|.vite|drizzle|dev-server.log) continue ;;
|
|
*) cp -r "$item" "$LEVEL_SNAPSHOT/" 2>/dev/null ;;
|
|
esac
|
|
done
|
|
echo " Saved to $LEVEL_SNAPSHOT"
|
|
fi
|
|
fi
|
|
|
|
# ─── Parse telemetry ─────────────────────────────────────────────────────────
|
|
|
|
echo ""
|
|
echo "=== $MODE_LABEL Complete ==="
|
|
echo " Started: $START_TIME"
|
|
echo " Ended: $END_TIME"
|
|
echo ""
|
|
|
|
# Resolve shared logs file path for telemetry parser
|
|
LOGS_FILE_NATIVE="$SHARED_TELEMETRY_DIR/logs.jsonl"
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
LOGS_FILE_NATIVE=$(cygpath -w "$SHARED_TELEMETRY_DIR/logs.jsonl")
|
|
fi
|
|
|
|
echo "Parsing telemetry..."
|
|
if node "$SCRIPT_DIR_NATIVE/parse-telemetry.mjs" "$RUN_DIR_NATIVE" "--logs-file=$LOGS_FILE_NATIVE" "--extract-raw"; then
|
|
echo ""
|
|
echo "=== Results ==="
|
|
echo " App: $APP_DIR_NATIVE"
|
|
echo " Cost: $RUN_DIR/COST_REPORT.md"
|
|
echo ""
|
|
if [[ -n "$FIX_MODE" ]]; then
|
|
echo "=== Next Step: Re-grade the app ==="
|
|
echo " In Claude Code, say:"
|
|
echo " Re-grade the app at $APP_DIR_NATIVE"
|
|
echo ""
|
|
elif [[ -n "$UPGRADE_MODE" ]]; then
|
|
echo "=== Next Step: Grade the upgraded app (level $LEVEL) ==="
|
|
echo " In Claude Code, say:"
|
|
echo " Grade the app at $APP_DIR_NATIVE at level $LEVEL"
|
|
echo ""
|
|
NEXT_LEVEL=$((LEVEL + 1))
|
|
NEXT_PROMPT="$SCRIPT_DIR/../llm-oneshot/apps/chat-app/prompts/composed/$(printf '%02d' "$NEXT_LEVEL")_"*".md"
|
|
if ls $NEXT_PROMPT &>/dev/null 2>&1; then
|
|
echo " To continue upgrading after grading:"
|
|
echo " ./run.sh --upgrade $APP_DIR --level $NEXT_LEVEL"
|
|
echo ""
|
|
fi
|
|
else
|
|
echo "=== Next Step: Grade the app ==="
|
|
echo " In Claude Code, say:"
|
|
echo " Grade the app at $APP_DIR_NATIVE"
|
|
echo ""
|
|
fi
|
|
else
|
|
echo "WARNING: Telemetry parsing failed. Raw logs at: $SHARED_TELEMETRY_DIR/logs.jsonl"
|
|
fi
|
|
|
|
# ─── Auto-grade with Playwright (if installed) ──────────────────────────────
|
|
|
|
PLAYWRIGHT_DIR="$SCRIPT_DIR/test-plans/playwright"
|
|
if [[ $EXIT_CODE -eq 0 && "$TEST_MODE" == "playwright" && -f "$PLAYWRIGHT_DIR/node_modules/.bin/playwright" ]]; then
|
|
echo ""
|
|
echo "=== Auto-grading with Playwright ==="
|
|
echo " App URL: http://localhost:$VITE_PORT"
|
|
|
|
# Wait for dev server to be ready
|
|
READY=0
|
|
for i in $(seq 1 30); do
|
|
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$VITE_PORT" 2>/dev/null | grep -q "200"; then
|
|
READY=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [[ $READY -eq 1 ]]; then
|
|
# Reset backend state for a clean test (fresh module or DB)
|
|
echo "Resetting backend state for clean test..."
|
|
"$SCRIPT_DIR/reset-app.sh" "$APP_DIR" || echo "WARNING: Backend reset failed — tests may use stale state"
|
|
|
|
# Wait for the app to reconnect after reset
|
|
sleep 3
|
|
|
|
# Determine which feature specs to run based on prompt level
|
|
# Level → max feature number mapping:
|
|
# 1=4, 2=5, 3=6, 4=7, 5=8, 6=9, 7=10, 8=11, 9=12, 10=13, 11=14, 12=15,
|
|
# 13=16, 14=17, 15=18, 16=19, 17=20, 18=21, 19=22
|
|
MAX_FEATURE=$((LEVEL + 3))
|
|
if [[ $MAX_FEATURE -gt 22 ]]; then MAX_FEATURE=22; fi
|
|
|
|
PW_SPEC_FILES=""
|
|
for feat_num in $(seq 1 $MAX_FEATURE); do
|
|
FEAT_PAD=$(printf '%02d' "$feat_num")
|
|
SPEC_FILE=$(ls "$PLAYWRIGHT_DIR/specs/feature-${FEAT_PAD}-"*.spec.ts 2>/dev/null | head -1)
|
|
if [[ -n "$SPEC_FILE" ]]; then
|
|
PW_SPEC_FILES="$PW_SPEC_FILES $SPEC_FILE"
|
|
fi
|
|
done
|
|
echo " Testing features 1-$MAX_FEATURE ($LEVEL prompt level)"
|
|
|
|
mkdir -p /tmp/pw-results-$RUN_INDEX
|
|
cd "$PLAYWRIGHT_DIR"
|
|
APP_URL="http://localhost:$VITE_PORT" npx playwright test $PW_SPEC_FILES --reporter=json \
|
|
1>/tmp/pw-results-$RUN_INDEX/results.json 2>/dev/null || true
|
|
cd "$APP_DIR"
|
|
|
|
RESULTS_SIZE=$(wc -c < /tmp/pw-results-$RUN_INDEX/results.json 2>/dev/null || echo "0")
|
|
if [[ "$RESULTS_SIZE" -gt 100 ]]; then
|
|
PW_RESULTS="/tmp/pw-results-$RUN_INDEX/results.json"
|
|
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
|
PW_RESULTS=$(cygpath -w "$PW_RESULTS")
|
|
fi
|
|
node "$SCRIPT_DIR_NATIVE/parse-playwright-results.mjs" "$PW_RESULTS" "$APP_DIR_NATIVE" "$BACKEND"
|
|
# Copy raw results into telemetry dir for archival
|
|
cp /tmp/pw-results-$RUN_INDEX/results.json "$RUN_DIR/playwright-results.json" 2>/dev/null || true
|
|
else
|
|
echo "WARNING: Playwright produced no results (app may not have loaded)"
|
|
fi
|
|
else
|
|
echo "WARNING: Dev server not responding on port $VITE_PORT — skipping Playwright grading"
|
|
fi
|
|
elif [[ $EXIT_CODE -eq 0 && "$TEST_MODE" == "agents" ]]; then
|
|
echo ""
|
|
echo "=== Auto-grading with Playwright Agents ==="
|
|
"$SCRIPT_DIR/grade-agents.sh" "$APP_DIR" 2>&1 || echo "WARNING: Agent grading failed"
|
|
elif [[ $EXIT_CODE -ne 0 ]]; then
|
|
echo "Skipping auto-grade — code generation failed (exit $EXIT_CODE)"
|
|
fi
|
|
|