outerloop-science 0.1.0.dev0__py3-none-any.whl → 0.1.0.dev2__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.
- outerloop/__init__.py +7 -7
- outerloop/appauth.py +3 -3
- outerloop/appmanifest.py +16 -11
- outerloop/attempt.py +80 -52
- outerloop/cli.py +170 -86
- outerloop/climbboard.py +209 -4
- outerloop/compute.py +84 -7
- outerloop/contract.py +3 -2
- outerloop/followup.py +37 -23
- outerloop/github.py +4 -4
- outerloop/harness.py +18 -7
- outerloop/image.py +372 -0
- outerloop/init.py +417 -29
- outerloop/limits.py +1 -1
- outerloop/measure.py +2 -2
- outerloop/orchestrator.py +1 -1
- outerloop/paths.py +14 -1
- outerloop/review.py +1 -1
- outerloop/steward.py +3 -3
- outerloop/syscall.py +1 -0
- outerloop/tick.py +400 -105
- {outerloop_science-0.1.0.dev0.dist-info → outerloop_science-0.1.0.dev2.dist-info}/METADATA +14 -10
- {outerloop_science-0.1.0.dev0.dist-info → outerloop_science-0.1.0.dev2.dist-info}/RECORD +27 -26
- {outerloop_science-0.1.0.dev0.dist-info → outerloop_science-0.1.0.dev2.dist-info}/WHEEL +0 -0
- {outerloop_science-0.1.0.dev0.dist-info → outerloop_science-0.1.0.dev2.dist-info}/entry_points.txt +0 -0
- {outerloop_science-0.1.0.dev0.dist-info → outerloop_science-0.1.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {outerloop_science-0.1.0.dev0.dist-info → outerloop_science-0.1.0.dev2.dist-info}/licenses/NOTICE +0 -0
outerloop/cli.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
With `sbatch` on PATH it submits the resident tick
|
|
4
4
|
(docs/design/resident-tick.md) and returns; without it, or with
|
|
5
|
-
|
|
5
|
+
OUTERLOOP_COMPUTE=local, it runs the local loop in the foreground.
|
|
6
6
|
Settings come from flags, then the process environment, then
|
|
7
7
|
~/.config/outerloop/.env, read once here at launch. The running chain
|
|
8
8
|
never takes identity or placement from that file (tick_deploy.sh reads an
|
|
@@ -18,48 +18,59 @@ import shutil
|
|
|
18
18
|
import stat
|
|
19
19
|
import subprocess
|
|
20
20
|
import sys
|
|
21
|
+
from collections.abc import Mapping
|
|
21
22
|
from dataclasses import dataclass
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
|
|
24
25
|
from outerloop import paths
|
|
25
26
|
|
|
26
|
-
RESIDENT_JOB_NAME = "
|
|
27
|
+
RESIDENT_JOB_NAME = "outerloop-resident"
|
|
28
|
+
# A resident submitted before the rename. Slurm's singleton serializes jobs by
|
|
29
|
+
# NAME, so `start` must refuse while one of these is queued or running (two
|
|
30
|
+
# residents on one root is what the singleton prevents). Dropped in the
|
|
31
|
+
# release after 0.1.
|
|
32
|
+
LEGACY_RESIDENT_JOB_NAME = "autoresearch-resident"
|
|
27
33
|
DEFAULT_RESIDENT_MINUTES = 360 # cpu_short's ceiling on Torch; the loop hands over to itself
|
|
28
|
-
DEFAULT_LOCAL_ROOT = Path.home() / ".
|
|
34
|
+
DEFAULT_LOCAL_ROOT = Path.home() / ".outerloop"
|
|
35
|
+
LEGACY_LOCAL_ROOT_NAME = ".autoresearch" # pre-rename; honored until the release after 0.1
|
|
29
36
|
ENV_FILE = paths.ENV_FILE # ~/.config/outerloop/.env, or the pre-rename dir (see paths.py)
|
|
30
37
|
|
|
31
38
|
# What start itself decides from: mode, placement, root, cadence, walltime.
|
|
32
39
|
START_KEYS = (
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
+
"OUTERLOOP_COMPUTE",
|
|
41
|
+
"OUTERLOOP_ROOT",
|
|
42
|
+
"OUTERLOOP_ACCOUNT",
|
|
43
|
+
"OUTERLOOP_PARTITION",
|
|
44
|
+
"OUTERLOOP_CADENCE_MIN",
|
|
45
|
+
"OUTERLOOP_RESIDENT_MINUTES",
|
|
46
|
+
"OUTERLOOP_PAT_FILE",
|
|
40
47
|
)
|
|
41
48
|
# The author knobs the chain's deploy step exports from .env every tick. The
|
|
42
49
|
# local loop has no deploy step, so start exports them once at launch; a test
|
|
43
50
|
# keeps this list identical to tick_deploy.sh's.
|
|
44
51
|
TICK_ENV_KEYS = (
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
52
|
+
"OUTERLOOP_AUTHOR_BACKEND",
|
|
53
|
+
"OUTERLOOP_AUTHOR_MODEL",
|
|
54
|
+
"OUTERLOOP_CLAUDE_BIN",
|
|
55
|
+
"OUTERLOOP_CODEX_BIN",
|
|
56
|
+
"OUTERLOOP_CODEX_KEY_FILE",
|
|
57
|
+
"OUTERLOOP_CLAUDE_KEY_FILE",
|
|
58
|
+
"OUTERLOOP_HARNESS_KEY_FILE",
|
|
59
|
+
"OUTERLOOP_VERTEX_PROJECT",
|
|
60
|
+
"OUTERLOOP_VERTEX_REGION",
|
|
61
|
+
"OUTERLOOP_VERTEX_ADC",
|
|
62
|
+
"OUTERLOOP_TARGET",
|
|
63
|
+
"OUTERLOOP_GITHUB_APP_FILE",
|
|
64
|
+
"OUTERLOOP_BOT_LOGIN",
|
|
65
|
+
"OUTERLOOP_BOT_ALIASES",
|
|
66
|
+
"OUTERLOOP_GPU_PARTITION",
|
|
67
|
+
"OUTERLOOP_GPU_ACCOUNT",
|
|
68
|
+
"OUTERLOOP_MAX_LAUNCH_GPUS",
|
|
69
|
+
"OUTERLOOP_IMAGE",
|
|
70
|
+
"OUTERLOOP_PANEL",
|
|
71
|
+
"OUTERLOOP_PANEL_KEY_FILE",
|
|
72
|
+
"OUTERLOOP_PANEL_CODEX_KEY_FILE",
|
|
73
|
+
"OUTERLOOP_PANEL_HERMES_KEY_FILE",
|
|
63
74
|
"REVIEW_HERMES_REPO",
|
|
64
75
|
"REVIEW_HERMES_PROVIDER",
|
|
65
76
|
)
|
|
@@ -88,20 +99,26 @@ def env_file_values(path: Path = ENV_FILE, keys: tuple[str, ...] = START_KEYS) -
|
|
|
88
99
|
except OSError as e:
|
|
89
100
|
raise StartError(f"cannot read {path}: {e}") from None
|
|
90
101
|
out: dict[str, str] = {}
|
|
102
|
+
canonical: set[str] = set() # keys set by their OUTERLOOP_ spelling
|
|
91
103
|
for raw in text.splitlines():
|
|
92
104
|
line = raw.rstrip("\r").strip()
|
|
93
105
|
if not line or line.startswith("#") or "=" not in line:
|
|
94
106
|
continue
|
|
95
107
|
key, value = line.split("=", 1)
|
|
96
108
|
key = key.strip()
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
legacy = key.startswith("AUTORESEARCH_") # the pre-rename name, one more release
|
|
110
|
+
if legacy:
|
|
111
|
+
key = "OUTERLOOP_" + key[len("AUTORESEARCH_") :]
|
|
99
112
|
if key not in keys:
|
|
100
113
|
continue
|
|
114
|
+
if legacy and key in canonical:
|
|
115
|
+
continue # the OUTERLOOP_ spelling wins whatever the file order
|
|
101
116
|
value = value.strip()
|
|
102
117
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
103
118
|
value = value[1:-1]
|
|
104
119
|
out[key] = value
|
|
120
|
+
if not legacy:
|
|
121
|
+
canonical.add(key)
|
|
105
122
|
return out
|
|
106
123
|
|
|
107
124
|
|
|
@@ -123,18 +140,19 @@ class StartPlan:
|
|
|
123
140
|
reads as "whichever frees up first") without corrupting the export
|
|
124
141
|
delimiter. start() merges these into the environment it hands sbatch."""
|
|
125
142
|
env = {
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"AUTORESEARCH_RESIDENT_MINUTES": str(self.resident_minutes), # successors reuse it
|
|
143
|
+
"OUTERLOOP_RESIDENT": "1",
|
|
144
|
+
"OUTERLOOP_HOME": str(self.home),
|
|
145
|
+
"OUTERLOOP_ROOT": str(self.root),
|
|
146
|
+
"OUTERLOOP_RESIDENT_MINUTES": str(self.resident_minutes), # successors reuse it
|
|
131
147
|
}
|
|
148
|
+
if self.account:
|
|
149
|
+
env["OUTERLOOP_ACCOUNT"] = self.account
|
|
132
150
|
if self.partition:
|
|
133
|
-
env["
|
|
151
|
+
env["OUTERLOOP_PARTITION"] = self.partition
|
|
134
152
|
if self.cadence_min:
|
|
135
|
-
env["
|
|
153
|
+
env["OUTERLOOP_CADENCE_MIN"] = self.cadence_min
|
|
136
154
|
if self.pat_file:
|
|
137
|
-
env["
|
|
155
|
+
env["OUTERLOOP_PAT_FILE"] = self.pat_file
|
|
138
156
|
return env
|
|
139
157
|
|
|
140
158
|
def command(self) -> list[str]:
|
|
@@ -146,8 +164,9 @@ class StartPlan:
|
|
|
146
164
|
"--dependency=singleton", # two starts can both submit; only one ever runs
|
|
147
165
|
f"--time={self.resident_minutes}",
|
|
148
166
|
f"--job-name={RESIDENT_JOB_NAME}",
|
|
149
|
-
f"--account={self.account}",
|
|
150
167
|
]
|
|
168
|
+
if self.account: # unset bills the caller's default Slurm association
|
|
169
|
+
argv.append(f"--account={self.account}")
|
|
151
170
|
if self.partition: # unset lets Slurm choose its default partition
|
|
152
171
|
argv.append(f"--partition={self.partition}")
|
|
153
172
|
# --export=ALL carries export_env() from the inherited environment; a
|
|
@@ -165,19 +184,23 @@ def _setting(key: str, flag: str, environ: dict[str, str], from_file: dict[str,
|
|
|
165
184
|
return from_file.get(key, "")
|
|
166
185
|
|
|
167
186
|
|
|
168
|
-
def _home(environ: dict[str, str], cwd: Path) -> Path:
|
|
169
|
-
"""The
|
|
170
|
-
|
|
171
|
-
and
|
|
172
|
-
|
|
173
|
-
|
|
187
|
+
def _home(environ: dict[str, str], cwd: Path, *, local: bool, root: Path) -> Path:
|
|
188
|
+
"""The directory the loop runs from. On Slurm it must be a source
|
|
189
|
+
checkout (OUTERLOOP_HOME, else the current directory): the chain
|
|
190
|
+
deploys from it and every job runs from a flight snapshot of its HEAD.
|
|
191
|
+
The local loop has no deploy step and runs the installed package, so it
|
|
192
|
+
uses a checkout when one is at hand and otherwise a `home` directory
|
|
193
|
+
under the state root, where flights and logs land."""
|
|
194
|
+
named = environ.get("OUTERLOOP_HOME") or environ.get("AUTORESEARCH_HOME")
|
|
195
|
+
home = Path(named).expanduser() if named else cwd
|
|
196
|
+
if (home / "scripts" / "tick_chain.sbatch").is_file():
|
|
197
|
+
return home
|
|
198
|
+
if local and not named:
|
|
199
|
+
return root / "home"
|
|
200
|
+
raise StartError(
|
|
201
|
+
f"{home} is not a source checkout (no scripts/tick_chain.sbatch); "
|
|
202
|
+
"run start from a clone of the kernel, or set OUTERLOOP_HOME to one"
|
|
174
203
|
)
|
|
175
|
-
if not (home / "scripts" / "tick_chain.sbatch").is_file():
|
|
176
|
-
raise StartError(
|
|
177
|
-
f"{home} is not an autoresearch checkout (no scripts/tick_chain.sbatch); "
|
|
178
|
-
"run start from the checkout the chain should deploy from, or set AUTORESEARCH_HOME"
|
|
179
|
-
)
|
|
180
|
-
return home
|
|
181
204
|
|
|
182
205
|
|
|
183
206
|
def plan_start(
|
|
@@ -191,10 +214,10 @@ def plan_start(
|
|
|
191
214
|
sbatch_on_path: bool,
|
|
192
215
|
cwd: Path,
|
|
193
216
|
) -> StartPlan:
|
|
194
|
-
compute = _setting("
|
|
217
|
+
compute = _setting("OUTERLOOP_COMPUTE", "local" if local else "", environ, from_file)
|
|
195
218
|
mode = "local" if compute.strip().lower() == "local" or not sbatch_on_path else "slurm"
|
|
196
|
-
root_s = _setting("
|
|
197
|
-
cadence = _setting("
|
|
219
|
+
root_s = _setting("OUTERLOOP_ROOT", root, environ, from_file)
|
|
220
|
+
cadence = _setting("OUTERLOOP_CADENCE_MIN", "", environ, from_file)
|
|
198
221
|
if cadence:
|
|
199
222
|
# the chain divides by it and the loop sleeps on it: a bad value would
|
|
200
223
|
# only surface after the job started
|
|
@@ -204,16 +227,18 @@ def plan_start(
|
|
|
204
227
|
cadence_ok = False
|
|
205
228
|
if not cadence_ok:
|
|
206
229
|
raise StartError(
|
|
207
|
-
f"
|
|
230
|
+
f"OUTERLOOP_CADENCE_MIN must be a positive number of minutes, got {cadence!r}"
|
|
208
231
|
)
|
|
209
|
-
pat = _setting("
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
|
|
232
|
+
pat = _setting("OUTERLOOP_PAT_FILE", "", environ, from_file)
|
|
233
|
+
# Slurm runs from a checkout; the local loop runs the installed package
|
|
234
|
+
# and needs only a directory (the launch lanes and GitHub servicing
|
|
235
|
+
# switch off without OUTERLOOP_HOME, so one is always set)
|
|
236
|
+
local_root = Path(root_s).expanduser() if root_s else default_local_root(environ)
|
|
237
|
+
home = _home(environ, cwd, local=(mode == "local"), root=local_root)
|
|
213
238
|
if mode == "local":
|
|
214
239
|
return StartPlan(
|
|
215
240
|
mode="local",
|
|
216
|
-
root=
|
|
241
|
+
root=local_root,
|
|
217
242
|
home=home,
|
|
218
243
|
cadence_min=cadence,
|
|
219
244
|
pat_file=pat,
|
|
@@ -221,23 +246,22 @@ def plan_start(
|
|
|
221
246
|
if not root_s:
|
|
222
247
|
raise StartError(
|
|
223
248
|
"Slurm mode needs the state root on the shared filesystem: "
|
|
224
|
-
"--root,
|
|
249
|
+
"--root, OUTERLOOP_ROOT in the environment, or OUTERLOOP_ROOT= in "
|
|
250
|
+
"~/.config/outerloop/.env"
|
|
225
251
|
)
|
|
226
|
-
acc = _setting("
|
|
227
|
-
part = _setting("
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
raise StartError("Slurm mode needs --account / AUTORESEARCH_ACCOUNT")
|
|
232
|
-
minutes_s = _setting("AUTORESEARCH_RESIDENT_MINUTES", "", environ, from_file)
|
|
252
|
+
acc = _setting("OUTERLOOP_ACCOUNT", account, environ, from_file)
|
|
253
|
+
part = _setting("OUTERLOOP_PARTITION", partition, environ, from_file)
|
|
254
|
+
# Account and partition are both optional: left unset, Slurm bills the
|
|
255
|
+
# caller's default association and places the job on its default partition.
|
|
256
|
+
minutes_s = _setting("OUTERLOOP_RESIDENT_MINUTES", "", environ, from_file)
|
|
233
257
|
try:
|
|
234
258
|
minutes = int(minutes_s) if minutes_s else DEFAULT_RESIDENT_MINUTES
|
|
235
259
|
except ValueError:
|
|
236
260
|
raise StartError(
|
|
237
|
-
f"
|
|
261
|
+
f"OUTERLOOP_RESIDENT_MINUTES must be a whole number of minutes, got {minutes_s!r}"
|
|
238
262
|
) from None
|
|
239
263
|
if minutes <= 0:
|
|
240
|
-
raise StartError("
|
|
264
|
+
raise StartError("OUTERLOOP_RESIDENT_MINUTES must be positive")
|
|
241
265
|
# These ride the inherited environment (sbatch --export=ALL), so a comma is
|
|
242
266
|
# safe now (a multi-partition `a,b` is valid) — only a newline would corrupt
|
|
243
267
|
# the environment or the sbatch argv.
|
|
@@ -263,16 +287,30 @@ def plan_start(
|
|
|
263
287
|
)
|
|
264
288
|
|
|
265
289
|
|
|
290
|
+
def default_local_root(environ: Mapping[str, str]) -> Path:
|
|
291
|
+
"""The local-mode state root when none is given: ~/.outerloop, or the
|
|
292
|
+
pre-rename ~/.autoresearch when only that one exists (state is never moved
|
|
293
|
+
behind the operator's back). The home comes from `environ` so a caller
|
|
294
|
+
with no HOME gets the plain default."""
|
|
295
|
+
home_s = environ.get("HOME", "")
|
|
296
|
+
if not home_s:
|
|
297
|
+
return DEFAULT_LOCAL_ROOT
|
|
298
|
+
home = Path(home_s)
|
|
299
|
+
new, old = home / DEFAULT_LOCAL_ROOT.name, home / LEGACY_LOCAL_ROOT_NAME
|
|
300
|
+
return old if (not new.exists() and old.is_dir()) else new
|
|
301
|
+
|
|
302
|
+
|
|
266
303
|
def _resident_jobs() -> list[str] | None:
|
|
267
|
-
"""Ids of queued or running resident ticks, lowest first;
|
|
268
|
-
scheduler could not be asked (a failed lookup must never
|
|
304
|
+
"""Ids of queued or running resident ticks under either name, lowest first;
|
|
305
|
+
None when the scheduler could not be asked (a failed lookup must never
|
|
306
|
+
read as 'none')."""
|
|
269
307
|
try:
|
|
270
308
|
proc = subprocess.run(
|
|
271
309
|
[
|
|
272
310
|
"squeue",
|
|
273
311
|
"-u",
|
|
274
312
|
os.environ.get("USER", ""),
|
|
275
|
-
f"--name={RESIDENT_JOB_NAME}",
|
|
313
|
+
f"--name={RESIDENT_JOB_NAME},{LEGACY_RESIDENT_JOB_NAME}",
|
|
276
314
|
"-h",
|
|
277
315
|
"-o",
|
|
278
316
|
"%i",
|
|
@@ -302,9 +340,37 @@ def _exec(cmd: list[str], env: dict[str, str]) -> int:
|
|
|
302
340
|
return 1 # unreachable; keeps the signature honest for tests that stub this
|
|
303
341
|
|
|
304
342
|
|
|
343
|
+
HARNESS_BIN_KEYS = ("OUTERLOOP_CLAUDE_BIN", "OUTERLOOP_CODEX_BIN")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _setting_of(key: str, values: Mapping[str, str], environ: Mapping[str, str]) -> str:
|
|
347
|
+
"""The process environment wins over .env, including an explicit empty value
|
|
348
|
+
(the way to clear a recorded path from the shell)."""
|
|
349
|
+
return (environ[key] if key in environ else values.get(key, "")).strip()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def missing_harness_binary(values: Mapping[str, str], environ: Mapping[str, str]) -> str:
|
|
353
|
+
"""Why start must not launch: the CONFIGURED author backend's recorded
|
|
354
|
+
binary is not there (init records the path; a moved or uninstalled CLI
|
|
355
|
+
would end every climb with spawn-error). The other backend's path is not
|
|
356
|
+
consulted: a stale entry for a backend the loop never spawns must not block
|
|
357
|
+
it. "" when the binary exists or none is recorded."""
|
|
358
|
+
backend = _setting_of("OUTERLOOP_AUTHOR_BACKEND", values, environ).lower() or "claude"
|
|
359
|
+
key = f"OUTERLOOP_{backend.upper()}_BIN"
|
|
360
|
+
if key not in HARNESS_BIN_KEYS:
|
|
361
|
+
return "" # an unknown backend is init's error to report, not this check's
|
|
362
|
+
path = _setting_of(key, values, environ)
|
|
363
|
+
if path and not Path(path).expanduser().is_file():
|
|
364
|
+
return f"{key}={path} is not a file; reinstall the CLI or run `outerloop init --force`"
|
|
365
|
+
return ""
|
|
366
|
+
|
|
367
|
+
|
|
305
368
|
def start(args: argparse.Namespace) -> int:
|
|
306
369
|
try:
|
|
307
370
|
values = env_file_values(ENV_FILE, START_KEYS + TICK_ENV_KEYS) # one read for everything
|
|
371
|
+
problem = missing_harness_binary(values, os.environ)
|
|
372
|
+
if problem:
|
|
373
|
+
raise StartError(problem)
|
|
308
374
|
from_file = {k: v for k, v in values.items() if k in START_KEYS}
|
|
309
375
|
plan = plan_start(
|
|
310
376
|
root=args.root or "",
|
|
@@ -330,13 +396,14 @@ def start(args: argparse.Namespace) -> int:
|
|
|
330
396
|
for key, value in values.items():
|
|
331
397
|
if key in TICK_ENV_KEYS:
|
|
332
398
|
env.setdefault(key, value)
|
|
333
|
-
env["
|
|
334
|
-
env["
|
|
335
|
-
env["
|
|
399
|
+
env["OUTERLOOP_COMPUTE"] = "local"
|
|
400
|
+
env["OUTERLOOP_ROOT"] = str(plan.root)
|
|
401
|
+
env["OUTERLOOP_HOME"] = str(plan.home)
|
|
402
|
+
plan.home.mkdir(parents=True, exist_ok=True) # <root>/home when there is no checkout
|
|
336
403
|
if plan.cadence_min:
|
|
337
|
-
env["
|
|
404
|
+
env["OUTERLOOP_CADENCE_MIN"] = plan.cadence_min
|
|
338
405
|
if plan.pat_file:
|
|
339
|
-
env["
|
|
406
|
+
env["OUTERLOOP_PAT_FILE"] = plan.pat_file
|
|
340
407
|
print(
|
|
341
408
|
f"local loop: state in {plan.root}; Ctrl-C stops it, the records resume it",
|
|
342
409
|
file=sys.stderr,
|
|
@@ -347,14 +414,14 @@ def start(args: argparse.Namespace) -> int:
|
|
|
347
414
|
print(
|
|
348
415
|
"outerloop start: could not ask the scheduler whether a resident tick "
|
|
349
416
|
"exists (squeue failed); nothing submitted. Retry, or check "
|
|
350
|
-
f"`squeue --name {RESIDENT_JOB_NAME}`.",
|
|
417
|
+
f"`squeue --name {RESIDENT_JOB_NAME},{LEGACY_RESIDENT_JOB_NAME}`.",
|
|
351
418
|
file=sys.stderr,
|
|
352
419
|
)
|
|
353
420
|
return 1
|
|
354
421
|
if existing:
|
|
355
422
|
print(
|
|
356
423
|
f"a resident tick is already queued or running (job {existing[0]}); nothing "
|
|
357
|
-
f"submitted. Stop it with `scancel
|
|
424
|
+
f"submitted. Stop it with `scancel {existing[0]}`, or pause it "
|
|
358
425
|
f"with `touch {plan.root}/PAUSE`.",
|
|
359
426
|
file=sys.stderr,
|
|
360
427
|
)
|
|
@@ -398,23 +465,40 @@ def start(args: argparse.Namespace) -> int:
|
|
|
398
465
|
|
|
399
466
|
|
|
400
467
|
def main(argv: list[str] | None = None) -> int:
|
|
468
|
+
from outerloop import __version__
|
|
469
|
+
|
|
401
470
|
parser = argparse.ArgumentParser(
|
|
402
|
-
prog="outerloop",
|
|
471
|
+
prog="outerloop",
|
|
472
|
+
description="autonomous research agents in an outer loop",
|
|
473
|
+
epilog="Run `outerloop init` once, then `outerloop start`. Settings live in "
|
|
474
|
+
"~/.config/outerloop/.env; the install guide is docs/install.md.",
|
|
403
475
|
)
|
|
476
|
+
parser.add_argument("--version", action="version", version=f"outerloop {__version__}")
|
|
404
477
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
405
478
|
p = sub.add_parser(
|
|
406
|
-
"start",
|
|
479
|
+
"start",
|
|
480
|
+
help="start the loop: the resident tick on Slurm, the local loop elsewhere",
|
|
481
|
+
description="Submit the resident tick to Slurm, or run the local loop in the "
|
|
482
|
+
"foreground where there is no sbatch. Every flag defaults from "
|
|
483
|
+
"~/.config/outerloop/.env, written by `outerloop init`.",
|
|
484
|
+
)
|
|
485
|
+
p.add_argument(
|
|
486
|
+
"--root", help="state root (shared filesystem on Slurm; default ~/.outerloop locally)"
|
|
407
487
|
)
|
|
408
488
|
p.add_argument(
|
|
409
|
-
"--
|
|
489
|
+
"--account", help="Slurm account (optional; unset bills your default association)"
|
|
490
|
+
)
|
|
491
|
+
p.add_argument(
|
|
492
|
+
"--partition",
|
|
493
|
+
help="Slurm partition for the tick (optional; unset lets Slurm choose; a,b = list)",
|
|
410
494
|
)
|
|
411
|
-
p.add_argument("--account", help="Slurm account")
|
|
412
|
-
p.add_argument("--partition", help="Slurm partition for the tick")
|
|
413
495
|
p.add_argument(
|
|
414
496
|
"--local", action="store_true", help="run the local loop even where sbatch exists"
|
|
415
497
|
)
|
|
416
498
|
p.add_argument("--dry-run", action="store_true", help="print the command and exit")
|
|
417
|
-
sub.add_parser(
|
|
499
|
+
sub.add_parser(
|
|
500
|
+
"tick", help="run one tick now; --loop keeps ticking (the local loop)", add_help=False
|
|
501
|
+
)
|
|
418
502
|
sub.add_parser(
|
|
419
503
|
"init",
|
|
420
504
|
help="guided setup: write ~/.config/outerloop/.env and the PAT file",
|