v0.2.0Apache 2.0Python 3.12+

ResearchForge Documentation

Local-first AI research and benchmarking workflow for teams that need evidence, not guesses.

💡
Start here: the open-source workflow is local and reproducible, while the enterprise layer adds shared control and team-scale coordination.
Open source
Included in the Apache-licensed CLI
  • • IDE-first workflow with Claude Code and Cursor
  • • literature search and ranking
  • • baseline, run, validate, and ship
  • • local worktrees, protected paths, and audit log
  • • Docker and local Python execution
Enterprise add-on
Optional team controls and shared infrastructure
  • • self-hosted Hub and approval workflow
  • • multi-user coordination across machines
  • • air-gapped deployment
  • • workload tagging and shared lineage
  • • policy and governance layers for regulated teams
Start here

The shortest path to a real ResearchForge run

2 min
01 · Install
Python 3.12+ and Git are enough to begin.
02 · Install for your IDE
Register the Claude/Cursor workflow once for your machine.
03 · Start in the IDE
Type the slash command or @mention and approve the steps.
bashbash
pip install "researchforge[serve]"
researchforge all install --user

# Open Claude Code or Cursor and run:
/researchforge-start
# or
@researchforge-start

This is the recommended entry point. The detailed command reference below is for advanced workflows, CI/CD, and automation — not the default path most users should start with.

How ResearchForge works

ResearchForge implements a six-stage loop that converts a research question into a validated, shippable result with full lineage:

The RF loopbash
  ┌─────────────────────────────────────────────────────────────┐
  │                    ResearchForge Loop                       │
  │                                                             │
  │  1. SEARCH     arXiv → ranked papers → local knowledge base │
  │       ↓                                                     │
  │  2. HYPOTHESES papers + domain → testable hypotheses        │
  │       ↓                                                     │
  │  3. BASELINE   freeze current metric cryptographically      │
  │       ↓                                                     │
  │  4. RUN        git worktrees × parallel subagents           │
  │                each variant isolated, baseline untouched    │
  │       ↓                                                     │
  │  5. VALIDATE   re-run winner N times → confirm stability    │
  │       ↓                                                     │
  │  6. SHIP       clean branch + report + audit trail          │
  └─────────────────────────────────────────────────────────────┘

The key design principle: nothing moves until it has evidence. The baseline is immovable. Experiments run in isolation. The winner is only shipped after validation confirms it isn't a lucky seed.

💡
You write the eval script once — or Claude Code writes it for you. ResearchForge runs it to measure the baseline, then runs it again inside each experiment worktree. The AI (Claude Code / Cursor) only patches your implementation code — it never touches your benchmark or evaluation logic once the contract is approved.

Where does the eval script come from?

This depends on your project. ResearchForge handles all three cases:

You already have a benchmark script
ResearchForge scans your repo and auto-detects scripts in benchmarks/, evaluate.py, scripts/eval.py, etc. It pre-fills the contract with full_command: python benchmarks/evaluate.py. You just review and approve.
🤖
You have tests but no benchmark script
Claude Code / Cursor writes the eval script for you during the project setup phase, before any experiments run. It writes artifacts/results.json in the standard format. You review it, then approve the contract.
✍️
You write it yourself
The contract wizard outputs full_command: "# TODO: command that writes the result file" as a placeholder. You write the eval script (or have Claude Code or Cursor write it), point the contract at it, and approve.
Once you approve the contract, the eval script's path is locked into protected_paths. No experiment can modify it. This is the guarantee that your benchmark stays stable across the entire experiment run.

Requirements

Claude Code or Cursor is required for the AI-driven workflow. ResearchForge's core value — reading papers, writing hypotheses, creating experiment patches, and writing the eval script — is delivered by Claude Code or Cursor. The Python CLI is the execution engine; the IDE is the brain.
RequirementVersionWhy
Python3.12+The ResearchForge CLI and execution engine
Gitany recentWorktree isolation — one worktree per experiment
Claude CodelatestAI layer: reads papers, writes patches, writes eval scripts.
CursorlatestAI layer: same capabilities via @mentions and MDC rules.
One of Claude Code or CursorRequired for AI-driven mode. CLI-only mode works without, but you write everything manually.

Install

Requires Python 3.12+ and Git. No Node.js required.

Standard install

pipbash
pip install "researchforge[serve]"

Install with IDE integrations

bashbash
# After pip install, register skills/rules:
researchforge all install --user

# Claude Code only:
researchforge claude install

# Cursor only:
researchforge cursor install

Install from source

bashbash
git clone https://github.com/forger-labs-hq/researchforge
cd researchforge
pip install -e ".[serve,dev]"

Docker (no Python on host)

bashbash
docker run --rm -v "$PWD":/workspace -w /workspace \
  ghcr.io/forger-labs-hq/researchforge:latest \
  researchforge research search "your query"
The Docker image includes Python 3.12, all dependencies, and Git. Mount your project at /workspace.

The IDE-first workflow

For most teams, this is the recommended path. You do not need to memorize CLI commands. In Claude Code or Cursor, you just start the workflow and approve each step.

bashbash
# Recommended — start in the IDE
/researchforge-start
# or
@researchforge-start
💡
This is the path most users should follow first. The CLI commands below are the underlying engine for CI/CD, automation, and advanced users.

Quickstart (2 minutes)

Install ResearchForge, open your IDE, and start the guided workflow. This is the shortest route for real usage.

bashbash
# 1. Install
pip install "researchforge[serve]"

# 2. Open Claude Code or Cursor
#    Type one of these:
/researchforge-start
# or
@researchforge-start

# 3. Approve the contract, run the baseline, and let the agent do the rest
The commands shown in this marketing site are examples of the ResearchForge CLI surface. This repo is a Next.js marketing site, not the Python CLI implementation itself, so they are documentation examples rather than commands you can execute in this project folder.

The IDE-first workflow

