From 73afe58cfe05647bb344cb5bb7251fc61afbd67f Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 5 Apr 2026 01:09:18 -0400 Subject: [PATCH] build: runner for opencode project-builder --- scripts/opencode-builder.sh | 146 ++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100755 scripts/opencode-builder.sh diff --git a/scripts/opencode-builder.sh b/scripts/opencode-builder.sh new file mode 100755 index 000000000..a8fa11dd0 --- /dev/null +++ b/scripts/opencode-builder.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# +# opencode-builder.sh +# +# Launches an OpenCode server, creates a session with the product-builder +# agent, and continuously sends "continue" messages in an infinite loop. +# +# Signal handling: +# Ctrl+C once — finishes the in-flight request, then stops the loop +# Ctrl+C twice — (within 2 seconds) kills the request and exits immediately +# + +set -uo pipefail + +# ── Configuration (override via environment) ────────────────────────────── +PORT="${OPENCODE_PORT:-4096}" +HOST="${OPENCODE_HOST:-127.0.0.1}" +BASE="http://${HOST}:${PORT}" +AGENT="product-builder" +PROMPT="Complete the current project's milestones up to and including v3.7.0 to a production ready state" +HEALTH_TIMEOUT=60 # seconds to wait for the server to become healthy + +# ── Internal state ──────────────────────────────────────────────────────── +SERVER_PID="" +CURL_PID="" +SESSION_ID="" +STOP_LOOP=false +LAST_SIGINT=0 + +# ── Helpers ─────────────────────────────────────────────────────────────── +die() { printf "ERROR: %s\n" "$*" >&2; exit 1; } +log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; } + +cleanup() { + if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then + log "Stopping opencode server (PID $SERVER_PID)..." + kill "$SERVER_PID" 2>/dev/null + wait "$SERVER_PID" 2>/dev/null || true + fi +} + +handle_sigint() { + local now + now=$(date +%s) + + # Second Ctrl+C within 2 seconds → force-quit + if $STOP_LOOP && (( now - LAST_SIGINT <= 2 )); then + printf "\nForce quit!\n" + [[ -n "$CURL_PID" ]] && kill "$CURL_PID" 2>/dev/null || true + cleanup + exit 130 + fi + + # First Ctrl+C → graceful stop after current request + STOP_LOOP=true + LAST_SIGINT=$now + printf "\nWill stop after the current request completes. Ctrl+C again within 2s to force-quit.\n" +} + +trap handle_sigint INT +trap cleanup EXIT + +# ── Preflight checks ───────────────────────────────────────────────────── +for cmd in curl jq opencode; do + command -v "$cmd" &>/dev/null || die "'$cmd' is required but not found in PATH" +done + +# ── Start the opencode server ───────────────────────────────────────────── +log "Starting opencode server on ${HOST}:${PORT}..." +opencode serve --port "$PORT" --hostname "$HOST" & +SERVER_PID=$! + +log "Waiting for server to become healthy (up to ${HEALTH_TIMEOUT}s)..." +healthy=false +for (( i = 1; i <= HEALTH_TIMEOUT; i++ )); do + if curl -sf "${BASE}/global/health" >/dev/null 2>&1; then + healthy=true + break + fi + kill -0 "$SERVER_PID" 2>/dev/null || die "Server process exited unexpectedly" + sleep 1 +done +$healthy || die "Server did not become healthy within ${HEALTH_TIMEOUT}s" +log "Server is ready." + +# ── Create a session ────────────────────────────────────────────────────── +log "Creating session..." +SESSION_ID=$( + curl -sf -X POST "${BASE}/session" \ + -H "Content-Type: application/json" \ + -d '{"title":"product-builder-auto"}' \ + | jq -r '.id' +) || die "Failed to create session" + +[[ -n "$SESSION_ID" && "$SESSION_ID" != "null" ]] || die "Session ID is empty or null" +log "Session ID: ${SESSION_ID}" + +# ── send_message ────────────────────────────────────────────────────────── +# Sends a JSON body to POST /session/:id/message and blocks until the LLM +# responds. curl is run in a subprocess that ignores SIGINT (SIG_IGN is +# inherited across exec), so a single Ctrl+C cannot kill the in-flight +# request. The parent re-waits whenever its own trap handler interrupts +# the `wait` builtin. +send_message() { + local body=$1 + + ( trap '' INT + exec curl -sf -X POST "${BASE}/session/${SESSION_ID}/message" \ + -H "Content-Type: application/json" \ + -d "$body" \ + -o /dev/null + ) & + CURL_PID=$! + + # Keep waiting until curl actually exits — `wait` can return early when + # our SIGINT handler fires, so we loop on kill -0 to re-wait. + while kill -0 "$CURL_PID" 2>/dev/null; do + wait "$CURL_PID" 2>/dev/null || true + done + CURL_PID="" +} + +# ── Send the initial prompt ────────────────────────────────────────────── +INIT_BODY=$(jq -nc --arg a "$AGENT" --arg t "$PROMPT" \ + '{agent: $a, parts: [{type: "text", text: $t}]}') + +log "Sending initial prompt (agent: ${AGENT})..." +log "Prompt: ${PROMPT}" +echo "" +send_message "$INIT_BODY" +log "Initial prompt complete." + +# ── Continue loop ───────────────────────────────────────────────────────── +CONT_BODY=$(jq -nc '{parts: [{type: "text", text: "continue"}]}') +N=0 + +while ! $STOP_LOOP; do + N=$((N + 1)) + log "── continue #${N} ──" + send_message "$CONT_BODY" + log "continue #${N} complete." +done + +echo "" +log "Loop stopped. Shutting down." +exit 0