outerloop-science 0.1.0.dev1__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 CHANGED
@@ -20,6 +20,7 @@ import argparse
20
20
  import getpass
21
21
  import json
22
22
  import os
23
+ import shutil
23
24
  import sys
24
25
  import time
25
26
  import urllib.error
@@ -29,6 +30,7 @@ from pathlib import Path
29
30
  from typing import Any
30
31
 
31
32
  from outerloop.cli import ENV_FILE
33
+ from outerloop.image import ensure_image
32
34
  from outerloop.paths import write_private
33
35
 
34
36
  CONFIG_DIR = ENV_FILE.parent
@@ -43,11 +45,14 @@ class InitAnswers:
43
45
  compute: str # "slurm" | "local"
44
46
  target: str # "owner/repo"
45
47
  root: str = "" # Slurm state root on the shared filesystem
46
- account: str = "" # Slurm account (required for Slurm)
48
+ account: str = "" # Slurm account (optional; unset -> the default association)
47
49
  partition: str = "" # Slurm partition (optional; unset -> Slurm default)
48
50
  author_backend: str = "" # optional: the climbing author's harness
49
51
  author_model: str = "" # optional
50
52
  author_key_file: str = "" # the author's model key file, when known
53
+ image: str = "" # the agent image (OUTERLOOP_IMAGE)
54
+ uncontained: bool = False # --no-image: write OUTERLOOP_IMAGE= so no image is picked up
55
+ author_bin: str = "" # the author harness binary (claude/codex), absolute, when found
51
56
 
52
57
 