The intended way to use ResearchForge is through your IDE. Type one slash command or @mention and Claude Code / Cursor takes over: scans your repo, writes the eval script if needed, searches literature, generates hypotheses, runs experiments, and presents results — asking your approval at every consequential step. You approve; they execute.

💡
The CLI commands further in this page are what the IDE runs under the hood. You can run them manually for CI/CD, but you never have to write them yourself.
Claude Code / Cursor creates all of this for you
The eval scriptbenchmarks/evaluate.py that writes artifacts/results.json — Claude writes it from scratch if you don't have one. You never need to.
The contract.researchforge/contract.yaml — objective, metric, protected paths, eval commands
HypothesesTestable ideas grounded in arXiv papers — for your approval before anything runs
Experiment patchesGit diffs applied to src/ only — eval scripts and tests are cryptographically locked
Engineering reportFull lineage, results, and reasoning shipped alongside the winner branch

The full loop — search to shipped branch

The complete research pipeline as it runs inside your IDE. Claude Code or Cursor drives every step — you only type your objective and approvals. Each dashboard panel below is presented inline in the chat, exactly as you’d see it in a real session.

01 Query
arXiv full-text search + relevance ranking
02 Landscape
Research directions · landmark papers · hypotheses
03 Experiments
Parallel worktrees · eval script · results dashboard
04 Ship
Validation × N · clean branch · engineering report
Claude Code · researchforge · ROGII wellbore geology
You
/researchforge-start
Claude Code / Cursor
Scanning repository...
📁 Python project · src/predictor/ · tests/ · pyproject.toml
⚠️ No benchmark script detected.
What do you want to improve?
You
Improve RMSE on the wellbore geology prediction task. Input is LWD log sequences, target is TVT (rock layer position).
Claude Code / Cursor
IDE sets up: writing benchmarks/evaluate.py · locking evaluation contract · defining metric & constraints
Searching arXiv for relevant literature...
01 · Literature Queryfull arXiv search · relevance scoring · stored to knowledge base847 candidates · 20 stored
[0.94]Sequence-based lithology prediction using deep learning
[0.91]Transfer learning for geophysical core analysis
[0.88]Attention mechanisms in subsurface sequential data
[0.85]Self-supervised pre-training on well log representations
[0.82]Multi-task learning for formation evaluation from LWD
02 · Research Landscapedirections · landmark papers · evidence claims · generated by IDE3 directions · 7 hypotheses
Sequential Modelling8 papers
🏆 "Sequence-based lithology prediction" [0.94]
LSTM/GRU encoders outperform feature-based — 3 studies agree
Multi-task Learning4 papers
🏆 "Multi-task formation evaluation" [0.85]
Auxiliary gamma+resistivity prediction → +10–15% on primary
Transfer & Self-supervised5 papers
🏆 "Transfer learning for geophysical" [0.91]
Pre-training on adjacent basins helps; limited evidence for LWD self-supervised
02 · Hypothesesgrounded in landmark papers · scored by evidence strength · your approval required
hyp-001Sequential encoding with depth positional normalisation
hyp-002Multi-task auxiliary prediction (gamma + resistivity)
hyp-003Per-well feature normalisation before training
hyp-004Self-supervised pre-training on unlabelled logs
hyp-005-0073 more lower-priority hypotheses
Approve hyp-001, hyp-002, hyp-003?
You
Approved. Run them all.
Claude Code / Cursor
Baseline frozen: rmse=15.2441·9 experiments · 4 parallel worktrees · main branch untouched
03 · Experiment Dashboardparallel git worktrees · eval script runs in each · live results vs baselinebaseline=15.24 · rmse lower is better
IDStatusHypothesis patchRMSEΔ
exp-001PASSPer-well normalisation (hyp-003)12.89+2.35
exp-002PASSGRU encoder + depth encoding (hyp-001)11.44+3.80
exp-003PASSMulti-task: gamma + resistivity (hyp-002)9.22+5.98
exp-004FAILSelf-supervised pre-training16.11−0.87
exp-005PASSMulti-scale rolling window (5/20/50)7.88+7.36
exp-006PASSGRU + per-well norm (builds on exp-001)7.11+8.13
exp-007REJETransformer encoder — constraint ✗ p95>budget9.03+6.21
exp-008REJEEnsemble weighting — Δ below threshold15.33−0.09
exp-009WINNGRU + multi-task + multi-scale (exp-003 × exp-005)6.10+9.14
04 · Validation — exp-009 × 5 runs
Re-running winner with different seeds to confirm stability before shipping
6.0891 · 6.1204 · 6.0977 · 6.1108 · 6.0985
✓ mean=6.1033 stdev=0.0114 — Stable
Ship as feat/multi-task-gru-winner?
You
Ship it.
Claude Code / Cursor
04 · Engineering Reportauto-generated · full lineage · shipped with branchexp-009 · feat/multi-task-gru-winner
Primary metric:rmse 15.24 → 6.10
Improvement:60.0% reduction
Papers:20 top results from arXiv
Hypotheses:7 generated · 3 tested
Experiments:9 run · 3 rejected · 1 winner
Validation:mean=6.10 stdev=0.011 n=5
✓ Branch: feat/multi-task-gru-winner
✓ Only winning changes applied — checkout clean
✓ Lineage: 9 experiments · 3 rejected with reasons · full audit trail
✓ Report: .researchforge/reports/exp-009-final.json

Claude Code — full walkthrough

From first command to shipped branch. You type 5 things; Claude does the rest.

Claude Code — chat session
You
/researchforge-start
Claude Code
Scanning repo... 📁 src/classifier/, tests/
⚠️ No benchmark script detected.
What do you want to improve?
You
Text classification. Improve F1, keep p95 latency under 200ms.
Claude Code
Writing benchmarks/evaluate.py:
import json, pathlib, time
from src.classifier import Classifier
clf = Classifier()
latencies, correct = [], []
for item in load_test_data():
    t0 = time.perf_counter()
    correct.append(clf.predict(item["text"]) == item["label"])
    latencies.append((time.perf_counter()-t0)*1000)
