#!/usr/bin/env bash set -uo pipefail # ============================================================ # XH-1 Research Harness v3 # # Pure Bash # OpenAI-compatible providers # OpenRouter + Ollama supported # # Pipeline: # # Research → Review # │ # ┌─────┴─────┐ # PASS FAIL # │ │ # accept Revision # │ # ▼ # Review # # API failures are NOT research failures. # # ============================================================ ROOT="${XH1_RESEARCH_ROOT:-research}" CONFIG="${XH1_RESEARCH_CONFIG:-xh1-research.conf}" STATE="${ROOT}/.xh1" mkdir -p \ "$STATE/logs" \ "$STATE/responses" \ "$STATE/runs" RUN_ID="" # ------------------------------------------------------------ # Defaults # ------------------------------------------------------------ API_BASE_URL="${API_BASE_URL:-https://openrouter.ai/api/v1}" API_KEY_ENV="${API_KEY_ENV:-OPENROUTER_API_KEY}" RESEARCH_PROVIDER="${RESEARCH_PROVIDER:-openrouter}" RESEARCH_MODEL="${RESEARCH_MODEL:-}" REVIEW_PROVIDER="${REVIEW_PROVIDER:-ollama}" REVIEW_MODEL="${REVIEW_MODEL:-gemma4:12b}" REVISION_PROVIDER="${REVISION_PROVIDER:-openrouter}" REVISION_MODEL="${REVISION_MODEL:-}" MAX_RESEARCH_ROUNDS="${MAX_RESEARCH_ROUNDS:-3}" MAX_API_RETRIES="${MAX_API_RETRIES:-4}" DELAY_SECONDS="${DELAY_SECONDS:-5}" MAX_ITERATIONS="${MAX_ITERATIONS:-0}" REVIEW_ENABLED="${REVIEW_ENABLED:-true}" AUTO_COMMIT="${AUTO_COMMIT:-false}" OLLAMA_CONTEXT="${OLLAMA_CONTEXT:-16384}" # ------------------------------------------------------------ # Provider defaults # ------------------------------------------------------------ OPENROUTER_URL="${OPENROUTER_URL:-https://openrouter.ai/api/v1}" OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11434/v1}" OPENROUTER_KEY_ENV="${OPENROUTER_KEY_ENV:-OPENROUTER_API_KEY}" # ------------------------------------------------------------ # Logging # ------------------------------------------------------------ timestamp() { date -u '+%Y%m%dT%H%M%SZ' } now() { date -u '+%Y-%m-%dT%H:%M:%SZ' } log() { local message="$*" echo "[$(now)] $message" | tee -a "$STATE/logs/harness.log" } warn() { log "WARNING: $*" } die() { echo "ERROR: $*" >&2 exit 1 } # ------------------------------------------------------------ # Dependencies # ------------------------------------------------------------ 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 require date } # ------------------------------------------------------------ # Configuration # ------------------------------------------------------------ load_config() { if [[ -f "$CONFIG" ]]; then # shellcheck disable=SC1090 source "$CONFIG" fi } init_config() { if [[ -f "$CONFIG" ]]; then echo "Configuration already exists:" echo " $CONFIG" return 0 fi cat > "$CONFIG" <<'CONFIG' # ============================================================ # XH-1 Research Harness Configuration # ============================================================ # ------------------------------------------------------------ # Researcher # ------------------------------------------------------------ RESEARCH_PROVIDER="openrouter" RESEARCH_MODEL="minimax/minimax-m3:free" # ------------------------------------------------------------ # Reviewer # # Recommended for 16GB Apple Silicon: # # Ollama + Gemma 4 12B # ------------------------------------------------------------ REVIEW_PROVIDER="ollama" REVIEW_MODEL="gemma4:12b" # ------------------------------------------------------------ # Revision # ------------------------------------------------------------ REVISION_PROVIDER="openrouter" REVISION_MODEL="minimax/minimax-m3:free" # ------------------------------------------------------------ # OpenRouter # ------------------------------------------------------------ OPENROUTER_URL="https://openrouter.ai/api/v1" OPENROUTER_KEY_ENV="OPENROUTER_API_KEY" # ------------------------------------------------------------ # Ollama # ------------------------------------------------------------ OLLAMA_URL="http://127.0.0.1:11434/v1" # Context size for local Ollama models. # # 8192 = safer memory usage # 16384 = recommended starting point # 32768 = larger context, more memory # OLLAMA_CONTEXT=16384 # ------------------------------------------------------------ # Research behavior # ------------------------------------------------------------ # Maximum Research -> Review -> Revision cycles. MAX_RESEARCH_ROUNDS=3 # Maximum API attempts for a request. MAX_API_RETRIES=4 # Delay between successful topic processing. DELAY_SECONDS=5 # Maximum topics processed by run-all. # # 0 = unlimited MAX_ITERATIONS=0 # Enable independent reviewer. REVIEW_ENABLED=true # Automatically commit accepted documents. AUTO_COMMIT=false CONFIG echo "Created $CONFIG" } # ------------------------------------------------------------ # Provider handling # ------------------------------------------------------------ provider_url() { local provider="$1" case "$provider" in openrouter) printf '%s' "$OPENROUTER_URL" ;; ollama) printf '%s' "$OLLAMA_URL" ;; *) local variable="PROVIDER_${provider}_URL" local value="${!variable:-}" [[ -n "$value" ]] || die "No URL configured for provider: $provider" printf '%s' "$value" ;; esac } provider_key() { local provider="$1" case "$provider" in ollama) printf '' ;; openrouter) local env="${OPENROUTER_KEY_ENV:-OPENROUTER_API_KEY}" printf '%s' "${!env:-}" ;; *) local env_var="PROVIDER_${provider}_KEY_ENV" local env="${!env_var:-}" if [[ -n "$env" ]]; then printf '%s' "${!env:-}" fi ;; esac } provider_extra_json() { local provider="$1" if [[ "$provider" == "ollama" ]]; then jq -n \ --argjson ctx "$OLLAMA_CONTEXT" \ '{ num_ctx: $ctx }' else printf '{}' fi } # ------------------------------------------------------------ # API request # ------------------------------------------------------------ api_request() { local provider="$1" local model="$2" local system_prompt="$3" local user_prompt="$4" local output_file="$5" local base_url local endpoint local key base_url="$(provider_url "$provider")" endpoint="${base_url%/}/chat/completions" key="$(provider_key "$provider")" local attempt local response_file local http_code local content local bytes for ((attempt=1; attempt<=MAX_API_RETRIES; attempt++)); do log "API: provider=$provider model=$model attempt=$attempt" response_file="$STATE/responses/${RUN_ID:-manual}-$(timestamp)-${RANDOM}.json" local extra extra="$(provider_extra_json "$provider")" local payload payload="$( jq -n \ --arg model "$model" \ --arg system "$system_prompt" \ --arg user "$user_prompt" \ --argjson extra "$extra" \ '{ model: $model, messages: [ { role: "system", content: $system }, { role: "user", content: $user } ], temperature: 0.2 } + $extra' )" local curl_args=( --silent --show-error --connect-timeout 30 --max-time 1800 -H "Content-Type: application/json" ) if [[ -n "$key" ]]; then curl_args+=( -H "Authorization: Bearer $key" ) fi if curl "${curl_args[@]}" \ -o "$response_file" \ -w '%{http_code}' \ --data "$payload" \ "$endpoint" \ > "${response_file}.code" then http_code="$(cat "${response_file}.code")" else http_code="$(cat "${response_file}.code" 2>/dev/null || echo "000")" fi rm -f "${response_file}.code" if [[ "$http_code" != "200" ]]; then log "API HTTP status: $http_code" case "$http_code" in 429) warn "Rate limited by $provider." local wait wait=$((attempt * attempt * 5)) log "Waiting ${wait}s before retry." sleep "$wait" ;; 000) warn "Network/API connection failure." sleep $((attempt * 3)) ;; 4*) warn "Client/API error from $provider." if (( attempt < MAX_API_RETRIES )); then sleep $((attempt * 3)) fi ;; 5*) warn "Provider server error." sleep $((attempt * 5)) ;; *) warn "Unexpected HTTP status." sleep $((attempt * 3)) ;; esac continue fi # ---------------------------------------------------- # Extract response # ---------------------------------------------------- if ! content="$( jq -er \ '.choices[0].message.content // empty' \ "$response_file" )"; then warn "Provider returned malformed response." sleep $((attempt * 2)) continue fi bytes="${#content}" # Empty responses are API failures. if (( bytes < 10 )); then warn "Provider returned an empty/near-empty response (${bytes} bytes)." sleep $((attempt * 2)) continue fi printf '%s\n' "$content" > "$output_file" log "Response received: ${bytes} bytes" return 0 done warn "API request exhausted all retries." return 1 } # ------------------------------------------------------------ # 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 [[ -z "$RUN_ID" ]] || { ! 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 </dev/null 2>&1; then git add "$file" git commit \ -m "research: complete ${file#"$ROOT"/}" \ || warn "Git commit failed." else warn "AUTO_COMMIT enabled but repository is not Git." fi fi } # ------------------------------------------------------------ # 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' \ "$(now)" \ "$file" \ "$round" \ "$phase" \ "$result" \ >> "$STATE/runs/$RUN_ID/attempts" } # ------------------------------------------------------------ # Single research topic # ------------------------------------------------------------ run_single() { local requested="${1:-}" if [[ -n "$requested" ]]; then file="$requested" else file="$(find_next_task || true)" fi [[ -n "${file:-}" ]] || die "No SOON research topics found." [[ -f "$file" ]] || die "Research document not found: $file" if [[ -z "$RUN_ID" ]]; then start_run fi log "==================================================" log "Researching: $file" log "==================================================" local round local candidate local review local revision local verdict candidate="$STATE/runs/$RUN_ID/candidate.md" review="$STATE/runs/$RUN_ID/review.md" revision="$STATE/runs/$RUN_ID/revision.md" for ((round=1; round<=MAX_RESEARCH_ROUNDS; round++)); do log "Research round $round/$MAX_RESEARCH_ROUNDS" # ---------------------------------------------------- # Initial research # ---------------------------------------------------- if (( round == 1 )); then log "Running researcher." if ! api_request \ "$RESEARCH_PROVIDER" \ "$RESEARCH_MODEL" \ "You are a senior CPU architecture research engineer." \ "$(research_prompt "$file")" \ "$candidate" then warn "Research API failed." warn "Topic will NOT be marked as failed." return 2 fi record_attempt "$file" "$round" "research" "success" else log "Running revision agent." if ! api_request \ "$REVISION_PROVIDER" \ "$REVISION_MODEL" \ "You are a senior CPU architecture revision engineer." \ "$(revision_prompt "$file" "$candidate" "$review")" \ "$revision" then warn "Revision API failed." warn "Research round was NOT counted as a valid revision." return 2 fi mv "$revision" "$candidate" record_attempt "$file" "$round" "revision" "success" fi # ---------------------------------------------------- # Reviewer # ---------------------------------------------------- if [[ "$REVIEW_ENABLED" != "true" ]]; then log "Review disabled." accept_document "$file" "$candidate" mark_completed "$file" return 0 fi log "Running reviewer." if ! api_request \ "$REVIEW_PROVIDER" \ "$REVIEW_MODEL" \ "You are an independent and highly skeptical CPU architecture reviewer." \ "$(review_prompt "$file" "$(cat "$candidate")")" \ "$review" then warn "Reviewer API failed." # IMPORTANT: # # Do not mark the research as failed. # Do not trigger revision. # # Return a special status so run-all can retry # the topic later. record_attempt "$file" "$round" "review" "api-error" return 2 fi verdict="$(parse_verdict "$review")" if [[ "$verdict" == "INVALID" ]]; then log "Reviewer did not provide a machine-readable verdict." log "Requesting verdict-format repair." local repaired_review repaired_review="$STATE/runs/$RUN_ID/review-repaired.md" if repair_review_verdict \ "$(cat "$review")" \ "$repaired_review" then cat "$repaired_review" >> "$review" verdict="$(parse_verdict "$repaired_review")" if [[ "$verdict" != "INVALID" ]]; then log "Recovered reviewer verdict: $verdict" fi fi fi echo echo "==========================================" echo "RESEARCH REVIEW" echo "==========================================" cat "$review" echo "==========================================" echo case "$verdict" in PASS) log "Research passed review." record_attempt "$file" "$round" "review" "PASS" accept_document "$file" "$candidate" mark_completed "$file" return 0 ;; FAIL) log "Research rejected by reviewer." record_attempt "$file" "$round" "review" "FAIL" if (( round < MAX_RESEARCH_ROUNDS )); then log "Preparing revision round $((round + 1))." sleep "$DELAY_SECONDS" else log "Maximum research rounds reached." log "Leaving original document unchanged." mark_failed "$file" return 1 fi ;; INVALID) warn "Reviewer returned no valid VERDICT." record_attempt "$file" "$round" "review" "invalid" warn "This is a reviewer failure, not a research failure." return 2 ;; esac done return 1 } # ------------------------------------------------------------ # Run all # ------------------------------------------------------------ run_all() { start_run local count=0 local file local result while true; do if (( MAX_ITERATIONS > 0 && count >= MAX_ITERATIONS )); then log "MAX_ITERATIONS reached: $MAX_ITERATIONS" break fi file="$(find_next_task || true)" if [[ -z "$file" ]]; then log "No remaining research topics." break fi count=$((count + 1)) run_single "$file" result=$? case "$result" in 0) log "Topic completed successfully." ;; 1) log "Topic failed after maximum research rounds." ;; 2) log "Topic paused because of an API/provider failure." log "It remains eligible for a future run." ;; *) warn "Unexpected result code: $result" ;; esac sleep "$DELAY_SECONDS" done log "==================================================" log "Run complete." log "Topics attempted: $count" log "Run ID: $RUN_ID" log "==================================================" } # ------------------------------------------------------------ # Status # ------------------------------------------------------------ status() { local total=0 local completed=0 local remaining=0 while IFS= read -r -d '' file; do [[ "$file" == "$STATE"/* ]] && continue total=$((total + 1)) if is_soon "$file"; then remaining=$((remaining + 1)) else completed=$((completed + 1)) fi done < <( find "$ROOT" \ -type f \ -name '*.md' \ -not -path "$STATE/*" \ -print0 ) echo echo "╔══════════════════════════════════════════╗" echo "║ XH-1 RESEARCH STATUS ║" echo "╚══════════════════════════════════════════╝" echo echo " Total documents : $total" echo " Completed : $completed" echo " Remaining SOON : $remaining" echo echo "Next topic:" local next next="$(find_next_task || true)" if [[ -n "$next" ]]; then echo " $next" else echo " None" fi echo } # ------------------------------------------------------------ # Reset failure # ------------------------------------------------------------ reset_failure() { local file="${1:-}" [[ -n "$file" ]] || die "Usage: ./xh1-research reset-failure " [[ -f "$file" ]] || die "File not found: $file" if [[ -n "$RUN_ID" && -f "$STATE/runs/$RUN_ID/failed" ]]; then sed -i.bak "\|^${file}$|d" \ "$STATE/runs/$RUN_ID/failed" rm -f "$STATE/runs/$RUN_ID/failed.bak" fi echo "Failure state reset for:" echo " $file" } # ------------------------------------------------------------ # Manual review # ------------------------------------------------------------ review_command() { local file="${1:-}" [[ -n "$file" ]] || die "Usage: ./xh1-research review " [[ -f "$file" ]] || die "File not found: $file" RUN_ID="manual-$(timestamp)" mkdir -p "$STATE/runs/$RUN_ID" local review="$STATE/runs/$RUN_ID/review.md" log "Manual review: $file" if ! api_request \ "$REVIEW_PROVIDER" \ "$REVIEW_MODEL" \ "You are an independent and highly skeptical CPU architecture reviewer." \ "$(review_prompt "$file" "$(cat "$file")")" \ "$review" then die "Reviewer API failed." fi echo echo "==========================================" echo "RESEARCH REVIEW" echo "==========================================" cat "$review" echo "==========================================" } # ------------------------------------------------------------ # Providers # ------------------------------------------------------------ providers() { echo echo "XH-1 RESEARCH PROVIDERS" echo "=======================" echo printf '%-15s %-45s %s\n' \ "PROVIDER" "URL" "MODEL" printf '%-15s %-45s %s\n' \ "openrouter" \ "$OPENROUTER_URL" \ "$RESEARCH_MODEL" printf '%-15s %-45s %s\n' \ "ollama" \ "$OLLAMA_URL" \ "$REVIEW_MODEL" echo echo "Research:" echo " $RESEARCH_PROVIDER / $RESEARCH_MODEL" echo echo "Review:" echo " $REVIEW_PROVIDER / $REVIEW_MODEL" echo echo "Revision:" echo " $REVISION_PROVIDER / $REVISION_MODEL" echo } # ------------------------------------------------------------ # Help # ------------------------------------------------------------ help() { cat <<'EOF' XH-1 RESEARCH HARNESS ===================== Autonomous research/review/revision pipeline for XH-1. USAGE ./xh1-research COMMANDS init Create the default configuration. status Show research progress and next topic. providers Show configured AI providers and models. next Show the next SOON document. run Research the next available topic. run Research a specific document. run-all Process all available research topics. API/provider failures do NOT mark a topic failed. The topic remains eligible for a future run. loop Alias for run-all. resume Alias for run-all. review Manually review a document. reset-failure Allow a failed topic to be retried. help Show this help. RESEARCH PIPELINE Research | v Review | +---- PASS ----> Replace SOON document | +---- FAIL ----> Revision | v Review | +---- PASS | +---- FAIL | v Maximum rounds IMPORTANT SAFETY A research document is ONLY replaced after: VERDICT: PASS API failures are NOT research failures. Examples: 429 timeout connection failure malformed response empty reviewer response invalid reviewer verdict These pause the topic instead of consuming a research round. If all research rounds genuinely fail: The original SOON document remains unchanged. The topic is marked failed for that run. PROVIDERS Built-in: openrouter ollama Other OpenAI-compatible providers can be configured with: PROVIDER__URL PROVIDER__KEY_ENV ENVIRONMENT XH1_RESEARCH_ROOT Default: research XH1_RESEARCH_CONFIG Default: xh1-research.conf CONFIGURATION RESEARCH_PROVIDER Provider used for research. RESEARCH_MODEL Model used for research. REVIEW_PROVIDER Provider used for independent review. REVIEW_MODEL Model used for independent review. REVISION_PROVIDER Provider used for revision. REVISION_MODEL Model used for revision. OPENROUTER_URL Default: https://openrouter.ai/api/v1 OPENROUTER_KEY_ENV Default: OPENROUTER_API_KEY OLLAMA_URL Default: http://127.0.0.1:11434/v1 OLLAMA_CONTEXT Ollama context size. Recommended for 16GB Apple Silicon: 16384 MAX_RESEARCH_ROUNDS Maximum research/revision cycles per topic. Default: 3 MAX_API_RETRIES API retry attempts. Default: 4 DELAY_SECONDS Delay between topics/rounds. Default: 5 MAX_ITERATIONS Maximum topics processed by run-all. 0 = unlimited. REVIEW_ENABLED true/false. AUTO_COMMIT true/false. EXAMPLE export OPENROUTER_API_KEY="..." ./xh1-research init ./xh1-research providers ./xh1-research status ./xh1-research run ./xh1-research run-all RECOMMENDED XH-1 SETUP RESEARCH_PROVIDER="openrouter" RESEARCH_MODEL="minimax/minimax-m3:free" REVIEW_PROVIDER="ollama" REVIEW_MODEL="gemma4:12b" REVISION_PROVIDER="openrouter" REVISION_MODEL="minimax/minimax-m3:free" OLLAMA_CONTEXT=16384 EOF } # ------------------------------------------------------------ # Main # ------------------------------------------------------------ load_config require_dependencies case "${1:-status}" in init) init_config ;; status) status ;; providers) providers ;; next) find_next_task || true ;; run) run_single "${2:-}" ;; run-all|loop|resume) run_all ;; review) review_command "${2:-}" ;; reset-failure) reset_failure "${2:-}" ;; help|-h|--help) help ;; *) die "Unknown command: $1. Use ./xh1-research help" ;; esac