Files

1348 lines
25 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# ============================================================
# XH-1 RESEARCH HARNESS
#
# Multi-provider / multi-model autonomous research system.
#
# Supports any OpenAI-compatible Chat Completions API:
#
# OpenRouter
# Ollama
# LM Studio
# vLLM
# LocalAI
# OpenAI
# Other compatible providers
#
# Pipeline:
#
# RESEARCH
# ↓
# REVIEW
# ↓
# PASS ───────────────→ COMMIT
# │
# FAIL
# ↓
# REVISION
# ↓
# REVIEW
# ↓
# ...
#
# ============================================================
ROOT="${XH1_RESEARCH_ROOT:-research}"
CONFIG="${XH1_RESEARCH_CONFIG:-xh1-research.conf}"
STATE="${ROOT}/.xh1"
mkdir -p \
"$STATE/logs" \
"$STATE/runs" \
"$STATE/responses"
# ============================================================
# Defaults
# ============================================================
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=""
# ============================================================
# Helpers
# ============================================================
die() {
echo "ERROR: $*" >&2
exit 1
}
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" |
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
}
# ============================================================
# Provider abstraction
# ============================================================
#
# Provider variables are defined in xh1-research.conf.
#
# Example:
#
# PROVIDER_openrouter_URL="https://openrouter.ai/api/v1"
# PROVIDER_openrouter_KEY_ENV="OPENROUTER_API_KEY"
#
# PROVIDER_ollama_URL="http://127.0.0.1:11434/v1"
# PROVIDER_ollama_KEY_ENV=""
#
# PROVIDER_openai_URL="https://api.openai.com/v1"
# PROVIDER_openai_KEY_ENV="OPENAI_API_KEY"
#
provider_url() {
local provider="$1"
local variable="PROVIDER_${provider}_URL"
printf '%s' "${!variable:-}"
}
provider_key_env() {
local provider="$1"
local variable="PROVIDER_${provider}_KEY_ENV"
printf '%s' "${!variable:-}"
}
provider_api_key() {
local provider="$1"
local key_env
key_env="$(provider_key_env "$provider")"
# Local providers such as Ollama normally require no key.
if [[ -z "$key_env" ]]; then
printf ''
return 0
fi
local key="${!key_env:-}"
[[ -n "$key" ]] ||
die "Provider '$provider' requires environment variable '$key_env'."
printf '%s' "$key"
}
provider_check() {
local provider="$1"
local url
url="$(provider_url "$provider")"
[[ -n "$url" ]] ||
die "Provider '$provider' has no URL."
local key
key="$(provider_api_key "$provider")"
local -a headers
headers=(
-H "Content-Type: application/json"
)
if [[ -n "$key" ]]; then
headers+=(
-H "Authorization: Bearer $key"
)
fi
curl \
--fail \
--silent \
--show-error \
--connect-timeout 10 \
--max-time 20 \
"${headers[@]}" \
"${url%/}/models" \
>/dev/null
}
# ============================================================
# API call
# ============================================================
api_call() {
local provider="$1"
local model="$2"
local system_prompt="$3"
local user_prompt="$4"
local output="$5"
local url
url="$(provider_url "$provider")"
[[ -n "$url" ]] ||
die "Unknown provider: $provider"
local key
key="$(provider_api_key "$provider")"
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_API_RETRIES )); do
local json="${output}.json"
log "API: provider=$provider model=$model attempt=$attempt"
local -a headers
headers=(
-H "Content-Type: application/json"
)
if [[ -n "$key" ]]; then
headers+=(
-H "Authorization: Bearer $key"
)
fi
# OpenRouter-specific attribution headers.
if [[ "$provider" == "openrouter" ]]; then
headers+=(
-H "HTTP-Referer: https://github.com/riscvcxh1/xh1-research"
-H "X-Title: XH-1 Research Harness"
)
fi
if curl \
--fail \
--silent \
--show-error \
--connect-timeout 30 \
--max-time 1800 \
"${headers[@]}" \
"${url%/}/chat/completions" \
-d "$payload" \
> "$json"
then
if jq -er \
'.choices[0].message.content' \
"$json" \
> "$output"
then
log "Response received: $(wc -c < "$output" | tr -d ' ') bytes"
return 0
fi
log "Provider returned an invalid response."
else
log "API request failed."
fi
sleep $((attempt * attempt))
((attempt++))
done
return 1
}
# ============================================================
# 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() {
grep -Fxq "$1" \
"$STATE/runs/$RUN_ID/completed" \
2>/dev/null
}
run_has_failed() {
grep -Fxq "$1" \
"$STATE/runs/$RUN_ID/failed" \
2>/dev/null
}
mark_completed() {
printf '%s\n' "$1" \
>> "$STATE/runs/$RUN_ID/completed"
}
mark_failed() {
printf '%s\n' "$1" \
>> "$STATE/runs/$RUN_ID/failed"
}
record_attempt() {
printf '%s\t%s\t%s\t%s\t%s\n' \
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
"$1" \
"$2" \
"$3" \
"$4" \
>> "$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" &&
! run_has_completed "$file" &&
! run_has_failed "$file"
then
printf '%s\n' "$file"
return 0
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 <<EOF
XH-1 RESEARCH PROJECT
=====================
Project:
XH-1
Architecture:
Custom 128-core RISC-V processor
Repository:
XH-1 Research
Current document:
$file
Research area:
$area
CURRENT DOCUMENT
================
$(cat "$file")
RELATED DOCUMENTS
=================
EOF
if [[ -d "$ROOT/$area" ]]; then
find "$ROOT/$area" \
-maxdepth 1 \
-type f \
-name '*.md' \
! -path "$file" |
sort |
head -20 |
while read -r related; do
echo
echo "----- $related -----"
cat "$related"
done
fi
}
# ============================================================
# Research prompts
# ============================================================
research_system_prompt() {
cat <<'EOF'
You are a senior computer architecture researcher working
on the XH-1 processor project.
XH-1 is a custom 128-core RISC-V CPU.
Produce rigorous engineering research.
Never invent:
- citations
- papers
- measurements
- benchmarks
- processor capabilities
- URLs
- experimental results
Clearly distinguish:
FACT
ASSUMPTION
PROPOSAL
RECOMMENDATION
OPEN QUESTION
If evidence is unavailable, say:
INSUFFICIENT EVIDENCE
Quantitative claims must have an identifiable source or must
be explicitly labeled as estimates.
Always consider:
- latency
- throughput
- area
- power
- energy
- bandwidth
- scalability
- contention
- implementation complexity
- verification complexity
- software implications
- 128-core replication
Do not assume XH-1 is in-order or out-of-order unless the
repository context establishes that fact.
Return ONLY the Markdown research document.
EOF
}
research_user_prompt() {
local file="$1"
cat <<EOF
Research the following XH-1 topic.
$(build_context "$file")
Produce a complete engineering research document.
Use sections where applicable:
# Topic
## Status
## Abstract
## Research Question
## Background
## Existing Approaches
## Alternative Designs
## Comparison
## Advantages
## Disadvantages
## XH-1 Considerations
## 128-Core Scalability
## Performance Considerations
## Area Considerations
## Power and Energy Considerations
## Implementation Considerations
## Verification Considerations
## Software Considerations
## Recommendation
## Confidence
## Open Questions
## Sources
Do not force a recommendation if evidence is insufficient.
EOF
}
# ============================================================
# Reviewer
# ============================================================
review_system_prompt() {
cat <<'EOF'
You are the independent technical reviewer for XH-1.
XH-1 is a custom 128-core RISC-V processor.
Aggressively check research for:
- factual errors
- incorrect RISC-V information
- unsupported claims
- fabricated citations
- fabricated measurements
- contradictory recommendations
- missing alternatives
- missing assumptions
- failure to consider 128-core scaling
- unrealistic implementation claims
- unsupported performance claims
- unsupported area claims
- unsupported power claims
- weak verification reasoning
- incorrect terminology
- insufficient source specificity
- assumptions presented as facts
Check quantitative claims especially carefully.
Check whether recommendations actually follow from
the analysis.
Return exactly:
VERDICT: PASS
or:
VERDICT: FAIL
Then:
ISSUES:
- ...
REQUIRED_FIXES:
- ...
CONFIDENCE: HIGH/MEDIUM/LOW
EOF
}
review_user_prompt() {
local document="$1"
cat <<EOF
Review this XH-1 research document independently.
DOCUMENT
========
$(cat "$document")
END DOCUMENT
Identify every substantive technical problem.
Do not rewrite the document.
Return PASS only when the research is technically acceptable,
internally consistent, and sufficiently supported.
EOF
}
# ============================================================
# Revision
# ============================================================
revision_system_prompt() {
cat <<'EOF'
You are a senior computer architecture researcher revising
research for the XH-1 128-core RISC-V CPU.
The previous version was rejected by an independent reviewer.
Fix every substantive reviewer issue.
You must:
- correct factual errors
- remove unsupported quantitative claims
- add missing alternatives
- reconcile contradictions
- make assumptions explicit
- improve citations
- reconsider unsupported recommendations
- consider 128-core scaling
- consider area
- consider power
- consider energy
- consider implementation
- consider verification
Never invent evidence.
If evidence cannot be established, write:
INSUFFICIENT EVIDENCE
Do not merely respond to the reviewer.
Produce a clean replacement document.
Return ONLY Markdown.
EOF
}
revision_user_prompt() {
local document="$1"
local review="$2"
cat <<EOF
Revise this XH-1 research document.
ORIGINAL
========
$(cat "$document")
REVIEW
======
$(cat "$review")
Fix every substantive issue.
Do not add a response-to-reviewer section.
Return the complete corrected Markdown document only.
EOF
}
# ============================================================
# Replace SOON
# ============================================================
replace_soon() {
local target="$1"
local replacement="$2"
is_soon "$target" ||
die "Safety check failed: $target is no longer SOON."
cp "$replacement" "$target"
}
# ============================================================
# Git
# ============================================================
git_commit() {
[[ "$AUTO_COMMIT" == "true" ]] || return 0
git rev-parse --is-inside-work-tree \
>/dev/null 2>&1 ||
return 0
git add "$1"
git commit \
-m "research: $(basename "$1" .md | tr '-' ' ')" ||
true
}
# ============================================================
# Research one topic
# ============================================================
research_topic() {
local file="$1"
log "=================================================="
log "Researching: $file"
log "=================================================="
local round=1
local current="$STATE/runs/$RUN_ID/${file//\//_}.current.md"
local review="$STATE/runs/$RUN_ID/${file//\//_}.review.md"
while (( round <= MAX_RESEARCH_ROUNDS )); do
log "Research round $round/$MAX_RESEARCH_ROUNDS"
local candidate="$STATE/runs/$RUN_ID/${file//\//_}.round${round}.md"
# ----------------------------------------------------
# Research
# ----------------------------------------------------
if (( round == 1 )); then
log "Running researcher."
if ! api_call \
"$RESEARCH_PROVIDER" \
"$RESEARCH_MODEL" \
"$(research_system_prompt)" \
"$(research_user_prompt "$file")" \
"$candidate"
then
record_attempt \
"$file" "$round" "research" "api-failure"
mark_failed "$file"
return 1
fi
record_attempt \
"$file" "$round" "research" "completed"
# ----------------------------------------------------
# Revision
# ----------------------------------------------------
else
log "Running revision agent."
if ! api_call \
"$REVISION_PROVIDER" \
"$REVISION_MODEL" \
"$(revision_system_prompt)" \
"$(revision_user_prompt "$current" "$review")" \
"$candidate"
then
record_attempt \
"$file" "$round" "revision" "api-failure"
mark_failed "$file"
return 1
fi
record_attempt \
"$file" "$round" "revision" "completed"
fi
cp "$candidate" "$current"
# ----------------------------------------------------
# Review
# ----------------------------------------------------
if [[ "$REVIEW_ENABLED" != "true" ]]; then
replace_soon "$file" "$candidate"
mark_completed "$file"
git_commit "$file"
return 0
fi
log "Running reviewer."
if ! api_call \
"$REVIEW_PROVIDER" \
"$REVIEW_MODEL" \
"$(review_system_prompt)" \
"$(review_user_prompt "$candidate")" \
"$review"
then
record_attempt \
"$file" "$round" "review" "api-failure"
mark_failed "$file"
return 1
fi
echo
echo "=========================================="
echo "RESEARCH REVIEW"
echo "=========================================="
cat "$review"
echo
echo "=========================================="
echo
local verdict
verdict="$(
grep -m1 '^VERDICT:' "$review" || true
)"
record_attempt \
"$file" "$round" "review" "$verdict"
# ----------------------------------------------------
# PASS
# ----------------------------------------------------
if [[ "$verdict" == "VERDICT: PASS" ]]; then
log "Research PASSED review."
replace_soon "$file" "$candidate"
mark_completed "$file"
git_commit "$file"
log "Completed: $file"
return 0
fi
# ----------------------------------------------------
# FAIL
# ----------------------------------------------------
log "Research rejected by reviewer."
if (( round >= MAX_RESEARCH_ROUNDS )); then
log "Maximum research rounds reached."
log "Leaving original document unchanged."
log "Marking topic failed for this run."
mark_failed "$file"
return 2
fi
log "Preparing revision round $((round + 1))."
((round++))
sleep "$DELAY_SECONDS"
done
mark_failed "$file"
return 2
}
# ============================================================
# Run one
# ============================================================
run_single() {
start_run
local file="${1:-}"
if [[ -z "$file" ]]; then
file="$(find_next_task || true)"
fi
[[ -n "$file" ]] ||
die "No research topics remain."
research_topic "$file"
}
# ============================================================
# Run all
# ============================================================
run_all() {
start_run
local iteration=0
while true; do
if (( MAX_ITERATIONS > 0 &&
iteration >= MAX_ITERATIONS )); then
log "Maximum run iterations reached."
break
fi
local file
file="$(find_next_task || true)"
if [[ -z "$file" ]]; then
log "No remaining research topics."
break
fi
((iteration += 1))
echo
echo "##################################################"
echo "XH-1 AUTONOMOUS RESEARCH"
echo "Iteration: $iteration"
echo "Topic: $file"
echo "##################################################"
echo
research_topic "$file" || true
sleep "$DELAY_SECONDS"
done
status
}
# ============================================================
# Status
# ============================================================
status() {
local total
local soon
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 ' '
)"
local completed=$((total - soon))
echo
echo "╔══════════════════════════════════════════╗"
echo "║ XH-1 RESEARCH STATUS ║"
echo "╚══════════════════════════════════════════╝"
echo
printf " Total documents : %s\n" "$total"
printf " Completed : %s\n" "$completed"
printf " Remaining SOON : %s\n" "$soon"
if (( total > 0 )); then
awk \
-v done="$completed" \
-v total="$total" \
'BEGIN {
printf " Progress : %.1f%%\n",
(done / total) * 100
}'
fi
echo
echo "Next topic:"
find_next_task || echo " None"
echo
}
# ============================================================
# Reset failure
# ============================================================
reset_failure() {
local file="${1:-}"
[[ -n "$file" ]] ||
die "Usage: $0 reset-failure <file>"
[[ -f "$file" ]] ||
die "File does not exist: $file"
if [[ -n "$RUN_ID" &&
-f "$STATE/runs/$RUN_ID/failed" ]]; then
sed -i "\|^${file}$|d" \
"$STATE/runs/$RUN_ID/failed"
fi
log "Failure reset: $file"
}
# ============================================================
# Manual review
# ============================================================
review_command() {
local file="${1:-}"
[[ -n "$file" ]] ||
die "Usage: $0 review <file>"
[[ -f "$file" ]] ||
die "File does not exist: $file"
local output="$STATE/manual-review-$(timestamp).md"
api_call \
"$REVIEW_PROVIDER" \
"$REVIEW_MODEL" \
"$(review_system_prompt)" \
"$(review_user_prompt "$file")" \
"$output"
cat "$output"
}
# ============================================================
# Provider status
# ============================================================
providers() {
echo
echo "XH-1 PROVIDERS"
echo "=============="
echo
local provider
for provider in $PROVIDERS; do
local url
url="$(provider_url "$provider")"
printf "%-15s %s" "$provider" "$url"
if provider_check "$provider" >/dev/null 2>&1; then
echo " [OK]"
else
echo " [OFFLINE/ERROR]"
fi
done
echo
}
# ============================================================
# Help
# ============================================================
help() {
cat <<'HELP'
XH-1 RESEARCH HARNESS
MULTI-PROVIDER AUTONOMOUS RESEARCH SYSTEM
COMMANDS
--------
init
Create a configuration template.
status
Show research progress.
providers
Test configured API providers.
next
Show next SOON document.
run
Research the next topic.
run <file>
Research a specific topic.
run-all
Process all SOON documents.
review <file>
Manually review a document.
reset-failure <file>
Retry a failed topic.
help
Show this help.
PIPELINE
--------
RESEARCH
|
v
REVIEW
|
+--+--+
| |
PASS FAIL
| |
v v
WRITE REVISE
|
v
REVIEW
PROVIDERS
---------
Each agent can use a different provider.
Example:
Researcher -> OpenRouter
Reviewer -> Ollama
Revision -> OpenRouter
SUPPORTED PROVIDER STYLE
------------------------
Any OpenAI-compatible API.
Examples:
OpenRouter
Ollama
LM Studio
vLLM
LocalAI
OpenAI
Other compatible APIs
CONFIGURATION
-------------
PROVIDERS
Space-separated provider names.
Example:
PROVIDERS="openrouter ollama"
Provider URL:
PROVIDER_<name>_URL
Provider API key environment variable:
PROVIDER_<name>_KEY_ENV
AGENTS
RESEARCH_PROVIDER
RESEARCH_MODEL
REVIEW_PROVIDER
REVIEW_MODEL
REVISION_PROVIDER
REVISION_MODEL
RESEARCH SETTINGS
MAX_RESEARCH_ROUNDS
MAX_API_RETRIES
DELAY_SECONDS
MAX_ITERATIONS
REVIEW_ENABLED
AUTO_COMMIT
EXAMPLE
-------
export OPENROUTER_API_KEY="..."
./xh1-research providers
./xh1-research status
./xh1-research run
./xh1-research run-all
HELP
}
# ============================================================
# Main
# ============================================================
load_config
require_dependencies
case "${1:-status}" in
init)
echo "Edit $CONFIG to configure providers."
;;
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"
;;
esac