pathlib.Path("artifacts").mkdir(exist_ok=True)
pathlib.Path("artifacts/results.json").write_text(json.dumps({
    "schema_version": 1,
    "primary_metric": {"name": "f1", "value": compute_f1(correct)},
    "secondary_metrics": {
        "p95_latency_ms": sorted(latencies)[int(len(latencies)*0.95)]
    },
    "sample_count": len(correct), "seed": 42
}))
Contract: f1 maximize · constraint: p95_ms ≤ 200
editable: src/, config/ · protected: benchmarks/, tests/
Approve?
You
Approved.
Claude Code
✓ Locked. Searching arXiv... 3 hypotheses generated:
hyp-001: Early-exit head on layer 4 (paper [0.88])
hyp-002: Knowledge distillation 12→4 layers (paper [0.91])
hyp-003: INT8 quantization of attention (paper [0.94])
Approve all three?
You
Yes.
Claude Code
3 parallel worktrees · main branch untouched
exp-001 f1=0.889 p95=94ms Δ=+0.021 ✓ PASS
exp-002 f1=0.901 p95=88ms Δ=+0.033 ✓ PASS ← winner
exp-003 f1=0.864 p95=71ms Δ=-0.004 ✗ REJECTED
Validated × 3 · mean=0.9012 stdev=0.0003 ✓
Ship as feat/distillation-winner?
You
Ship it.
Claude Code
✓ Branch: feat/distillation-winner
✓ Report: .researchforge/reports/exp-002-final.json
✓ Checkout clean · 3 experiments in lineage

Cursor — full walkthrough

Same workflow via @researchforge-start. This repo already has a benchmark script — RF detects it automatically.

Cursor — chat session
You
@researchforge-start Reduce RAG cost without ROUGE-L dropping below 0.85.
Cursor
Found benchmarks/eval_rag.py
Contract: rouge maximize · constraint: cost ≤ $0.005/q
Approve? (y/n)
You
y
Cursor
2 hypotheses from literature:
hyp-001: gpt-4o-mini for non-critical steps
hyp-002: Result caching for repeated queries
Run both?
You
y
Cursor
exp-001 rouge=0.871 cost=$0.0028 ✓
exp-002 rouge=0.889 cost=$0.0041 ✓ winner
Validated × 3. Ship?
You
Ship.
Cursor
✓ Branch + report + lineage ready.

✨ Cross-IDE state sharing

This is one of ResearchForge's strongest features and almost always overlooked. Both Claude Code and Cursor read and write the exact same .researchforge/ directory. They share 100% of state — papers, hypotheses, baselines, experiment results, lineage — in real time.

🔄Start in Claude Code. Continue in Cursor. Or run both simultaneously.
🤖
One searches, one ships
Have Claude Code run the literature search and generate hypotheses while Cursor reviews results and prepares the ship command — both working on the same project state.
🔁
Resume anywhere
Start a research loop in Claude Code, close it, open Cursor, type @researchforge-results — it picks up exactly where Claude left off. No re-running, no lost state.
👥
Two AI brains, one experiment set
Claude Code generates hypotheses from papers. Cursor writes the experiment patches. Both write to the same lineage. The results are indistinguishable from a single-IDE run.

What exactly is shared

File / directoryWhat it containsBoth IDEs can
.researchforge/papers/Knowledge base — all retrieved arXiv papersRead papers, add papers
.researchforge/contract.yamlObjective, metric, protected paths, eval commandsRead contract, propose amendments
.researchforge/baseline.jsonFrozen baseline measurement + HMACRead, cannot modify
.researchforge/hypotheses.yamlGenerated + reviewed hypothesesRead, generate, approve/reject
.researchforge/plan.yamlApproved experiment plan with patchesRead, generate, import
.researchforge/experiments/Per-experiment results JSON filesRead results, interpret
.researchforge/lineage.jsonFull experiment DAGRead, analyse, visualise
.researchforge/audit.logImmutable append-only action logRead (append-only)

Example — resume mid-loop in a different IDE

Session A: Claude Code (earlier today)
✓ researchforge research search "wellbore geology"
✓ researchforge hypotheses generate → 7 hypotheses
✓ researchforge baseline run → rmse=15.24
⚡ Claude Code session closed — laptop restarted
Session B: Cursor (right now — different IDE, same project directory)
You
@researchforge-results
Cursor
Reading .researchforge/ ...
📋 Project: wellbore-geology · baseline: rmse=15.24
📚 20 papers stored · 7 hypotheses (3 approved, awaiting run)
Ready to run experiments. Approve and I'll start?
You
Yes, run them.
Cursor
Running 3 experiments from where Claude Code left off... (same lineage, same baseline)
💡
Commit .researchforge/ to git and your whole team shares the research state. Every team member's Claude Code or Cursor session will see the same papers, hypotheses, and results — regardless of machine.

Install IDE skills/rules

bashbash
pip install "researchforge[serve]"
researchforge all install --user   # → ~/.claude/skills/ and ~/.cursor/rules/
researchforge all status
Claude Code
/researchforge-start
/researchforge-baseline
/researchforge-run
/researchforge-results
/researchforge-ship
Cursor
@researchforge-start
@researchforge-baseline
@researchforge-run
@researchforge-results
@researchforge-ship
Both IDEs share .researchforge/ state — start in one, continue in the other.

Core concepts

Baseline
A frozen measurement of your metric committed before any experiments run. Immovable unless explicitly reset. The reference all improvements are measured against.
Hypothesis
A testable idea grounded in retrieved literature. e.g. “Layer norm before attention (paper-003) will improve accuracy by ≥2%”. Each hypothesis maps to one or more AI-generated patches.
Experiment
One isolated run of your code with a specific config applied. Gets its own git worktree, its own venv, its own env vars. Completely independent from every other run.
Worktree
A git worktree is a secondary checkout of your repo at the baseline commit. Your main branch is never touched. When the experiment finishes, the worktree is cleaned up.
Subagent
The process running inside a worktree. Runs the eval script after the AI patch is applied. Reports results via artifacts/results.json.
Lineage
The full directed acyclic graph of baseline → experiments → rejections → promotions. Every result, every rejection reason, every config is immutably stored.
Stall
N consecutive experiments with no improvement over the current best. When stall is reached, the run loop stops automatically.
Protected path
A file or directory no experiment may modify. Enforced cryptographically — any experiment that touches one is killed immediately and the violation logged.

