researchloop 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- researchloop/__init__.py +1 -0
- researchloop/__main__.py +3 -0
- researchloop/cli.py +1138 -0
- researchloop/clusters/__init__.py +4 -0
- researchloop/clusters/monitor.py +199 -0
- researchloop/clusters/ssh.py +183 -0
- researchloop/comms/__init__.py +0 -0
- researchloop/comms/base.py +34 -0
- researchloop/comms/conversation.py +465 -0
- researchloop/comms/ntfy.py +95 -0
- researchloop/comms/router.py +71 -0
- researchloop/comms/slack.py +188 -0
- researchloop/core/__init__.py +0 -0
- researchloop/core/auth.py +78 -0
- researchloop/core/config.py +328 -0
- researchloop/core/credentials.py +38 -0
- researchloop/core/models.py +119 -0
- researchloop/core/orchestrator.py +910 -0
- researchloop/dashboard/__init__.py +0 -0
- researchloop/dashboard/app.py +15 -0
- researchloop/dashboard/auth.py +60 -0
- researchloop/dashboard/routes.py +912 -0
- researchloop/dashboard/templates/base.html +84 -0
- researchloop/dashboard/templates/login.html +12 -0
- researchloop/dashboard/templates/loop_detail.html +58 -0
- researchloop/dashboard/templates/loops.html +61 -0
- researchloop/dashboard/templates/setup.html +14 -0
- researchloop/dashboard/templates/sprint_detail.html +109 -0
- researchloop/dashboard/templates/sprints.html +48 -0
- researchloop/dashboard/templates/studies.html +18 -0
- researchloop/dashboard/templates/study_detail.html +64 -0
- researchloop/db/__init__.py +5 -0
- researchloop/db/database.py +86 -0
- researchloop/db/migrations.py +172 -0
- researchloop/db/queries.py +351 -0
- researchloop/runner/__init__.py +1 -0
- researchloop/runner/claude.py +169 -0
- researchloop/runner/job_templates/sge.sh.j2 +319 -0
- researchloop/runner/job_templates/slurm.sh.j2 +336 -0
- researchloop/runner/main.py +156 -0
- researchloop/runner/pipeline.py +272 -0
- researchloop/runner/templates/fix_issues.md.j2 +11 -0
- researchloop/runner/templates/idea_generator.md.j2 +16 -0
- researchloop/runner/templates/red_team.md.j2 +15 -0
- researchloop/runner/templates/report.md.j2 +31 -0
- researchloop/runner/templates/research_sprint.md.j2 +51 -0
- researchloop/runner/templates/summarizer.md.j2 +7 -0
- researchloop/runner/upload.py +153 -0
- researchloop/schedulers/__init__.py +11 -0
- researchloop/schedulers/base.py +43 -0
- researchloop/schedulers/local.py +188 -0
- researchloop/schedulers/sge.py +163 -0
- researchloop/schedulers/slurm.py +179 -0
- researchloop/sprints/__init__.py +0 -0
- researchloop/sprints/auto_loop.py +458 -0
- researchloop/sprints/manager.py +750 -0
- researchloop/studies/__init__.py +0 -0
- researchloop/studies/manager.py +102 -0
- researchloop-0.1.0.dist-info/METADATA +596 -0
- researchloop-0.1.0.dist-info/RECORD +63 -0
- researchloop-0.1.0.dist-info/WHEEL +4 -0
- researchloop-0.1.0.dist-info/entry_points.txt +3 -0
- researchloop-0.1.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Study management -- syncs study configuration to the database."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from researchloop.core.config import ClusterConfig, Config
|
|
12
|
+
from researchloop.db.database import Database
|
|
13
|
+
|
|
14
|
+
from researchloop.db import queries
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StudyManager:
|
|
20
|
+
"""High-level operations on research studies.
|
|
21
|
+
|
|
22
|
+
Reads study definitions from :class:`Config` and persists them into
|
|
23
|
+
the database so they can be referenced at runtime.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, db: Database, config: Config) -> None:
|
|
27
|
+
self.db = db
|
|
28
|
+
self.config = config
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# Sync from config
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
async def sync_from_config(self) -> None:
|
|
35
|
+
"""Upsert every study from the TOML config into the database.
|
|
36
|
+
|
|
37
|
+
Studies that already exist are updated with the latest values;
|
|
38
|
+
new studies are created.
|
|
39
|
+
"""
|
|
40
|
+
for study_cfg in self.config.studies:
|
|
41
|
+
existing = await queries.get_study(self.db, study_cfg.name)
|
|
42
|
+
config_json = json.dumps(asdict(study_cfg))
|
|
43
|
+
|
|
44
|
+
if existing is None:
|
|
45
|
+
logger.info("Creating study %r in database", study_cfg.name)
|
|
46
|
+
await queries.create_study(
|
|
47
|
+
self.db,
|
|
48
|
+
name=study_cfg.name,
|
|
49
|
+
cluster=study_cfg.cluster,
|
|
50
|
+
description=study_cfg.description or None,
|
|
51
|
+
claude_md_path=study_cfg.claude_md_path or None,
|
|
52
|
+
sprints_dir=study_cfg.sprints_dir,
|
|
53
|
+
config_json=config_json,
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
logger.info("Updating study %r in database", study_cfg.name)
|
|
57
|
+
await queries.update_study(
|
|
58
|
+
self.db,
|
|
59
|
+
study_cfg.name,
|
|
60
|
+
cluster=study_cfg.cluster,
|
|
61
|
+
description=study_cfg.description or None,
|
|
62
|
+
claude_md_path=study_cfg.claude_md_path or None,
|
|
63
|
+
sprints_dir=study_cfg.sprints_dir,
|
|
64
|
+
config_json=config_json,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
logger.info(
|
|
68
|
+
"Study sync complete: %d study/studies processed",
|
|
69
|
+
len(self.config.studies),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Lookups
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
async def get(self, name: str) -> dict | None:
|
|
77
|
+
"""Return a single study by name, or ``None``."""
|
|
78
|
+
return await queries.get_study(self.db, name)
|
|
79
|
+
|
|
80
|
+
async def list_all(self) -> list[dict]:
|
|
81
|
+
"""Return all studies."""
|
|
82
|
+
return await queries.list_studies(self.db)
|
|
83
|
+
|
|
84
|
+
async def get_cluster_config(self, study_name: str) -> ClusterConfig:
|
|
85
|
+
"""Return the :class:`ClusterConfig` for the cluster associated
|
|
86
|
+
with *study_name*.
|
|
87
|
+
|
|
88
|
+
Raises :class:`ValueError` if the study or cluster is not found.
|
|
89
|
+
"""
|
|
90
|
+
study = await queries.get_study(self.db, study_name)
|
|
91
|
+
if study is None:
|
|
92
|
+
raise ValueError(f"Study not found: {study_name}")
|
|
93
|
+
|
|
94
|
+
cluster_name = study["cluster"]
|
|
95
|
+
for cluster in self.config.clusters:
|
|
96
|
+
if cluster.name == cluster_name:
|
|
97
|
+
return cluster
|
|
98
|
+
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Cluster {cluster_name!r} (referenced by study "
|
|
101
|
+
f"{study_name!r}) not found in config"
|
|
102
|
+
)
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: researchloop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automated research sprint platform for HPC clusters
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: aiosqlite<1,>=0.19
|
|
18
|
+
Requires-Dist: asyncssh<3,>=2.14
|
|
19
|
+
Requires-Dist: bcrypt<5,>=4.0
|
|
20
|
+
Requires-Dist: click<9,>=8.0
|
|
21
|
+
Requires-Dist: fastapi<1,>=0.100
|
|
22
|
+
Requires-Dist: httpx<1,>=0.24
|
|
23
|
+
Requires-Dist: itsdangerous<3,>=2.1
|
|
24
|
+
Requires-Dist: jinja2<4,>=3.1
|
|
25
|
+
Requires-Dist: markdown<4,>=3.4
|
|
26
|
+
Requires-Dist: python-multipart<1,>=0.0.6
|
|
27
|
+
Requires-Dist: tomli<3,>=2.0; python_version < '3.11'
|
|
28
|
+
Requires-Dist: uvicorn[standard]<1,>=0.20
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# ResearchLoop
|
|
32
|
+
|
|
33
|
+
**Automated AI research sprints on HPC clusters.**
|
|
34
|
+
|
|
35
|
+
[](https://github.com/chanind/researchloop/actions/workflows/ci.yml)
|
|
36
|
+
[](https://www.python.org/downloads/)
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
ResearchLoop automates multi-step AI research pipelines on SLURM and SGE clusters. You describe a research idea, and ResearchLoop submits it to your HPC cluster where [Claude Code](https://docs.anthropic.com/en/docs/claude-code) executes a full research pipeline -- coding, red-teaming, fixing, reporting -- inside a single job. Results are reported back via webhooks, Slack, or push notifications, and you can monitor everything from a web dashboard or the CLI.
|
|
42
|
+
|
|
43
|
+
The platform is built for researchers who run experiments on shared HPC infrastructure and want to iterate faster without babysitting jobs. Define your studies, point ResearchLoop at your cluster, and let it handle the rest: job submission, progress tracking, artifact collection, and even automatic generation of follow-up research ideas.
|
|
44
|
+
|
|
45
|
+
ResearchLoop's **auto-loop** feature chains sprints together automatically. After each sprint completes, Claude analyzes the results and proposes the next experiment. You set how many iterations to run, and the system handles the rest -- turning a single research question into a sustained investigation.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
ResearchLoop has two components:
|
|
50
|
+
|
|
51
|
+
1. **Orchestrator** (`researchloop serve`) -- a lightweight server that manages studies and sprints in SQLite, submits jobs to HPC clusters via SSH, receives completion webhooks, stores artifacts, and serves the web dashboard.
|
|
52
|
+
2. **Sprint Runner** -- runs inside each SLURM/SGE job on the HPC cluster. Chains `claude -p` calls through the research pipeline (research, red-team, fix, report, summarize), then sends artifacts and results back to the orchestrator.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
You (CLI / Dashboard / Slack)
|
|
56
|
+
|
|
|
57
|
+
v
|
|
58
|
+
Orchestrator (Docker / Fly.io) HPC Cluster
|
|
59
|
+
+--------------------------+ +----------------------------+
|
|
60
|
+
| FastAPI API + Dashboard |---SSH------>| SLURM / SGE scheduler |
|
|
61
|
+
| SQLite metadata | | |
|
|
62
|
+
| Artifact storage |<--webhook--| Sprint Runner |
|
|
63
|
+
| Slack bot |<--upload---| 1. claude -p "research" |
|
|
64
|
+
| ntfy.sh notifications | | 2. claude -p "red-team" |
|
|
65
|
+
+--------------------------+ | 3. claude -p "fix" |
|
|
66
|
+
| 4. claude -p "report" |
|
|
67
|
+
| 5. claude -p "summarize" |
|
|
68
|
+
+----------------------------+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Core concepts
|
|
72
|
+
|
|
73
|
+
| Concept | Description |
|
|
74
|
+
|---------|-------------|
|
|
75
|
+
| **Study** | A sustained research effort (e.g., "synthetic SAE improvements"). Tied to a cluster, has its own context and configuration. |
|
|
76
|
+
| **Sprint** | A single research attempt within a study. Gets a short ID (`sp-a3f7b2`), its own directory, and runs the full pipeline. |
|
|
77
|
+
| **Auto-loop** | Automatic sequential sprint execution. After each sprint, Claude analyzes results and generates the next research idea. |
|
|
78
|
+
|
|
79
|
+
### Sprint pipeline
|
|
80
|
+
|
|
81
|
+
Each sprint runs these steps inside a single SLURM/SGE job:
|
|
82
|
+
|
|
83
|
+
1. **Research** -- execute the research idea (coding, experiments, analysis)
|
|
84
|
+
2. **Red-team** -- critique the work, find flaws (up to N rounds with fix steps)
|
|
85
|
+
3. **Fix** -- address issues found by the red-team
|
|
86
|
+
4. **Report** -- generate a comprehensive markdown report
|
|
87
|
+
5. **Summarize** -- write a short summary for notifications and the dashboard
|
|
88
|
+
|
|
89
|
+
All steps share a single Claude session (via `--resume`), so Claude maintains full context of the sprint's work across steps.
|
|
90
|
+
|
|
91
|
+
## Features
|
|
92
|
+
|
|
93
|
+
- **HPC cluster integration** -- submit, monitor, and cancel jobs on SLURM and SGE clusters via SSH
|
|
94
|
+
- **Multi-step research pipeline** -- research, red-team, fix, report, summarize with configurable rounds
|
|
95
|
+
- **Auto-loop** -- chain sprints automatically with AI-generated follow-up ideas
|
|
96
|
+
- **Web dashboard** -- monitor studies, sprints, and loops from a browser with live status refresh
|
|
97
|
+
- **Slack bot** -- start sprints, check status, and have research conversations via Slack DMs or channels
|
|
98
|
+
- **CLI** -- full remote management from the command line with token-based auth
|
|
99
|
+
- **Progress tracking** -- live `progress.md` and `output.log` streaming from cluster to dashboard
|
|
100
|
+
- **Notifications** -- push notifications via ntfy.sh and Slack with PDF report attachments
|
|
101
|
+
- **Per-sprint security** -- webhook tokens, CSRF protection, signed session cookies, bcrypt password hashing
|
|
102
|
+
- **Context hierarchy** -- global, cluster, and study-level context files and inline configuration
|
|
103
|
+
|
|
104
|
+
## Quick start
|
|
105
|
+
|
|
106
|
+
### Prerequisites
|
|
107
|
+
|
|
108
|
+
- Python 3.10+
|
|
109
|
+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
|
110
|
+
- SSH access to an HPC cluster with SLURM or SGE
|
|
111
|
+
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated on the HPC cluster
|
|
112
|
+
|
|
113
|
+
### Install
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pip install git+https://github.com/chanind/researchloop.git
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or for development:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
git clone https://github.com/chanind/researchloop.git
|
|
123
|
+
cd researchloop
|
|
124
|
+
uv sync
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Initialize a project
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
researchloop init
|
|
131
|
+
# Creates researchloop.toml and artifacts/ directory
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Configure
|
|
135
|
+
|
|
136
|
+
Edit `researchloop.toml`:
|
|
137
|
+
|
|
138
|
+
```toml
|
|
139
|
+
shared_secret = "change-me"
|
|
140
|
+
orchestrator_url = "https://your-server.fly.dev"
|
|
141
|
+
|
|
142
|
+
[[cluster]]
|
|
143
|
+
name = "hpc"
|
|
144
|
+
host = "login.cluster.example.com"
|
|
145
|
+
user = "researcher"
|
|
146
|
+
key_path = "~/.ssh/id_ed25519"
|
|
147
|
+
scheduler_type = "slurm" # "slurm", "sge", or "local"
|
|
148
|
+
working_dir = "/scratch/researcher/researchloop"
|
|
149
|
+
|
|
150
|
+
[cluster.job_options]
|
|
151
|
+
gres = "gpu:1"
|
|
152
|
+
mem = "64G"
|
|
153
|
+
cpus-per-task = "8"
|
|
154
|
+
|
|
155
|
+
[[study]]
|
|
156
|
+
name = "my-research"
|
|
157
|
+
cluster = "hpc"
|
|
158
|
+
description = "Investigating feature X"
|
|
159
|
+
max_sprint_duration_hours = 8
|
|
160
|
+
red_team_max_rounds = 3
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Start the server and run a sprint
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Start the orchestrator
|
|
167
|
+
researchloop serve
|
|
168
|
+
|
|
169
|
+
# In another terminal, connect the CLI to the server
|
|
170
|
+
researchloop connect https://localhost:8080
|
|
171
|
+
|
|
172
|
+
# Submit a sprint
|
|
173
|
+
researchloop sprint run "try approach X on dataset Y" --study my-research
|
|
174
|
+
|
|
175
|
+
# Check status
|
|
176
|
+
researchloop sprint list
|
|
177
|
+
researchloop sprint show sp-a3f7b2
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Configuration reference
|
|
181
|
+
|
|
182
|
+
### Complete `researchloop.toml` example
|
|
183
|
+
|
|
184
|
+
```toml
|
|
185
|
+
# -- Top-level settings --
|
|
186
|
+
db_path = "researchloop.db" # SQLite database location
|
|
187
|
+
artifact_dir = "artifacts" # Local directory for uploaded artifacts
|
|
188
|
+
shared_secret = "your-secret" # Auth between runner and orchestrator
|
|
189
|
+
orchestrator_url = "https://example.com" # Public URL for webhooks
|
|
190
|
+
claude_command = "" # Override claude command globally
|
|
191
|
+
|
|
192
|
+
# Global context (included in all sprints)
|
|
193
|
+
context = "Always use Python 3.10+ features."
|
|
194
|
+
context_paths = ["./global-context.md"] # Files to include as context
|
|
195
|
+
|
|
196
|
+
# -- Cluster configuration --
|
|
197
|
+
[[cluster]]
|
|
198
|
+
name = "hpc"
|
|
199
|
+
host = "login.cluster.example.com"
|
|
200
|
+
port = 22
|
|
201
|
+
user = "researcher"
|
|
202
|
+
key_path = "~/.ssh/id_ed25519"
|
|
203
|
+
scheduler_type = "slurm" # "slurm", "sge", or "local"
|
|
204
|
+
working_dir = "/scratch/user/researchloop"
|
|
205
|
+
max_concurrent_jobs = 4
|
|
206
|
+
claude_command = "claude --dangerously-skip-permissions"
|
|
207
|
+
|
|
208
|
+
# Context specific to this cluster
|
|
209
|
+
context = "GPUs are NVIDIA L40. Check CUDA_VISIBLE_DEVICES."
|
|
210
|
+
context_paths = ["./cluster-notes.md"]
|
|
211
|
+
|
|
212
|
+
# Environment variables set in SLURM jobs
|
|
213
|
+
[cluster.environment]
|
|
214
|
+
# ANTHROPIC_API_KEY = "sk-ant-..." # Only if not using claude login
|
|
215
|
+
|
|
216
|
+
# SLURM job options (passed as #SBATCH directives)
|
|
217
|
+
[cluster.job_options]
|
|
218
|
+
gres = "gpu:l40:1"
|
|
219
|
+
cpus-per-task = "8"
|
|
220
|
+
mem = "64G"
|
|
221
|
+
|
|
222
|
+
# -- Study configuration --
|
|
223
|
+
[[study]]
|
|
224
|
+
name = "my-study"
|
|
225
|
+
cluster = "hpc" # Must match a cluster name
|
|
226
|
+
description = "Research into X"
|
|
227
|
+
claude_md_path = "./studies/my-study/CLAUDE.md" # Study-specific context file
|
|
228
|
+
sprints_dir = "/scratch/user/my-study" # Where sprints go (default: working_dir/<study>)
|
|
229
|
+
max_sprint_duration_hours = 8 # SLURM time limit
|
|
230
|
+
red_team_max_rounds = 3 # Red-team/fix cycles
|
|
231
|
+
allow_loop = true # Allow auto-loops for this study
|
|
232
|
+
claude_command = "" # Override claude command for this study
|
|
233
|
+
|
|
234
|
+
# Inline study context (included in research prompts)
|
|
235
|
+
context = """
|
|
236
|
+
Focus on improving F1 score. Use batch size 1024.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
# Per-study SLURM overrides
|
|
240
|
+
[study.job_options]
|
|
241
|
+
gres = "gpu:a100:2"
|
|
242
|
+
|
|
243
|
+
# -- Notifications --
|
|
244
|
+
[ntfy]
|
|
245
|
+
url = "https://ntfy.sh" # Self-hosted ntfy server URL
|
|
246
|
+
topic = "researchloop" # ntfy topic name
|
|
247
|
+
|
|
248
|
+
# -- Slack integration --
|
|
249
|
+
[slack]
|
|
250
|
+
bot_token = "" # xoxb-... (prefer env var)
|
|
251
|
+
signing_secret = "" # Slack signing secret (prefer env var)
|
|
252
|
+
channel_id = "C0123456789" # Channel or user ID for notifications
|
|
253
|
+
allowed_user_ids = ["U0123456789"] # Users allowed to interact with bot
|
|
254
|
+
restrict_to_channel = false # If true, only respond in channel_id
|
|
255
|
+
|
|
256
|
+
# -- Dashboard --
|
|
257
|
+
[dashboard]
|
|
258
|
+
enabled = true
|
|
259
|
+
host = "0.0.0.0"
|
|
260
|
+
port = 8080
|
|
261
|
+
password_hash = "" # bcrypt hash (prefer env var or first-run setup)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Environment variable overrides
|
|
265
|
+
|
|
266
|
+
All secrets and sensitive settings can be set via environment variables with the `RESEARCHLOOP_` prefix. Environment variables take precedence over TOML values.
|
|
267
|
+
|
|
268
|
+
| Environment variable | Overrides |
|
|
269
|
+
|---------------------|-----------|
|
|
270
|
+
| `RESEARCHLOOP_SHARED_SECRET` | `shared_secret` |
|
|
271
|
+
| `RESEARCHLOOP_ORCHESTRATOR_URL` | `orchestrator_url` |
|
|
272
|
+
| `RESEARCHLOOP_DB_PATH` | `db_path` |
|
|
273
|
+
| `RESEARCHLOOP_ARTIFACT_DIR` | `artifact_dir` |
|
|
274
|
+
| `RESEARCHLOOP_SLACK_BOT_TOKEN` | `slack.bot_token` |
|
|
275
|
+
| `RESEARCHLOOP_SLACK_SIGNING_SECRET` | `slack.signing_secret` |
|
|
276
|
+
| `RESEARCHLOOP_SLACK_CHANNEL_ID` | `slack.channel_id` |
|
|
277
|
+
| `RESEARCHLOOP_SLACK_ALLOWED_USER_IDS` | `slack.allowed_user_ids` (comma-separated) |
|
|
278
|
+
| `RESEARCHLOOP_NTFY_TOPIC` | `ntfy.topic` |
|
|
279
|
+
| `RESEARCHLOOP_NTFY_URL` | `ntfy.url` |
|
|
280
|
+
| `RESEARCHLOOP_DASHBOARD_PASSWORD` | Auto-hashed on startup |
|
|
281
|
+
| `RESEARCHLOOP_DASHBOARD_PASSWORD_HASH` | `dashboard.password_hash` |
|
|
282
|
+
| `RESEARCHLOOP_DASHBOARD_PORT` | `dashboard.port` |
|
|
283
|
+
| `RESEARCHLOOP_DASHBOARD_HOST` | `dashboard.host` |
|
|
284
|
+
|
|
285
|
+
## Deployment
|
|
286
|
+
|
|
287
|
+
### Docker
|
|
288
|
+
|
|
289
|
+
```dockerfile
|
|
290
|
+
FROM python:3.12-slim
|
|
291
|
+
|
|
292
|
+
RUN apt-get update && \
|
|
293
|
+
apt-get install -y --no-install-recommends openssh-client curl git && \
|
|
294
|
+
rm -rf /var/lib/apt/lists/*
|
|
295
|
+
|
|
296
|
+
# Install Claude CLI
|
|
297
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
298
|
+
|
|
299
|
+
# Install researchloop
|
|
300
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
301
|
+
RUN uv venv /app/.venv && \
|
|
302
|
+
uv pip install --python /app/.venv/bin/python --no-cache \
|
|
303
|
+
"researchloop @ git+https://github.com/chanind/researchloop.git"
|
|
304
|
+
|
|
305
|
+
WORKDIR /app
|
|
306
|
+
COPY researchloop.toml .
|
|
307
|
+
ENV PATH="/root/.local/bin:/root/.claude/bin:/app/.venv/bin:$PATH"
|
|
308
|
+
ENV RESEARCHLOOP_DB_PATH="/data/researchloop.db"
|
|
309
|
+
ENV RESEARCHLOOP_ARTIFACT_DIR="/data/artifacts"
|
|
310
|
+
|
|
311
|
+
EXPOSE 8080
|
|
312
|
+
CMD ["researchloop", "serve"]
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Fly.io
|
|
316
|
+
|
|
317
|
+
ResearchLoop works well on [Fly.io](https://fly.io) with a persistent volume for the database and artifacts:
|
|
318
|
+
|
|
319
|
+
```toml
|
|
320
|
+
# fly.toml
|
|
321
|
+
app = "my-researchloop"
|
|
322
|
+
primary_region = "iad"
|
|
323
|
+
|
|
324
|
+
[build]
|
|
325
|
+
|
|
326
|
+
[[mounts]]
|
|
327
|
+
source = "researchloop_data"
|
|
328
|
+
destination = "/data"
|
|
329
|
+
|
|
330
|
+
[http_service]
|
|
331
|
+
internal_port = 8080
|
|
332
|
+
force_https = true
|
|
333
|
+
auto_stop_machines = "stop"
|
|
334
|
+
auto_start_machines = true
|
|
335
|
+
min_machines_running = 0
|
|
336
|
+
|
|
337
|
+
[[vm]]
|
|
338
|
+
size = "shared-cpu-1x"
|
|
339
|
+
memory = "2gb"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Set secrets:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
fly secrets set \
|
|
346
|
+
RESEARCHLOOP_SHARED_SECRET="your-secret" \
|
|
347
|
+
RESEARCHLOOP_ORCHESTRATOR_URL="https://my-researchloop.fly.dev" \
|
|
348
|
+
SSH_PRIVATE_KEY="$(cat ~/.ssh/id_ed25519)" \
|
|
349
|
+
RESEARCHLOOP_DASHBOARD_PASSWORD="your-password" \
|
|
350
|
+
-a my-researchloop
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Deploy:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
fly deploy
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### SSH key setup for Docker/Fly.io
|
|
360
|
+
|
|
361
|
+
The orchestrator needs SSH access to your HPC cluster. Add an entrypoint script that writes the key from a secret:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
#!/bin/bash
|
|
365
|
+
set -euo pipefail
|
|
366
|
+
if [ -n "${SSH_PRIVATE_KEY:-}" ]; then
|
|
367
|
+
mkdir -p ~/.ssh
|
|
368
|
+
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
|
369
|
+
chmod 600 ~/.ssh/id_ed25519
|
|
370
|
+
cat > ~/.ssh/config <<EOF
|
|
371
|
+
Host *
|
|
372
|
+
StrictHostKeyChecking no
|
|
373
|
+
UserKnownHostsFile /dev/null
|
|
374
|
+
LogLevel ERROR
|
|
375
|
+
EOF
|
|
376
|
+
chmod 600 ~/.ssh/config
|
|
377
|
+
fi
|
|
378
|
+
mkdir -p /data/artifacts
|
|
379
|
+
exec "$@"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Dashboard
|
|
383
|
+
|
|
384
|
+
The web dashboard provides a browser-based interface for managing ResearchLoop. It is served by the orchestrator at `/dashboard/`.
|
|
385
|
+
|
|
386
|
+
### Features
|
|
387
|
+
|
|
388
|
+
- **Studies list** -- overview of all configured studies with sprint counts
|
|
389
|
+
- **Study detail** -- view study configuration, submit new sprints with GPU/memory overrides
|
|
390
|
+
- **Sprint list** -- filterable list of all sprints across studies
|
|
391
|
+
- **Sprint detail** -- live status with progress.md display, tool log, script output, report rendering (markdown to HTML), PDF download, and artifact listing
|
|
392
|
+
- **Auto-loop management** -- start, stop, and resume loops with context guidance and job option overrides
|
|
393
|
+
- **Loop detail** -- progress tracking with links to individual loop sprints
|
|
394
|
+
- **Refresh** -- pull live status from the cluster via SSH (detects current pipeline step, reads logs)
|
|
395
|
+
|
|
396
|
+
### Authentication
|
|
397
|
+
|
|
398
|
+
On first visit, the dashboard prompts you to set a password. Alternatively, set `RESEARCHLOOP_DASHBOARD_PASSWORD` as an environment variable and the password is auto-hashed on startup.
|
|
399
|
+
|
|
400
|
+
Sessions use signed cookies (7-day expiry) with a signing key persisted in the database. All mutating dashboard actions are protected by CSRF tokens.
|
|
401
|
+
|
|
402
|
+
### CLI authentication
|
|
403
|
+
|
|
404
|
+
The CLI authenticates to the orchestrator using password-based token auth:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
researchloop connect https://your-server.fly.dev
|
|
408
|
+
# Prompts for password, saves token to ~/.config/researchloop/credentials.json
|
|
409
|
+
|
|
410
|
+
researchloop status # Check connection
|
|
411
|
+
researchloop disconnect # Remove saved credentials
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Slack integration
|
|
415
|
+
|
|
416
|
+
### Setup
|
|
417
|
+
|
|
418
|
+
1. Go to [api.slack.com/apps](https://api.slack.com/apps) and create a new app
|
|
419
|
+
2. Enable **Event Subscriptions** with request URL: `https://your-server.fly.dev/api/slack/events`
|
|
420
|
+
3. Subscribe to bot events: `app_mention`, `message.im`
|
|
421
|
+
4. Add **OAuth Scopes**: `chat:write`, `files:write`
|
|
422
|
+
5. Install the app to your workspace
|
|
423
|
+
6. Set environment variables:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
RESEARCHLOOP_SLACK_BOT_TOKEN="xoxb-..."
|
|
427
|
+
RESEARCHLOOP_SLACK_SIGNING_SECRET="..."
|
|
428
|
+
RESEARCHLOOP_SLACK_CHANNEL_ID="C0123456789" # For notifications
|
|
429
|
+
RESEARCHLOOP_SLACK_ALLOWED_USER_IDS="U01,U02" # Comma-separated
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Commands
|
|
433
|
+
|
|
434
|
+
| Command | Description |
|
|
435
|
+
|---------|-------------|
|
|
436
|
+
| `sprint run <study> <idea>` | Submit a new sprint |
|
|
437
|
+
| `sprint list` | List recent sprints |
|
|
438
|
+
| `auth status` | Check if Claude CLI is authenticated |
|
|
439
|
+
| `help` | Show available commands |
|
|
440
|
+
|
|
441
|
+
### Conversational mode
|
|
442
|
+
|
|
443
|
+
Beyond commands, the Slack bot supports free-form conversations. Messages in a thread are tracked as a Claude session (via `--resume`), so the bot remembers context within a thread. The bot can:
|
|
444
|
+
|
|
445
|
+
- Discuss research ideas and help plan sprints
|
|
446
|
+
- Review results from completed sprints
|
|
447
|
+
- Look up papers and references (web search)
|
|
448
|
+
- Execute actions (start sprints, loops) when you ask
|
|
449
|
+
|
|
450
|
+
### Notifications
|
|
451
|
+
|
|
452
|
+
When sprints complete or fail, the bot sends notifications to the configured channel. Completed sprint notifications include the summary and a link to the dashboard. If a PDF report was generated, it is uploaded as an attachment.
|
|
453
|
+
|
|
454
|
+
## CLI reference
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
researchloop [OPTIONS] COMMAND
|
|
458
|
+
|
|
459
|
+
Options:
|
|
460
|
+
-c, --config PATH Path to researchloop.toml
|
|
461
|
+
--version Show version
|
|
462
|
+
--help Show help
|
|
463
|
+
|
|
464
|
+
Commands:
|
|
465
|
+
init Initialize a new project with example config
|
|
466
|
+
serve Start the orchestrator server
|
|
467
|
+
connect [URL] Authenticate CLI to a remote orchestrator
|
|
468
|
+
disconnect Remove saved credentials
|
|
469
|
+
status Show connection status
|
|
470
|
+
|
|
471
|
+
study list List all configured studies
|
|
472
|
+
study show NAME Show study details and recent sprints
|
|
473
|
+
study init NAME Scaffold a new study directory with starter CLAUDE.md
|
|
474
|
+
|
|
475
|
+
sprint run IDEA Submit a new sprint (-s/--study required)
|
|
476
|
+
sprint list List sprints (--study, --limit options)
|
|
477
|
+
sprint show ID Show sprint details, artifacts, and summary
|
|
478
|
+
sprint cancel ID Cancel a running sprint
|
|
479
|
+
|
|
480
|
+
loop start Start an auto-loop (-s/--study, -n/--count, -m/--context)
|
|
481
|
+
loop status Show all auto-loops
|
|
482
|
+
loop stop LOOP_ID Stop a running auto-loop
|
|
483
|
+
|
|
484
|
+
cluster list List configured clusters
|
|
485
|
+
cluster check Test SSH connectivity (--name for specific cluster)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## API endpoints
|
|
489
|
+
|
|
490
|
+
The orchestrator exposes a REST API at `/api/`:
|
|
491
|
+
|
|
492
|
+
| Method | Path | Auth | Description |
|
|
493
|
+
|--------|------|------|-------------|
|
|
494
|
+
| `POST` | `/api/auth` | Password | Get API token |
|
|
495
|
+
| `GET` | `/api/studies` | Token/Secret | List all studies |
|
|
496
|
+
| `GET` | `/api/sprints` | Token/Secret | List sprints (`?study_name=`, `?limit=`) |
|
|
497
|
+
| `GET` | `/api/sprints/{id}` | Token/Secret | Get sprint details |
|
|
498
|
+
| `POST` | `/api/sprints` | Token/Secret | Create and submit a sprint |
|
|
499
|
+
| `POST` | `/api/sprints/{id}/cancel` | Token/Secret | Cancel a sprint |
|
|
500
|
+
| `POST` | `/api/loops` | Token/Secret | Start an auto-loop |
|
|
501
|
+
| `POST` | `/api/loops/{id}/stop` | Token/Secret | Stop an auto-loop |
|
|
502
|
+
| `POST` | `/api/webhook/sprint-complete` | Webhook token | Sprint completion callback |
|
|
503
|
+
| `POST` | `/api/webhook/heartbeat` | Webhook token | Runner heartbeat with logs |
|
|
504
|
+
| `POST` | `/api/artifacts/{sprint_id}` | Webhook token | Upload artifact file |
|
|
505
|
+
| `POST` | `/api/slack/events` | Slack signature | Slack Events API handler |
|
|
506
|
+
|
|
507
|
+
Authentication uses either a bearer token (from `/api/auth`) or the `X-Shared-Secret` header. Webhook endpoints use per-sprint `X-Webhook-Token` headers.
|
|
508
|
+
|
|
509
|
+
## Development
|
|
510
|
+
|
|
511
|
+
### Setup
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
git clone https://github.com/chanind/researchloop.git
|
|
515
|
+
cd researchloop
|
|
516
|
+
uv sync
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Run tests
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
# Unit tests (339 tests, ~3s)
|
|
523
|
+
uv run pytest tests/ -v -m "not integration"
|
|
524
|
+
|
|
525
|
+
# Integration tests (requires Docker for SLURM container)
|
|
526
|
+
docker build -t researchloop-slurm-test tests/docker/slurm/
|
|
527
|
+
uv run pytest tests/integration/ -v --timeout=120
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Code quality
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
uv run ruff check . # Lint
|
|
534
|
+
uv run ruff format --check . # Format check
|
|
535
|
+
uv run pyright researchloop/ # Type check
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Project structure
|
|
539
|
+
|
|
540
|
+
```
|
|
541
|
+
researchloop/
|
|
542
|
+
core/
|
|
543
|
+
config.py TOML config loading into dataclasses
|
|
544
|
+
models.py SprintStatus enum, Sprint/Study/AutoLoop dataclasses
|
|
545
|
+
orchestrator.py Orchestrator class + create_app() FastAPI factory
|
|
546
|
+
credentials.py CLI credential storage (~/.config/researchloop/)
|
|
547
|
+
auth.py Claude CLI auth checking
|
|
548
|
+
db/
|
|
549
|
+
database.py Async SQLite wrapper (WAL mode, auto-migrations)
|
|
550
|
+
migrations.py Schema definitions (7 tables + indexes)
|
|
551
|
+
queries.py Async CRUD functions (parameterized SQL, return dicts)
|
|
552
|
+
clusters/
|
|
553
|
+
ssh.py SSHConnection + SSHManager (connection pooling)
|
|
554
|
+
monitor.py JobMonitor (polls active jobs, heartbeat tracking)
|
|
555
|
+
schedulers/
|
|
556
|
+
base.py BaseScheduler ABC
|
|
557
|
+
slurm.py SlurmScheduler (sbatch/squeue/sacct/scancel)
|
|
558
|
+
sge.py SGEScheduler (qsub/qstat/qacct/qdel)
|
|
559
|
+
local.py LocalScheduler (subprocesses, for testing)
|
|
560
|
+
sprints/
|
|
561
|
+
manager.py SprintManager (create/submit/cancel/handle_completion)
|
|
562
|
+
auto_loop.py AutoLoopController (start/stop/resume, idea generation)
|
|
563
|
+
studies/
|
|
564
|
+
manager.py StudyManager (config-to-DB sync, cluster resolution)
|
|
565
|
+
runner/
|
|
566
|
+
pipeline.py Pipeline class (research pipeline steps)
|
|
567
|
+
claude.py run_claude() wrapper + render_template()
|
|
568
|
+
upload.py upload_artifacts(), send_webhook(), send_heartbeat()
|
|
569
|
+
main.py Runner CLI entry point (researchloop-runner)
|
|
570
|
+
templates/ Jinja2 prompt templates (6 templates)
|
|
571
|
+
job_templates/ SLURM (slurm.sh.j2) and SGE (sge.sh.j2) job scripts
|
|
572
|
+
comms/
|
|
573
|
+
base.py BaseNotifier ABC
|
|
574
|
+
ntfy.py NtfyNotifier (ntfy.sh push notifications)
|
|
575
|
+
slack.py SlackNotifier + verify_slack_signature()
|
|
576
|
+
conversation.py ConversationManager (Slack threads to Claude sessions)
|
|
577
|
+
router.py NotificationRouter (fan-out to all backends)
|
|
578
|
+
dashboard/
|
|
579
|
+
app.py ASGI app entry point
|
|
580
|
+
auth.py Password auth (bcrypt + signed session cookies + CSRF)
|
|
581
|
+
routes.py Dashboard HTML routes
|
|
582
|
+
templates/ Jinja2 HTML templates (9 templates)
|
|
583
|
+
cli.py Click CLI entry point
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### CI
|
|
587
|
+
|
|
588
|
+
GitHub Actions runs on every push and PR to `main`:
|
|
589
|
+
|
|
590
|
+
- **Lint** -- `ruff check`, `ruff format --check`, `pyright`
|
|
591
|
+
- **Test** -- `pytest` on Python 3.10, 3.12, 3.13
|
|
592
|
+
- **Integration** -- builds a Docker SLURM container and runs integration tests
|
|
593
|
+
|
|
594
|
+
## License
|
|
595
|
+
|
|
596
|
+
MIT
|