lalph 0.1.1 → 0.1.2

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.
package/README.md CHANGED
@@ -6,22 +6,14 @@ A small CLI that connects to Linear, pulls the next set of unstarted issues into
6
6
 
7
7
  - Install dependencies: `pnpm install`
8
8
  - Build the CLI: `pnpm build`
9
+ - Add `.lalph/` to `.gitignore` to keep local state private
9
10
 
10
11
  ## CLI usage
11
12
 
12
13
  - Run the main loop: `npx -y lalph@latest`
14
+ - Run multiple iterations with concurrency: `npx -y lalph@latest --iterations 4 --concurrency 2`
13
15
  - Select a Linear project: `npx -y lalph@latest select-project`
14
16
  - Select a label filter: `npx -y lalph@latest select-label`
15
17
  - Select a CLI agent: `npx -y lalph@latest select-agent`
16
18
 
17
19
  The first run opens a Linear OAuth flow and stores the token locally.
18
-
19
- ## Generated files
20
-
21
- - `.lalph/prd.json`: synced task list pulled from Linear; update task states here.
22
- - `PROGRESS.md`: append-only log of work completed by the agent.
23
- - `.lalph/config`: local key-value store for Linear tokens and user selections.
24
-
25
- ## Checks
26
-
27
- - Type check + format: `pnpm check`
package/dist/cli.mjs CHANGED
@@ -8270,8 +8270,8 @@ const whileLoop$1 = /* @__PURE__ */ makePrimitive$1({
8270
8270
  /** @internal */
8271
8271
  const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "function", (iterable, f, options) => withFiber$1((parent) => {
8272
8272
  const concurrencyOption = options?.concurrency === "inherit" ? parent.getRef(CurrentConcurrency) : options?.concurrency ?? 1;
8273
- const concurrency = concurrencyOption === "unbounded" ? Number.POSITIVE_INFINITY : Math.max(1, concurrencyOption);
8274
- if (concurrency === 1) return forEachSequential(iterable, f, options);
8273
+ const concurrency$1 = concurrencyOption === "unbounded" ? Number.POSITIVE_INFINITY : Math.max(1, concurrencyOption);
8274
+ if (concurrency$1 === 1) return forEachSequential(iterable, f, options);
8275
8275
  const items = fromIterable$2(iterable);
8276
8276
  let length = items.length;
8277
8277
  if (length === 0) return options?.discard ? void_$3 : succeed$4([]);
@@ -8288,7 +8288,7 @@ const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "functio
8288
8288
  let interrupted = false;
8289
8289
  function pump() {
8290
8290
  pumping = true;
8291
- while (inProgress < concurrency && index < length) {
8291
+ while (inProgress < concurrency$1 && index < length) {
8292
8292
  const currentIndex = index;
8293
8293
  const item = items[currentIndex];
8294
8294
  index++;
@@ -8312,7 +8312,7 @@ const forEach$2 = /* @__PURE__ */ dual((args$1) => typeof args$1[1] === "functio
8312
8312
  doneCount++;
8313
8313
  inProgress--;
8314
8314
  if (doneCount === length) resume(failures.length > 0 ? exitFailCause(causeFromFailures(failures)) : succeed$4(out));
8315
- else if (!pumping && !failed && inProgress < concurrency) pump();
8315
+ else if (!pumping && !failed && inProgress < concurrency$1) pump();
8316
8316
  });
8317
8317
  } catch (err) {
8318
8318
  failed = true;
@@ -18354,6 +18354,35 @@ const logWithLevel = logWithLevel$1;
18354
18354
  */
18355
18355
  const log$1 = /* @__PURE__ */ logWithLevel$1();
18356
18356
  /**
18357
+ * Logs one or more messages at the WARNING level.
18358
+ *
18359
+ * @example
18360
+ * ```ts
18361
+ * import { Effect } from "effect"
18362
+ *
18363
+ * const program = Effect.gen(function*() {
18364
+ * yield* Effect.logWarning("API rate limit approaching")
18365
+ * yield* Effect.logWarning("Retries remaining:", 2, "Operation:", "fetchData")
18366
+ *
18367
+ * // Useful for non-critical issues
18368
+ * const deprecated = true
18369
+ * if (deprecated) {
18370
+ * yield* Effect.logWarning("Using deprecated API endpoint")
18371
+ * }
18372
+ * })
18373
+ *
18374
+ * Effect.runPromise(program)
18375
+ * // Output:
18376
+ * // timestamp=2023-... level=WARN message="API rate limit approaching"
18377
+ * // timestamp=2023-... level=WARN message="Retries remaining: 2 Operation: fetchData"
18378
+ * // timestamp=2023-... level=WARN message="Using deprecated API endpoint"
18379
+ * ```
18380
+ *
18381
+ * @since 2.0.0
18382
+ * @category logging
18383
+ */
18384
+ const logWarning = /* @__PURE__ */ logWithLevel$1("Warn");
18385
+ /**
18357
18386
  * Logs one or more messages at the ERROR level.
18358
18387
  *
18359
18388
  * @example
@@ -53211,12 +53240,12 @@ var require_limiter = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
53211
53240
  * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
53212
53241
  * to run concurrently
53213
53242
  */
53214
- constructor(concurrency) {
53243
+ constructor(concurrency$1) {
53215
53244
  this[kDone] = () => {
53216
53245
  this.pending--;
53217
53246
  this[kRun]();
53218
53247
  };
53219
- this.concurrency = concurrency || Infinity;
53248
+ this.concurrency = concurrency$1 || Infinity;
53220
53249
  this.jobs = [];
53221
53250
  this.pending = 0;
53222
53251
  }
@@ -132176,9 +132205,42 @@ const teamSelect = fnUntraced(function* (project) {
132176
132205
  yield* selectedTeamId.set(some(teamId));
132177
132206
  });
132178
132207
 
132208
+ //#endregion
132209
+ //#region src/Worktree.ts
132210
+ var Worktree = class extends Service()("lalph/Worktree", { make: gen(function* () {
132211
+ const fs = yield* FileSystem;
132212
+ const pathService = yield* Path$1;
132213
+ const directory = yield* fs.makeTempDirectory();
132214
+ yield* addFinalizer(fnUntraced(function* () {
132215
+ yield* execIgnore(make$21`git worktree remove ${directory}`);
132216
+ }));
132217
+ yield* exec(make$21`git worktree add ${directory} -d HEAD`);
132218
+ yield* fs.makeDirectory(pathService.join(directory, ".lalph"), { recursive: true });
132219
+ yield* forEach$1([
132220
+ make$21({
132221
+ cwd: directory,
132222
+ extendEnv: true,
132223
+ shell: process.env.SHELL ?? true
132224
+ })`direnv allow`,
132225
+ make$21({
132226
+ cwd: directory,
132227
+ extendEnv: true,
132228
+ shell: process.env.SHELL ?? true
132229
+ })`devenv allow`,
132230
+ make$21({ cwd: directory })`git submodule update --init --recursive`
132231
+ ], execIgnore, { concurrency: "unbounded" });
132232
+ return { directory };
132233
+ }) }) {
132234
+ static layer = effect(this, this.make);
132235
+ };
132236
+ const exec = (command) => command.asEffect().pipe(flatMap((proc) => proc.exitCode), scoped$1);
132237
+ const execIgnore = (command) => command.asEffect().pipe(flatMap((proc) => proc.exitCode), catchCause$1(logWarning), scoped$1);
132238
+
132179
132239
  //#endregion
132180
132240
  //#region src/Prd.ts
132181
132241
  var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
132242
+ const worktree = yield* Worktree;
132243
+ const pathService = yield* Path$1;
132182
132244
  const fs = yield* FileSystem;
132183
132245
  const linear = yield* Linear;
132184
132246
  const project = yield* CurrentProject;
@@ -132190,7 +132252,7 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
132190
132252
  } })).pipe(runCollect).pipe(map$4(PrdList.fromLinearIssues));
132191
132253
  if (initial.issues.size === 0) return yield* new NoMoreWork({});
132192
132254
  const current = yield* make$22(initial);
132193
- const prdFile = `.lalph/prd.json`;
132255
+ const prdFile = pathService.join(worktree.directory, `.lalph/prd.json`);
132194
132256
  yield* fs.writeFileString(prdFile, initial.toJson());
132195
132257
  const sync$2 = gen(function* () {
132196
132258
  const json = yield* fs.readFileString(prdFile);
@@ -132224,13 +132286,12 @@ var Prd = class extends Service()("lalph/Prd", { make: gen(function* () {
132224
132286
  capacity: 1,
132225
132287
  strategy: "dropping"
132226
132288
  }), runForEach(() => ignore(sync$2)), forkScoped);
132227
- return { current };
132289
+ return {
132290
+ current,
132291
+ path: prdFile
132292
+ };
132228
132293
  }) }) {
132229
- static layer = effect(this, this.make).pipe(provide$2([
132230
- Linear.layer,
132231
- Settings.layer,
132232
- CurrentProject.layer
132233
- ]));
132294
+ static layer = effect(this, this.make).pipe(provide$2([CurrentProject.layer, Worktree.layer]));
132234
132295
  };
132235
132296
  var NoMoreWork = class extends ErrorClass("lalph/Prd/NoMoreWork")({ _tag: tag("NoMoreWork") }) {
132236
132297
  message = "No more work to be done!";
@@ -132293,8 +132354,6 @@ var PrdList = class PrdList extends Class$1 {
132293
132354
  //#region src/PromptGen.ts
132294
132355
  var PromptGen = class extends Service()("lalph/PromptGen", { make: gen(function* () {
132295
132356
  const linear = yield* Linear;
132296
- const fs = yield* FileSystem;
132297
- yield* scoped$1(fs.open("PROGRESS.md", { flag: "a+" }));
132298
132357
  return { prompt: `# Instructions
132299
132358
 
132300
132359
  1. Decide which single task to work on next from the prd.json file. This should
@@ -132349,6 +132408,7 @@ ${JSON.stringify(PrdIssue.jsonSchema, null, 2)}
132349
132408
  //#endregion
132350
132409
  //#region src/Runner.ts
132351
132410
  const run = gen(function* () {
132411
+ const worktree = yield* Worktree;
132352
132412
  const promptGen = yield* PromptGen;
132353
132413
  const cliCommand = (yield* getOrSelectCliAgent).command({
132354
132414
  prompt: promptGen.prompt,
@@ -132356,13 +132416,18 @@ const run = gen(function* () {
132356
132416
  progressFilePath: "PROGRESS.md"
132357
132417
  });
132358
132418
  const exitCode = yield* (yield* make$21(cliCommand[0], cliCommand.slice(1), {
132419
+ cwd: worktree.directory,
132359
132420
  extendEnv: true,
132360
132421
  stdout: "inherit",
132361
132422
  stderr: "inherit",
132362
132423
  stdin: "inherit"
132363
132424
  })).exitCode;
132364
132425
  yield* log$1(`Agent exited with code: ${exitCode}`);
132365
- }).pipe(scoped$1, provide([PromptGen.layer, Prd.layer]));
132426
+ }).pipe(scoped$1, provide([
132427
+ PromptGen.layer,
132428
+ Prd.layer,
132429
+ Worktree.layer
132430
+ ]));
132366
132431
  const selectCliAgent = gen(function* () {
132367
132432
  const agent = yield* select({
132368
132433
  message: "Select the CLI agent to use",
@@ -132392,18 +132457,51 @@ const selectLabel = make$26("select-label").pipe(withDescription("Select the lab
132392
132457
  onNone: () => "No Label",
132393
132458
  onSome: (l) => l.name
132394
132459
  })}`);
132395
- }, provide(Linear.layer))));
132460
+ })));
132396
132461
  const selectAgent = make$26("select-agent").pipe(withDescription("Select the CLI agent to use"), withHandler(() => selectCliAgent));