How metrics are captured — the results.json contract

ResearchForge does not scan stdout. Your benchmark script writes a structured artifacts/results.json file after every run. ResearchForge reads that file to compare experiments against the baseline.

💡
You write this script once when setting up the project. It lives in a protected path (e.g. benchmarks/) and is never modified by the AI during experiments. The AI only patches your implementation code insrc/ or config/.
benchmarks/evaluate.py — your eval script (write this once)python
"""
Your benchmark script. Lives in a protected path.
ResearchForge runs this to measure the baseline, then runs it again
inside each experiment worktree (with the AI's patch applied to src/).
"""
import json
import pathlib
from my_model import load_and_eval  # ← AI can patch this

# Run your evaluation
accuracy, p95_ms, cost = load_and_eval(dataset="benchmark-v2")

# Write results in the standard ResearchForge format
pathlib.Path("artifacts").mkdir(exist_ok=True)
pathlib.Path("artifacts/results.json").write_text(json.dumps({
    "schema_version": 1,
    "primary_metric": {"name": "accuracy", "value": accuracy},
    "secondary_metrics": {
        "p95_latency_ms": p95_ms,
        "average_cost_usd": cost,
    },
    "sample_count": 1200,
    "seed": 42,
    "metadata": {"dataset_version": "benchmark-v2"},
}), encoding="utf-8")

print("evaluation complete")

What happens during an experiment

1. ResearchForge creates a git worktree at the baseline commit
.rf-worktrees/exp-003/ ← isolated copy of your repo
2. Claude Code / Cursor generates a git patch from the hypothesis
diff --git a/src/model.py b/src/model.py
+NORMALIZATION = "layer"
+USE_POSITIONAL_ENCODING = True
3. Patch applied to worktree (editable paths only)
git apply change.patch ✓
4. Path guard checks patch didn't touch protected paths
benchmarks/ → protected ✓ untouched
5. Your eval script runs in the worktree
python benchmarks/evaluate.py --subset full
6. ResearchForge reads artifacts/results.json
accuracy=0.891 → Δ+0.031 vs baseline 0.860 → PASS ✓

Multiple metrics & constraints

Your eval script can write as many secondary metrics as needed. Specify hard constraints to automatically reject experiments that trade too much quality for speed (or cost).

artifacts/results.json schemajson
{
  "schema_version": 1,
  "primary_metric": {"name": "accuracy", "value": 0.891},
  "secondary_metrics": {
    "p95_latency_ms": 143.2,
    "average_cost_usd": 0.0031,
    "f1_macro": 0.877
  },
  "sample_count": 1200,
  "seed": 42,
  "metadata": {"dataset_version": "benchmark-v2", "model_params": 7340032}
}
contract — define constraints in the objectiveyaml
objective:
  description: >
    Improve accuracy on the classification benchmark while keeping
    p95 latency under 200ms and cost under $0.005 per query.
  primary_metric:
    name: accuracy
    direction: maximize
  hard_constraints:
    - name: p95_latency_ms
      operator: <=
      value: 200
    - name: average_cost_usd
      operator: <=
      value: 0.005
💡
The contract wizard guesses metric name and direction from plain-English objectives. "Improve accuracy"accuracy / maximize. "Reduce p95 latency below 200ms"latency_ms / minimize. You can always edit the contract YAML manually afterward.

Screening funnel

For slow full benchmarks, define a fast screening subset. Experiments must beat the baseline on the cheap screen before the expensive full eval runs.

contract YAMLyaml
execution:
  screening_command: python benchmarks/evaluate.py --subset screening
  full_command:      python benchmarks/evaluate.py --subset full
  result_file: artifacts/results.json
benchmarks/evaluate.py — handle --subsetpython
import sys
import json, pathlib

subset = "screening" if "--subset" in sys.argv and "screening" in sys.argv else "full"
# screening = fast 10% sample; full = complete eval
dataset_size = 120 if subset == "screening" else 1200

accuracy = run_eval(n_samples=dataset_size)

pathlib.Path("artifacts").mkdir(exist_ok=True)
pathlib.Path("artifacts/results.json").write_text(json.dumps({
    "schema_version": 1,
    "primary_metric": {"name": "accuracy", "value": accuracy},
    "sample_count": dataset_size,
    "seed": 42,
}), encoding="utf-8")
Experiments that fail screening are marked REJECTED (screen) in the lineage — they never run the expensive full eval, saving significant compute on non-promising hypotheses.

Searches arXiv end-to-end, ranks results by relevance, stores top papers in the local knowledge base. Searches the full arXiv corpus — not just a sample.

bashbash
researchforge research search "your query" [flags]
FlagDefaultDescription
--n20Max papers to store in the knowledge base
--min-score0.6Minimum relevance score (0–1.0)
--sincenoneOnly papers after this date (YYYY-MM-DD)
--categoriesallarXiv category filter (e.g. cs.LG,stat.ML)
--outputpapers.jsonAlso save ranked results to this file
--reranktrueRe-rank with cross-encoder after BM25 retrieval

papers (manage knowledge base)

bashbash
# List stored papers
researchforge papers list

# Show details for a specific paper
researchforge papers show paper-003

# Export all papers (for air-gap transfer)
researchforge papers export papers.json

# Import papers from export file
researchforge papers import papers.json

# Delete a paper
researchforge papers delete paper-007

baseline run

Runs your benchmark script once to freeze the current metric as the immovable reference. Must be run before any experiments.

