ResearchForge Documentation
Local-first AI research and benchmarking workflow for teams that need evidence, not guesses.
- • 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
- • 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
The shortest path to a real ResearchForge run
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:
┌─────────────────────────────────────────────────────────────┐ │ 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.
Where does the eval script come from?
This depends on your project. ResearchForge handles all three cases:
protected_paths. No experiment can modify it. This is the guarantee that your benchmark stays stable across the entire experiment run.Requirements
| Requirement | Version | Why |
|---|---|---|
| Python | 3.12+ | The ResearchForge CLI and execution engine |
| Git | any recent | Worktree isolation — one worktree per experiment |
| Claude Code | latest | AI layer: reads papers, writes patches, writes eval scripts. |
| Cursor | latest | AI layer: same capabilities via @mentions and MDC rules. |
| One of Claude Code or Cursor | — | Required 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
pip install "researchforge[serve]"
Install with IDE integrations
# After pip install, register skills/rules: researchforge all install --user # Claude Code only: researchforge claude install # Cursor only: researchforge cursor install
Install from source
git clone https://github.com/forger-labs-hq/researchforge cd researchforge pip install -e ".[serve,dev]"
Docker (no Python on host)
docker run --rm -v "$PWD":/workspace -w /workspace \ ghcr.io/forger-labs-hq/researchforge:latest \ researchforge research search "your query"
/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.
# Recommended — start in the IDE /researchforge-start # or @researchforge-start
Quickstart (2 minutes)
Install ResearchForge, open your IDE, and start the guided workflow. This is the shortest route for real usage.
# 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 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 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.
Claude Code — full walkthrough
From first command to shipped branch. You type 5 things; Claude does the rest.
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
}))Cursor — full walkthrough
Same workflow via @researchforge-start. This repo already has a benchmark script — RF detects it automatically.
✨ 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.
What exactly is shared
| File / directory | What it contains | Both IDEs can |
|---|---|---|
| .researchforge/papers/ | Knowledge base — all retrieved arXiv papers | Read papers, add papers |
| .researchforge/contract.yaml | Objective, metric, protected paths, eval commands | Read contract, propose amendments |
| .researchforge/baseline.json | Frozen baseline measurement + HMAC | Read, cannot modify |
| .researchforge/hypotheses.yaml | Generated + reviewed hypotheses | Read, generate, approve/reject |
| .researchforge/plan.yaml | Approved experiment plan with patches | Read, generate, import |
| .researchforge/experiments/ | Per-experiment results JSON files | Read results, interpret |
| .researchforge/lineage.json | Full experiment DAG | Read, analyse, visualise |
| .researchforge/audit.log | Immutable append-only action log | Read (append-only) |
Example — resume mid-loop in a different IDE
.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
pip install "researchforge[serve]" researchforge all install --user # → ~/.claude/skills/ and ~/.cursor/rules/ researchforge all status
.researchforge/ state — start in one, continue in the other.Core concepts
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.
benchmarks/) and is never modified by the AI during experiments. The AI only patches your implementation code insrc/ or config/."""
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
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).
{
"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}
}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"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.
execution: screening_command: python benchmarks/evaluate.py --subset screening full_command: python benchmarks/evaluate.py --subset full result_file: artifacts/results.json
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")REJECTED (screen) in the lineage — they never run the expensive full eval, saving significant compute on non-promising hypotheses.research search
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.
researchforge research search "your query" [flags]
| Flag | Default | Description |
|---|---|---|
| --n | 20 | Max papers to store in the knowledge base |
| --min-score | 0.6 | Minimum relevance score (0–1.0) |
| --since | none | Only papers after this date (YYYY-MM-DD) |
| --categories | all | arXiv category filter (e.g. cs.LG,stat.ML) |
| --output | papers.json | Also save ranked results to this file |
| --rerank | true | Re-rank with cross-encoder after BM25 retrieval |
papers (manage knowledge base)
# 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.
researchforge baseline run [flags]
| Flag | Default | Description |
|---|---|---|
| --objective | required (wizard) | Plain-English objective — RF guesses metric name and direction |
| --n-runs | 1 | Average over N runs (use 3+ for noisy metrics) |
| --timeout | 20min | Timeout in minutes for the baseline eval |
researchforge baseline reset --confirm. This is intentional — it prevents "baseline creep" where you unconsciously measure against a moving target.# Check current baseline status researchforge baseline status # Reset (requires explicit confirmation) researchforge baseline reset --confirm
hypotheses
# 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
# 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.
researchforge run [flags]
| Flag | Default | Description |
|---|---|---|
| --stall | none | Stop after N consecutive non-improving experiments |
| --parallel | 4 | Max concurrent worktree subagents |
| --timeout | 20min | Per-experiment timeout (minutes, from contract) |
| --threshold | 0.01 | Minimum delta to count as improvement |
| --metric | contract | Override primary metric from contract |
| --screen-first | contract | Run screening pass before full benchmark |
| --dry-run | false | Print plan without executing |
| --experiments | all | Comma-separated IDs to run (e.g. exp-001,exp-003) |
| --worker | false | Enterprise: run as Hub worker (pull from queue) |
| --tags | none | Enterprise: worker hardware tags (e.g. gpu-a100) |
validate
researchforge validate [flags]
| Flag | Default | Description |
|---|---|---|
| --n | 3 | Number of validation runs |
| --experiment | best | Experiment ID to validate |
| --stdev-max | none | Fail if standard deviation exceeds this |
| --seeds | random | Comma-separated seeds (e.g. 42,123,456) |
ship
researchforge ship [flags]
| Flag | Default | Description |
|---|---|---|
| --experiment | best | Experiment ID to ship |
| --branch | auto | Branch name (default: feat/<id>-winner) |
| --report | .rf/report.json | Path for engineering report JSON |
| --pr | false | Open a GitHub draft PR after shipping |
hub
# 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
# 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 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 approvalplan.yaml — hypothesis format
Generated automatically by researchforge plan generate, or write by hand and import with researchforge plan import.
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-003Environment variables
# 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:
| Command | What it does |
|---|---|
| /researchforge-start | Begin a full research loop: search → baseline → hypotheses → run |
| /researchforge-baseline | Freeze the current baseline for the active project |
| /researchforge-run | Run the current experiment plan (with stall=3 by default) |
| /researchforge-results | Show the current experiment lineage and results |
| /researchforge-ship | Validate and ship the current best experiment |
| /researchforge-status | Check 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.
.researchforge/ state directory, so experiments started in one IDE are visible in the other.scikit-learn
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")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
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
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.
# 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
executor: type: docker image: python:3.12-slim # or your custom image build_context: . # uses your Dockerfile if present
# Or override at runtime: researchforge run --executor docker --image my-ml-image:latest
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.
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 2Hub & 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.
# 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.
# Stop after 3 consecutive non-improvements researchforge run --stall 3 --threshold 0.005
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.
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
...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.
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
# 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:
Lineage & audit log
Every action ResearchForge takes is recorded in .researchforge/audit.log — immutable, append-only, structured JSON.
# 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
{"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 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.
# 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
RESEARCHFORGE_HUB_URL=https://hub.yourcompany.com RESEARCHFORGE_API_KEY=rf_live_xxxxxxxxxxxx
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
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
# 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
# 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
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:
.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
Upgrading
# 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
researchforge migrate --dry-run first to preview any state format changes before applying them.