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/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
- AUTORESEARCH_COMPUTE=local, it runs the local loop in the foreground.
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 = "autoresearch-resident"
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() / ".autoresearch"
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
- "AUTORESEARCH_COMPUTE",
34
- "AUTORESEARCH_ROOT",
35
- "AUTORESEARCH_ACCOUNT",
36
- "AUTORESEARCH_PARTITION",
37
- "AUTORESEARCH_CADENCE_MIN",
38
- "AUTORESEARCH_RESIDENT_MINUTES",
39
- "AUTORESEARCH_PAT_FILE",
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
- "AUTORESEARCH_AUTHOR_BACKEND",
46
- "AUTORESEARCH_AUTHOR_MODEL",
47
- "AUTORESEARCH_CODEX_BIN",
48
- "AUTORESEARCH_CODEX_KEY_FILE",
49
- "AUTORESEARCH_HARNESS_KEY_FILE",
50
- "AUTORESEARCH_VERTEX_PROJECT",
51
- "AUTORESEARCH_VERTEX_REGION",
52
- "AUTORESEARCH_VERTEX_ADC",
53
- "AUTORESEARCH_TARGET",
54
- "AUTORESEARCH_GITHUB_APP_FILE",
55
- "AUTORESEARCH_BOT_LOGIN",
56
- "AUTORESEARCH_BOT_ALIASES",
57
- "AUTORESEARCH_GPU_PARTITION",
58
- "AUTORESEARCH_GPU_ACCOUNT",
59
- "AUTORESEARCH_PANEL",
60
- "AUTORESEARCH_PANEL_KEY_FILE",
61
- "AUTORESEARCH_PANEL_CODEX_KEY_FILE",
62
- "AUTORESEARCH_PANEL_HERMES_KEY_FILE",
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
- if key.startswith("OUTERLOOP_"): # the public name; `keys` are canonical
98
- key = "AUTORESEARCH_" + key[len("OUTERLOOP_") :]
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
- "AUTORESEARCH_RESIDENT": "1",
127
- "AUTORESEARCH_HOME": str(self.home),
128
- "AUTORESEARCH_ROOT": str(self.root),
129
- "AUTORESEARCH_ACCOUNT": self.account,
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["AUTORESEARCH_PARTITION"] = self.partition
151
+ env["OUTERLOOP_PARTITION"] = self.partition
134
152
  if self.cadence_min:
135
- env["AUTORESEARCH_CADENCE_MIN"] = self.cadence_min
153
+ env["OUTERLOOP_CADENCE_MIN"] = self.cadence_min
136
154
  if self.pat_file:
137
- env["AUTORESEARCH_PAT_FILE"] = self.pat_file
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 checkout the loop runs from: AUTORESEARCH_HOME, else the current
170
- directory when it is one. Both modes need it; the tick's launch lanes
171
- and GitHub servicing switch off without it."""
172
- home = (
173
- Path(environ["AUTORESEARCH_HOME"]).expanduser() if environ.get("AUTORESEARCH_HOME") else cwd
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("AUTORESEARCH_COMPUTE", "local" if local else "", environ, from_file)
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("AUTORESEARCH_ROOT", root, environ, from_file)
197
- cadence = _setting("AUTORESEARCH_CADENCE_MIN", "", environ, from_file)
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"AUTORESEARCH_CADENCE_MIN must be a positive number of minutes, got {cadence!r}"
230
+ f"OUTERLOOP_CADENCE_MIN must be a positive number of minutes, got {cadence!r}"
208
231
  )
209
- pat = _setting("AUTORESEARCH_PAT_FILE", "", environ, from_file)
210
- # both modes run from a checkout: the tick's launch lanes and GitHub
211
- # servicing switch off without AUTORESEARCH_HOME
212
- home = _home(environ, cwd)
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=Path(root_s).expanduser() if root_s else DEFAULT_LOCAL_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, AUTORESEARCH_ROOT, or AUTORESEARCH_ROOT= in ~/.config/outerloop/.env"
249
+ "--root, OUTERLOOP_ROOT in the environment, or OUTERLOOP_ROOT= in "
250
+ "~/.config/outerloop/.env"
225
251
  )
226
- acc = _setting("AUTORESEARCH_ACCOUNT", account, environ, from_file)
227
- part = _setting("AUTORESEARCH_PARTITION", partition, environ, from_file)
228
- # Partition is optional: left unset, Slurm places the job on its default
229
- # partition. Account stays required (clusters bill by it).
230
- if not acc:
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"AUTORESEARCH_RESIDENT_MINUTES must be a whole number of minutes, got {minutes_s!r}"
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("AUTORESEARCH_RESIDENT_MINUTES must be positive")
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; None when the
268
- scheduler could not be asked (a failed lookup must never read as 'none')."""
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["AUTORESEARCH_COMPUTE"] = "local"
334
- env["AUTORESEARCH_ROOT"] = str(plan.root)
335
- env["AUTORESEARCH_HOME"] = str(plan.home)
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["AUTORESEARCH_CADENCE_MIN"] = plan.cadence_min
404
+ env["OUTERLOOP_CADENCE_MIN"] = plan.cadence_min
338
405
  if plan.pat_file:
339
- env["AUTORESEARCH_PAT_FILE"] = plan.pat_file
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 --name {RESIDENT_JOB_NAME}`, or pause it "
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", description="autonomous research agents in an outer loop"
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", help="start the loop: the resident tick on Slurm, the local loop elsewhere"
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
- "--root", help="state root (shared filesystem on Slurm; default ~/.autoresearch locally)"
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("tick", help="one tick, or --loop; the chain's own entry", add_help=False)
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",