bashbash
researchforge baseline run [flags]
FlagDefaultDescription
--objectiverequired (wizard)Plain-English objective — RF guesses metric name and direction
--n-runs1Average over N runs (use 3+ for noisy metrics)
--timeout20minTimeout in minutes for the baseline eval
Once frozen, the baseline cannot change without researchforge baseline reset --confirm. This is intentional — it prevents "baseline creep" where you unconsciously measure against a moving target.
bashbash
# Check current baseline status
researchforge baseline status

# Reset (requires explicit confirmation)
researchforge baseline reset --confirm

hypotheses

bashbash
# Generate hypotheses from stored papers
researchforge hypotheses generate [--n 10] [--model claude-sonnet]

# Interactive review: approve/reject/edit each hypothesis
researchforge hypotheses review

# List all hypotheses and their status
researchforge hypotheses list

# Show details for one hypothesis
researchforge hypotheses show hyp-002

# Manually add a hypothesis
researchforge hypotheses add \
  --title "Per-well normalization" \
  --description "Normalize each well's features independently before training" \
  --evidence "Expert domain knowledge, paper-007"

# Approve/reject without interactive review
researchforge hypotheses approve hyp-001 hyp-003
researchforge hypotheses reject hyp-005 --reason "compute cost too high"

plan

bashbash
# Auto-generate experiment plan from approved hypotheses
researchforge plan generate

# Import a hand-written plan.yaml
researchforge plan import plan.yaml

# View current plan
researchforge plan show

# Validate plan (check for conflicts, missing scripts, etc.)
researchforge plan validate

run

Executes all planned experiments in parallel git worktrees. The central command.

bashbash
researchforge run [flags]
FlagDefaultDescription
--stallnoneStop after N consecutive non-improving experiments
--parallel4Max concurrent worktree subagents
--timeout20minPer-experiment timeout (minutes, from contract)
--threshold0.01Minimum delta to count as improvement
--metriccontractOverride primary metric from contract
--screen-firstcontractRun screening pass before full benchmark
--dry-runfalsePrint plan without executing
--experimentsallComma-separated IDs to run (e.g. exp-001,exp-003)
--workerfalseEnterprise: run as Hub worker (pull from queue)
--tagsnoneEnterprise: worker hardware tags (e.g. gpu-a100)

validate

bashbash
researchforge validate [flags]
FlagDefaultDescription
--n3Number of validation runs
--experimentbestExperiment ID to validate
--stdev-maxnoneFail if standard deviation exceeds this
--seedsrandomComma-separated seeds (e.g. 42,123,456)

ship

bashbash
researchforge ship [flags]
FlagDefaultDescription
--experimentbestExperiment ID to ship
--branchautoBranch name (default: feat/<id>-winner)
--report.rf/report.jsonPath for engineering report JSON
--prfalseOpen a GitHub draft PR after shipping

hub

bashbash
# Start local hub server
researchforge hub start [--port 8080]

# Hub status
researchforge hub status

# List all experiments across team (requires Hub API key)
researchforge hub experiments --workload nlp-v2

# Approve queued experiments (team lead)
researchforge hub approve exp-012 exp-013

all install

bashbash
# Install both Claude Code skills and Cursor rules
researchforge all install [--user] [--global]

# --user: installs to ~/.claude/skills/ and ~/.cursor/rules/
# --global: installs to system-wide config (requires admin)

# Verify installation
researchforge all status

researchforge.yaml — complete reference

researchforge.yamlyaml
# ResearchForge project configuration — full reference
version: "1"

# ── Execution contract ─────────────────────────────────────────
execution:
  setup_command: python -m pip install -e .
  screening_command: python benchmarks/evaluate.py --subset screening
  full_command:      python benchmarks/evaluate.py --subset full
  result_file: artifacts/results.json   # ← your eval script writes here
  timeout_minutes: 20
  max_experiments: 8

# ── Run loop ─────────────────────────────────────────────────────
run:
  stall: 3                  # stop after N non-improvements (optional)
  parallel: 4               # max concurrent subagents
  threshold: 0.005          # minimum Δ to count as improvement (0.5%)

# ── Executor ─────────────────────────────────────────────────────
executor:
  type: venv                # venv | docker | remote
  # docker options:
  image: null               # e.g. "python:3.12-slim"
  build_context: null       # path to Dockerfile context

# ── Permissions ──────────────────────────────────────────────────
permissions:
  editable_paths:
    - src/            # AI can patch these
    - config/
  protected_paths:
    - benchmarks/     # eval script — AI cannot touch
    - evaluator/
    - tests/

# ── Paper search ─────────────────────────────────────────────────
search:
  categories: []            # arXiv category filter
  min_score: 0.60
  max_papers: 20
  rerank: true

# ── Enterprise Hub ───────────────────────────────────────────────
hub:
  url: ${RESEARCHFORGE_HUB_URL}
  api_key: ${RESEARCHFORGE_API_KEY}
  workload: null            # tag all runs under this workload name
  require_approval: false   # queue experiments for team lead approval

plan.yaml — hypothesis format

Generated automatically by researchforge plan generate, or write by hand and import with researchforge plan import.

plan.yamlyaml
version: "1"
baseline_commit: abc1234
baseline_metric: 15.2441

experiments:
  - id: exp-001
    hypothesis: hyp-005
    description: Per-well normalization before feature engineering
    env:
      NORMALIZE_PER_WELL: "true"
      SCALER: standard

  - id: exp-002
    hypothesis: hyp-006
    description: Multi-scale rolling window features
    env:
      WINDOW_SIZES: "5,20,50"
      FEATURE_TYPE: rolling

  - id: exp-003
    hypothesis: hyp-001
    description: GRU encoder with depth positional encoding
    env:
      MODEL_TYPE: gru
      POSITIONAL_ENCODING: depth_normalized
      HIDDEN_SIZE: "128"
    # This experiment builds on exp-001 (run exp-001 first)
    depends_on: exp-001

  - id: exp-004
    hypothesis: hyp-003
    description: Multi-task aux prediction
    env:
      AUX_TARGETS: "gamma,resistivity"
      AUX_WEIGHT: "0.3"
    # Only run if exp-003 passed
    requires_pass: exp-003