132397
- const iterations = integer("iterations").pipe(withAlias("i"), withDefault(1));
132398
- const root = make$26("lalph", { iterations }).pipe(withHandler(fnUntraced(function* ({ iterations: iterations$1 }) {
132399
- yield* log$1(`Executing ${iterations$1} iteration(s)`);
132400
- for (let i = 0; i < iterations$1; i++) yield* run;
132462
+ const iterations = integer("iterations").pipe(withDescription$1("Number of iterations to run, defaults to unlimited"), withAlias("i"), withDefault(Number.POSITIVE_INFINITY));
132463
+ const concurrency = integer("concurrency").pipe(withDescription$1("Number of concurrent agents, defaults to 1"), withAlias("c"), withDefault(1));
132464
+ const root = make$26("lalph", {
132465
+ iterations,
132466
+ concurrency
132467
+ }).pipe(withHandler(fnUntraced(function* ({ iterations: iterations$1, concurrency: concurrency$1 }) {
132468
+ const isFinite$3 = Number.isFinite(iterations$1);
132469
+ const iterationsDisplay = isFinite$3 ? iterations$1 : "unlimited";
132470
+ const runConcurrency = Math.max(1, concurrency$1);
132471
+ const semaphore = makeSemaphoreUnsafe(runConcurrency);
132472
+ yield* log$1(`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`);
132473
+ let iteration = 0;
132474
+ let lastStartedAt = makeUnsafe$3(0);
132475
+ let inProgress = 0;
132476
+ while (true) {
132477
+ yield* semaphore.take(1);
132478
+ if (isFinite$3 && iteration >= iterations$1) break;
132479
+ const currentIteration = iteration;
132480
+ if (inProgress > 0) {
132481
+ const nextEarliestStart = lastStartedAt.pipe(add$1({ seconds: 30 }));
132482
+ const diff = distance(yield* now, nextEarliestStart);
132483
+ if (diff > 0) yield* sleep(diff);
132484
+ }
132485
+ lastStartedAt = yield* now;
132486
+ inProgress++;
132487
+ yield* run.pipe(catchTag("NoMoreWork", (e) => {
132488
+ if (isFinite$3) {
132489
+ iterations$1 = currentIteration;
132490
+ return fail$3(e);
132491
+ }
132492
+ return log$1("No more work to process, waiting 30 seconds...").pipe(andThen(sleep("30 seconds")));
132493
+ }), catchCause$1(logWarning), annotateLogs({ iteration: currentIteration }), ensuring(suspend$2(() => {
132494
+ inProgress--;
132495
+ return semaphore.release(1);
132496
+ })), forkChild);
132497
+ iteration++;
132498
+ }
132401
132499
  })), withSubcommands([
132402
132500
  selectProject,
132403
132501
  selectLabel,
132404
132502
  selectAgent
132405
132503
  ]));
132406
- run$1(root, { version: "0.1.0" }).pipe(provide(Settings.layer.pipe(provideMerge(layer$1))), runMain);
132504
+ run$1(root, { version: "0.1.0" }).pipe(provide(mergeAll(Settings.layer, Linear.layer).pipe(provideMerge(layer$1))), runMain);
132407
132505
 
132408
132506
  //#endregion
132409
132507
  export { };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/Prd.ts CHANGED
@@ -4,17 +4,21 @@ import {
4
4
  FileSystem,
5
5
  Layer,
6
6
  Option,
7
+ Path,
7
8
  Schema,
8
9
  ServiceMap,
9
10
  Stream,
10
11
  SubscriptionRef,
11
12
  } from "effect"
12
13
  import { CurrentProject, Linear } from "./Linear.ts"
13
- import { selectedLabelId, selectedTeamId, Settings } from "./Settings.ts"
14
+ import { selectedLabelId, selectedTeamId } from "./Settings.ts"
14
15
  import type { Issue } from "@linear/sdk"
16
+ import { Worktree } from "./Worktree.ts"
15
17
 
16
18
  export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
17
19
  make: Effect.gen(function* () {
20
+ const worktree = yield* Worktree
21
+ const pathService = yield* Path.Path
18
22
  const fs = yield* FileSystem.FileSystem
19
23
  const linear = yield* Linear
20
24
  const project = yield* CurrentProject
@@ -46,7 +50,7 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
46
50
 
47
51
  const current = yield* SubscriptionRef.make(initial)
48
52
 
49
- const prdFile = `.lalph/prd.json`
53
+ const prdFile = pathService.join(worktree.directory, `.lalph/prd.json`)
50
54
 
51
55
  yield* fs.writeFileString(prdFile, initial.toJson())
52
56
 
@@ -97,11 +101,11 @@ export class Prd extends ServiceMap.Service<Prd>()("lalph/Prd", {
97
101
  Effect.forkScoped,
98
102
  )
99
103
 
100
- return { current } as const
104
+ return { current, path: prdFile } as const
101
105
  }),
102
106
  }) {
103
107
  static layer = Layer.effect(this, this.make).pipe(
104
- Layer.provide([Linear.layer, Settings.layer, CurrentProject.layer]),
108
+ Layer.provide([CurrentProject.layer, Worktree.layer]),
105
109
  )
106
110
  }
107
111
 
package/src/PromptGen.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, FileSystem, Layer, ServiceMap } from "effect"
1
+ import { Effect, Layer, ServiceMap } from "effect"
2
2
  import { Linear } from "./Linear.ts"
3
3
  import { PrdIssue } from "./Prd.ts"
4
4
 
@@ -7,13 +7,6 @@ export class PromptGen extends ServiceMap.Service<PromptGen>()(
7
7
  {
8
8
  make: Effect.gen(function* () {
9
9
  const linear = yield* Linear
10
- const fs = yield* FileSystem.FileSystem
11
-
12
- yield* Effect.scoped(
13
- fs.open("PROGRESS.md", {
14
- flag: "a+",
15
- }),
16
- )
17
10
 
18
11
  const prompt = `# Instructions
19
12
 
package/src/Runner.ts CHANGED
@@ -5,16 +5,20 @@ import { ChildProcess } from "effect/unstable/process"
5
5
  import { Prompt } from "effect/unstable/cli"
6
6
  import { allCliAgents } from "./domain/CliAgent.ts"
7
7
  import { selectedCliAgentId } from "./Settings.ts"
8
+ import { Worktree } from "./Worktree.ts"
8
9
 
9
10
  export const run = Effect.gen(function* () {
11
+ const worktree = yield* Worktree
10
12
  const promptGen = yield* PromptGen
11
13
  const cliAgent = yield* getOrSelectCliAgent
14
+
12
15
  const cliCommand = cliAgent.command({
13
16
  prompt: promptGen.prompt,
14
17
  prdFilePath: ".lalph/prd.json",
15
18
  progressFilePath: "PROGRESS.md",
16
19
  })
17
20
  const command = ChildProcess.make(cliCommand[0]!, cliCommand.slice(1), {
21
+ cwd: worktree.directory,
18
22
  extendEnv: true,
19
23
  stdout: "inherit",
20
24
  stderr: "inherit",
@@ -25,7 +29,10 @@ export const run = Effect.gen(function* () {
25
29
  const exitCode = yield* agent.exitCode
26
30
 
27
31
  yield* Effect.log(`Agent exited with code: ${exitCode}`)
28
- }).pipe(Effect.scoped, Effect.provide([PromptGen.layer, Prd.layer]))
32
+ }).pipe(
33
+ Effect.scoped,
34
+ Effect.provide([PromptGen.layer, Prd.layer, Worktree.layer]),
35
+ )
29
36
 
30
37
  export const selectCliAgent = Effect.gen(function* () {
31
38
  const agent = yield* Prompt.select({
@@ -0,0 +1,59 @@
1
+ import { Effect, FileSystem, Layer, Path, ServiceMap } from "effect"
2
+ import { ChildProcess } from "effect/unstable/process"
3
+
4
+ export class Worktree extends ServiceMap.Service<Worktree>()("lalph/Worktree", {
5
+ make: Effect.gen(function* () {
6
+ const fs = yield* FileSystem.FileSystem
7
+ const pathService = yield* Path.Path
8
+ const directory = yield* fs.makeTempDirectory()
9
+
10
+ yield* Effect.addFinalizer(
11
+ Effect.fnUntraced(function* () {
12
+ yield* execIgnore(ChildProcess.make`git worktree remove ${directory}`)
13
+ }),
14
+ )
15
+
16
+ yield* exec(ChildProcess.make`git worktree add ${directory} -d HEAD`)
17
+
18
+ yield* fs.makeDirectory(pathService.join(directory, ".lalph"), {
19
+ recursive: true,
20
+ })
21
+
22
+ yield* Effect.forEach(
23
+ [
24
+ ChildProcess.make({
25
+ cwd: directory,
26
+ extendEnv: true,
27
+ shell: process.env.SHELL ?? true,
28
+ })`direnv allow`,
29
+ ChildProcess.make({
30
+ cwd: directory,
31
+ extendEnv: true,
32
+ shell: process.env.SHELL ?? true,
33
+ })`devenv allow`,
34
+ ChildProcess.make({
35
+ cwd: directory,
36
+ })`git submodule update --init --recursive`,
37
+ ],
38
+ execIgnore,
39
+ { concurrency: "unbounded" },
40
+ )
41
+
42
+ return { directory } as const
43
+ }),
44
+ }) {
45
+ static layer = Layer.effect(this, this.make)
46
+ }
47
+
48
+ const exec = (command: ChildProcess.Command) =>
49
+ command.asEffect().pipe(
50
+ Effect.flatMap((proc) => proc.exitCode),
51
+ Effect.scoped,
52
+ )
53
+
54
+ const execIgnore = (command: ChildProcess.Command) =>
55
+ command.asEffect().pipe(
56
+ Effect.flatMap((proc) => proc.exitCode),
57
+ Effect.catchCause(Effect.logWarning),
58
+ Effect.scoped,
59
+ )
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command, Flag } from "effect/unstable/cli"
4
- import { Effect, Layer, Option } from "effect"
4
+ import { DateTime, Effect, Layer, Option } from "effect"
5
5
  import { NodeRuntime, NodeServices } from "@effect/platform-node"
6
6
  import { CurrentProject, labelSelect, Linear } from "./Linear.ts"
7
7
  import { layerKvs } from "./Kvs.ts"
@@ -34,7 +34,7 @@ const selectLabel = Command.make("select-label").pipe(
34
34
  onSome: (l) => l.name,
35
35
  })}`,
36
36
  )
37
- }, Effect.provide(Linear.layer)),
37
+ }),
38
38
  ),
