researchloop 0.1.0__tar.gz
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-0.1.0/.github/workflows/ci.yml +73 -0
- researchloop-0.1.0/.github/workflows/docs.yml +52 -0
- researchloop-0.1.0/.github/workflows/release.yml +41 -0
- researchloop-0.1.0/.gitignore +55 -0
- researchloop-0.1.0/CLAUDE.md +170 -0
- researchloop-0.1.0/Dockerfile +43 -0
- researchloop-0.1.0/LICENSE +21 -0
- researchloop-0.1.0/PKG-INFO +596 -0
- researchloop-0.1.0/README.md +566 -0
- researchloop-0.1.0/docs/cli.md +231 -0
- researchloop-0.1.0/docs/configuration.md +176 -0
- researchloop-0.1.0/docs/dashboard.md +115 -0
- researchloop-0.1.0/docs/deployment.md +195 -0
- researchloop-0.1.0/docs/development.md +186 -0
- researchloop-0.1.0/docs/getting-started.md +180 -0
- researchloop-0.1.0/docs/index.md +73 -0
- researchloop-0.1.0/docs/security.md +120 -0
- researchloop-0.1.0/docs/slack.md +133 -0
- researchloop-0.1.0/mkdocs.yml +53 -0
- researchloop-0.1.0/pyproject.toml +67 -0
- researchloop-0.1.0/researchloop/__init__.py +1 -0
- researchloop-0.1.0/researchloop/__main__.py +3 -0
- researchloop-0.1.0/researchloop/cli.py +1138 -0
- researchloop-0.1.0/researchloop/clusters/__init__.py +4 -0
- researchloop-0.1.0/researchloop/clusters/monitor.py +199 -0
- researchloop-0.1.0/researchloop/clusters/ssh.py +183 -0
- researchloop-0.1.0/researchloop/comms/__init__.py +0 -0
- researchloop-0.1.0/researchloop/comms/base.py +34 -0
- researchloop-0.1.0/researchloop/comms/conversation.py +465 -0
- researchloop-0.1.0/researchloop/comms/ntfy.py +95 -0
- researchloop-0.1.0/researchloop/comms/router.py +71 -0
- researchloop-0.1.0/researchloop/comms/slack.py +188 -0
- researchloop-0.1.0/researchloop/core/__init__.py +0 -0
- researchloop-0.1.0/researchloop/core/auth.py +78 -0
- researchloop-0.1.0/researchloop/core/config.py +328 -0
- researchloop-0.1.0/researchloop/core/credentials.py +38 -0
- researchloop-0.1.0/researchloop/core/models.py +119 -0
- researchloop-0.1.0/researchloop/core/orchestrator.py +910 -0
- researchloop-0.1.0/researchloop/dashboard/__init__.py +0 -0
- researchloop-0.1.0/researchloop/dashboard/app.py +15 -0
- researchloop-0.1.0/researchloop/dashboard/auth.py +60 -0
- researchloop-0.1.0/researchloop/dashboard/routes.py +912 -0
- researchloop-0.1.0/researchloop/dashboard/templates/base.html +84 -0
- researchloop-0.1.0/researchloop/dashboard/templates/login.html +12 -0
- researchloop-0.1.0/researchloop/dashboard/templates/loop_detail.html +58 -0
- researchloop-0.1.0/researchloop/dashboard/templates/loops.html +61 -0
- researchloop-0.1.0/researchloop/dashboard/templates/setup.html +14 -0
- researchloop-0.1.0/researchloop/dashboard/templates/sprint_detail.html +109 -0
- researchloop-0.1.0/researchloop/dashboard/templates/sprints.html +48 -0
- researchloop-0.1.0/researchloop/dashboard/templates/studies.html +18 -0
- researchloop-0.1.0/researchloop/dashboard/templates/study_detail.html +64 -0
- researchloop-0.1.0/researchloop/db/__init__.py +5 -0
- researchloop-0.1.0/researchloop/db/database.py +86 -0
- researchloop-0.1.0/researchloop/db/migrations.py +172 -0
- researchloop-0.1.0/researchloop/db/queries.py +351 -0
- researchloop-0.1.0/researchloop/runner/__init__.py +1 -0
- researchloop-0.1.0/researchloop/runner/claude.py +169 -0
- researchloop-0.1.0/researchloop/runner/job_templates/sge.sh.j2 +319 -0
- researchloop-0.1.0/researchloop/runner/job_templates/slurm.sh.j2 +336 -0
- researchloop-0.1.0/researchloop/runner/main.py +156 -0
- researchloop-0.1.0/researchloop/runner/pipeline.py +272 -0
- researchloop-0.1.0/researchloop/runner/templates/fix_issues.md.j2 +11 -0
- researchloop-0.1.0/researchloop/runner/templates/idea_generator.md.j2 +16 -0
- researchloop-0.1.0/researchloop/runner/templates/red_team.md.j2 +15 -0
- researchloop-0.1.0/researchloop/runner/templates/report.md.j2 +31 -0
- researchloop-0.1.0/researchloop/runner/templates/research_sprint.md.j2 +51 -0
- researchloop-0.1.0/researchloop/runner/templates/summarizer.md.j2 +7 -0
- researchloop-0.1.0/researchloop/runner/upload.py +153 -0
- researchloop-0.1.0/researchloop/schedulers/__init__.py +11 -0
- researchloop-0.1.0/researchloop/schedulers/base.py +43 -0
- researchloop-0.1.0/researchloop/schedulers/local.py +188 -0
- researchloop-0.1.0/researchloop/schedulers/sge.py +163 -0
- researchloop-0.1.0/researchloop/schedulers/slurm.py +179 -0
- researchloop-0.1.0/researchloop/sprints/__init__.py +0 -0
- researchloop-0.1.0/researchloop/sprints/auto_loop.py +458 -0
- researchloop-0.1.0/researchloop/sprints/manager.py +750 -0
- researchloop-0.1.0/researchloop/studies/__init__.py +0 -0
- researchloop-0.1.0/researchloop/studies/manager.py +102 -0
- researchloop-0.1.0/researchloop.toml.example +91 -0
- researchloop-0.1.0/slack-app-manifest.yml +32 -0
- researchloop-0.1.0/tests/__init__.py +0 -0
- researchloop-0.1.0/tests/conftest.py +104 -0
- researchloop-0.1.0/tests/docker/slurm/Dockerfile +28 -0
- researchloop-0.1.0/tests/docker/slurm/entrypoint.sh +21 -0
- researchloop-0.1.0/tests/docker/slurm/mock_claude.sh +41 -0
- researchloop-0.1.0/tests/integration/__init__.py +0 -0
- researchloop-0.1.0/tests/integration/conftest.py +288 -0
- researchloop-0.1.0/tests/integration/test_loop_advancement.py +687 -0
- researchloop-0.1.0/tests/integration/test_loop_and_monitor.py +341 -0
- researchloop-0.1.0/tests/integration/test_slurm_scheduler.py +175 -0
- researchloop-0.1.0/tests/integration/test_sprint_slurm.py +302 -0
- researchloop-0.1.0/tests/integration/test_webhook_and_refresh.py +693 -0
- researchloop-0.1.0/tests/test_api.py +601 -0
- researchloop-0.1.0/tests/test_auto_loop.py +668 -0
- researchloop-0.1.0/tests/test_cli.py +357 -0
- researchloop-0.1.0/tests/test_config.py +200 -0
- researchloop-0.1.0/tests/test_conversation.py +366 -0
- researchloop-0.1.0/tests/test_dashboard.py +465 -0
- researchloop-0.1.0/tests/test_database.py +78 -0
- researchloop-0.1.0/tests/test_models.py +63 -0
- researchloop-0.1.0/tests/test_notification.py +58 -0
- researchloop-0.1.0/tests/test_queries.py +242 -0
- researchloop-0.1.0/tests/test_runner.py +110 -0
- researchloop-0.1.0/tests/test_schedulers.py +167 -0
- researchloop-0.1.0/tests/test_sge.py +255 -0
- researchloop-0.1.0/tests/test_slack.py +180 -0
- researchloop-0.1.0/tests/test_slack_events.py +420 -0
- researchloop-0.1.0/tests/test_sprint_manager.py +618 -0
- researchloop-0.1.0/tests/test_study_manager.py +77 -0
- researchloop-0.1.0/uv.lock +1609 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
run: uv python install 3.12
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: uv sync
|
|
23
|
+
|
|
24
|
+
- name: Ruff check
|
|
25
|
+
run: uv run ruff check .
|
|
26
|
+
|
|
27
|
+
- name: Ruff format check
|
|
28
|
+
run: uv run ruff format --check .
|
|
29
|
+
|
|
30
|
+
- name: Pyright type check
|
|
31
|
+
run: uv run pyright researchloop/
|
|
32
|
+
|
|
33
|
+
test:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
strategy:
|
|
36
|
+
matrix:
|
|
37
|
+
python-version: ["3.10", "3.12", "3.13"]
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Install uv
|
|
42
|
+
uses: astral-sh/setup-uv@v4
|
|
43
|
+
|
|
44
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
45
|
+
run: uv python install ${{ matrix.python-version }}
|
|
46
|
+
|
|
47
|
+
- name: Install dependencies
|
|
48
|
+
run: uv sync
|
|
49
|
+
|
|
50
|
+
- name: Run tests
|
|
51
|
+
run: uv run pytest tests/ -v --tb=short -m "not integration"
|
|
52
|
+
|
|
53
|
+
integration:
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
uses: astral-sh/setup-uv@v4
|
|
60
|
+
|
|
61
|
+
- name: Set up Python
|
|
62
|
+
run: uv python install 3.12
|
|
63
|
+
|
|
64
|
+
- name: Install dependencies
|
|
65
|
+
run: uv sync
|
|
66
|
+
|
|
67
|
+
- name: Build SLURM test container
|
|
68
|
+
run: docker build -t researchloop-slurm-test tests/docker/slurm/
|
|
69
|
+
|
|
70
|
+
- name: Run integration tests
|
|
71
|
+
run: uv run pytest tests/integration/ -v --tb=short -x --timeout=120
|
|
72
|
+
env:
|
|
73
|
+
SLURM_TEST_IMAGE: researchloop-slurm-test
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
paths:
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- "mkdocs.yml"
|
|
10
|
+
- "README.md"
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
pages: write
|
|
15
|
+
id-token: write
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: "pages"
|
|
19
|
+
cancel-in-progress: true
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
deploy:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
environment:
|
|
25
|
+
name: github-pages
|
|
26
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
|
|
30
|
+
- name: Install uv
|
|
31
|
+
uses: astral-sh/setup-uv@v4
|
|
32
|
+
|
|
33
|
+
- name: Set up Python
|
|
34
|
+
run: uv python install 3.12
|
|
35
|
+
|
|
36
|
+
- name: Install docs dependencies
|
|
37
|
+
run: uv venv /tmp/docs-venv && uv pip install --python /tmp/docs-venv/bin/python mkdocs-material
|
|
38
|
+
|
|
39
|
+
- name: Build docs
|
|
40
|
+
run: /tmp/docs-venv/bin/mkdocs build --strict
|
|
41
|
+
|
|
42
|
+
- name: Setup Pages
|
|
43
|
+
uses: actions/configure-pages@v5
|
|
44
|
+
|
|
45
|
+
- name: Upload artifact
|
|
46
|
+
uses: actions/upload-pages-artifact@v3
|
|
47
|
+
with:
|
|
48
|
+
path: site
|
|
49
|
+
|
|
50
|
+
- name: Deploy to GitHub Pages
|
|
51
|
+
id: deployment
|
|
52
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
run: uv python install 3.12
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: uv run pytest tests/ -v --tb=short -m "not integration"
|
|
30
|
+
|
|
31
|
+
- name: Build package
|
|
32
|
+
run: uv build
|
|
33
|
+
|
|
34
|
+
- name: Publish to PyPI
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
36
|
+
|
|
37
|
+
- name: Create GitHub Release
|
|
38
|
+
uses: softprops/action-gh-release@v2
|
|
39
|
+
with:
|
|
40
|
+
generate_release_notes: true
|
|
41
|
+
files: dist/*
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Virtual environments
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
ENV/
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
*.egg
|
|
18
|
+
|
|
19
|
+
# Environment files
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
|
|
23
|
+
# Databases
|
|
24
|
+
*.db
|
|
25
|
+
*.sqlite
|
|
26
|
+
*.sqlite3
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.vscode/
|
|
30
|
+
.idea/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
*~
|
|
34
|
+
|
|
35
|
+
# OS
|
|
36
|
+
.DS_Store
|
|
37
|
+
Thumbs.db
|
|
38
|
+
|
|
39
|
+
# Testing / coverage
|
|
40
|
+
.coverage
|
|
41
|
+
htmlcov/
|
|
42
|
+
.pytest_cache/
|
|
43
|
+
|
|
44
|
+
# Type checking
|
|
45
|
+
.pyright/
|
|
46
|
+
.mypy_cache/
|
|
47
|
+
|
|
48
|
+
# Ruff
|
|
49
|
+
.ruff_cache/
|
|
50
|
+
|
|
51
|
+
# Artifacts
|
|
52
|
+
artifacts/
|
|
53
|
+
|
|
54
|
+
# MkDocs
|
|
55
|
+
site/
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Project overview
|
|
4
|
+
|
|
5
|
+
ResearchLoop is an automated research sprint platform for HPC clusters. It orchestrates multi-step AI research pipelines on SLURM/SGE clusters using `claude -p` for all AI work. The orchestrator is a lightweight Docker container; all heavy compute runs on HPC.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
Two processes:
|
|
10
|
+
|
|
11
|
+
1. **Orchestrator** (`researchloop serve`) — FastAPI server that manages studies/sprints in SQLite, submits jobs via SSH, receives webhooks from runners, stores artifacts. Also serves the web dashboard and handles Slack events.
|
|
12
|
+
2. **Sprint Runner** — runs inside each SLURM/SGE job on HPC. Self-contained bash scripts chain `claude -p` calls through a pipeline (research → red-team → fix → report → summarize), then upload artifacts and send a completion webhook.
|
|
13
|
+
|
|
14
|
+
Key design decisions:
|
|
15
|
+
|
|
16
|
+
- All AI work runs on HPC, never on the orchestrator (except Slack conversations and auto-loop idea generation, which use `claude -p` locally with restricted tools)
|
|
17
|
+
- `claude -p --output-format stream-json` for sprint steps (enables live progress), `--output-format json` for conversations
|
|
18
|
+
- SSH to HPC login nodes for sbatch/squeue/scancel/qsub/qdel
|
|
19
|
+
- Job completion via per-sprint webhook tokens (runner → orchestrator), SSH polling as fallback
|
|
20
|
+
- SQLite (aiosqlite, WAL mode) for metadata, with a `settings` table for persistent config (signing key, password hash)
|
|
21
|
+
- Jinja2 templates for all prompts and job scripts — prompts are pre-rendered by the orchestrator and embedded as base64 in the job script
|
|
22
|
+
- Auto-loop sprints generate their own ideas on the cluster (where Claude is authenticated) rather than on the orchestrator
|
|
23
|
+
- Context hierarchy: global → cluster → study (inline text + file paths at each level)
|
|
24
|
+
|
|
25
|
+
## Tech stack
|
|
26
|
+
|
|
27
|
+
Python 3.10+, uv, asyncio throughout. Key deps: click (CLI), FastAPI (API + dashboard), aiosqlite (DB), asyncssh (SSH), httpx (HTTP client), Jinja2 (templates), bcrypt + itsdangerous (dashboard auth), markdown (report rendering in dashboard).
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv sync # install deps
|
|
33
|
+
uv run pytest tests/ -v -m "not integration" # unit tests (339 tests, ~3s)
|
|
34
|
+
uv run pytest tests/integration/ -v --timeout=120 # integration tests (needs Docker)
|
|
35
|
+
uv run ruff check . # lint
|
|
36
|
+
uv run ruff format . # format
|
|
37
|
+
uv run pyright researchloop/ # type check
|
|
38
|
+
uv run researchloop --help # CLI help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Package layout
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
researchloop/
|
|
45
|
+
__init__.py — __version__
|
|
46
|
+
__main__.py — python -m researchloop entry point
|
|
47
|
+
cli.py — Click CLI (init, serve, connect, disconnect, status, study, sprint, loop, cluster commands)
|
|
48
|
+
core/
|
|
49
|
+
__init__.py
|
|
50
|
+
config.py — TOML config loading into dataclasses (Config, ClusterConfig, StudyConfig, SlackConfig, NtfyConfig, DashboardConfig) + env var overrides
|
|
51
|
+
models.py — SprintStatus enum, Sprint/Study/AutoLoop dataclasses, generate_sprint_id(), format_sprint_dirname()
|
|
52
|
+
orchestrator.py — Orchestrator class + create_app() FastAPI factory (API + Slack + dashboard)
|
|
53
|
+
credentials.py — CLI credential storage (~/.config/researchloop/credentials.json) for remote orchestrator auth
|
|
54
|
+
auth.py — check_claude_auth_async() helper for verifying Claude CLI auth status
|
|
55
|
+
db/
|
|
56
|
+
__init__.py
|
|
57
|
+
database.py — async SQLite wrapper (WAL mode, auto-migrations, fetch_one/fetch_all/execute)
|
|
58
|
+
migrations.py — CREATE TABLE statements (7 tables: studies, sprints, auto_loops, artifacts, slack_sessions, events, settings) + indexes + incremental column migrations
|
|
59
|
+
queries.py — async CRUD functions (all take Database as first arg, return dicts)
|
|
60
|
+
clusters/
|
|
61
|
+
__init__.py
|
|
62
|
+
ssh.py — SSHConnection (connect/run/upload_file/download_file) + SSHManager (connection pooling via asyncssh)
|
|
63
|
+
monitor.py — JobMonitor (polls active jobs via SSH, detects abandoned sprints via heartbeat timeout)
|
|
64
|
+
schedulers/
|
|
65
|
+
__init__.py
|
|
66
|
+
base.py — BaseScheduler ABC (submit/status/cancel)
|
|
67
|
+
slurm.py — SlurmScheduler (sbatch/squeue/sacct/scancel)
|
|
68
|
+
sge.py — SGEScheduler (qsub/qstat/qacct/qdel)
|
|
69
|
+
local.py — LocalScheduler (subprocesses, for testing)
|
|
70
|
+
sprints/
|
|
71
|
+
__init__.py
|
|
72
|
+
manager.py — SprintManager (create/submit/run/cancel/handle_completion + PDF fetch + idea fetch)
|
|
73
|
+
auto_loop.py — AutoLoopController (start/stop/resume/on_sprint_complete, LLM idea generation)
|
|
74
|
+
studies/
|
|
75
|
+
__init__.py
|
|
76
|
+
manager.py — StudyManager (config→DB sync, cluster config resolution)
|
|
77
|
+
runner/
|
|
78
|
+
__init__.py
|
|
79
|
+
main.py — Runner CLI entry point (researchloop-runner run)
|
|
80
|
+
pipeline.py — Pipeline class (runs the multi-step research pipeline)
|
|
81
|
+
claude.py — run_claude() wrapper + render_template()
|
|
82
|
+
upload.py — upload_artifacts(), send_webhook(), send_heartbeat()
|
|
83
|
+
templates/ — 6 Jinja2 prompt templates:
|
|
84
|
+
research_sprint.md.j2 — main research prompt (includes progress.md + output.log instructions)
|
|
85
|
+
red_team.md.j2 — critique/red-team prompt
|
|
86
|
+
fix_issues.md.j2 — fix prompt after red-team
|
|
87
|
+
report.md.j2 — comprehensive report generation
|
|
88
|
+
summarizer.md.j2 — short summary for notifications
|
|
89
|
+
idea_generator.md.j2 — next idea generation for auto-loops
|
|
90
|
+
job_templates/ — 2 job script templates:
|
|
91
|
+
slurm.sh.j2 — self-contained SLURM job script (includes stream-json processing, heartbeat loop, prompt embedding)
|
|
92
|
+
sge.sh.j2 — SGE equivalent
|
|
93
|
+
comms/
|
|
94
|
+
__init__.py
|
|
95
|
+
base.py — BaseNotifier ABC (notify_sprint_started/completed/failed)
|
|
96
|
+
ntfy.py — NtfyNotifier (ntfy.sh push notifications)
|
|
97
|
+
slack.py — SlackNotifier (chat:write + files:write) + verify_slack_signature()
|
|
98
|
+
conversation.py — ConversationManager (Slack threads → Claude sessions via --resume, action execution, markdown→Slack conversion)
|
|
99
|
+
router.py — NotificationRouter (fan-out to all configured notifiers)
|
|
100
|
+
dashboard/
|
|
101
|
+
__init__.py
|
|
102
|
+
app.py — ASGI app factory for `researchloop serve`
|
|
103
|
+
auth.py — Password auth (bcrypt hash/check, signed session cookies via itsdangerous, CSRF token generation/verification)
|
|
104
|
+
routes.py — Dashboard HTML routes (setup, login/logout, studies, sprints, loops, artifacts, refresh, cancel, delete, resubmit)
|
|
105
|
+
templates/ — 9 Jinja2 HTML templates:
|
|
106
|
+
base.html — layout with nav and auth state
|
|
107
|
+
setup.html — first-run password setup
|
|
108
|
+
login.html — login form
|
|
109
|
+
studies.html — study list
|
|
110
|
+
study_detail.html — study detail + sprint submission form
|
|
111
|
+
sprints.html — sprint list + new sprint form
|
|
112
|
+
sprint_detail.html — sprint detail (status, progress, log, report, artifacts, actions)
|
|
113
|
+
loops.html — auto-loop list + new loop form
|
|
114
|
+
loop_detail.html — loop detail with sprint list
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Database
|
|
118
|
+
|
|
119
|
+
SQLite with 7 tables: `studies`, `sprints`, `auto_loops`, `artifacts`, `slack_sessions`, `events`, `settings`. Schema in `db/migrations.py`. All queries in `db/queries.py` use parameterized SQL and return plain dicts.
|
|
120
|
+
|
|
121
|
+
Key columns:
|
|
122
|
+
- `sprints.webhook_token` — per-sprint token for webhook auth (generated at creation)
|
|
123
|
+
- `sprints.loop_id` — links sprint to its auto-loop
|
|
124
|
+
- `sprints.metadata_json` — stores report text, has_pdf flag, heartbeat info
|
|
125
|
+
- `sprints.error` — stores live progress (progress.md + output.log + tool log) during running sprints
|
|
126
|
+
- `auto_loops.metadata_json` — stores loop context and job_options
|
|
127
|
+
- `settings` — key/value store for signing_key and dashboard_password_hash
|
|
128
|
+
|
|
129
|
+
## Key patterns
|
|
130
|
+
|
|
131
|
+
- All source files use `from __future__ import annotations` for 3.10 compat
|
|
132
|
+
- Config is loaded from `researchloop.toml` (TOML) via `core/config.py`, with env var overrides (RESEARCHLOOP_* prefix)
|
|
133
|
+
- Database queries are plain async functions in `db/queries.py`, not methods on Database
|
|
134
|
+
- Schedulers take an SSH connection object but LocalScheduler ignores it
|
|
135
|
+
- SprintManager.submit_sprint() handles the full workflow: render prompt templates → render job template → SSH mkdir → base64 encode + write script → submit → update DB → notify
|
|
136
|
+
- Prompt templates are pre-rendered by the orchestrator and embedded as base64 in the job script, so the runner has no dependency on the orchestrator's template files
|
|
137
|
+
- Context hierarchy: global (config.context + config.context_paths) → cluster (cluster.context + cluster.context_paths) → study (study.context + study.claude_md_path), all concatenated into study_context
|
|
138
|
+
- The job script uses `--output-format stream-json` and pipes through a Python filter that logs tool usage summaries and captures the session ID
|
|
139
|
+
- Claude sessions persist across pipeline steps via `--resume $SESSION_ID`
|
|
140
|
+
- The runner writes `progress.md` (researcher updates) and pipes script output to `output.log` — both are sent via heartbeat and displayed in the dashboard
|
|
141
|
+
- Background heartbeat loop in job script sends log tail + progress.md + output.log + recent files every 60s
|
|
142
|
+
- Webhook retries: completion webhook retries 3 times with 10s delay
|
|
143
|
+
- Per-sprint webhook tokens: each sprint gets a unique token at creation, passed via X-Webhook-Token header
|
|
144
|
+
- Dashboard signing key is auto-generated and persisted in DB `settings` table (survives restarts)
|
|
145
|
+
- Dashboard password can come from: config TOML → env var (RESEARCHLOOP_DASHBOARD_PASSWORD auto-hashes) → DB settings table (set via first-run setup page)
|
|
146
|
+
- CSRF protection: HMAC-based tokens derived from session token + signing secret, checked on all mutating dashboard POST routes
|
|
147
|
+
- Dashboard refresh: pulls live status from cluster via SSH (reads logs, progress.md, output.log, report.md, findings.md, summary.txt, idea.txt, checks for PDF)
|
|
148
|
+
- Slack events: deduplication via event_id set, signature verification, background task processing (return 200 immediately), bot message filtering
|
|
149
|
+
- Slack conversation: thread → session mapping in DB, context building with study/sprint info, action execution via [ACTION: ...] tags
|
|
150
|
+
- Auto-loop: sprint idea=None → job script generates idea on cluster → idea.txt read back via SSH/webhook
|
|
151
|
+
- CLI auth: `researchloop connect` gets a bearer token via /api/auth, stored in ~/.config/researchloop/credentials.json with 600 permissions
|
|
152
|
+
- CLI auto-reauth: on 401, prompts for password, gets new token, saves it
|
|
153
|
+
- Always ensure the codebase passes type checking with pyright and ruff check before committing. Fix any errors that exist.
|
|
154
|
+
|
|
155
|
+
## Testing
|
|
156
|
+
|
|
157
|
+
339 unit tests covering: models, config parsing, database operations, all query functions, SLURM scheduler (mock SSH), SGE scheduler (mock SSH), local scheduler (real subprocesses), study/sprint managers, auto-loop controller (with mock claude), notification router, Slack notifier + signature verification + conversation manager + Slack events API, FastAPI API endpoints (TestClient), dashboard routes + auth + setup + CSRF, CLI commands (CliRunner), runner output parsing, and template rendering.
|
|
158
|
+
|
|
159
|
+
Integration tests (in tests/integration/) use a Docker SLURM container to test real job submission.
|
|
160
|
+
|
|
161
|
+
Tests use in-memory SQLite (`:memory:`) and mock SSH via AsyncMock.
|
|
162
|
+
|
|
163
|
+
Always add tests for any new functionality!
|
|
164
|
+
|
|
165
|
+
## CI
|
|
166
|
+
|
|
167
|
+
GitHub Actions (`.github/workflows/ci.yml`):
|
|
168
|
+
- **lint** — ruff check + ruff format --check + pyright type check
|
|
169
|
+
- **test** — pytest on Python 3.10, 3.12, 3.13 (unit tests only, `-m "not integration"`)
|
|
170
|
+
- **integration** — builds Docker SLURM container, runs integration tests with 120s timeout
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Stage 1: Build
|
|
2
|
+
FROM python:3.12-slim AS builder
|
|
3
|
+
|
|
4
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# Copy project files
|
|
9
|
+
COPY pyproject.toml .
|
|
10
|
+
COPY researchloop/ researchloop/
|
|
11
|
+
|
|
12
|
+
# Install dependencies into a virtual environment
|
|
13
|
+
RUN uv venv /app/.venv && \
|
|
14
|
+
uv pip install --python /app/.venv/bin/python .
|
|
15
|
+
|
|
16
|
+
# Stage 2: Runtime
|
|
17
|
+
FROM python:3.12-slim
|
|
18
|
+
|
|
19
|
+
# Install SSH client (needed to connect to HPC clusters)
|
|
20
|
+
RUN apt-get update && \
|
|
21
|
+
apt-get install -y --no-install-recommends openssh-client curl && \
|
|
22
|
+
rm -rf /var/lib/apt/lists/*
|
|
23
|
+
|
|
24
|
+
# Install Claude CLI (needed for auto-loop idea generation + Slack)
|
|
25
|
+
RUN curl -fsSL https://claude.ai/install.sh | sh || true
|
|
26
|
+
|
|
27
|
+
WORKDIR /app
|
|
28
|
+
|
|
29
|
+
# Copy virtual environment and application from builder
|
|
30
|
+
COPY --from=builder /app/.venv /app/.venv
|
|
31
|
+
COPY --from=builder /app/researchloop /app/researchloop
|
|
32
|
+
|
|
33
|
+
# Put venv and claude on PATH
|
|
34
|
+
ENV PATH="/root/.claude/bin:/app/.venv/bin:$PATH"
|
|
35
|
+
|
|
36
|
+
# Data directory — mount a persistent volume here
|
|
37
|
+
ENV RESEARCHLOOP_DB_PATH="/data/researchloop.db"
|
|
38
|
+
ENV RESEARCHLOOP_ARTIFACT_DIR="/data/artifacts"
|
|
39
|
+
RUN mkdir -p /data/artifacts
|
|
40
|
+
|
|
41
|
+
EXPOSE 8080
|
|
42
|
+
|
|
43
|
+
CMD ["researchloop", "serve"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ResearchLoop Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|