Environment variables

.env.local or shellbash
# Required for enterprise features only
RESEARCHFORGE_HUB_URL=https://hub.yourcompany.com
RESEARCHFORGE_API_KEY=rf_live_xxxxxxxxxxxx

# Optional: override default model for hypothesis generation
RESEARCHFORGE_LLM=claude-sonnet-4-5   # default
# RESEARCHFORGE_LLM=gpt-4.1
# RESEARCHFORGE_LLM=http://localhost:11434/api (Ollama)

# Air-gap mode: disable all external calls
RF_OFFLINE=false
RF_ARXIV_DISABLED=false

# Logging
RF_LOG_LEVEL=info          # debug | info | warn | error

Claude Code

After researchforge claude install, the following slash commands are available in any Claude Code session:

CommandWhat it does
/researchforge-startBegin a full research loop: search → baseline → hypotheses → run
/researchforge-baselineFreeze the current baseline for the active project
/researchforge-runRun the current experiment plan (with stall=3 by default)
/researchforge-resultsShow the current experiment lineage and results
/researchforge-shipValidate and ship the current best experiment
/researchforge-statusCheck ResearchForge installation and project status

Cursor

After researchforge cursor install, use @researchforge-start in Cursor chat. The MDC rule instructs Cursor to follow the RF workflow automatically.

💡
You can use both Claude Code and Cursor simultaneously — they share the same .researchforge/ state directory, so experiments started in one IDE are visible in the other.

scikit-learn

train.pypython
import os
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error
import numpy as np

# ResearchForge injects these via worktree env vars
model_type = os.environ.get("MODEL_TYPE", "gbm")
n_estimators = int(os.environ.get("N_ESTIMATORS", "100"))
normalize = os.environ.get("NORMALIZE", "false").lower() == "true"

X_train, X_val, y_train, y_val = load_data()

if normalize:
    scaler = StandardScaler()
    X_train = scaler.fit_transform(X_train)
    X_val = scaler.transform(X_val)

if model_type == "rf":
    model = RandomForestRegressor(n_estimators=n_estimators, random_state=42)
else:
    model = GradientBoostingRegressor(n_estimators=n_estimators, random_state=42)

model.fit(X_train, y_train)
preds = model.predict(X_val)
rmse = np.sqrt(mean_squared_error(y_val, preds))

# Write results.json — NOT print(RF_METRIC)
import json, pathlib
pathlib.Path("artifacts").mkdir(exist_ok=True)
pathlib.Path("artifacts/results.json").write_text(json.dumps({
    "schema_version": 1,
    "primary_metric": {"name": "rmse", "value": float(rmse)},
    "sample_count": len(y_val),
    "seed": 42,
}), encoding="utf-8")
plan.yaml (sklearn)yaml
experiments:
  - id: exp-001
    env: { MODEL_TYPE: gbm, N_ESTIMATORS: "200" }
  - id: exp-002
    env: { MODEL_TYPE: rf, N_ESTIMATORS: "200" }
  - id: exp-003
    env: { MODEL_TYPE: gbm, N_ESTIMATORS: "200", NORMALIZE: "true" }

PyTorch / Lightning

train.pypython
import os
import torch
import pytorch_lightning as pl

lr = float(os.environ.get("LR", "1e-3"))
hidden = int(os.environ.get("HIDDEN_SIZE", "256"))
dropout = float(os.environ.get("DROPOUT", "0.1"))
use_batchnorm = os.environ.get("BATCHNORM", "false") == "true"

class MyModel(pl.LightningModule):
    def __init__(self):
        super().__init__()
        self.net = build_net(hidden, dropout, use_batchnorm)
        self.lr = lr

    def training_step(self, batch, idx):
        loss = self.net(batch)
        return loss

    def validation_step(self, batch, idx):
        val_loss = self.net(batch)
        # Emit to ResearchForge
        self.log("rf_val_loss", val_loss)
        return val_loss

trainer.fit(model, train_dl, val_dl)

# Write results.json from best checkpoint metrics
import json, pathlib
best_val = trainer.callback_metrics.get("val_loss", float("inf"))
pathlib.Path("artifacts").mkdir(exist_ok=True)
pathlib.Path("artifacts/results.json").write_text(json.dumps({
    "schema_version": 1,
    "primary_metric": {"name": "val_loss", "value": float(best_val)},
    "sample_count": len(val_dl.dataset),
    "seed": 42,
}), encoding="utf-8")

HuggingFace Transformers

train.pypython
import os
from transformers import AutoModelForSequenceClassification, TrainingArguments, Trainer
from datasets import load_dataset
import numpy as np

model_name = os.environ.get("MODEL_NAME", "distilbert-base-uncased")
lr = float(os.environ.get("LR", "2e-5"))
epochs = int(os.environ.get("EPOCHS", "3"))
warmup = float(os.environ.get("WARMUP_RATIO", "0.1"))

model = AutoModelForSequenceClassification.from_pretrained(model_name)

args = TrainingArguments(
    output_dir="./out",
    learning_rate=lr,
    num_train_epochs=epochs,
    warmup_ratio=warmup,
    evaluation_strategy="epoch",
    save_strategy="no",
    load_best_model_at_end=False,
    report_to="none",  # disable wandb/mlflow — RF handles tracking
)

trainer = Trainer(model=model, args=args, ...)
trainer.train()
results = trainer.evaluate()

# Write results.json
import json, pathlib
pathlib.Path("artifacts").mkdir(exist_ok=True)
pathlib.Path("artifacts/results.json").write_text(json.dumps({
    "schema_version": 1,
    "primary_metric": {"name": "f1", "value": results["eval_f1"]},
    "secondary_metrics": {"eval_loss": results["eval_loss"]},
    "sample_count": len(eval_dataset),
    "seed": 42,
}), encoding="utf-8")

