fe30961990
CI / docker (push) Blocked by required conditions
CI / helm (push) Waiting to run
CI / push-validation (push) Waiting to run
CI / status-check (push) Blocked by required conditions
CI / unit_tests (push) Has started running
CI / benchmark-publish (push) Failing after 39s
CI / lint (push) Successful in 1m6s
CI / typecheck (push) Successful in 1m17s
CI / quality (push) Successful in 1m45s
CI / security (push) Successful in 2m27s
CI / coverage (push) Waiting to run
CI / build (push) Successful in 36s
CI / integration_tests (push) Failing after 7m7s
CI / e2e_tests (push) Successful in 8m2s
173 lines
6.8 KiB
Bash
Executable File
173 lines
6.8 KiB
Bash
Executable File
#!/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="auto-agents"
|
|
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
|
|
OWN_SERVER=false
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
die() { printf "ERROR: %s\n" "$*" >&2; exit 1; }
|
|
log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; }
|
|
|
|
check_opencode_running() {
|
|
# Check if port is open and responding as OpenCode server
|
|
if curl -sf "${BASE}/global/health" >/dev/null 2>&1; then
|
|
return 0 # OpenCode server is running
|
|
else
|
|
return 1 # No OpenCode server detected
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
if $OWN_SERVER && [[ -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
|
|
elif ! $OWN_SERVER; then
|
|
log "Leaving existing OpenCode server running (not managed by this script)"
|
|
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 ─────────────────────────────────────────────
|
|
# Check if OpenCode server is already running
|
|
if check_opencode_running; then
|
|
echo "┌─────────────────────────────────────────────────────────────────┐"
|
|
echo "│ ⚠️ WARNING: OpenCode server already running on ${HOST}:${PORT}"
|
|
echo "│ 🔗 Connecting to existing server instead of starting new one"
|
|
echo "│ 💡 The script will NOT manage this server's lifecycle"
|
|
echo "└─────────────────────────────────────────────────────────────────┘"
|
|
echo ""
|
|
log "Using existing OpenCode server at ${HOST}:${PORT}"
|
|
OWN_SERVER=false
|
|
else
|
|
# Start our own server as before
|
|
log "Starting opencode server on ${HOST}:${PORT}..."
|
|
opencode serve --port "$PORT" --hostname "$HOST" &
|
|
SERVER_PID=$!
|
|
OWN_SERVER=true
|
|
|
|
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."
|
|
fi
|
|
|
|
# ── 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 --arg a "$AGENT" '{agent: $a, parts: [{type: "text", text: "continue with your mainloop, monitor the supervisors, keep them alive, and sleep in an infinite loop."}]}')
|
|
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
|