Files
wehub-resource-sync aa1a5d5763
CI / Release hygiene (push) Has been cancelled
CI / Python targeted tests (push) Has been cancelled
CI / Frontend build (push) Has been cancelled
Pages / Build and deploy public pages (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:17 +08:00

1304 lines
39 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${GOD_ENV_FILE:-$ROOT_DIR/.env}"
# Internal monorepo layout. Hidden from the outward-facing config surface.
BACKEND_ROOT="${BACKEND_ROOT:-$ROOT_DIR/agentsociety}"
RUNTIME_ROOT="${RUNTIME_ROOT:-$ROOT_DIR/jiuwenclaw}"
GOD_EXPERIMENT_WAS_EXPLICIT="${GOD_EXPERIMENT+x}"
GOD_EXPERIMENT_RUN_WAS_EXPLICIT="${GOD_EXPERIMENT_RUN+x}"
GOD_HYPOTHESIS_ID_WAS_EXPLICIT="${GOD_HYPOTHESIS_ID+x}"
GOD_EXPERIMENT_ID_WAS_EXPLICIT="${GOD_EXPERIMENT_ID+x}"
LIVE_WORKSPACE_PATH_WAS_EXPLICIT="${LIVE_WORKSPACE_PATH+x}"
GOD_MAP_ID_WAS_EXPLICIT="${GOD_MAP_ID+x}"
GOD_EXPERIMENT_EXPLICIT_VALUE="${GOD_EXPERIMENT:-}"
GOD_EXPERIMENT_RUN_EXPLICIT_VALUE="${GOD_EXPERIMENT_RUN:-}"
GOD_HYPOTHESIS_ID_EXPLICIT_VALUE="${GOD_HYPOTHESIS_ID:-}"
GOD_EXPERIMENT_ID_EXPLICIT_VALUE="${GOD_EXPERIMENT_ID:-}"
LIVE_WORKSPACE_PATH_EXPLICIT_VALUE="${LIVE_WORKSPACE_PATH:-}"
GOD_MAP_ID_EXPLICIT_VALUE="${GOD_MAP_ID:-}"
LIVE_WORKSPACE_PATH="${LIVE_WORKSPACE_PATH:-$BACKEND_ROOT/quick_experiments}"
# State dirs.
STATE_DIR="$ROOT_DIR/.god"
LOG_DIR="$STATE_DIR/logs"
PID_DIR="$STATE_DIR/pids"
RUN_DIR="$STATE_DIR/run"
mkdir -p "$LOG_DIR" "$PID_DIR" "$RUN_DIR"
BACKEND_PID_FILE="$PID_DIR/backend.pid"
FRONTEND_PID_FILE="$PID_DIR/frontend.pid"
RUNTIME_PID_FILE="$PID_DIR/runtime.pid"
CURRENT_EXPERIMENT_FILE="$STATE_DIR/current_experiment.json"
START_REQUEST_FILE="$RUN_DIR/start-request.json"
RUNTIME_INSTANCE="${RUNTIME_INSTANCE:-god-town}"
RUNTIME_MODE="${RUNTIME_MODE:-dev}"
RUNTIME_LANGUAGE="${RUNTIME_LANGUAGE:-zh}"
RUNTIME_LEGACY_INSTANCES="${RUNTIME_LEGACY_INSTANCES:-jiuwenclaw-town jiuwenclaw-town-native-skill}"
RUNTIME_AGENT_PORT="${RUNTIME_AGENT_PORT:-19092}"
RUNTIME_WEB_PORT="${RUNTIME_WEB_PORT:-20000}"
RUNTIME_GATEWAY_PORT="${RUNTIME_GATEWAY_PORT:-20001}"
RUNTIME_UI_PORT="${RUNTIME_UI_PORT:-6173}"
GOD_EXTRA_STOP_PORTS="${GOD_EXTRA_STOP_PORTS:-20092 21000 21001 7173}"
# Default user-facing config (mirrors .env.example).
GOD_EXPERIMENT="${GOD_EXPERIMENT:-god_town}"
GOD_EXPERIMENT_RUN="${GOD_EXPERIMENT_RUN:-1}"
GOD_BACKEND_HOST="${GOD_BACKEND_HOST:-127.0.0.1}"
GOD_BACKEND_PORT="${GOD_BACKEND_PORT:-8001}"
GOD_FRONTEND_PORT="${GOD_FRONTEND_PORT:-5174}"
GOD_LIVE_STEP_TIMEOUT="${GOD_LIVE_STEP_TIMEOUT:-${AGENTSOCIETY_LIVE_STEP_TIMEOUT:-900}}"
backend_url=""
frontend_url=""
runtime_ui_url=""
log() {
printf '[GOD] %s\n' "$*"
}
die() {
printf '[GOD] error: %s\n' "$*" >&2
exit 1
}
usage() {
cat <<EOF
Usage: ./scripts/god.sh [menu|setup|configure|start|restart|new-run|stop|status|tail|open]
menu Interactive menu.
setup Install or check Python and Node dependencies only.
configure Open the experiment setup wizard and wait for a new experiment request.
start Start GOD (idempotent; reuses running services) and open frontend pages.
restart Stop everything cleanly, then start.
new-run Stop, wipe the current run, then start a fresh session.
stop Stop GOD and release its ports.
status Print URLs, ports, and model status.
tail Follow GOD service logs.
open Open the GOD frontend pages in the default browser.
EOF
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
shell_quote() {
printf '%q' "$1"
}
screen_list() {
screen -ls 2>/dev/null || true
}
require_base_tools() {
for tool in uv npm curl lsof python3; do
command_exists "$tool" || die "Required command not found: $tool"
done
}
refresh_derived() {
backend_url="http://$GOD_BACKEND_HOST:$GOD_BACKEND_PORT"
frontend_url="http://127.0.0.1:$GOD_FRONTEND_PORT"
runtime_ui_url="http://localhost:$RUNTIME_UI_PORT"
}
load_current_experiment() {
[[ -f "$CURRENT_EXPERIMENT_FILE" ]] || return 0
local exports
exports="$(
python3 - "$CURRENT_EXPERIMENT_FILE" <<'PY'
import json
import shlex
import sys
from pathlib import Path
path = Path(sys.argv[1])
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception:
raise SystemExit(0)
if not isinstance(data, dict):
raise SystemExit(0)
mapping = {
"GOD_EXPERIMENT": data.get("hypothesis_id"),
"GOD_EXPERIMENT_RUN": data.get("experiment_id"),
"LIVE_WORKSPACE_PATH": data.get("workspace_path"),
}
for key, value in mapping.items():
if value:
print(f"{key}={shlex.quote(str(value))}")
PY
)"
[[ -n "$exports" ]] || return 0
if [[ -n "$GOD_EXPERIMENT_WAS_EXPLICIT" || -n "$GOD_HYPOTHESIS_ID_WAS_EXPLICIT" ]]; then
exports="$(printf '%s\n' "$exports" | grep -v '^GOD_EXPERIMENT=' || true)"
fi
if [[ -n "$GOD_EXPERIMENT_RUN_WAS_EXPLICIT" || -n "$GOD_EXPERIMENT_ID_WAS_EXPLICIT" ]]; then
exports="$(printf '%s\n' "$exports" | grep -v '^GOD_EXPERIMENT_RUN=' || true)"
fi
if [[ -n "$LIVE_WORKSPACE_PATH_WAS_EXPLICIT" ]]; then
exports="$(printf '%s\n' "$exports" | grep -v '^LIVE_WORKSPACE_PATH=' || true)"
fi
[[ -n "$exports" ]] || return 0
eval "$exports"
export GOD_EXPERIMENT GOD_EXPERIMENT_RUN LIVE_WORKSPACE_PATH
}
load_env() {
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
if [[ -n "$GOD_EXPERIMENT_WAS_EXPLICIT" ]]; then
GOD_EXPERIMENT="$GOD_EXPERIMENT_EXPLICIT_VALUE"
elif [[ -n "$GOD_HYPOTHESIS_ID_WAS_EXPLICIT" ]]; then
GOD_EXPERIMENT="$GOD_HYPOTHESIS_ID_EXPLICIT_VALUE"
else
unset GOD_EXPERIMENT
unset GOD_HYPOTHESIS_ID
fi
if [[ -n "$GOD_EXPERIMENT_RUN_WAS_EXPLICIT" ]]; then
GOD_EXPERIMENT_RUN="$GOD_EXPERIMENT_RUN_EXPLICIT_VALUE"
elif [[ -n "$GOD_EXPERIMENT_ID_WAS_EXPLICIT" ]]; then
GOD_EXPERIMENT_RUN="$GOD_EXPERIMENT_ID_EXPLICIT_VALUE"
else
unset GOD_EXPERIMENT_RUN
unset GOD_EXPERIMENT_ID
fi
if [[ -n "$LIVE_WORKSPACE_PATH_WAS_EXPLICIT" ]]; then
LIVE_WORKSPACE_PATH="$LIVE_WORKSPACE_PATH_EXPLICIT_VALUE"
fi
if [[ -n "$GOD_MAP_ID_WAS_EXPLICIT" ]]; then
GOD_MAP_ID="$GOD_MAP_ID_EXPLICIT_VALUE"
else
unset GOD_MAP_ID
fi
export_internal_env
load_current_experiment
export_internal_env
refresh_derived
}
# Map outward-facing GOD_* config to the internal env names the backend and
# runtime read directly. Kept in one place so the rest of the script and the
# user only ever sees GOD_* names. Legacy variable names from previous
# installations are accepted silently and migrated forward.
export_internal_env() {
GOD_LLM_API_KEY="${GOD_LLM_API_KEY:-${AGENTSOCIETY_LLM_API_KEY:-${JIUWENCLAW_API_KEY:-}}}"
GOD_LLM_API_BASE="${GOD_LLM_API_BASE:-${AGENTSOCIETY_LLM_API_BASE:-${JIUWENCLAW_API_BASE:-}}}"
GOD_LLM_MODEL="${GOD_LLM_MODEL:-${AGENTSOCIETY_LLM_MODEL:-${JIUWENCLAW_MODEL:-}}}"
GOD_EMBEDDING_API_KEY="${GOD_EMBEDDING_API_KEY:-${AGENTSOCIETY_EMBEDDING_API_KEY:-${JIUWENCLAW_EMBED_API_KEY:-}}}"
GOD_EMBEDDING_API_BASE="${GOD_EMBEDDING_API_BASE:-${AGENTSOCIETY_EMBEDDING_API_BASE:-${JIUWENCLAW_EMBED_API_BASE:-}}}"
GOD_EMBEDDING_MODEL="${GOD_EMBEDDING_MODEL:-${AGENTSOCIETY_EMBEDDING_MODEL:-${JIUWENCLAW_EMBED_MODEL:-}}}"
GOD_BACKEND_PORT="${GOD_BACKEND_PORT:-${BACKEND_PORT:-8001}}"
GOD_FRONTEND_PORT="${GOD_FRONTEND_PORT:-${AGENTSOCIETY_FRONTEND_PORT:-5174}}"
GOD_LIVE_STEP_TIMEOUT="${GOD_LIVE_STEP_TIMEOUT:-${AGENTSOCIETY_LIVE_STEP_TIMEOUT:-900}}"
GOD_EXPERIMENT="${GOD_EXPERIMENT:-${GOD_HYPOTHESIS_ID:-god_town}}"
GOD_EXPERIMENT_RUN="${GOD_EXPERIMENT_RUN:-${GOD_EXPERIMENT_ID:-1}}"
local llm_key="${GOD_LLM_API_KEY:-}"
local llm_base="${GOD_LLM_API_BASE:-https://api.openai.com/v1}"
local llm_model="${GOD_LLM_MODEL:-}"
local nano_model="${GOD_LLM_NANO_MODEL:-$llm_model}"
local embed_key="${GOD_EMBEDDING_API_KEY:-$llm_key}"
local embed_base="${GOD_EMBEDDING_API_BASE:-$llm_base}"
local embed_model="${GOD_EMBEDDING_MODEL:-text-embedding-3-large}"
export AGENTSOCIETY_LLM_API_KEY="$llm_key"
export AGENTSOCIETY_LLM_API_BASE="$llm_base"
export AGENTSOCIETY_LLM_MODEL="$llm_model"
export AGENTSOCIETY_NANO_LLM_MODEL="$nano_model"
export AGENTSOCIETY_EMBEDDING_API_KEY="$embed_key"
export AGENTSOCIETY_EMBEDDING_API_BASE="$embed_base"
export AGENTSOCIETY_EMBEDDING_MODEL="$embed_model"
export JIUWENCLAW_API_KEY="$llm_key"
export JIUWENCLAW_API_BASE="$llm_base"
export JIUWENCLAW_MODEL="$llm_model"
export JIUWENCLAW_MODEL_PROVIDER="${JIUWENCLAW_MODEL_PROVIDER:-OpenAI}"
export JIUWENCLAW_EMBED_API_KEY="$embed_key"
export JIUWENCLAW_EMBED_API_BASE="$embed_base"
export JIUWENCLAW_EMBED_MODEL="$embed_model"
export BACKEND_HOST="$GOD_BACKEND_HOST"
export BACKEND_PORT="$GOD_BACKEND_PORT"
export AGENTSOCIETY_FRONTEND_PORT="$GOD_FRONTEND_PORT"
export GOD_HYPOTHESIS_ID="$GOD_EXPERIMENT"
export GOD_EXPERIMENT_ID="$GOD_EXPERIMENT_RUN"
}
set_env_value() {
local key="$1"
local value="$2"
python3 - "$ENV_FILE" "$key" "$value" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
key = sys.argv[2]
value = sys.argv[3]
lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
out = []
seen = False
for line in lines:
if line.startswith(f"{key}="):
out.append(f"{key}={value}")
seen = True
else:
out.append(line)
if not seen:
if out and out[-1] != "":
out.append("")
out.append(f"{key}={value}")
path.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
PY
export "$key=$value"
}
set_env_values_in_file() {
local file="$1"
shift
python3 - "$file" "$@" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1]).expanduser()
items = list(zip(sys.argv[2::2], sys.argv[3::2]))
path.parent.mkdir(parents=True, exist_ok=True)
lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
out = []
seen = set()
for line in lines:
replaced = False
for key, value in items:
if line.startswith(f"{key}="):
out.append(f"{key}={value}")
seen.add(key)
replaced = True
break
if not replaced:
out.append(line)
for key, value in items:
if key not in seen:
if out and out[-1] != "":
out.append("")
out.append(f"{key}={value}")
path.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
PY
}
configured_state() {
if [[ -n "${1:-}" ]]; then
printf 'configured'
else
printf 'missing'
fi
}
runtime_workspace() {
printf '%s/.jiuwenclaw-instances/%s\n' "$HOME" "$RUNTIME_INSTANCE"
}
ensure_env_file() {
local env_was_created=0
if [[ ! -f "$ENV_FILE" ]]; then
cp "$ROOT_DIR/.env.example" "$ENV_FILE"
env_was_created=1
log "Created .env from .env.example"
fi
load_env
if [[ "${GOD_SETUP_MODE:-0}" == "1" ]]; then
set_env_value "GOD_LLM_API_BASE" "${GOD_LLM_API_BASE:-https://api.openai.com/v1}"
set_env_value "GOD_EMBEDDING_MODEL" "${GOD_EMBEDDING_MODEL:-text-embedding-3-large}"
load_env
return 0
fi
if [[ -z "${GOD_LLM_API_KEY:-}" ]]; then
if [[ -t 0 ]]; then
printf '[GOD] LLM API key is required. Paste it now (input hidden): '
stty -echo
read -r api_key
stty echo
printf '\n'
[[ -n "$api_key" ]] || die "LLM API key is empty"
set_env_value "GOD_LLM_API_KEY" "$api_key"
else
die "GOD_LLM_API_KEY is empty. Fill $ENV_FILE first."
fi
fi
if [[ "$env_was_created" == "1" && -t 0 ]]; then
local default_api_base="${GOD_LLM_API_BASE:-https://api.openai.com/v1}"
local api_base_input
printf '[GOD] LLM API base URL [%s]: ' "$default_api_base"
read -r api_base_input
set_env_value "GOD_LLM_API_BASE" "${api_base_input:-$default_api_base}"
load_env
fi
if [[ -z "${GOD_LLM_MODEL:-}" ]]; then
if [[ -t 0 ]]; then
local model_input
printf '[GOD] LLM model is required (examples: qwen-plus, gpt-4o-mini): '
read -r model_input
[[ -n "$model_input" ]] || die "LLM model is empty"
set_env_value "GOD_LLM_MODEL" "$model_input"
else
die "GOD_LLM_MODEL is empty. Fill $ENV_FILE first."
fi
fi
set_env_value "GOD_LLM_API_BASE" "${GOD_LLM_API_BASE:-https://api.openai.com/v1}"
set_env_value "GOD_EMBEDDING_MODEL" "${GOD_EMBEDDING_MODEL:-text-embedding-3-large}"
load_env
}
is_port_open() {
local port="$1"
python3 - "$port" <<'PY'
import socket
import sys
port = int(sys.argv[1])
for host in ("127.0.0.1", "::1", "localhost"):
try:
with socket.create_connection((host, port), timeout=0.35):
raise SystemExit(0)
except OSError:
pass
raise SystemExit(1)
PY
}
wait_for_port() {
local port="$1"
local label="$2"
local timeout="${3:-90}"
local deadline=$((SECONDS + timeout))
while (( SECONDS < deadline )); do
if is_port_open "$port"; then
log "$label ready on port $port"
return 0
fi
sleep 1
done
print_wait_timeout_context "$label" "$port"
die "Timed out waiting for $label on port $port"
}
print_wait_timeout_context() {
local label="$1"
local port="$2"
printf '[GOD] timeout diagnostics for %s on port %s\n' "$label" "$port" >&2
lsof -nP -iTCP:"$port" -sTCP:LISTEN >&2 2>/dev/null || true
if [[ "$label" == "Agent runtime"* && -f "$LOG_DIR/runtime.log" ]]; then
printf '[GOD] recent runtime log:\n' >&2
tail -n 80 "$LOG_DIR/runtime.log" >&2 || true
fi
if [[ "$label" == "Backend" && -f "$LOG_DIR/backend.log" ]]; then
printf '[GOD] recent backend log:\n' >&2
tail -n 80 "$LOG_DIR/backend.log" >&2 || true
fi
}
urlencode() {
python3 - "$1" <<'PY'
import sys
from urllib.parse import quote
print(quote(sys.argv[1], safe=""))
PY
}
replay_url() {
printf '%s/pixel-replay/%s/%s\n' \
"$frontend_url" \
"$GOD_EXPERIMENT" \
"$GOD_EXPERIMENT_RUN"
}
session_url() {
printf '%s/api/v1/live-experiments/%s/%s/sessions?workspace_path=%s\n' \
"$backend_url" \
"$GOD_EXPERIMENT" \
"$GOD_EXPERIMENT_RUN" \
"$(urlencode "$LIVE_WORKSPACE_PATH")"
}
run_step_url() {
printf '%s/api/v1/live-experiments/%s/%s/run-step?workspace_path=%s\n' \
"$backend_url" \
"$GOD_EXPERIMENT" \
"$GOD_EXPERIMENT_RUN" \
"$(urlencode "$LIVE_WORKSPACE_PATH")"
}
stop_live_url() {
printf '%s/api/v1/live-experiments/%s/%s/stop?workspace_path=%s\n' \
"$backend_url" \
"$GOD_EXPERIMENT" \
"$GOD_EXPERIMENT_RUN" \
"$(urlencode "$LIVE_WORKSPACE_PATH")"
}
print_frontend_links() {
printf 'Control room: %s\n' "$(replay_url)"
printf 'Agent runtime UI: %s\n' "$runtime_ui_url"
}
open_browser_window() {
local url="$1"
[[ "${GOD_OPEN_BROWSER:-1}" == "1" ]] || return 0
if command_exists open; then
open "$url" >/dev/null 2>&1 || true
fi
}
open_frontend_pages() {
command_exists open || return 0
log "Opening frontend pages"
open_browser_window "$(replay_url)"
open_browser_window "$runtime_ui_url"
}
setup_url() {
printf '%s/setup\n' "$frontend_url"
}
open_setup_page() {
log "Opening setup wizard"
open_browser_window "$(setup_url)"
printf 'Setup wizard: %s\n' "$(setup_url)"
}
current_experiment_config_path() {
[[ -f "$CURRENT_EXPERIMENT_FILE" ]] || return 1
python3 - "$CURRENT_EXPERIMENT_FILE" "$LIVE_WORKSPACE_PATH" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
fallback_workspace = Path(sys.argv[2])
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception:
raise SystemExit(1)
if not isinstance(data, dict):
raise SystemExit(1)
hypothesis_id = str(data.get("hypothesis_id") or "").strip()
experiment_id = str(data.get("experiment_id") or "1").strip()
if not hypothesis_id or not experiment_id:
raise SystemExit(1)
workspace = Path(data.get("workspace_path") or fallback_workspace).expanduser()
print(workspace / f"hypothesis_{hypothesis_id}" / f"experiment_{experiment_id}" / "init" / "init_config.json")
PY
}
has_current_experiment_config() {
local config_path
config_path="$(current_experiment_config_path)" || return 1
[[ -f "$config_path" ]] || return 1
}
has_pending_start_request() {
[[ -f "$START_REQUEST_FILE" ]] || return 1
if [[ -f "$CURRENT_EXPERIMENT_FILE" && "$START_REQUEST_FILE" -ot "$CURRENT_EXPERIMENT_FILE" ]]; then
local stale_path="$START_REQUEST_FILE.stale.$(date +%Y%m%d_%H%M%S)"
log "Ignoring stale setup startup request: $START_REQUEST_FILE"
mv "$START_REQUEST_FILE" "$stale_path" 2>/dev/null || rm -f "$START_REQUEST_FILE"
return 1
fi
return 0
}
has_ready_start_config() {
load_env
[[ -n "${GOD_LLM_API_KEY:-}" ]] || return 1
has_current_experiment_config
}
require_current_experiment_config() {
if has_current_experiment_config; then
return 0
fi
die "No current experiment is configured. Run ./scripts/god.sh start and choose an experiment in setup."
}
stop_control_services() {
kill_pid_file "$FRONTEND_PID_FILE" "control room"
kill_pid_file "$BACKEND_PID_FILE" "backend"
kill_listeners_on_port "$GOD_FRONTEND_PORT"
kill_listeners_on_port "$GOD_BACKEND_PORT"
}
kill_pid_file() {
local file="$1"
local label="$2"
[[ -f "$file" ]] || return 0
local pid
pid="$(cat "$file" 2>/dev/null || true)"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
log "Stopping $label pid=$pid"
kill "$pid" 2>/dev/null || true
local deadline=$((SECONDS + 8))
while (( SECONDS < deadline )); do
kill -0 "$pid" 2>/dev/null || break
sleep 0.3
done
kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$file"
}
kill_listeners_on_port() {
local port="$1"
local pids
pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | tr '\n' ' ' || true)"
[[ -n "${pids// }" ]] || return 0
log "Clearing port $port"
# shellcheck disable=SC2086
kill $pids 2>/dev/null || true
sleep 0.6
pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | tr '\n' ' ' || true)"
if [[ -n "${pids// }" ]]; then
# shellcheck disable=SC2086
kill -9 $pids 2>/dev/null || true
fi
}
clear_live_ports() {
for port in \
"$GOD_BACKEND_PORT" \
"$GOD_FRONTEND_PORT" \
"$RUNTIME_AGENT_PORT" \
"$RUNTIME_WEB_PORT" \
"$RUNTIME_GATEWAY_PORT" \
"$RUNTIME_UI_PORT" \
$GOD_EXTRA_STOP_PORTS
do
kill_listeners_on_port "$port"
done
}
stop_known_screens() {
command_exists screen || return 0
for name in "$RUNTIME_INSTANCE" $RUNTIME_LEGACY_INSTANCES god-town god-backend god-frontend agentsociety-backend agentsociety-frontend; do
if screen_list | grep -q "[.]$name[[:space:]]"; then
log "Stopping leftover session: $name"
screen -S "$name" -X quit >/dev/null 2>&1 || true
fi
done
}
start_detached_service() {
local session_name="$1"
local pid_file="$2"
local command="$3"
if command_exists screen; then
if screen_list | grep -q "[.]$session_name[[:space:]]"; then
log "Replacing old session: $session_name"
screen -S "$session_name" -X quit >/dev/null 2>&1 || true
sleep 0.5
fi
screen -dmS "$session_name" bash -lc "$command"
sleep 0.5
local screen_pid
screen_pid="$(
screen_list \
| awk -v name="$session_name" '$1 ~ "[.]" name "$" { split($1, a, "."); print a[1]; exit }'
)"
[[ -n "$screen_pid" ]] || die "Failed to create session: $session_name"
printf '%s\n' "$screen_pid" > "$pid_file"
else
nohup bash -lc "$command" >/dev/null 2>&1 &
printf '%s\n' "$!" > "$pid_file"
fi
}
stop_runtime_instance() {
local name="$1"
[[ -d "$RUNTIME_ROOT" ]] || return 0
command_exists uv || return 0
(
cd "$RUNTIME_ROOT"
uv run jiuwenclaw-start --stop "$name" >/dev/null 2>&1 || true
)
}
stop_all() {
log "Stopping GOD services"
if is_port_open "$GOD_BACKEND_PORT"; then
curl -fsS -X POST "$(stop_live_url)" >/dev/null 2>&1 || true
fi
kill_pid_file "$FRONTEND_PID_FILE" "control room"
kill_pid_file "$BACKEND_PID_FILE" "backend"
kill_pid_file "$RUNTIME_PID_FILE" "agent runtime"
stop_known_screens
stop_runtime_instance "$RUNTIME_INSTANCE"
for name in $RUNTIME_LEGACY_INSTANCES; do
stop_runtime_instance "$name"
done
clear_live_ports
}
remove_runtime_registry_entries() {
[[ -d "$RUNTIME_ROOT" ]] || return 0
command_exists uv || return 0
(
cd "$RUNTIME_ROOT"
uv run python - "$RUNTIME_INSTANCE" $RUNTIME_LEGACY_INSTANCES <<'PY'
import sys
from pathlib import Path
from ruamel.yaml import YAML
names = set(sys.argv[1:])
path = Path.home() / ".jiuwenclaw" / "instances.yaml"
if not path.exists():
raise SystemExit(0)
yaml = YAML()
data = yaml.load(path.read_text(encoding="utf-8")) or {}
instances = data.get("instances")
if not isinstance(instances, dict):
raise SystemExit(0)
for name in names:
instances.pop(name, None)
if instances:
with path.open("w", encoding="utf-8") as f:
yaml.dump(data, f)
else:
path.unlink()
PY
) || log "Could not update runtime registry; continuing reset"
}
remove_path() {
local path="$1"
local label="$2"
if [[ -e "$path" || -L "$path" ]]; then
log "Removing $label"
rm -rf "$path"
fi
}
remove_experiment_runs() {
[[ -d "$LIVE_WORKSPACE_PATH" ]] || return 0
log "Removing experiment run directories"
find "$LIVE_WORKSPACE_PATH" -maxdepth 4 \( -name run -o -name 'run_*' \) -type d -prune -exec rm -rf {} +
}
factory_reset() {
require_base_tools
log "Factory reset"
stop_all
remove_experiment_runs
remove_runtime_registry_entries
for name in "$RUNTIME_INSTANCE" $RUNTIME_LEGACY_INSTANCES; do
remove_path "$HOME/.jiuwenclaw-instances/$name" "runtime instance state"
done
remove_path "$ENV_FILE" ".env"
remove_path "$STATE_DIR" "state and logs"
remove_path "$BACKEND_ROOT/.venv" "backend virtualenv"
remove_path "$RUNTIME_ROOT/.venv" "runtime virtualenv"
remove_path "$BACKEND_ROOT/frontend/node_modules" "frontend dependencies"
remove_path "$RUNTIME_ROOT/jiuwenclaw/channels/web/frontend/node_modules" "runtime UI dependencies"
find "$ROOT_DIR" -maxdepth 5 \( -name .pytest_cache -o -name .ruff_cache -o -name __pycache__ \) -type d -prune -exec rm -rf {} + 2>/dev/null || true
log "Factory reset complete."
}
setup_deps() {
require_base_tools
if [[ "${GOD_SKIP_SETUP:-0}" == "1" ]]; then
log "Skipping dependency setup (GOD_SKIP_SETUP=1)"
return 0
fi
if [[ ! -d "$BACKEND_ROOT/.venv" || "${GOD_FORCE_SETUP:-0}" == "1" ]]; then
log "Syncing backend Python dependencies"
(cd "$BACKEND_ROOT" && uv sync)
fi
if [[ ! -d "$RUNTIME_ROOT/.venv" || "${GOD_FORCE_SETUP:-0}" == "1" ]]; then
log "Syncing agent runtime Python dependencies"
(cd "$RUNTIME_ROOT" && uv sync)
fi
if [[ ! -d "$BACKEND_ROOT/frontend/node_modules" || "${GOD_FORCE_SETUP:-0}" == "1" ]]; then
log "Installing control-room dependencies"
npm install --no-audit --no-fund --loglevel=error --prefix "$BACKEND_ROOT/frontend"
fi
local runtime_frontend="$RUNTIME_ROOT/jiuwenclaw/channels/web/frontend"
if [[ ! -d "$runtime_frontend/node_modules" || "${GOD_FORCE_SETUP:-0}" == "1" ]]; then
log "Installing runtime UI dependencies"
npm install --no-audit --no-fund --loglevel=error --prefix "$runtime_frontend"
fi
}
ensure_runtime_instance() {
setup_deps
local existing=0
(
cd "$RUNTIME_ROOT"
uv run jiuwenclaw-start --status "$RUNTIME_INSTANCE" >/dev/null 2>&1
) || existing=1
if [[ "$existing" != "0" ]]; then
log "Initializing agent runtime instance (default language: $RUNTIME_LANGUAGE)"
local workspace="$HOME/.jiuwenclaw-instances/$RUNTIME_INSTANCE"
: > "$LOG_DIR/runtime-init.log"
if [[ -d "$workspace" ]]; then
(
cd "$RUNTIME_ROOT"
printf 'yes\n%s\n' "$RUNTIME_LANGUAGE" | uv run jiuwenclaw-init --name "$RUNTIME_INSTANCE" >> "$LOG_DIR/runtime-init.log" 2>&1
) || {
tail -n 80 "$LOG_DIR/runtime-init.log" >&2 || true
die "Failed to initialize agent runtime instance"
}
else
(
cd "$RUNTIME_ROOT"
printf '%s\n' "$RUNTIME_LANGUAGE" | uv run jiuwenclaw-init --name "$RUNTIME_INSTANCE" >> "$LOG_DIR/runtime-init.log" 2>&1
) || {
tail -n 80 "$LOG_DIR/runtime-init.log" >&2 || true
die "Failed to initialize agent runtime instance"
}
fi
fi
(
cd "$RUNTIME_ROOT"
RUNTIME_INSTANCE="$RUNTIME_INSTANCE" \
RUNTIME_AGENT_PORT="$RUNTIME_AGENT_PORT" \
RUNTIME_WEB_PORT="$RUNTIME_WEB_PORT" \
RUNTIME_GATEWAY_PORT="$RUNTIME_GATEWAY_PORT" \
RUNTIME_UI_PORT="$RUNTIME_UI_PORT" \
uv run python - <<'PY'
import os
from pathlib import Path
from ruamel.yaml import YAML
name = os.environ["RUNTIME_INSTANCE"]
path = Path.home() / ".jiuwenclaw" / "instances.yaml"
path.parent.mkdir(parents=True, exist_ok=True)
yaml = YAML()
data = yaml.load(path.read_text(encoding="utf-8")) if path.exists() else None
if not isinstance(data, dict):
data = {"instances": {}}
instances = data.setdefault("instances", {})
entry = instances.setdefault(name, {})
workspace = entry.get("workspace") or str(Path.home() / ".jiuwenclaw-instances" / name)
entry["workspace"] = workspace
entry["ports"] = {
"agent_server": int(os.environ["RUNTIME_AGENT_PORT"]),
"web": int(os.environ["RUNTIME_WEB_PORT"]),
"gateway": int(os.environ["RUNTIME_GATEWAY_PORT"]),
"frontend": int(os.environ["RUNTIME_UI_PORT"]),
}
with path.open("w", encoding="utf-8") as f:
yaml.dump(data, f)
workspace_path = Path(workspace).expanduser()
workspace_path.mkdir(parents=True, exist_ok=True)
bootstrap_env = workspace_path / ".env"
bootstrap_env.write_text(
"\n".join(
[
f"# Bootstrap .env for runtime instance: {name}",
f"JIUWENCLAW_DATA_DIR={workspace_path}",
f"JIUWENCLAW_INSTANCE={name}",
f"AGENT_SERVER_PORT={os.environ['RUNTIME_AGENT_PORT']}",
f"WEB_PORT={os.environ['RUNTIME_WEB_PORT']}",
f"GATEWAY_PORT={os.environ['RUNTIME_GATEWAY_PORT']}",
f"FRONTEND_PORT={os.environ['RUNTIME_UI_PORT']}",
"",
]
),
encoding="utf-8",
)
PY
)
sync_runtime_model_env
}
sync_runtime_model_env() {
local workspace
workspace="$(runtime_workspace)"
local config_env="$workspace/config/.env"
[[ -d "$workspace" ]] || return 0
local api_key="${GOD_LLM_API_KEY:-}"
local api_base="${GOD_LLM_API_BASE:-}"
local model="${GOD_LLM_MODEL:-}"
local provider="${JIUWENCLAW_MODEL_PROVIDER:-OpenAI}"
local embed_key="${GOD_EMBEDDING_API_KEY:-$api_key}"
local embed_base="${GOD_EMBEDDING_API_BASE:-$api_base}"
local embed_model="${GOD_EMBEDDING_MODEL:-}"
[[ -n "$api_key" ]] || return 0
[[ -n "$api_base" ]] || return 0
[[ -n "$model" ]] || return 0
log "Syncing runtime model config"
set_env_values_in_file "$config_env" \
API_KEY "$api_key" \
API_BASE "$api_base" \
MODEL_NAME "$model" \
MODEL_PROVIDER "$provider" \
EMBED_API_KEY "$embed_key" \
EMBED_API_BASE "$embed_base" \
EMBED_MODEL "$embed_model"
}
prepare_experiment() {
local experiment_path="$LIVE_WORKSPACE_PATH/hypothesis_${GOD_EXPERIMENT}/experiment_${GOD_EXPERIMENT_RUN}"
local config_path="$experiment_path/init/init_config.json"
[[ -f "$config_path" ]] || die "Experiment config not found: $config_path"
local session_prefix="${GOD_SESSION_PREFIX:-${GOD_EXPERIMENT}_run_${GOD_EXPERIMENT_RUN}}"
log "Preparing experiment: $GOD_EXPERIMENT (run $GOD_EXPERIMENT_RUN)"
BACKEND_ROOT="$BACKEND_ROOT" \
RUNTIME_AGENT_PORT="$RUNTIME_AGENT_PORT" \
GOD_SESSION_PREFIX="$session_prefix" \
GOD_MAP_ID_WAS_EXPLICIT="$GOD_MAP_ID_WAS_EXPLICIT" \
GOD_MAP_ID="${GOD_MAP_ID:-}" \
python3 - "$config_path" <<'PY'
import json
import os
import sys
from pathlib import Path
path = Path(sys.argv[1])
config = json.loads(path.read_text(encoding="utf-8"))
agent_root = str(Path(os.environ["BACKEND_ROOT"]).resolve())
ws_url = f"ws://127.0.0.1:{os.environ['RUNTIME_AGENT_PORT']}"
session_prefix = os.environ["GOD_SESSION_PREFIX"]
requested_map_id = (
os.environ.get("GOD_MAP_ID", "").strip()
if os.environ.get("GOD_MAP_ID_WAS_EXPLICIT")
else ""
)
for module in config.get("env_modules", []):
if module.get("module_type") == "PixelTownSocialEnv":
kwargs = module.setdefault("kwargs", {})
if requested_map_id:
kwargs["map_id"] = requested_map_id
kwargs["map_manifest_path"] = f"custom/maps/{requested_map_id}/map.yaml"
else:
map_id = str(kwargs.get("map_id") or "").strip()
manifest_path = str(kwargs.get("map_manifest_path") or "").strip()
if not manifest_path:
if not map_id:
map_id = "the_ville"
kwargs["map_id"] = map_id
kwargs["map_manifest_path"] = f"custom/maps/{map_id}/map.yaml"
for agent in config.get("agents", []):
kwargs = agent.setdefault("kwargs", {})
agent_id = int(agent.get("agent_id") or kwargs.get("id") or 0)
kwargs["jiuwenclaw_ws_url"] = ws_url
kwargs["session_id"] = f"{session_prefix}_agent_{agent_id}"
kwargs["trusted_dirs"] = [agent_root]
path.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
PY
}
start_runtime() {
ensure_runtime_instance
if is_port_open "$RUNTIME_AGENT_PORT" \
&& is_port_open "$RUNTIME_WEB_PORT" \
&& is_port_open "$RUNTIME_GATEWAY_PORT" \
&& is_port_open "$RUNTIME_UI_PORT"; then
log "Agent runtime already up"
return 0
fi
log "Starting agent runtime"
: > "$LOG_DIR/runtime.log"
local runtime_cmd
local runtime_data_dir
runtime_data_dir="$(runtime_workspace)"
runtime_cmd="cd $(shell_quote "$RUNTIME_ROOT")"
runtime_cmd+=" && export JIUWENCLAW_DATA_DIR=$(shell_quote "$runtime_data_dir")"
runtime_cmd+=" && export JIUWENCLAW_ROOT=$(shell_quote "$RUNTIME_ROOT")"
runtime_cmd+=" && export JIUWENCLAW_PROJECT_ROOT=$(shell_quote "$RUNTIME_ROOT")"
runtime_cmd+=" && export AGENT_SERVER_PORT=$(shell_quote "$RUNTIME_AGENT_PORT")"
runtime_cmd+=" && export WEB_PORT=$(shell_quote "$RUNTIME_WEB_PORT")"
runtime_cmd+=" && export GATEWAY_PORT=$(shell_quote "$RUNTIME_GATEWAY_PORT")"
runtime_cmd+=" && export FRONTEND_PORT=$(shell_quote "$RUNTIME_UI_PORT")"
runtime_cmd+=" && exec uv run jiuwenclaw-start $(shell_quote "$RUNTIME_MODE") --name $(shell_quote "$RUNTIME_INSTANCE") >> $(shell_quote "$LOG_DIR/runtime.log") 2>&1"
start_detached_service "$RUNTIME_INSTANCE" "$RUNTIME_PID_FILE" "$runtime_cmd"
wait_for_port "$RUNTIME_AGENT_PORT" "Agent runtime" 180
wait_for_port "$RUNTIME_WEB_PORT" "Agent runtime web" 120
wait_for_port "$RUNTIME_GATEWAY_PORT" "Agent runtime gateway" 120
wait_for_port "$RUNTIME_UI_PORT" "Agent runtime UI" 120
}
start_backend() {
ensure_env_file
if is_port_open "$GOD_BACKEND_PORT" && curl -fsS "$backend_url/health" >/dev/null 2>&1; then
log "Backend already up"
return 0
fi
log "Starting backend"
: > "$LOG_DIR/backend.log"
local backend_log_level="${BACKEND_LOG_LEVEL:-info}"
local backend_cmd
backend_cmd="cd $(shell_quote "$BACKEND_ROOT")"
backend_cmd+=" && set -a && source $(shell_quote "$ENV_FILE") && set +a"
backend_cmd+=" && export GOD_ROOT=$(shell_quote "$ROOT_DIR")"
backend_cmd+=" && export GOD_ENV_FILE=$(shell_quote "$ENV_FILE")"
backend_cmd+=" && export LIVE_WORKSPACE_PATH=$(shell_quote "$LIVE_WORKSPACE_PATH")"
backend_cmd+=" && export AGENTSOCIETY_LLM_API_KEY=\"\${GOD_LLM_API_KEY:-}\""
backend_cmd+=" && export AGENTSOCIETY_LLM_API_BASE=\"\${GOD_LLM_API_BASE:-https://api.openai.com/v1}\""
backend_cmd+=" && export AGENTSOCIETY_LLM_MODEL=\"\${GOD_LLM_MODEL:-}\""
backend_cmd+=" && export AGENTSOCIETY_NANO_LLM_MODEL=\"\${GOD_LLM_NANO_MODEL:-\${GOD_LLM_MODEL:-}}\""
backend_cmd+=" && export AGENTSOCIETY_EMBEDDING_API_KEY=\"\${GOD_EMBEDDING_API_KEY:-\$AGENTSOCIETY_LLM_API_KEY}\""
backend_cmd+=" && export AGENTSOCIETY_EMBEDDING_API_BASE=\"\${GOD_EMBEDDING_API_BASE:-\$AGENTSOCIETY_LLM_API_BASE}\""
backend_cmd+=" && export AGENTSOCIETY_EMBEDDING_MODEL=\"\${GOD_EMBEDDING_MODEL:-text-embedding-3-large}\""
backend_cmd+=" && export BACKEND_HOST=$(shell_quote "$GOD_BACKEND_HOST")"
backend_cmd+=" && export BACKEND_PORT=$(shell_quote "$GOD_BACKEND_PORT")"
backend_cmd+=" && export AGENTSOCIETY_LIVE_STEP_TIMEOUT=$(shell_quote "$GOD_LIVE_STEP_TIMEOUT")"
backend_cmd+=" && export BACKEND_LOG_LEVEL=$(shell_quote "$backend_log_level")"
backend_cmd+=" && exec uv run python -m agentsociety2.backend.run --log-level $(shell_quote "$backend_log_level") >> $(shell_quote "$LOG_DIR/backend.log") 2>&1"
start_detached_service "god-backend" "$BACKEND_PID_FILE" "$backend_cmd"
wait_for_port "$GOD_BACKEND_PORT" "Backend" 120
local deadline=$((SECONDS + 30))
while (( SECONDS < deadline )); do
curl -fsS "$backend_url/health" >/dev/null 2>&1 && return 0
sleep 1
done
die "Backend port is open but /health did not respond"
}
start_frontend() {
if is_port_open "$GOD_FRONTEND_PORT"; then
log "Control room already up"
return 0
fi
log "Starting control room"
: > "$LOG_DIR/frontend.log"
local frontend_cmd
frontend_cmd="cd $(shell_quote "$BACKEND_ROOT/frontend")"
frontend_cmd+=" && export VITE_REPLAY_WORKSPACE_PATH=$(shell_quote "$LIVE_WORKSPACE_PATH")"
frontend_cmd+=" && export VITE_DEFAULT_REPLAY_HYPOTHESIS_ID=$(shell_quote "$GOD_EXPERIMENT")"
frontend_cmd+=" && export VITE_DEFAULT_REPLAY_EXPERIMENT_ID=$(shell_quote "$GOD_EXPERIMENT_RUN")"
frontend_cmd+=" && exec npm run dev -- --host 127.0.0.1 --port $(shell_quote "$GOD_FRONTEND_PORT") >> $(shell_quote "$LOG_DIR/frontend.log") 2>&1"
start_detached_service "god-frontend" "$FRONTEND_PID_FILE" "$frontend_cmd"
wait_for_port "$GOD_FRONTEND_PORT" "Control room" 120
}
start_setup_services() {
setup_deps
export GOD_SETUP_MODE=1
ensure_env_file
rm -f "$START_REQUEST_FILE"
stop_all
start_backend
start_frontend
open_setup_page
}
consume_start_request() {
local parsed hypothesis_id experiment_id workspace_path
parsed="$(
python3 - "$START_REQUEST_FILE" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
data = json.loads(path.read_text(encoding="utf-8"))
print(
"\t".join(
[
str(data.get("hypothesis_id") or ""),
str(data.get("experiment_id") or "1"),
str(data.get("workspace_path") or ""),
]
)
)
PY
)" || die "Could not read setup start request"
IFS=$'\t' read -r hypothesis_id experiment_id workspace_path <<< "$parsed"
[[ -n "$hypothesis_id" ]] || die "Setup start request is missing hypothesis_id"
mv "$START_REQUEST_FILE" "$START_REQUEST_FILE.consumed" 2>/dev/null || rm -f "$START_REQUEST_FILE"
export GOD_EXPERIMENT="$hypothesis_id"
export GOD_EXPERIMENT_RUN="${experiment_id:-1}"
if [[ -n "$workspace_path" ]]; then
export LIVE_WORKSPACE_PATH="$workspace_path"
fi
load_env
log "Received start request: $GOD_EXPERIMENT / experiment_$GOD_EXPERIMENT_RUN"
}
wait_for_start_request() {
log "Waiting for setup wizard to save and request startup"
local next_notice=$((SECONDS + 30))
while [[ ! -f "$START_REQUEST_FILE" ]]; do
sleep 2
if (( SECONDS >= next_notice )); then
log "Still waiting on $(setup_url)"
next_notice=$((SECONDS + 30))
fi
done
consume_start_request
}
start_with_setup_if_needed() {
require_base_tools
load_env
if has_pending_start_request; then
log "Applying pending setup startup request"
consume_start_request
unset GOD_SETUP_MODE
stop_all
start_all
return 0
fi
if has_ready_start_config; then
start_all
return 0
fi
log "No complete experiment configuration found; starting setup wizard first"
start_setup_services
wait_for_start_request
unset GOD_SETUP_MODE
stop_control_services
start_all
}
configure_experiment() {
require_base_tools
log "Starting new experiment setup wizard"
start_setup_services
wait_for_start_request
unset GOD_SETUP_MODE
stop_control_services
start_all
}
create_session() {
if ! is_port_open "$GOD_BACKEND_PORT" || ! curl -fsS "$backend_url/health" >/dev/null 2>&1; then
start_backend
fi
log "Creating live session"
local status_json
status_json="$(curl -fsS -X POST "$(session_url)" \
-H 'content-type: application/json' \
-d '{}')" || die "Failed to create live session"
local step_count
step_count="$(python3 -c 'import json, sys; print(json.load(sys.stdin).get("step_count", 0))' <<< "$status_json")"
if [[ "${GOD_PRIME_FIRST_STEP:-1}" != "0" && "$step_count" == "0" ]]; then
log "Priming live session (step 1)"
curl -fsS -X POST "$(run_step_url)" \
-H 'content-type: application/json' \
-d '{}' >/dev/null || die "Failed to run the first live step"
fi
}
start_all() {
load_env
require_current_experiment_config
setup_deps
ensure_env_file
prepare_experiment
start_runtime
start_backend
start_frontend
create_session
print_status
}
restart_all() {
require_base_tools
stop_all
start_with_setup_if_needed
}
new_run() {
require_base_tools
load_env
require_current_experiment_config
export GOD_SESSION_PREFIX="${GOD_EXPERIMENT}_fresh_$(date +%Y%m%d_%H%M%S)"
stop_all
local run_dir="$LIVE_WORKSPACE_PATH/hypothesis_${GOD_EXPERIMENT}/experiment_${GOD_EXPERIMENT_RUN}/run"
log "New run will clear: $run_dir"
rm -rf "$run_dir"
log "Cleared previous run"
start_all
}
print_port_status() {
local label="$1"
local port="$2"
if is_port_open "$port"; then
printf '%-24s %s up\n' "$label" "$port"
else
printf '%-24s %s down\n' "$label" "$port"
fi
}
print_status() {
load_env
printf '\nGOD status\n'
printf '%s\n' '----------'
print_port_status "Backend" "$GOD_BACKEND_PORT"
print_port_status "Control room" "$GOD_FRONTEND_PORT"
print_port_status "Agent runtime" "$RUNTIME_AGENT_PORT"
print_port_status "Agent runtime web" "$RUNTIME_WEB_PORT"
print_port_status "Agent runtime gateway" "$RUNTIME_GATEWAY_PORT"
print_port_status "Agent runtime UI" "$RUNTIME_UI_PORT"
printf '\nURLs\n'
print_frontend_links
printf 'Backend: %s/health\n' "$backend_url"
printf '\nCurrent experiment\n'
if has_current_experiment_config; then
printf 'Experiment: %s / experiment_%s\n' "$GOD_EXPERIMENT" "$GOD_EXPERIMENT_RUN"
printf 'Workspace: %s\n' "$LIVE_WORKSPACE_PATH"
else
printf 'Experiment: <not configured>\n'
fi
printf '\nModel\n'
printf 'API key: %s\n' "$(configured_state "${GOD_LLM_API_KEY:-}")"
printf 'API base: %s\n' "${GOD_LLM_API_BASE:-<unset>}"
printf 'Model: %s\n' "${GOD_LLM_MODEL:-<unset>}"
}
tail_logs() {
touch "$LOG_DIR/runtime.log" "$LOG_DIR/backend.log" "$LOG_DIR/frontend.log"
tail -f "$LOG_DIR/runtime.log" "$LOG_DIR/backend.log" "$LOG_DIR/frontend.log"
}
open_replay() {
load_env
if ! has_current_experiment_config; then
open_setup_page
return 0
fi
if command_exists open; then
open_frontend_pages
else
print_status
fi
}
interactive_menu() {
cat <<'EOF'
GOD - Govern, Observe, Direct
1. Start
2. Restart
3. New run (reset replay and start fresh)
4. Configure new experiment
5. Status
6. Stop
7. Tail logs
8. Setup dependencies only
EOF
printf 'Choose: '
read -r choice
case "$choice" in
1|"") start_with_setup_if_needed ;;
2) restart_all ;;
3) new_run ;;
4) configure_experiment; open_frontend_pages ;;
5) print_status ;;
6) stop_all ;;
7) tail_logs ;;
8) setup_deps; GOD_SETUP_MODE=1 ensure_env_file ;;
*) usage; exit 2 ;;
esac
}
ACTION="${1:-menu}"
load_env
case "$ACTION" in
menu)
interactive_menu
;;
setup)
setup_deps
GOD_SETUP_MODE=1 ensure_env_file
;;
configure)
configure_experiment
open_frontend_pages
;;
start)
start_with_setup_if_needed
open_frontend_pages
;;
restart)
restart_all
open_frontend_pages
;;
new-run)
new_run
open_frontend_pages
;;
factory-reset|reset)
factory_reset
;;
session)
require_base_tools
prepare_experiment
create_session
;;
stop)
require_base_tools
stop_all
;;
status)
require_base_tools
print_status
;;
tail)
tail_logs
;;
open)
open_replay
;;
-h|--help|help)
usage
;;
*)
usage
exit 2
;;
esac