Python environments

Each worktree gets an isolated venv cloned from the baseline environment. This ensures every experiment starts from exactly the same dependency state.

bashbash
# ResearchForge uses your active venv as the template
# Activate your env, then run baseline:
source .venv/bin/activate
researchforge baseline run

# Worktrees are created at:
# .rf-worktrees/exp-001/venv/   ← isolated copy
# .rf-worktrees/exp-001/repo/   ← git worktree at baseline commit

# To add extra deps for a specific experiment, use the plan:
#   experiments:
#     - id: exp-001
#       pip_install: ["torch-geometric==2.5.0"]

Docker executor

researchforge.yamlyaml
executor:
  type: docker
  image: python:3.12-slim    # or your custom image
  build_context: .           # uses your Dockerfile if present
bashbash
# Or override at runtime:
researchforge run --executor docker --image my-ml-image:latest
With the Docker executor, each experiment gets its own container. The container is built once (cached), then a new container is spun up per experiment with the appropriate env vars injected.

GitHub Actions / CI

GitHub Actions is an example of how to run the ResearchForge loop in CI, not a built-in product connector. The CLI still reads your benchmark output file and runs worktrees locally.

.github/workflows/research.ymlyaml
name: ResearchForge experiments

on:
  workflow_dispatch:
  push:
    branches: [research/**]

jobs:
  run-experiments:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }

      - name: Install ResearchForge
        run: pip install "researchforge[serve]"

      - name: Install project deps
        run: pip install -r requirements.txt

      - name: Freeze baseline
        run: researchforge baseline run

      - name: Run experiments
        run: researchforge run --stall 3 --parallel 2

Hub & local monitor

The hub and local monitor are first-class features in the ResearchForge CLI. They are local-only dashboards that help you inspect runs, project state, and experiment lineage.

bashbash
# Start the local monitoring server for this project
researchforge serve --background

# Start the machine-wide hub dashboard
researchforge hub --background

# Inspect project state and monitor status
researchforge status
researchforge paths

Stall & convergence

The stall parameter stops the run loop after N consecutive experiments that all fail to improve on the current best result.

bashbash
# Stop after 3 consecutive non-improvements
researchforge run --stall 3 --threshold 0.005
exp-001 accuracy=0.891 best=0.860 Δ=+0.031 stall=0 → PASS
exp-002 accuracy=0.858 best=0.891 Δ=-0.033 stall=1 → REJECTED
exp-003 accuracy=0.862 best=0.891 Δ=-0.029 stall=2 → REJECTED
exp-004 accuracy=0.890 best=0.891 Δ=-0.001 stall=3 → STOP
Stall counts consecutive non-improvements from the current best, not the original baseline. It resets to 0 whenever a new best is found.

Subagents & parallelism

Each experiment runs as an isolated subagent: a git worktree at the baseline commit + its own venv (or Docker container) + specific env vars.

process diagrambash
Orchestrator
├─ subagent: exp-001  (.rf-worktrees/exp-001/)
│    env: MODEL_TYPE=gru, LR=1e-3
│    runs: python benchmarks/evaluate.py --subset full
│    writes: artifacts/results.json → accuracy=0.891
│    reports back to orchestrator
│    cleans up: ✓
│
├─ subagent: exp-002  (.rf-worktrees/exp-002/)  ← runs in parallel
│    env: MODEL_TYPE=transformer, LR=1e-3
│    ...
│
└─ subagent: exp-003  (.rf-worktrees/exp-003/)  ← runs in parallel
     env: MODEL_TYPE=gru, LR=3e-4
     ...
If a subagent crashes, is killed, or times out, the orchestrator marks it FAIL, cleans up the worktree, and continues with the remaining experiments. Your main checkout is never affected.

Protected paths

Protected paths are enforced cryptographically. Before the experiment runs, ResearchForge hashes all protected files and records the hashes in the experiment contract. After the run, it re-hashes and compares. Any divergence kills the experiment.

researchforge.yamlyaml
protected:
  - config/prod.yaml     # production config must not change
  - src/api/             # API surface must not change
  - tests/               # test suite must not be modified
  - data/raw/            # raw data must not be touched
bashbash
# Check which files are currently protected
researchforge protected list

# Verify no experiments have pending violations
researchforge protected verify

Security model

ResearchForge's security model is based on three principles:

🔒
Cryptographic baseline
The baseline commit hash and metric are stored with a SHA-256 HMAC. Any tampering with the baseline record is detected immediately.
🔒
Worktree isolation
Each experiment runs at the exact baseline commit. There is no way for experiment code to access uncommitted changes in the main working tree.
🔒
Protected path enforcement
File hashes are computed before and after each experiment run. Violations are recorded in the audit log with the specific file that was modified.

Lineage & audit log

Every action ResearchForge takes is recorded in .researchforge/audit.log — immutable, append-only, structured JSON.

bashbash
# View audit log
researchforge audit log [--last 20]

# Export full audit log as JSON
researchforge audit export audit.json

# Verify log integrity (detects tampering)
researchforge audit verify
audit.log entry (JSON Lines)json
{"ts":"2026-08-06T09:14:22Z","event":"baseline.frozen",
 "commit":"abc1234","metric":"rmse","value":15.2441,
 "user":"manas@forger-labs.com","hmac":"a3f7..."}

{"ts":"2026-08-06T09:31:05Z","event":"experiment.completed",
 "id":"exp-003","status":"pass","metric":"rmse","value":11.44,
 "delta":3.80,"stall_count":0,"worktree":".rf-worktrees/exp-003"}

Enterprise add-ons: Hub setup

These capabilities are additive to the open-source CLI. The base ResearchForge product remains local-first and framework-agnostic; the Enterprise layer adds shared infrastructure, governance, and team coordination.

The Enterprise Hub is not required for the OSS workflow. It is an optional shared control plane for teams that want a single dashboard, approvals, and multi-user coordination.

The Hub is a self-hosted server that aggregates team experiments, provides a shared dashboard, and exposes the approval queue. Runs as a Docker container inside your VPC.

bashbash
# Pull and start the hub
docker pull ghcr.io/forger-labs-hq/researchforge-hub:latest

docker run -d \
  --name rf-hub \
  -p 8080:8080 \
  -v /data/rf-hub:/data \
  -e RF_SECRET_KEY=$(openssl rand -hex 32) \
  -e RF_ADMIN_EMAIL=admin@yourcompany.com \
  ghcr.io/forger-labs-hq/researchforge-hub:latest

# Dashboard available at http://your-server:8080

The Hub dashboard shows: all team experiments with full lineage, live run status, metric history across days/weeks, approval queue for team lead review, and the full audit log export.

API key & workloads

~/.researchforgercbash
RESEARCHFORGE_HUB_URL=https://hub.yourcompany.com
RESEARCHFORGE_API_KEY=rf_live_xxxxxxxxxxxx
bash (per-run override)bash
RF_HUB_URL=https://hub.yourcompany.com \
RF_API_KEY=rf_live_xxxx \
researchforge run --workload search-ranking-v3

Workloads are stable project identifiers (for example a x-rf-workload header). All experiments tagged with the same workload are grouped together in the Hub dashboard for cross-run comparison.

CI/CD plugin

.github/workflows/rf-pr.ymlyaml
name: RF experiments on PR

on:
  pull_request:
    paths: ["src/**", "experiments/**"]

jobs:
  experiments:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install "researchforge[serve]"
      - run: pip install -r requirements.txt
      - run: |
          researchforge baseline run
          researchforge run --stall 3 --workload "pr-${{ github.event.number }}"
        env:
          RESEARCHFORGE_HUB_URL: ${{ secrets.RF_HUB_URL }}
          RESEARCHFORGE_API_KEY: ${{ secrets.RF_API_KEY }}
      - run: |
          # Post experiment results as PR comment
          researchforge hub comment \
            --pr ${{ github.event.number }} \
            --repo ${{ github.repository }}

Air-gapped deployment

bashbash
# 1. Export hub image on connected machine
docker save ghcr.io/forger-labs-hq/researchforge-hub:latest \
  | gzip > rf-hub.tar.gz

# 2. Export pip wheels on connected machine
pip download "researchforge[serve]" -d ./rf-wheels/
tar -czf rf-wheels.tar.gz rf-wheels/

# 3. Transfer both tarballs to air-gapped machine, then:
docker load < rf-hub.tar.gz
tar -xzf rf-wheels.tar.gz
pip install --no-index --find-links=./rf-wheels "researchforge[serve]"

# 4. Pre-populate paper cache from connected machine:
researchforge papers export papers.json   # on connected machine
# Transfer papers.json, then:
researchforge papers import papers.json   # on air-gapped machine

# 5. Disable external calls
export RF_OFFLINE=true
export RF_ARXIV_DISABLED=true

Multi-user coordination

bash (multi-machine worker setup)bash
# Machine A — researcher 1
RESEARCHFORGE_HUB_URL=https://hub.internal
RESEARCHFORGE_API_KEY=rf_live_xxxx
researchforge run --worker --tags gpu-a100 --parallel 4

# Machine B — researcher 2 (picks up remaining experiments from queue)
RESEARCHFORGE_HUB_URL=https://hub.internal
RESEARCHFORGE_API_KEY=rf_live_yyyy
researchforge run --worker --tags gpu-rtx6000 --parallel 2
In worker mode, each machine pulls unassigned experiments from the Hub queue. Experiments tagged requires:gpu-a100 in the plan only run on workers with that tag. The Hub aggregates all results into a unified lineage view.

Output artifacts

All ResearchForge state is stored in .researchforge/ in your project root:

directory structurebash
.researchforge/
├── config.yaml          ← researchforge.yaml (symlink)
├── baseline.json        ← frozen baseline record + HMAC
├── papers/              ← local knowledge base (JSON)
├── hypotheses.yaml      ← generated + reviewed hypotheses
├── plan.yaml            ← current experiment plan
├── experiments/
│   ├── exp-001.json     ← per-experiment result record
│   ├── exp-002.json
│   └── ...
├── reports/
│   └── exp-009-final.json  ← engineering report from ship
├── audit.log            ← append-only audit trail (JSON Lines)
└── lineage.json         ← full experiment DAG

Troubleshooting

artifacts/results.json not found after experiment run
Your eval script did not write artifacts/results.json. Check that it runs to completion and that pathlib.Path("artifacts/results.json").write_text(...) is called even on early stopping or error paths.
Baseline already frozen — use baseline reset
You're trying to run baseline run when one already exists. Run researchforge baseline status to see what's frozen. Use researchforge baseline reset --confirm to clear it.
Protected path violation: config/prod.yaml
An experiment modified a protected file. Check your experiment code for writes to that path. The experiment is already marked FAIL in the lineage.
Worktree checkout failed: unstaged changes
Git can't create a worktree when there are unstaged changes to tracked files. Run git stash or git add + git commit before running experiments.
venv clone failed: pip not found
ResearchForge couldn't find pip in the active venv. Make sure a venv is active (source .venv/bin/activate) before running baseline run.
Hub connection refused
The Hub server isn't reachable at the URL in RESEARCHFORGE_HUB_URL. Check that the Docker container is running (docker ps | grep rf-hub) and the port is open.

Upgrading

bashbash
# Upgrade to latest
pip install --upgrade "researchforge[serve]"

# Re-install IDE integrations after upgrade
researchforge all install --user

# Check version
researchforge --version

# Migrate project state to new format (if breaking change)
researchforge migrate --dry-run  # preview changes
researchforge migrate            # apply
When upgrading across major versions (e.g. 0.x → 1.x), always run researchforge migrate --dry-run first to preview any state format changes before applying them.
ResearchForge — Apache 2.0 · Made by Forger Labs HQ