omnirun 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.
Files changed (61) hide show
  1. omnirun-0.1.0/.envrc +3 -0
  2. omnirun-0.1.0/.github/workflows/checks.yml +36 -0
  3. omnirun-0.1.0/.github/workflows/ci.yml +10 -0
  4. omnirun-0.1.0/.github/workflows/publish.yml +38 -0
  5. omnirun-0.1.0/.gitignore +20 -0
  6. omnirun-0.1.0/.python-version +1 -0
  7. omnirun-0.1.0/DESIGN.md +289 -0
  8. omnirun-0.1.0/PKG-INFO +312 -0
  9. omnirun-0.1.0/README.md +292 -0
  10. omnirun-0.1.0/TESTING.md +212 -0
  11. omnirun-0.1.0/flake.lock +113 -0
  12. omnirun-0.1.0/flake.nix +81 -0
  13. omnirun-0.1.0/main.py +6 -0
  14. omnirun-0.1.0/pyproject.toml +49 -0
  15. omnirun-0.1.0/research/colab-kaggle.md +187 -0
  16. omnirun-0.1.0/research/gpu-marketplaces.md +196 -0
  17. omnirun-0.1.0/research/launcher-landscape.md +75 -0
  18. omnirun-0.1.0/research/ml-job-queues.md +79 -0
  19. omnirun-0.1.0/research/slurm-ssh.md +156 -0
  20. omnirun-0.1.0/src/omnirun/__init__.py +3 -0
  21. omnirun-0.1.0/src/omnirun/backends/__init__.py +3 -0
  22. omnirun-0.1.0/src/omnirun/backends/base.py +118 -0
  23. omnirun-0.1.0/src/omnirun/backends/colab.py +605 -0
  24. omnirun-0.1.0/src/omnirun/backends/jobdir.py +225 -0
  25. omnirun-0.1.0/src/omnirun/backends/kaggle.py +605 -0
  26. omnirun-0.1.0/src/omnirun/backends/local.py +193 -0
  27. omnirun-0.1.0/src/omnirun/backends/marketplace.py +464 -0
  28. omnirun-0.1.0/src/omnirun/backends/runpod.py +171 -0
  29. omnirun-0.1.0/src/omnirun/backends/slurm.py +500 -0
  30. omnirun-0.1.0/src/omnirun/backends/ssh.py +312 -0
  31. omnirun-0.1.0/src/omnirun/backends/thunder.py +182 -0
  32. omnirun-0.1.0/src/omnirun/backends/vast.py +201 -0
  33. omnirun-0.1.0/src/omnirun/bootstrap.py +325 -0
  34. omnirun-0.1.0/src/omnirun/chooser.py +219 -0
  35. omnirun-0.1.0/src/omnirun/cli.py +893 -0
  36. omnirun-0.1.0/src/omnirun/config.py +150 -0
  37. omnirun-0.1.0/src/omnirun/daemon.py +486 -0
  38. omnirun-0.1.0/src/omnirun/execlayer/__init__.py +3 -0
  39. omnirun-0.1.0/src/omnirun/execlayer/base.py +97 -0
  40. omnirun-0.1.0/src/omnirun/execlayer/local.py +81 -0
  41. omnirun-0.1.0/src/omnirun/execlayer/ssh.py +259 -0
  42. omnirun-0.1.0/src/omnirun/models.py +196 -0
  43. omnirun-0.1.0/src/omnirun/queue.py +106 -0
  44. omnirun-0.1.0/src/omnirun/repo.py +160 -0
  45. omnirun-0.1.0/src/omnirun/store.py +107 -0
  46. omnirun-0.1.0/tests/__init__.py +0 -0
  47. omnirun-0.1.0/tests/conftest.py +80 -0
  48. omnirun-0.1.0/tests/test_bootstrap.py +123 -0
  49. omnirun-0.1.0/tests/test_chooser.py +243 -0
  50. omnirun-0.1.0/tests/test_cli.py +525 -0
  51. omnirun-0.1.0/tests/test_colab.py +385 -0
  52. omnirun-0.1.0/tests/test_kaggle.py +474 -0
  53. omnirun-0.1.0/tests/test_local_backend.py +182 -0
  54. omnirun-0.1.0/tests/test_marketplaces.py +687 -0
  55. omnirun-0.1.0/tests/test_queue.py +321 -0
  56. omnirun-0.1.0/tests/test_repo.py +175 -0
  57. omnirun-0.1.0/tests/test_slurm.py +613 -0
  58. omnirun-0.1.0/tests/test_ssh_backend.py +413 -0
  59. omnirun-0.1.0/tests/test_ssh_exec.py +327 -0
  60. omnirun-0.1.0/tests/test_store.py +85 -0
  61. omnirun-0.1.0/uv.lock +1321 -0