39
39
  )
40
40
 
@@ -44,17 +44,80 @@ const selectAgent = Command.make("select-agent").pipe(
44
44
  )
45
45
 
46
46
  const iterations = Flag.integer("iterations").pipe(
47
+ Flag.withDescription("Number of iterations to run, defaults to unlimited"),
47
48
  Flag.withAlias("i"),
49
+ Flag.withDefault(Number.POSITIVE_INFINITY),
50
+ )
51
+
52
+ const concurrency = Flag.integer("concurrency").pipe(
53
+ Flag.withDescription("Number of concurrent agents, defaults to 1"),
54
+ Flag.withAlias("c"),
48
55
  Flag.withDefault(1),
49
56
  )
50
57
 
51
- const root = Command.make("lalph", { iterations }).pipe(
58
+ const root = Command.make("lalph", { iterations, concurrency }).pipe(
52
59
  Command.withHandler(
53
- Effect.fnUntraced(function* ({ iterations }) {
54
- yield* Effect.log(`Executing ${iterations} iteration(s)`)
60
+ Effect.fnUntraced(function* ({ iterations, concurrency }) {
61
+ const isFinite = Number.isFinite(iterations)
62
+ const iterationsDisplay = isFinite ? iterations : "unlimited"
63
+ const runConcurrency = Math.max(1, concurrency)
64
+ const semaphore = Effect.makeSemaphoreUnsafe(runConcurrency)
65
+
66
+ yield* Effect.log(
67
+ `Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`,
68
+ )
69
+
70
+ let iteration = 0
71
+ let lastStartedAt = DateTime.makeUnsafe(0)
72
+ let inProgress = 0
55
73
 
56
- for (let i = 0; i < iterations; i++) {
57
- yield* run
74
+ while (true) {
75
+ yield* semaphore.take(1)
76
+ if (isFinite && iteration >= iterations) {
77
+ break
78
+ }
79
+
80
+ const currentIteration = iteration
81
+
82
+ if (inProgress > 0) {
83
+ // add delay to try keep task list in sync
84
+ const nextEarliestStart = lastStartedAt.pipe(
85
+ DateTime.add({ seconds: 30 }),
86
+ )
87
+ const diff = DateTime.distance(yield* DateTime.now, nextEarliestStart)
88
+ if (diff > 0) {
89
+ yield* Effect.sleep(diff)
90
+ }
91
+ }
92
+
93
+ lastStartedAt = yield* DateTime.now
94
+ inProgress++
95
+
96
+ yield* run.pipe(
97
+ Effect.catchTag("NoMoreWork", (e) => {
98
+ if (isFinite) {
99
+ // If we have a finite number of iterations, we exit when no more
100
+ // work is found
101
+ iterations = currentIteration
102
+ return Effect.fail(e)
103
+ }
104
+ return Effect.log(
105
+ "No more work to process, waiting 30 seconds...",
106
+ ).pipe(Effect.andThen(Effect.sleep("30 seconds")))
107
+ }),
108
+ Effect.catchCause(Effect.logWarning),
109
+ Effect.annotateLogs({
110
+ iteration: currentIteration,
111
+ }),
112
+ Effect.ensuring(
113
+ Effect.suspend(() => {
114
+ inProgress--
115
+ return semaphore.release(1)
116
+ }),
117
+ ),
118
+ Effect.forkChild,
119
+ )
120
+ iteration++
58
121
  }
59
122
  }),
60
123
  ),
@@ -64,6 +127,10 @@ const root = Command.make("lalph", { iterations }).pipe(
64
127
  Command.run(root, {
65
128
  version: "0.1.0",
66
129
  }).pipe(
67
- Effect.provide(Settings.layer.pipe(Layer.provideMerge(NodeServices.layer))),
130
+ Effect.provide(
131
+ Layer.mergeAll(Settings.layer, Linear.layer).pipe(
132
+ Layer.provideMerge(NodeServices.layer),
133
+ ),
134
+ ),
68
135
  NodeRuntime.runMain,
69
136
  )