53
58
  def render_env(
@@ -60,9 +65,16 @@ def render_env(
60
65
  lines = [f"OUTERLOOP_COMPUTE={a.compute}"]
61
66
  if a.compute == "slurm":
62
67
  lines.append(f"OUTERLOOP_ROOT={a.root}")
63
- lines.append(f"OUTERLOOP_ACCOUNT={a.account}")
68
+ if a.account: # optional: unset bills the caller's default association
69
+ lines.append(f"OUTERLOOP_ACCOUNT={a.account}")
64
70
  if a.partition: # optional: unset lets Slurm pick its default partition
65
71
  lines.append(f"OUTERLOOP_PARTITION={a.partition}")
72
+ if a.image:
73
+ lines.append(f"OUTERLOOP_IMAGE={a.image}")
74
+ elif a.uncontained:
75
+ # the empty value is the off-switch: without it the tick would still
76
+ # pick up an image already at the default path
77
+ lines.append("OUTERLOOP_IMAGE=")
66
78
  lines.append(f"OUTERLOOP_TARGET={a.target}")
67
79
  if app_file:
68
80
  lines.append(f"OUTERLOOP_GITHUB_APP_FILE={app_file}")
@@ -78,9 +90,68 @@ def render_env(
78
90
  lines.append(f"OUTERLOOP_AUTHOR_MODEL={a.author_model}")
79
91
  if a.author_key_file:
80
92
  lines.append(f"{author_key_env(a.author_backend)}={a.author_key_file}")
93
+ if a.author_bin:
94
+ lines.append(f"{author_bin_env(a.author_backend)}={a.author_bin}")
81
95
  return "\n".join(lines) + "\n"
82
96
 
83
97
 
98
+ def author_bin_env(backend: str) -> str:
99
+ """The `.env` key naming `backend`'s harness binary (`OUTERLOOP_CLAUDE_BIN`,
100
+ `OUTERLOOP_CODEX_BIN`), the same names `attempt` reads."""
101
+ return f"OUTERLOOP_{(backend or AUTHOR_BACKENDS[0]).upper()}_BIN"
102
+
103
+
104
+ HARNESS_INSTALL = {
105
+ "claude": "npm install -g @anthropic-ai/claude-code "
106
+ "(or: curl -fsSL https://claude.ai/install.sh | bash)",
107
+ "codex": "npm install -g @openai/codex",
108
+ }
109
+
110
+
111
+ def locate_harness(backend: str) -> str:
112
+ """The absolute path of `backend`'s CLI on this machine: PATH first, then
113
+ ~/.local/bin (where the native installers put it and where a Slurm job,
114
+ with no login PATH, would not find it); "" when absent. Recorded in .env so
115
+ every job spawns the same binary the operator installed."""
116
+ name = backend or AUTHOR_BACKENDS[0]
117
+ found = shutil.which(name)
118
+ if found:
119
+ return str(Path(found).resolve())
120
+ local = Path.home() / ".local" / "bin" / name
121
+ return str(local) if local.is_file() and os.access(local, os.X_OK) else ""
122
+
123
+
124
+ def _harness_hint(answers: InitAnswers) -> None:
125
+ if not answers.author_bin:
126
+ name = answers.author_backend or AUTHOR_BACKENDS[0]
127
+ print(
128
+ f" no `{name}` binary found on PATH or in ~/.local/bin — the first climb would end\n"
129
+ f" with spawn-error. Install it: {HARNESS_INSTALL.get(name, 'see its docs')}\n"
130
+ " then run `outerloop init --force` so its path is recorded."
131
+ )
132
+
133
+
134
+ def _token_login(token: str) -> str:
135
+ """The login a PAT acts as (GET /user), or "" when GitHub does not say."""
136
+ req = urllib.request.Request(
137
+ f"{API}/user",
138
+ headers={"Authorization": f"token {token}", "Accept": "application/vnd.github+json"},
139
+ )
140
+ try:
141
+ with urllib.request.urlopen(req, timeout=15) as resp:
142
+ return str(json.loads(resp.read()).get("login") or "")
143
+ except (urllib.error.URLError, OSError, ValueError):
144
+ return ""
145
+
146
+
147
+ def _auth_is_fatal(problem: str) -> bool:
148
+ """A credential the loop cannot open pull requests with fails setup; not
149
+ being able to ask GitHub right now (network, 5xx, rate limit) does not."""
150
+ return bool(problem) and not problem.startswith(
151
+ ("could not reach GitHub", "could not read the App", "could not check the App")
152
+ )
153
+
154
+
84
155
  def author_key_env(backend: str) -> str:
85
156
  """The `.env` key naming `backend`'s model-key file: one rule for every
86
157
  backend (`OUTERLOOP_CLAUDE_KEY_FILE`, `OUTERLOOP_CODEX_KEY_FILE`)."""
@@ -138,6 +209,13 @@ def _check_repo_access(token: str, target: str) -> str:
138
209
  except urllib.error.HTTPError as exc:
139
210
  if exc.code == 404:
140
211
  return f"{target} not found, or the token cannot see it"
212
+ if exc.code == 401:
213
+ return "the token is invalid or expired (GitHub returned 401)"
214
+ if exc.code == 403:
215
+ return f"the token is not allowed to access {target} (GitHub returned 403)"
216
+ if exc.code == 429 or exc.code >= 500:
217
+ # GitHub's problem, not the credential's: never fatal
218
+ return f"could not reach GitHub: it returned {exc.code} for {target}"
141
219
  return f"GitHub returned {exc.code} for {target}"
142
220
  except urllib.error.URLError as exc:
143
221
  return f"could not reach GitHub: {exc.reason}"
@@ -185,6 +263,8 @@ def _check_app_access(provider: Any, target: str) -> str:
185
263
  )
186
264
  return ""
187
265
  except urllib.error.HTTPError as exc:
266
+ if exc.code == 429 or exc.code >= 500:
267
+ return f"could not reach GitHub: it returned {exc.code} while checking the App"
188
268
  return f"GitHub returned {exc.code} while checking the App"
189
269
  except urllib.error.URLError as exc:
190
270
  return f"could not reach GitHub: {exc.reason}"
@@ -226,7 +306,9 @@ def _collect(args: argparse.Namespace, interactive: bool) -> tuple[InitAnswers,
226
306
  root = args.root or (
227
307
  _ask("Slurm state root (shared filesystem)", required=True) if interactive else ""
228
308
  )
229
- account = args.account or (_ask("Slurm account", required=True) if interactive else "")
309
+ account = args.account or (
310
+ _ask("Slurm account (blank = your default association)") if interactive else ""
311
+ )
230
312
  partition = args.partition or (
231
313
  _ask("Slurm partition (blank = Slurm default; a,b for a list)") if interactive else ""
232
314
  )
@@ -239,6 +321,10 @@ def _collect(args: argparse.Namespace, interactive: bool) -> tuple[InitAnswers,
239
321
  model = args.author_model or (
240
322
  _ask("Author model (blank = the backend's default)") if ask_author else ""
241
323
  )
324
+ # An explicit --image is recorded here (absolute: jobs read it from their
325
+ # own directory); the published one is fetched in main, after every check
326
+ # that could still end the run.
327
+ image = str(Path(args.image).expanduser().absolute()) if args.image else ""
242
328
  answers = InitAnswers(
243
329
  compute=compute,
244
330
  target=target,
@@ -247,6 +333,9 @@ def _collect(args: argparse.Namespace, interactive: bool) -> tuple[InitAnswers,
247
333
  partition=partition,
248
334
  author_backend=backend,
249
335
  author_model=model,
336
+ image=image,
337
+ author_bin=locate_harness(backend),
338
+ uncontained=bool(args.no_image) and not image,
250
339
  )
251
340
  return answers, (args.pat_file or "")
252
341
 
@@ -271,12 +360,72 @@ def _owner_type(owner: str) -> str:
271
360
  return ""
272
361
 
273
362
 
274
- def _github_app_setup(answers: InitAnswers, app_name: str, org: str) -> int:
363
+ def _app_failure(answers: InitAnswers, slug: str, problem: str) -> int:
364
+ """The App cannot write the target: say so, keep the credentials, and point
365
+ at the fix and the re-check. Never `start` — the check just failed."""
366
+ print(f" auth check: FAILED — {problem}", file=sys.stderr)
367
+ print(
368
+ f"outerloop init: the App '{slug}' cannot write {answers.target}. On\n"
369
+ f" https://github.com/apps/{slug}/installations/new (or the App's page under\n"
370
+ " Settings > Installations > Configure) install it on that repository and accept\n"
371
+ " contents, issues and pull-request write access, then run\n"
372
+ " `outerloop init --force --github-app` to re-check with these credentials.",
373
+ file=sys.stderr,
374
+ )
375
+ return 1
376
+
377
+
378
+ def _github_app_recheck(answers: InitAnswers, app_json: Path) -> int:
379
+ """Re-run Step 3 for an App this machine already created: confirm it can
380
+ write the target, then point the .env at it."""
381
+ from outerloop import appmanifest
382
+ from outerloop.appauth import app_provider_from_file
383
+
384
+ slug = app_json.name[len("github_app.") : -len(".json")]
385
+ print(f"Re-checking the existing App '{slug}' ({app_json}) against {answers.target}.")
386
+ try:
387
+ data = json.loads(app_json.read_text())
388
+ if not int(data.get("installation_id") or 0):
389
+ # the earlier run ended before the App was installed: look again
390
+ iid = appmanifest.capture_installation_id(
391
+ int(data["app_id"]), Path(str(data["private_key"])), answers.target.split("/")[0]
392
+ )
393
+ if iid:
394
+ appmanifest.set_installation_id(app_json, iid)
395
+ print(f" installation id {iid} recorded")
396
+ problem = _check_app_access(app_provider_from_file(app_json), answers.target)
397
+ except Exception as exc:
398
+ problem = f"could not read the App credentials: {exc}"
399
+ env_path = CONFIG_DIR / ENV_FILE.name
400
+ write_private(env_path, render_env(answers, app_file=str(app_json), bot_login=f"{slug}[bot]"))
401
+ if _auth_is_fatal(problem):
402
+ return _app_failure(answers, slug, problem)
403
+ print(f" auth check: {'ok' if not problem else 'WARNING — ' + problem}")
404
+ print(f"wrote {env_path}")
405
+ _author_key_hint(answers)
406
+ _harness_hint(answers)
407
+ print("next: outerloop start")
408
+ return 0
409
+
410
+
411
+ def _github_app_setup(
412
+ answers: InitAnswers, app_name: str, org: str, *, interactive: bool = True
413
+ ) -> int:
275
414
  """The `--github-app` path: create the adopter's own App via the manifest
276
415
  flow, write its creds, help install it, then point the .env at the App file.
277
416
  Interactive by nature (a browser click + install), so no `--yes` variant."""
278
417
  from outerloop import appmanifest
279
418
 
419
+ existing = sorted(CONFIG_DIR.glob("github_app.*.json"))
420
+ if len(existing) == 1:
421
+ # a re-run after fixing the installation: re-check the App this machine
422
+ # already has rather than creating a second one
423
+ reuse = True
424
+ if interactive:
425
+ answer = _ask(f"Reuse the App credentials at {existing[0]}? (Y/n)", "y")
426
+ reuse = not answer.lower().startswith("n")
427
+ if reuse:
428
+ return _github_app_recheck(answers, existing[0])
280
429
  owner = org or answers.target.split("/")[0]
281
430
  name = app_name or f"outerloop-{owner}"[:34] # GitHub caps App names at 34
282
431
  code = appmanifest.request_manifest_code(
@@ -322,12 +471,21 @@ def _github_app_setup(answers: InitAnswers, app_name: str, org: str) -> int:
322
471
  problem = _check_app_access(app_provider_from_file(app_json), answers.target)
323
472
  except Exception as exc: # a check failure is a warning, never fails setup
324
473
  problem = f"could not read the App credentials: {exc}"
474
+ if _auth_is_fatal(problem):
475
+ write_private(
476
+ CONFIG_DIR / ENV_FILE.name,
477
+ render_env(answers, app_file=str(app_json), bot_login=f"{conversion['slug']}[bot]"),
478
+ )
479
+ return _app_failure(answers, str(conversion["slug"]), problem)
325
480
  print(f" auth check: {'ok' if not problem else 'WARNING — ' + problem}")
326
481
  else:
327
- print(
328
- f" still no installation. When it is installed, put its id into {app_json} as "
329
- f"installation_id (GitHub shows it in the URL of the App's page under "
330
- f"Settings > Installations), then run outerloop start."
482
+ # the credentials are kept; nothing can run until the App is installed
483
+ write_private(
484
+ CONFIG_DIR / ENV_FILE.name,
485
+ render_env(answers, app_file=str(app_json), bot_login=f"{conversion['slug']}[bot]"),
486
+ )
487
+ return _app_failure(
488
+ answers, str(conversion["slug"]), f"the App is not installed on {answers.target}"
331
489
  )
332
490
  env_path = CONFIG_DIR / ENV_FILE.name
333
491
  write_private(
@@ -336,18 +494,23 @@ def _github_app_setup(answers: InitAnswers, app_name: str, org: str) -> int:
336
494
  )
337
495
  print(f"wrote {env_path}")
338
496
  _author_key_hint(answers)
497
+ _harness_hint(answers)
339
498
  print("next: outerloop start")
340
499
  return 0
341
500
 
342
501
 
343
502
  def main(argv: list[str] | None = None) -> int:
344
503
  parser = argparse.ArgumentParser(
345
- prog="outerloop init", description="guided setup for outerloop"
504
+ prog="outerloop init",
505
+ description="Guided setup: asks for anything not given as a flag, checks the "
506
+ "credentials, and writes ~/.config/outerloop/.env.",
346
507
  )
347
508
  parser.add_argument("--compute", choices=["slurm", "local"], help="where the loop runs")
348
509
  parser.add_argument("--target", help="the repo the agents work on, owner/repo")
349
510
  parser.add_argument("--root", help="Slurm state root on the shared filesystem")
350
- parser.add_argument("--account", help="Slurm account")
511
+ parser.add_argument(
512
+ "--account", help="Slurm account (optional; unset bills your default association)"
513
+ )
351
514
  parser.add_argument(
352
515
  "--partition", help="Slurm partition (optional; blank = default; a,b = list)"
353
516
  )
@@ -362,8 +525,24 @@ def main(argv: list[str] | None = None) -> int:
362
525
  )
363
526
  parser.add_argument("--app-name", dest="app_name", help="name for the created GitHub App")
364
527
  parser.add_argument("--org", help="create the App under this org (default: your account)")
365
- parser.add_argument("--author-backend", dest="author_backend", help="climbing author's backend")
528
+ parser.add_argument(
529
+ "--author-backend",
530
+ dest="author_backend",
531
+ help=f"climbing author's backend ({' or '.join(AUTHOR_BACKENDS)}; default claude)",
532
+ )
366
533
  parser.add_argument("--author-model", dest="author_model", help="climbing author's model")
534
+ image_flags = parser.add_mutually_exclusive_group() # one or the other, never both
535
+ image_flags.add_argument(
536
+ "--image",
537
+ help="path to an Apptainer image to run sessions and evals in (default: the "
538
+ "published one, downloaded on Linux when apptainer is installed)",
539
+ )
540
+ image_flags.add_argument(
541
+ "--no-image",
542
+ dest="no_image",
543
+ action="store_true",
544
+ help="no image at all: runs stay uncontained even if one is already on disk",
545
+ )
367
546
  parser.add_argument(
368
547
  "--author-key-file",
369
548
  dest="author_key_file",
@@ -382,8 +561,9 @@ def main(argv: list[str] | None = None) -> int:
382
561
  if not answers.target:
383
562
  print("outerloop init: a target repo is required (--target owner/repo)", file=sys.stderr)
384
563
  return 2
385
- if answers.compute == "slurm" and not (answers.root and answers.account):
386
- print("outerloop init: Slurm needs --root and --account", file=sys.stderr)
564
+ if answers.compute == "slurm" and not answers.root:
565
+ # the account and the partition are optional: Slurm's defaults apply
566
+ print("outerloop init: Slurm needs --root", file=sys.stderr)
387
567
  return 2
388
568
  if answers.author_backend and answers.author_backend not in AUTHOR_BACKENDS:
389
569
  print(
@@ -391,6 +571,10 @@ def main(argv: list[str] | None = None) -> int:
391
571
  file=sys.stderr,
392
572
  )
393
573
  return 2
574
+ if answers.image and not Path(answers.image).is_file():
575
+ # a typo here would surface as an uncontained loop later
576
+ print(f"outerloop init: --image {answers.image} is not a file", file=sys.stderr)
577
+ return 2
394
578
 
395
579
  # Never clobber a working setup silently: a re-run of init on a configured
396
580
  # machine must ask (or be told --force). Checked before any App is created.
@@ -430,6 +614,15 @@ def main(argv: list[str] | None = None) -> int:
430
614
  answers.author_key_file = str(key_path)
431
615
  print(f"wrote {key_path} (0600)")
432
616
 
617
+ # The image, only now: every check that could still end the run has passed
618
+ # and the overwrite question is answered, so a download is never wasted.
619
+ # An existing image is used, else the published one is fetched on a machine
620
+ # that can run it (asked first when interactive); --no-image opts out.
621
+ if not answers.image and not args.no_image:
622
+ # the container probe is for local mode: on Slurm the image runs on
623
+ # compute nodes, which the login node cannot speak for
624
+ answers.image = ensure_image(interactive=interactive, probe=(answers.compute == "local"))
625
+
433
626
  # The App is the recommended credential (scoped, revocable, no plaintext
434
627
  # token); the PAT is the fallback. Offer it first when interactive.
435
628
  if not args.github_app and not pat_file and interactive:
@@ -453,7 +646,7 @@ def main(argv: list[str] | None = None) -> int:
453
646
  kind = "User" if answer.strip().lower().startswith("u") else "Organization"
454
647
  if kind == "User":
455
648
  org = "" # a user account: GitHub's personal App page, not an org page
456
- return _github_app_setup(answers, args.app_name or "", org)
649
+ return _github_app_setup(answers, args.app_name or "", org, interactive=interactive)
457
650
 
458
651
  token = ""
459
652
  if not pat_file and interactive:
@@ -469,10 +662,32 @@ def main(argv: list[str] | None = None) -> int:
469
662
  effective_pat = str(pat_path) if pat_path else pat_file
470
663
  if effective_pat:
471
664
  problem = validate_pat(effective_pat, answers.target)
665
+ if _auth_is_fatal(problem):
666
+ # the loop opens pull requests with this token; sending the adopter
667
+ # to `start` would fail later and further from the cause
668
+ print(f" auth check: FAILED — {problem}", file=sys.stderr)
669
+ print(
670
+ f"outerloop init: fix the token in {effective_pat} (write access to "
671
+ f"{answers.target}), then run `outerloop init --force`",
672
+ file=sys.stderr,
673
+ )
674
+ return 1
472
675
  print(f" auth check: {'ok' if not problem else 'WARNING — ' + problem}")
676
+ if not problem:
677
+ # record the login the kernel will post as: every own-comment and
678
+ # own-PR filter keys on it, and a default that is not this token's
679
+ # identity would hide the adopter's own PRs from the kernel
680
+ try:
681
+ login = _token_login(Path(effective_pat).expanduser().read_text().strip())
682
+ except OSError:
683
+ login = ""
684
+ if login:
685
+ write_private(env_path, render_env(answers, effective_pat, bot_login=login))
686
+ print(f" posting as {login} (OUTERLOOP_BOT_LOGIN)")
473
687
  else:
474
688
  print(" no PAT set — add OUTERLOOP_PAT_FILE before the agents can open PRs")
475
689
  _author_key_hint(answers)
690
+ _harness_hint(answers)
476
691
  print("next: outerloop start")
477
692
  return 0
478
693
 
outerloop/limits.py CHANGED
@@ -26,7 +26,7 @@ from typing import Any
26
26
  # work — session budgets sized for solver tweaks starve construction work.
27
27
  # floor = session floor + overhead + self-deadline margin: even at the
28
28
  # floors, a session must fit inside its job with the ending's runway.
29
- # Public: the tick's AUTORESEARCH_MAX_JOB_MINUTES knob floors here too.
29
+ # Public: the tick's OUTERLOOP_MAX_JOB_MINUTES knob floors here too.
30
30
  ATTEMPT_JOB_MINUTES_FLOOR = 40
31
31
 
32
32
  # Public: the tick shrinks a capped job's session with the same floor the
outerloop/measure.py CHANGED
@@ -278,7 +278,7 @@ class DispatchedMeasurer:
278
278
  if not self.gpu_partition:
279
279
  raise ValueError(
280
280
  f"measure {m.name} needs {m.gpus} GPU(s) but no GPU lane is configured "
281
- "(set AUTORESEARCH_GPU_PARTITION)"
281
+ "(set OUTERLOOP_GPU_PARTITION)"
282
282
  )
283
283
  return self.gpu_account or self.account, self.gpu_partition
284
284
 
@@ -494,7 +494,7 @@ class DispatchSettings:
494
494
  if not self.gpu_partition:
495
495
  raise ValueError(
496
496
  f"benchmark needs {gpus} GPU(s) but no GPU lane is configured "
497
- "(set AUTORESEARCH_GPU_PARTITION)"
497
+ "(set OUTERLOOP_GPU_PARTITION)"
498
498
  )
499
499
  return self.gpu_account or self.account, self.gpu_partition
500
500
 
outerloop/orchestrator.py CHANGED
@@ -361,7 +361,7 @@ class SubprocessEvaluator:
361
361
  )
362
362
  except OSError as exc:
363
363
  raise EvalError(f"could not create check home: {exc}") from exc
364
- cache_dir = Path(tempfile.mkdtemp(prefix="autoresearch-check-cache-"))
364
+ cache_dir = Path(tempfile.mkdtemp(prefix="outerloop-check-cache-"))
365
365
  try:
366
366
  self._run(workspace, command, eval_home, cache_dir)
367
367
  finally:
outerloop/paths.py CHANGED
@@ -12,7 +12,8 @@ from __future__ import annotations
12
12
  import os
13
13
  from pathlib import Path
14
14
 
15
- CONFIG_DIR_NAMES: tuple[str, ...] = ("outerloop", "autoresearch") # new first
15
+ # New name first; the pre-rename name is dropped in the release after 0.1.
16
+ CONFIG_DIR_NAMES: tuple[str, ...] = ("outerloop", "autoresearch")
16
17
 
17
18
 
18
19
  def config_dir(home: Path | None = None) -> Path:
outerloop/review.py CHANGED
@@ -34,7 +34,7 @@ OPT_OUT_LABEL = label_name("no-review")
34
34
  # One calm line: the mechanical defense against forged endorsements is the
35
35
  # approval-language redaction in sanitize(), not header volume.
36
36
  ADVISORY_HEADER = (
37
- "*Advisory findings from `autoresearch` — the code owner decides. "
37
+ "*Advisory findings from `outerloop` — the code owner decides. "
38
38
  f"Reply to disagree; the `{OPT_OUT_LABEL}` label opts this PR out.*"
39
39
  )
40
40
  MAX_DIFF_CHARS = 200_000
outerloop/steward.py CHANGED
@@ -43,7 +43,7 @@ from outerloop.github import (
43
43
  bot_login_from_env,
44
44
  is_own_login,
45
45
  )
46
- from outerloop.harness import Harness, budget_exhausted, outage, redact
46
+ from outerloop.harness import Harness, budget_exhausted, default_binary, outage, redact
47
47
  from outerloop.intake import (
48
48
  CLAIM_MARKER,
49
49
  RELEASE_MARKER,
@@ -768,7 +768,7 @@ def main() -> int:
768
768
  action="store_true",
769
769
  help="run WITHOUT a container (dev only)",
770
770
  )
771
- parser.add_argument("--claude-bin", default=os.path.expanduser("~/.local/bin/claude"))
771
+ parser.add_argument("--claude-bin", default=default_binary("claude"))
772
772
  parser.add_argument("--model", default="claude-opus-5")
773
773
  parser.add_argument("--max-turns", type=int, default=60)
774
774
  parser.add_argument("--session-minutes", type=int, default=60)
@@ -777,7 +777,7 @@ def main() -> int:
777
777
  parser.add_argument("--pat-file", default=str(CONFIG_DIR / "bot_pat"))
778
778
  parser.add_argument(
779
779
  "--github-app-file",
780
- default=os.environ.get("AUTORESEARCH_GITHUB_APP_FILE", ""),
780
+ default=os.environ.get("OUTERLOOP_GITHUB_APP_FILE", ""),
781
781
  help="GitHub App config (JSON: app_id, installation_id, private_key); "
782
782
  "when set, installation tokens replace the PAT",
783
783
  )
outerloop/syscall.py CHANGED
@@ -50,6 +50,7 @@ from outerloop.compute import GONE
50
50
  # workspace and the session's own memory of the path both predate the rename.
51
51
  # `channel_dir(ws)` resolves per workspace: existing dir (new name first), else
52
52
  # the new default. Every site keys off it, so a resumed run finds its own path.
53
+ # The pre-rename name is dropped in the release after 0.1.
53
54
  CHANNEL_DIR_NAMES: tuple[str, ...] = (".outerloop", ".autoresearch")
54
55
  SYSCALL_DIR = CHANNEL_DIR_NAMES[0] # the new default (a fresh clone installs this)
55
56
  SYSCALL_FILE = "syscall.json"