omnirun-0.1.0/.envrc ADDED
@@ -0,0 +1,3 @@
1
+ if has nix; then
2
+ use flake .
3
+ fi
@@ -0,0 +1,36 @@
1
+ name: Checks
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ jobs:
7
+ lint-typecheck-test:
8
+ runs-on: ubuntu-latest
9
+ env:
10
+ UV_CACHE_DIR: /tmp/.uv-cache
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v7
14
+
15
+ - name: Install Nix
16
+ uses: DeterminateSystems/determinate-nix-action@v3
17
+
18
+ - name: Restore uv cache
19
+ uses: actions/cache@v4
20
+ with:
21
+ path: /tmp/.uv-cache
22
+ key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
23
+ restore-keys: |
24
+ uv-${{ runner.os }}-
25
+
26
+ - name: ruff + ruff-format
27
+ run: nix flake check -L
28
+
29
+ - name: basedpyright
30
+ run: nix develop --command basedpyright
31
+
32
+ - name: Tests
33
+ run: nix develop --command uv run pytest -q
34
+
35
+ - name: Minimize uv cache
36
+ run: nix develop --command uv cache prune --ci
@@ -0,0 +1,10 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ checks:
10
+ uses: ./.github/workflows/checks.yml
@@ -0,0 +1,38 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ checks:
10
+ uses: ./.github/workflows/checks.yml
11
+
12
+ publish:
13
+ needs: checks
14
+ runs-on: ubuntu-latest
15
+ environment:
16
+ name: pypi
17
+ url: https://pypi.org/project/omnirun/
18
+ permissions:
19
+ id-token: write
20
+ contents: read
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v7
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
27
+ with:
28
+ enable-cache: true
29
+ cache-dependency-glob: "uv.lock"
30
+
31
+ - name: Install Python
32
+ run: uv python install 3.12
33
+
34
+ - name: Build
35
+ run: uv build
36
+
37
+ - name: Publish
38
+ run: uv publish
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .direnv/
6
+ .pytest_cache/
7
+ .hypothesis/
8
+ .ruff_cache/
9
+ .env
10
+ dist/
11
+ result
12
+ # generated by git-hooks.nix shellHook; bakes in local /nix/store paths
13
+ .pre-commit-config.yaml
14
+
15
+ # transient state from running examples/ against real R2
16
+ .dload-cache*/
17
+ .fsdd-download/
18
+ .cli-demo/
19
+ .dload-tmp/
20
+ .downloads/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,289 @@
1
+ # omnirun — design
2
+
3
+ > Run a job from your repo anywhere: uni Slurm cluster, a friend's gaming rig over SSH,
4
+ > Kaggle, Colab, or an auto-provisioned marketplace GPU — with one command, picking the
5
+ > cheapest/fastest option that fits.
6
+
7
+ Status: v0 design, finalized against the research reports in `research/`
8
+ (landscape verdict: no existing tool covers Slurm-over-SSH + Colab + Kaggle +
9
+ marketplaces + cost-vs-wait choice; closest are SkyPilot and dstack, both heavy
10
+ and structurally unable to reach notebooks or user-space-only clusters. We build,
11
+ stealing: dstack's offer-table UX, SkyPilot's slurm-as-a-cloud shape, ClearML's
12
+ job-envelope idea, submitit's two-layer resource config).
13
+
14
+ ## 1. Philosophy
15
+
16
+ - **The repo is the unit of deployment.** A job is `(git revision, command, resources)`.
17
+ omnirun ensures the revision is pushed, materializes it on the worker (worktree per
18
+ branch), sets up the env, runs the command, captures outputs. No image building, no
19
+ data syncing — jobs own their data.
20
+ - **One bootstrap script, many wrappers.** Every backend ultimately executes the same
21
+ generated POSIX-ish bash payload. Backends differ only in *how* the payload gets
22
+ executed (nohup over ssh, sbatch, Kaggle kernel cell, Colab cell, provisioned VM).
23
+ - **No control plane.** State lives in `~/.local/share/omnirun/` on the client. Polling,
24
+ not callbacks. If the laptop is off, jobs still run; state re-syncs on next `omnirun ps`.
25
+ - **Choice is a first-class output.** `submit` produces *offers* (cost × wait × fit).
26
+ Clear winner → auto-submit. Genuine tradeoff → show the table, let the human pick.
27
+
28
+ ## 2. Core model
29
+
30
+ ```
31
+ JobSpec
32
+ name: str # human label; job_id = <name>-<hex6>
33
+ command: list[str] | str # executed via bash -c in repo root on worker
34
+ resources: ResourceSpec
35
+ env: EnvSpec # auto-detected, overridable
36
+ outputs: list[str] # globs relative to repo root, collected post-run
37
+ repo: RepoRef # remote url, sha, branch (captured at submit)
38
+
39
+ ResourceSpec
40
+ gpus: int = 0
41
+ gpu_type: str | None # "H100", "A100-80", "4090", ... (normalized names)
42
+ min_vram_gb: float | None # alternative to gpu_type
43
+ cpus: int | None
44
+ mem_gb: float | None
45
+ time: timedelta | None # est. duration — drives cost math + slurm --time
46
+ disk_gb: float | None
47
+
48
+ Offer
49
+ backend: str # config key, e.g. "uni", "runpod"
50
+ label: str # "uni: gpu partition (A100)", "runpod: H100 SXM $2.79/hr"
51
+ fits: bool
52
+ unfit_reasons: list[str]
53
+ cost_estimate: Money | None # None = free
54
+ wait_estimate: timedelta | None # None = unknown
55
+ attended: bool # True = requires a human click (Colab)
56
+ details: dict # backend-specific (instance id template, partition, ...)
57
+
58
+ JobStatus: QUEUED | PROVISIONING | STARTING | RUNNING | SUCCEEDED | FAILED |
59
+ CANCELLED | LOST # LOST = can't reach worker / handle stale
60
+ ```
61
+
62
+ ### Backend protocol
63
+
64
+ ```python
65
+ class Backend(Protocol):
66
+ name: str
67
+ def probe(self, res: ResourceSpec) -> list[Offer]: ...
68
+ def submit(self, spec: JobSpec, offer: Offer) -> JobHandle: ...
69
+ def status(self, h: JobHandle) -> JobStatus: ...
70
+ def logs(self, h: JobHandle, follow: bool) -> Iterator[str]: ...
71
+ def cancel(self, h: JobHandle) -> None: ...
72
+ def pull_outputs(self, h: JobHandle, dest: Path) -> None: ...
73
+ ```
74
+
75
+ `probe` must be fast (<10s) and safe to run speculatively; probes run in parallel
76
+ with per-backend timeout. A backend that errors during probe yields a not-fit offer
77
+ with the error as reason — never crashes the chooser.
78
+
79
+ ### Backend composition matrix
80
+
81
+ Two orthogonal layers for the SSH family:
82
+
83
+ - **Transport**: `LocalExec` (testing) | `SSHExec` (openssh binary, ControlMaster
84
+ multiplexing, respects `~/.ssh/config` so jump hosts/2FA/kerberos just work).
85
+ - **Runtime**: `detached` (setsid+nohup, pidfile) | `slurm` (sbatch codegen + sacct
86
+ polling).
87
+
88
+ Concrete backends:
89
+
90
+ | backend key | transport | runtime | provisioning | attended |
91
+ |---|---|---|---|---|
92
+ | `local` | local | detached | — | no |
93
+ | `ssh` (uncle's rig) | ssh | detached | — | no |
94
+ | `slurm` (uni) | ssh | slurm | — | no |
95
+ | `runpod` / `vast` / `thunder` | ssh | detached | REST API create→ssh→terminate | no |
96
+ | `kaggle` | kernels API | kernel wraps bootstrap | — | no |
97
+ | `colab` | `google-colab-cli` | exec cell wraps bootstrap | — | no (one-time OAuth) |
98
+
99
+ ## 3. The bootstrap payload
100
+
101
+ Generated per job: `bootstrap.sh`, parameterized by a small `job.json` sidecar. Steps:
102
+
103
+ 1. `mkdir -p $OMNIRUN_ROOT/{repos,jobs}` (default `~/.omnirun`, configurable per backend
104
+ — on clusters typically `$SCRATCH/omnirun`).
105
+ 2. **Code**: bare-ish mirror clone per repo (`repos/<slug>.git`), `git fetch`, then
106
+ `git worktree add jobs/<job_id>/tree <sha>` (worktree per job, pruned on cleanup;
107
+ cheap because objects are shared via the mirror). Private repos: see §6.
108
+ 3. **Env** (in `jobs/<job_id>/tree`): detection order
109
+ `uv.lock → uv sync`, `pyproject.toml → uv sync` (installs uv via standalone
110
+ installer if missing — static binary, works on old-glibc HPC),
111
+ `requirements.txt → uv venv + uv pip install -r`,
112
+ `environment.yml → micromamba` (static binary bootstrap).
113
+ Overridable: `env.setup = ["module load cuda/12.4", "uv sync"]` in config for
114
+ cluster quirks.
115
+ 4. **Run**: `cd tree && <command>` with stdout+stderr tee'd to `jobs/<job_id>/logs/`,
116
+ heartbeat file touched every 30s, `result.json` written on exit
117
+ `{exit_code, started_at, finished_at, hostname}`.
118
+ 5. **Outputs**: copy `outputs` globs → `jobs/<job_id>/outputs/`. Client pulls via
119
+ rsync/scp (ssh family), kernel output download (Kaggle), Drive (Colab).
120
+
121
+ Status without a control plane: the job dir *is* the status API — presence of
122
+ `result.json`, heartbeat freshness, plus runtime-native signals (slurm state, PID
123
+ alive, kernel status) are merged into one JobStatus.
124
+
125
+ ## 4. Chooser
126
+
127
+ 1. Probe all enabled backends in parallel (timeout 10s each).
128
+ 2. Partition offers: fit / unfit.
129
+ 3. Score fitting offers. Default policy (configurable weights):
130
+ - `total_cost = hourly × est_time` (0 for free backends)
131
+ - `time_to_result = wait_estimate + est_time`
132
+ - **Auto-pick** iff a free offer has `wait < auto_wait_threshold` (default 15m),
133
+ or exactly one offer fits, or `--yes` with a `--max-cost`.
134
+ - Otherwise render the table (rich): backend, GPU, $/hr, est. total $, est. wait,
135
+ time-to-result, notes — user picks by number. `--yes` picks the top-ranked.
136
+
137
+ Wait estimation is honest about uncertainty. Slurm: `sinfo` idle-node check
138
+ ("likely immediate") → `sbatch --test-only` pre-submit estimate + script validation
139
+ → post-submit `squeue --start`, always labeled "backfill estimate, usually
140
+ pessimistic"; plus a local history of our own (partition, resources)→actual-wait
141
+ medians. Marketplaces report provisioning latency (~1–3 min). Kaggle/Colab report
142
+ queue-free but quota/session-bounded (unfit if `resources.time` > 12h etc.).
143
+
144
+ ## 5. State & config
145
+
146
+ - Client state: `~/.local/share/omnirun/jobs/<job_id>/meta.json` (spec, handle,
147
+ last-known status, offer chosen). Plain JSON, greppable, no daemon.
148
+ - Config: `~/.config/omnirun/config.toml` (backends, credentials refs, policy) +
149
+ optional per-repo `omnirun.toml` (resource defaults, outputs, env.setup overrides).
150
+
151
+ ```toml
152
+ [policy]
153
+ auto_wait_threshold = "15m"
154
+ max_hourly_default = 5.0
155
+
156
+ [backends.uni]
157
+ type = "slurm"
158
+ host = "hpc-login" # ssh config alias; ProxyJump etc. live in ~/.ssh/config
159
+ partition = "gpu"
160
+ account = "myproject"
161
+ gpu_types = { "A100-80" = "a100:{n}", "V100" = "v100:{n}" } # → --gres
162
+ root = "$SCRATCH/omnirun"
163
+ env_setup = ["module load cuda/12.4"]
164
+
165
+ [backends.rig]
166
+ type = "ssh"
167
+ host = "uncle-gaming"
168
+ gpus = [{ type = "4090", count = 1 }] # static capability declaration
169
+
170
+ [backends.kaggle]
171
+ type = "kaggle" # creds from ~/.config/kaggle/kaggle.json
172
+
173
+ [backends.colab]
174
+ type = "colab"
175
+ drive_dir = "omnirun" # folder in My Drive used as mailbox
176
+
177
+ [backends.runpod]
178
+ type = "runpod" # RUNPOD_API_KEY env
179
+ max_hourly = 3.5
180
+
181
+ [backends.vast]
182
+ type = "vast" # VAST_API_KEY env
183
+
184
+ [backends.thunder]
185
+ type = "thunder" # TNR_API_TOKEN env
186
+ ```
187
+
188
+ ## 6. Repos & credentials
189
+
190
+ Submit-time invariant: working tree clean (or `--dirty` to auto-stash-commit onto a
191
+ `omnirun/<job_id>` ref — v1), HEAD pushed to remote (offer to push).
192
+
193
+ Worker access to private repos — **no git credentials ever leave the laptop**:
194
+ - **ssh/slurm/marketplace**: at submit time the client `git push`es the exact sha
195
+ over its own SSH connection into the worker-side bare repo
196
+ (`repos/<slug>.git`, created on demand). Nothing on the worker can or needs to
197
+ reach the origin remote. (Documented alternatives: agent-forwarded
198
+ `git fetch origin` for huge repos; per-repo deploy keys.)
199
+ - **kaggle/colab**: the revision travels as a **`git bundle`** — uploaded via
200
+ `colab upload` (Colab) or packed into a private per-job Kaggle dataset attached
201
+ through `dataset_sources` (Kaggle). The bootstrap clones from the bundle.
202
+ Optional GitHub-PAT-in-Kaggle-secret flow only if the user prefers fetching.
203
+
204
+ ## 7. Notebook backends
205
+
206
+ - **Kaggle**: fully automated via the `kaggle` Python API (`kernels_push` /
207
+ `kernels_status` / `kernels_output`). Per job: a private script kernel
208
+ (`title == slug == omnirun-<job_id>`, `enable_internet`, `machine_shape` mapped
209
+ from ResourceSpec: free set = P100 / 2×T4; L4/A100/H100 exist but are
210
+ Colab-Pro-gated → surfaced as conditional offers) + a private per-job dataset
211
+ carrying the git bundle. The kernel script unpacks the bundle and runs the
212
+ bootstrap; outputs land in `/kaggle/working` (≤20 GB, ≤500 files → tar).
213
+ Probe constraints: 12h GPU / 9h TPU session cap → unfit if `resources.time`
214
+ exceeds; ~30 GPU-h/week quota is not queryable → tracked locally in state as a
215
+ budget. Poll ≥30s with backoff honoring 429/Retry-After.
216
+ - **Colab**: fully automated via the official `google-colab-cli` (v0.6+, June 2026;
217
+ one-time OAuth). Submit = `colab new --gpu <T4|L4|G4|A100|H100>` → `colab upload`
218
+ bundle+bootstrap → `colab exec` a launcher cell that starts `bootstrap.sh`
219
+ detached under the kernel and returns → CLI keep-alive daemon holds the VM.
220
+ Status = short `colab exec` beacon reads (or heartbeat-file check); logs via
221
+ incremental file reads; `colab download` outputs; `colab stop` to release.
222
+ Probe honesty: free tier = T4 lottery + ~12h cap; paid = compute-unit burn
223
+ surfaced as approximate cost. Caveat: keep-alive daemon runs on the client — a
224
+ sleeping laptop may lose idle sessions (the running kernel itself counts as
225
+ activity, so mid-job this is mostly moot).
226
+
227
+ ## 8. Marketplace backends
228
+
229
+ `probe` = price/availability query filtered by `gpu_type`/`min_vram`, returns
230
+ cheapest few as offers. `submit` = create instance (stock CUDA+ssh image) → wait for
231
+ ssh → run bootstrap detached → poll. Auto-terminate on completion (configurable
232
+ `keep_alive`), plus `omnirun gc` to reap leaked instances (safety net against
233
+ billing surprises). Idle-timeout watchdog baked into the payload wrapper.
234
+
235
+ ## 9. CLI
236
+
237
+ ```
238
+ omnirun submit [--name N] [--gpus 1] [--gpu-type H100 | --vram 40] [--time 15h]
239
+ [--backend uni] [--yes] [--max-cost 30] [--dirty] -- python train.py ...
240
+ omnirun offers [same resource flags] # probe & table, no submit
241
+ omnirun ps # all known jobs, refreshed statuses
242
+ omnirun status <job> | logs [-f] <job> | cancel <job> | pull <job> [dest]
243
+ omnirun backends check # config + connectivity sanity per backend
244
+ omnirun gc # reap finished worktrees, leaked instances
245
+ ```
246
+
247
+ ## 10. Implementation notes
248
+
249
+ - Python ≥3.12, deps: `typer`, `rich`, `httpx`, `pydantic`, `kaggle` (thin API
250
+ client), `google-colab-cli` (optional extra). Marketplaces via plain REST
251
+ (httpx): RunPod REST (`rest.runpod.io/v1`) + one GraphQL pricing query; Vast
252
+ `console.vast.ai/api/v0` (`POST /bundles/` search → `PUT /asks/{id}/` rent);
253
+ Thunder `api.thundercompute.com:8443/v1` (public `/pricing`, `/v2/status`).
254
+ No vendor SDKs, no gpuhunt (live price calls are single cheap requests).
255
+ - SSH: shell out to `ssh`/`rsync` binaries with tool-managed ControlMaster sockets
256
+ (`ControlPath` under `~/.ssh/` with `%C` hash, `ControlPersist=10m`,
257
+ `BatchMode=yes` for background polls only). Only the OpenSSH binary rides
258
+ existing 2FA/Kerberos sessions and honors ProxyJump/Match from `~/.ssh/config` —
259
+ no paramiko/asyncssh.
260
+ - Slurm specifics: submit via `ssh host 'sbatch --parsable' < script`; prefer
261
+ `--gres=gpu:<type>:{n}` (with per-site `--constraint` alternative in gpu_map);
262
+ explicit `--output/--error` under the job dir; namespaced `--job-name`;
263
+ batch-poll all live jobs in one `squeue`/`sacct -X --parsable2` call ≥30s;
264
+ `echo $? > result` backstop for accounting-less clusters; `--dry-run` prints the
265
+ exact sbatch script.
266
+ - Everything async-free: parallel probing via `concurrent.futures`. Simplicity > perf.
267
+ - Module layout:
268
+ ```
269
+ src/omnirun/
270
+ models.py # JobSpec, ResourceSpec, Offer, JobStatus, JobHandle
271
+ config.py # TOML load, backend registry, policy
272
+ repo.py # git state, clean/pushed checks, RepoRef, bundle creation
273
+ bootstrap.py # bootstrap.sh generation (shared payload)
274
+ store.py # ~/.local/share/omnirun/jobs/<id>/meta.json
275
+ chooser.py # parallel probing, ranking, offer table
276
+ execlayer/ # base.py (Exec protocol), local.py, ssh.py (ControlMaster)
277
+ backends/ # base.py, local.py, ssh.py, slurm.py, kaggle.py, colab.py,
278
+ # marketplace.py (shared provision→ssh→run), runpod.py,
279
+ # vast.py, thunder.py
280
+ cli.py # typer app
281
+ ```
282
+ - Testing tiers: (1) pure unit (codegen, ranking, repo state); (2) e2e through
283
+ `local` backend — full submit→run→pull without network; (3) dockerized sshd and
284
+ slurm cluster integration tests, opt-in; (4) live backends — needs user creds.
285
+
286
+ ## 11. Non-goals (v0)
287
+
288
+ Data syncing, artifact versioning, DAGs/pipelines, multi-node jobs, spot-preemption
289
+ recovery, image building, a web UI.