Files
xh1-research-fork/r
T

1930 lines
38 KiB
Bash
Executable File

#!/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}"
QWEN_URL="${QWEN_URL:-https://dashscope-intl.aliyuncs.com/compatible-mode/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"
;;
qwen)
printf '%s' "$QWEN_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:-}"
;;
qwen)
local env="${QWEN_KEY_ENV:-QWEN_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 <<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
The XH-1 processor is a research CPU project.
It is a custom 128-core RISC-V architecture.
This document is one part of the project's research
documentation.
Do not invent implementation facts.
Clearly distinguish:
- established facts
- RISC-V specification requirements
- published research
- industry practice
- reasonable engineering proposals
- assumptions
- unresolved questions
Prefer technically precise terminology.
If evidence is unavailable, explicitly say so.
CURRENT DOCUMENT
================
EOF
cat "$file"
}
# ------------------------------------------------------------
# Research prompt
# ------------------------------------------------------------
research_prompt() {
local file="$1"
cat <<EOF
You are the primary research engineer for the XH-1 CPU project.
XH-1 is a custom 128-core RISC-V processor.
Research the topic represented by the supplied document.
Your task is to produce a technically rigorous research document
that can eventually become part of the XH-1 architecture documentation.
TOPIC
=====
$file
REQUIREMENTS
============
1. Explain the topic thoroughly.
2. Relate the topic specifically to a 128-core RISC-V processor.
3. Identify relevant RISC-V architectural requirements.
4. Discuss realistic implementation approaches.
5. Compare meaningful alternatives.
6. Discuss advantages and disadvantages.
7. Identify scalability problems caused by 128 cores.
8. Identify interactions with:
- pipeline
- cache hierarchy
- memory system
- interconnect
- coherence
- interrupts
- operating system
- verification
- performance
9. Do not present assumptions as facts.
10. Cite standards, papers, manuals, or reputable technical
sources where possible.
11. Do not fabricate citations.
12. Identify unresolved design questions.
13. Make recommendations only when sufficient evidence exists.
14. If the XH-1 implementation is not yet decided, explicitly
mark it as a proposal rather than an existing feature.
15. Prefer quantitative analysis where reasonable.
OUTPUT FORMAT
=============
Return ONLY the proposed Markdown document.
Do not include:
- conversational commentary
- "here is the document"
- analysis outside the document
- review commentary
The result should be suitable for directly replacing the
SOON placeholder if it passes independent review.
CURRENT DOCUMENT
================
$(build_context "$file")
EOF
}
# ------------------------------------------------------------
# Review prompt
# ------------------------------------------------------------
review_prompt() {
local file="$1"
local candidate="$2"
cat <<EOF
You are the INDEPENDENT TECHNICAL REVIEWER for the XH-1 CPU project.
XH-1 is a custom 128-core RISC-V processor.
Your task is to critically review the proposed research document.
You are NOT the author.
You must NOT rewrite the document.
Your job is to identify:
- factual errors
- RISC-V specification errors
- incorrect terminology
- unsupported claims
- missing evidence
- bad citations
- internal contradictions
- missing design alternatives
- unrealistic assumptions
- scalability problems
- 128-core implications
- memory-system interactions
- cache/coherence interactions
- interconnect implications
- verification problems
- power/area implications
- software implications
- recommendations that do not follow from the evidence
IMPORTANT
Do not reject a document merely because XH-1 has not made
a final architectural decision.
A research document may contain:
- proposals
- assumptions
- alternatives
- open questions
- preliminary recommendations
Those are acceptable when clearly identified.
Do reject statements that present speculation as established fact.
Do not invent facts.
Do not invent citations.
Do not claim that RISC-V requires something unless it actually does.
Pay particular attention to whether claims are:
FACT
SPECIFICATION
RESEARCH
ASSUMPTION
PROPOSAL
OPEN QUESTION
DOCUMENT
========
$file
$candidate
============================================================
REVIEW CRITERIA
============================================================
1. Technical correctness
2. RISC-V compliance
3. Microarchitectural correctness
4. Memory-model correctness
5. Cache/coherence correctness
6. Interconnect/scalability
7. 128-core implications
8. Performance
9. Area
10. Power/energy
11. Verification
12. Software/OS implications
13. Evidence and citations
14. Internal consistency
15. Quality of recommendations
============================================================
VERDICT RULE
============================================================
PASS means:
The document is technically credible for its research stage.
Minor improvements may exist.
FAIL means:
There are substantive technical errors, unsupported claims,
contradictions, missing critical alternatives, or other issues
that should be corrected before acceptance.
============================================================
CRITICAL OUTPUT REQUIREMENT
============================================================
Your response MUST contain a machine-readable JSON object
as the FINAL thing in your response.
Do not omit it.
The JSON MUST have exactly this general structure:
{
"verdict": "PASS",
"confidence": "HIGH",
"issues": [],
"required_fixes": []
}
For a failing document:
{
"verdict": "FAIL",
"confidence": "HIGH",
"issues": [
"Description of issue"
],
"required_fixes": [
"Description of required fix"
]
}
The value of "verdict" MUST be exactly:
PASS
or:
FAIL
The value of "confidence" MUST be:
HIGH
MEDIUM
or:
LOW
The JSON object MUST be valid JSON.
Do not put Markdown fences around the JSON.
You may provide a normal human-readable review before the JSON.
The FINAL JSON object is the authoritative verdict.
============================================================
Begin your review now.
EOF
}
# ------------------------------------------------------------
# Revision prompt
# ------------------------------------------------------------
revision_prompt() {
local file="$1"
local candidate="$2"
local review="$3"
cat <<EOF
You are the revision engineer for the XH-1 CPU research project.
XH-1 is a custom 128-core RISC-V processor.
Revise the research document using the independent review.
Do NOT blindly accept every reviewer statement.
If the reviewer is technically incorrect, correct the issue
using sound engineering reasoning.
Do not invent facts.
Do not fabricate citations.
Clearly mark assumptions and proposals.
Preserve useful research from the existing document.
The revised result must be a complete standalone Markdown
document.
OUTPUT ONLY THE REVISED MARKDOWN DOCUMENT.
TOPIC
=====
$file
CURRENT DOCUMENT
================
$candidate
INDEPENDENT REVIEW
==================
$review
EOF
}
# ------------------------------------------------------------
# Review verdict
# ------------------------------------------------------------
parse_verdict() {
local review="$1"
python3 - "$review" <<'PY'
import json
import re
import sys
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
text = f.read()
# ------------------------------------------------------------
# 1. Find JSON objects containing a "verdict" field.
# ------------------------------------------------------------
matches = re.findall(
r'\{[\s\S]*?"verdict"\s*:\s*"(?:PASS|FAIL)"[\s\S]*?\}',
text,
re.IGNORECASE
)
for candidate in reversed(matches):
try:
obj = json.loads(candidate)
verdict = str(obj.get("verdict", "")).upper()
if verdict in ("PASS", "FAIL"):
print(verdict)
sys.exit(0)
except json.JSONDecodeError:
pass
# ------------------------------------------------------------
# 2. Legacy format.
# ------------------------------------------------------------
matches = re.findall(
r'VERDICT\s*:\s*(PASS|FAIL)',
text,
re.IGNORECASE
)
if matches:
print(matches[-1].upper())
sys.exit(0)
print("INVALID")
PY
}
repair_review_verdict() {
local original="$1"
local repaired="$2"
local prompt
prompt=$(cat <<EOF
You are a review-output formatter.
The following is a technical review of an XH-1 RISC-V CPU research
document.
The review itself has already been completed.
Your ONLY task is to determine whether the reviewer intended
PASS or FAIL and return valid JSON.
Return ONLY this JSON object:
{
"verdict": "PASS",
"confidence": "HIGH",
"issues": [],
"required_fixes": []
}
OR:
{
"verdict": "FAIL",
"confidence": "HIGH",
"issues": ["..."],
"required_fixes": ["..."]
}
Do not add Markdown.
Do not add explanation.
Do not use code fences.
Original review:
$original
EOF
)
api_request \
"$REVIEW_PROVIDER" \
"$REVIEW_MODEL" \
"You convert technical review output into strict JSON." \
"$prompt" \
"$repaired"
}
# ------------------------------------------------------------
# Atomic document replacement
# ------------------------------------------------------------
accept_document() {
local file="$1"
local candidate="$2"
local temp="${file}.xh1.tmp"
cp "$candidate" "$temp"
mv "$temp" "$file"
log "Accepted research document: $file"
if [[ "$AUTO_COMMIT" == "true" ]]; then
if git rev-parse --is-inside-work-tree >/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 <file>"
[[ -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 <file>"
[[ -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"
printf '%-15s %-45s %s\n' \
"qwen" \
"$QWEN_URL" \
"$RESEARCH_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 <command>
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 <file>
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 <file>
Manually review a document.
reset-failure <file>
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_<name>_URL
PROVIDER_<name>_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