#!/usr/bin/env bash set -euo pipefail # ============================================================ # XH-1 Research Harness # Pure Bash / OpenAI-compatible API # ============================================================ ROOT="${XH1_RESEARCH_ROOT:-research}" STATE="${ROOT}/.xh1" CONFIG="${XH1_RESEARCH_CONFIG:-xh1-research.conf}" mkdir -p "$STATE"/{logs,responses} # ------------------------------------------------------------ # Defaults # ------------------------------------------------------------ API_BASE_URL="${API_BASE_URL:-https://api.openai.com/v1}" API_KEY_ENV="${API_KEY_ENV:-OPENAI_API_KEY}" MODEL="${MODEL:-}" MAX_RETRIES="${MAX_RETRIES:-3}" DELAY_SECONDS="${DELAY_SECONDS:-5}" MAX_ITERATIONS="${MAX_ITERATIONS:-0}" REVIEW_ENABLED="${REVIEW_ENABLED:-true}" AUTO_COMMIT="${AUTO_COMMIT:-false}" # ------------------------------------------------------------ # Helpers # ------------------------------------------------------------ die() { echo "ERROR: $*" >&2 exit 1 } log() { local msg="$*" echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $msg" \ | tee -a "$STATE/logs/harness.log" } require() { command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" } load_config() { if [[ -f "$CONFIG" ]]; then # shellcheck disable=SC1090 source "$CONFIG" fi } require_dependencies() { require bash require curl require jq } get_api_key() { local key="${!API_KEY_ENV:-}" [[ -n "$key" ]] || die "API key missing. Set ${API_KEY_ENV}." printf '%s' "$key" } timestamp() { date -u '+%Y%m%dT%H%M%SZ' } # ------------------------------------------------------------ # Configuration # ------------------------------------------------------------ init_config() { if [[ -f "$CONFIG" ]]; then echo "Configuration already exists:" echo " $CONFIG" return fi cat > "$CONFIG" <<'EOF' # ============================================================ # XH-1 Research Harness Configuration # ============================================================ # OpenAI-compatible API API_BASE_URL="https://api.openai.com/v1" # Environment variable containing API key API_KEY_ENV="OPENAI_API_KEY" # Model name MODEL="" # Research behavior MAX_RETRIES=3 DELAY_SECONDS=5 MAX_ITERATIONS=0 # Run reviewer after researcher REVIEW_ENABLED=true # Automatically create git commits AUTO_COMMIT=false EOF echo "Created $CONFIG" } # ------------------------------------------------------------ # API # ------------------------------------------------------------ api_call() { local system_prompt="$1" local user_prompt="$2" local output="$3" local key key="$(get_api_key)" [[ -n "$MODEL" ]] || die "MODEL is not configured." local payload payload="$( jq -n \ --arg model "$MODEL" \ --arg system "$system_prompt" \ --arg user "$user_prompt" \ '{ model: $model, messages: [ { role: "system", content: $system }, { role: "user", content: $user } ], temperature: 0.2 }' )" local attempt=1 while (( attempt <= MAX_RETRIES )); do if curl \ --fail \ --silent \ --show-error \ --connect-timeout 30 \ --max-time 1800 \ -H "Authorization: Bearer ${key}" \ -H "Content-Type: application/json" \ "${API_BASE_URL%/}/chat/completions" \ -d "$payload" \ > "${output}.json" then if jq -er \ '.choices[0].message.content' \ "${output}.json" \ > "$output" then return 0 fi fi log "API attempt $attempt failed." sleep $((attempt * attempt)) ((attempt++)) done return 1 } # ------------------------------------------------------------ # Research discovery # ------------------------------------------------------------ is_research_file() { [[ "$1" == *.md ]] && [[ "$1" != "$STATE"/* ]] } is_soon() { grep -qE \ '^SOON[[:space:]]*$' \ "$1" } find_next_task() { while IFS= read -r -d '' file; do if is_research_file "$file" && is_soon "$file" then printf '%s\n' "$file" return 0 fi done < <( find "$ROOT" \ -type f \ -name '*.md' \ -not -path "$STATE/*" \ -print0 ) return 1 } # ------------------------------------------------------------ # Research context # ------------------------------------------------------------ build_context() { local file="$1" local relative relative="${file#"$ROOT"/}" local area area="${relative%%/*}" cat < "$temporary" mv "$temporary" "$target" } # ------------------------------------------------------------ # Git # ------------------------------------------------------------ git_commit() { local file="$1" [[ "$AUTO_COMMIT" == "true" ]] || return 0 git rev-parse \ --is-inside-work-tree \ >/dev/null 2>&1 || return 0 git add "$file" local title title="$( basename "$file" .md | tr '-' ' ' )" git commit \ -m "research: ${title}" \ || true } # ------------------------------------------------------------ # Research one document # ------------------------------------------------------------ run_one() { local file="${1:-}" if [[ -z "$file" ]]; then file="$(find_next_task || true)" fi [[ -n "$file" ]] || { echo "No incomplete research topics." return 0 } [[ -f "$file" ]] || die "Research file does not exist: $file" log "Starting research: $file" local id id="$(timestamp)_$(basename "$file" .md)" local response response="$STATE/responses/${id}.md" log "Running researcher." if ! api_call \ "$(research_system_prompt)" \ "$(research_user_prompt "$file")" \ "$response" then log "Research agent failed." return 1 fi log "Research completed." # -------------------------------------------------------- # Review # -------------------------------------------------------- if [[ "$REVIEW_ENABLED" == "true" ]]; then local review review="${response}.review" log "Running reviewer." if ! review_document \ "$response" \ "$review" then log "Reviewer failed." return 1 fi local verdict verdict="$( grep -m1 '^VERDICT:' "$review" | tr -d '\r' )" echo echo "==========================================" echo "RESEARCH REVIEW" echo "==========================================" cat "$review" echo "==========================================" echo if [[ "$verdict" != "VERDICT: PASS" ]]; then log "Research rejected by reviewer." return 2 fi log "Research passed review." fi # -------------------------------------------------------- # Replace SOON # -------------------------------------------------------- replace_soon "$file" "$response" printf '%s\t%s\t%s\n' \ "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ "completed" \ "$file" \ >> "$STATE/state.tsv" git_commit "$file" log "Completed: $file" return 0 } # ------------------------------------------------------------ # Loop # ------------------------------------------------------------ run_loop() { local iteration=0 while true; do if (( MAX_ITERATIONS > 0 && iteration >= MAX_ITERATIONS )); then log "Maximum iterations reached." break fi local file file="$(find_next_task || true)" if [[ -z "$file" ]]; then log "No SOON documents remain." break fi echo echo "==========================================" echo "XH-1 RESEARCH ITERATION $((iteration + 1))" echo "==========================================" echo echo "Topic:" echo " $file" echo if run_one "$file"; then log "Iteration successful." else log "Iteration failed." echo echo "Research failed." echo "The document was NOT modified." echo fi ((iteration += 1)) sleep "$DELAY_SECONDS" done } # ------------------------------------------------------------ # Status # ------------------------------------------------------------ status() { [[ -d "$ROOT" ]] || die "Research directory not found: $ROOT" local total local soon local researched total="$( find "$ROOT" \ -type f \ -name '*.md' \ -not -path "$STATE/*" | wc -l | tr -d ' ' )" soon="$( grep -RIl \ '^SOON[[:space:]]*$' \ "$ROOT" \ --include='*.md' \ --exclude-dir='.xh1' \ 2>/dev/null | wc -l | tr -d ' ' )" researched=$((total - soon)) echo echo "╔══════════════════════════════════════╗" echo "║ XH-1 RESEARCH STATUS ║" echo "╚══════════════════════════════════════╝" echo printf " Total documents : %s\n" "$total" printf " Research ready : %s\n" "$researched" printf " Remaining SOON : %s\n" "$soon" if (( total > 0 )); then awk \ -v done="$researched" \ -v total="$total" \ 'BEGIN { printf " Progress : %.1f%%\n", (done / total) * 100 }' fi echo echo "Next topic:" find_next_task || echo " None" echo } # ------------------------------------------------------------ # Review command # ------------------------------------------------------------ review_command() { local file="${1:-}" [[ -n "$file" ]] || die "Usage: $0 review " [[ -f "$file" ]] || die "File does not exist: $file" local output output="$STATE/responses/manual_review_$(timestamp).txt" review_document "$file" "$output" cat "$output" } # ------------------------------------------------------------ # Initialization # ------------------------------------------------------------ initialize() { mkdir -p \ "$STATE/logs" \ "$STATE/responses" touch \ "$STATE/state.tsv" init_config echo echo "XH-1 research harness initialized." echo echo "Configuration:" echo " $CONFIG" echo echo "State:" echo " $STATE" echo } # ------------------------------------------------------------ # Help # ------------------------------------------------------------ help() { cat <<'EOF' XH-1 RESEARCH HARNESS ===================== Pure Bash autonomous research system. USAGE ./xh1-research COMMANDS init Initialize the harness. status Show research progress. next Show the next SOON document. run Research the next topic. run Research a specific document. run-all Continuously research every SOON document. loop Alias for run-all. resume Resume autonomous research. review Manually review a research document. help Show this help. EXAMPLES ./xh1-research init ./xh1-research status ./xh1-research next ./xh1-research run ./xh1-research run research/04-128-core/topology.md ./xh1-research run-all ./xh1-research review research/05-memory/cache-coherency.md ENVIRONMENT XH1_RESEARCH_ROOT Research directory. Default: research XH1_RESEARCH_CONFIG Configuration file. Default: xh1-research.conf CONFIGURATION API_BASE_URL OpenAI-compatible API base. API_KEY_ENV Environment variable containing API key. MODEL Model identifier. MAX_RETRIES API retry count. DELAY_SECONDS Delay between research iterations. MAX_ITERATIONS Maximum autonomous iterations. 0 = unlimited. REVIEW_ENABLED Enable reviewer agent. AUTO_COMMIT Automatically commit completed research. SAFETY Existing research is never overwritten. A document is only modified if it still contains a standalone SOON marker. Git push is never performed automatically. EOF } # ------------------------------------------------------------ # Main # ------------------------------------------------------------ load_config require_dependencies COMMAND="${1:-status}" case "$COMMAND" in init) initialize ;; status) status ;; next) find_next_task || true ;; run) run_one "${2:-}" ;; run-all|loop|resume) run_loop ;; review) review_command "${2:-}" ;; help|-h|--help) help ;; *) die "Unknown command: $COMMAND. Use --help." esac