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.
Files changed (63) hide show
  1. researchloop/__init__.py +1 -0
  2. researchloop/__main__.py +3 -0
  3. researchloop/cli.py +1138 -0
  4. researchloop/clusters/__init__.py +4 -0
  5. researchloop/clusters/monitor.py +199 -0
  6. researchloop/clusters/ssh.py +183 -0
  7. researchloop/comms/__init__.py +0 -0
  8. researchloop/comms/base.py +34 -0
  9. researchloop/comms/conversation.py +465 -0
  10. researchloop/comms/ntfy.py +95 -0
  11. researchloop/comms/router.py +71 -0
  12. researchloop/comms/slack.py +188 -0
  13. researchloop/core/__init__.py +0 -0
  14. researchloop/core/auth.py +78 -0
  15. researchloop/core/config.py +328 -0
  16. researchloop/core/credentials.py +38 -0
  17. researchloop/core/models.py +119 -0
  18. researchloop/core/orchestrator.py +910 -0
  19. researchloop/dashboard/__init__.py +0 -0
  20. researchloop/dashboard/app.py +15 -0
  21. researchloop/dashboard/auth.py +60 -0
  22. researchloop/dashboard/routes.py +912 -0
  23. researchloop/dashboard/templates/base.html +84 -0
  24. researchloop/dashboard/templates/login.html +12 -0
  25. researchloop/dashboard/templates/loop_detail.html +58 -0
  26. researchloop/dashboard/templates/loops.html +61 -0
  27. researchloop/dashboard/templates/setup.html +14 -0
  28. researchloop/dashboard/templates/sprint_detail.html +109 -0
  29. researchloop/dashboard/templates/sprints.html +48 -0
  30. researchloop/dashboard/templates/studies.html +18 -0
  31. researchloop/dashboard/templates/study_detail.html +64 -0
  32. researchloop/db/__init__.py +5 -0
  33. researchloop/db/database.py +86 -0
  34. researchloop/db/migrations.py +172 -0
  35. researchloop/db/queries.py +351 -0
  36. researchloop/runner/__init__.py +1 -0
  37. researchloop/runner/claude.py +169 -0
  38. researchloop/runner/job_templates/sge.sh.j2 +319 -0
  39. researchloop/runner/job_templates/slurm.sh.j2 +336 -0
  40. researchloop/runner/main.py +156 -0
  41. researchloop/runner/pipeline.py +272 -0
  42. researchloop/runner/templates/fix_issues.md.j2 +11 -0
  43. researchloop/runner/templates/idea_generator.md.j2 +16 -0
  44. researchloop/runner/templates/red_team.md.j2 +15 -0
  45. researchloop/runner/templates/report.md.j2 +31 -0
  46. researchloop/runner/templates/research_sprint.md.j2 +51 -0
  47. researchloop/runner/templates/summarizer.md.j2 +7 -0
  48. researchloop/runner/upload.py +153 -0
  49. researchloop/schedulers/__init__.py +11 -0
  50. researchloop/schedulers/base.py +43 -0
  51. researchloop/schedulers/local.py +188 -0
  52. researchloop/schedulers/sge.py +163 -0
  53. researchloop/schedulers/slurm.py +179 -0
  54. researchloop/sprints/__init__.py +0 -0
  55. researchloop/sprints/auto_loop.py +458 -0
  56. researchloop/sprints/manager.py +750 -0
  57. researchloop/studies/__init__.py +0 -0
  58. researchloop/studies/manager.py +102 -0
  59. researchloop-0.1.0.dist-info/METADATA +596 -0
  60. researchloop-0.1.0.dist-info/RECORD +63 -0
  61. researchloop-0.1.0.dist-info/WHEEL +4 -0
  62. researchloop-0.1.0.dist-info/entry_points.txt +3 -0
  63. 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
+ [![CI](https://github.com/chanind/researchloop/actions/workflows/ci.yml/badge.svg)](https://github.com/chanind/researchloop/actions/workflows/ci.yml)
36
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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