#!/usr/bin/env bash set -euo pipefail # ============================================================ # XH-1 Research Harness # Autonomous Research / Review / Revision Pipeline # # Pure Bash # OpenAI-compatible APIs # Designed for OpenRouter # ============================================================ ROOT="${XH1_RESEARCH_ROOT:-research}" CONFIG="${XH1_RESEARCH_CONFIG:-xh1-research.conf}" STATE="${ROOT}/.xh1" mkdir -p \ "$STATE/logs" \ "$STATE/responses" \ "$STATE/runs" # ------------------------------------------------------------ # Defaults # ------------------------------------------------------------ API_BASE_URL="${API_BASE_URL:-https://openrouter.ai/api/v1}" API_KEY_ENV="${API_KEY_ENV:-OPENROUTER_API_KEY}" RESEARCH_MODEL="${RESEARCH_MODEL:-}" REVIEW_MODEL="${REVIEW_MODEL:-$RESEARCH_MODEL}" REVISION_MODEL="${REVISION_MODEL:-$RESEARCH_MODEL}" MAX_RESEARCH_ROUNDS="${MAX_RESEARCH_ROUNDS:-3}" MAX_API_RETRIES="${MAX_API_RETRIES:-3}" DELAY_SECONDS="${DELAY_SECONDS:-5}" MAX_ITERATIONS="${MAX_ITERATIONS:-0}" REVIEW_ENABLED="${REVIEW_ENABLED:-true}" AUTO_COMMIT="${AUTO_COMMIT:-false}" RUN_ID="" # ------------------------------------------------------------ # General # ------------------------------------------------------------ die() { echo "ERROR: $*" >&2 exit 1 } log() { local message="$*" echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $message" \ | tee -a "$STATE/logs/harness.log" } require() { command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" } require_dependencies() { require bash require curl require jq require find require grep require sed require awk } timestamp() { date -u '+%Y%m%dT%H%M%SZ' } load_config() { if [[ -f "$CONFIG" ]]; then # shellcheck disable=SC1090 source "$CONFIG" fi } get_api_key() { local key="${!API_KEY_ENV:-}" [[ -n "$key" ]] || die "Missing API key. Set ${API_KEY_ENV}." printf '%s' "$key" } # ------------------------------------------------------------ # Configuration # ------------------------------------------------------------ init_config() { if [[ -f "$CONFIG" ]]; then echo "Configuration already exists:" echo " $CONFIG" return fi cat > "$CONFIG" <<'CONFIG' # ============================================================ # XH-1 Research Harness Configuration # ============================================================ # OpenAI-compatible API endpoint. # OpenRouter: API_BASE_URL="https://openrouter.ai/api/v1" # Environment variable containing the API key. API_KEY_ENV="OPENROUTER_API_KEY" # ------------------------------------------------------------ # Models # ------------------------------------------------------------ # Model used for initial research. RESEARCH_MODEL="" # Model used for independent review. REVIEW_MODEL="" # Model used to revise rejected research. REVISION_MODEL="" # ------------------------------------------------------------ # Research behavior # ------------------------------------------------------------ # Maximum Research -> Review -> Revision cycles for ONE topic. MAX_RESEARCH_ROUNDS=3 # API retries per individual request. MAX_API_RETRIES=3 # Delay between topics. DELAY_SECONDS=5 # 0 = unlimited. MAX_ITERATIONS=0 # Enable independent reviewer. REVIEW_ENABLED=true # Automatically create git commits. AUTO_COMMIT=false CONFIG echo "Created $CONFIG" } # ------------------------------------------------------------ # Run state # ------------------------------------------------------------ start_run() { RUN_ID="$(timestamp)" mkdir -p "$STATE/runs/$RUN_ID" : > "$STATE/runs/$RUN_ID/completed" : > "$STATE/runs/$RUN_ID/failed" : > "$STATE/runs/$RUN_ID/attempts" log "Started run: $RUN_ID" } run_has_completed() { local file="$1" grep -Fxq "$file" \ "$STATE/runs/$RUN_ID/completed" 2>/dev/null } run_has_failed() { local file="$1" grep -Fxq "$file" \ "$STATE/runs/$RUN_ID/failed" 2>/dev/null } mark_completed() { local file="$1" printf '%s\n' "$file" \ >> "$STATE/runs/$RUN_ID/completed" } mark_failed() { local file="$1" printf '%s\n' "$file" \ >> "$STATE/runs/$RUN_ID/failed" } record_attempt() { local file="$1" local round="$2" local phase="$3" local result="$4" printf '%s\t%s\t%s\t%s\t%s\n' \ "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ "$file" \ "$round" \ "$phase" \ "$result" \ >> "$STATE/runs/$RUN_ID/attempts" } # ------------------------------------------------------------ # Research discovery # ------------------------------------------------------------ is_soon() { grep -qE '^SOON[[:space:]]*$' "$1" } find_next_task() { while IFS= read -r -d '' file; do [[ "$file" == "$STATE"/* ]] && continue if is_soon "$file"; then if ! run_has_completed "$file" && ! run_has_failed "$file" then printf '%s\n' "$file" return 0 fi fi done < <( find "$ROOT" \ -type f \ -name '*.md' \ -not -path "$STATE/*" \ -print0 ) return 1 } # ------------------------------------------------------------ # Context # ------------------------------------------------------------ build_context() { local file="$1" local relative="${file#"$ROOT"/}" local area="${relative%%/*}" cat <