lalph 0.1.77 → 0.1.78

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/dist/cli.mjs CHANGED
@@ -135161,6 +135161,18 @@ var IssueSourceError = class extends ErrorClass("lalph/IssueSourceError")({
135161
135161
  const checkForWork = gen(function* () {
135162
135162
  if (!(yield* (yield* IssueSource).issues).some((issue) => issue.state === "todo" && issue.blockedBy.length === 0)) return yield* new NoMoreWork({});
135163
135163
  });
135164
+ const resetInProgress = gen(function* () {
135165
+ const source = yield* IssueSource;
135166
+ const inProgress = (yield* source.issues).filter((issue) => issue.state === "in-progress" && issue.id !== null);
135167
+ if (inProgress.length === 0) return;
135168
+ yield* forEach$1(inProgress, (issue) => source.updateIssue({
135169
+ issueId: issue.id,
135170
+ state: "todo"
135171
+ }), {
135172
+ concurrency: 5,
135173
+ discard: true
135174
+ });
135175
+ });
135164
135176
  var NoMoreWork = class extends ErrorClass("lalph/Prd/NoMoreWork")({ _tag: tag("NoMoreWork") }) {
135165
135177
  message = "No more work to be done!";
135166
135178
  };
@@ -135260,9 +135272,8 @@ const LinearIssueSource = effect(IssueSource, gen(function* () {
135260
135272
  return isGreaterThanOrEqualTo$1(completed, threeDaysAgo);
135261
135273
  }), mapEffect$1(fnUntraced(function* (issue) {
135262
135274
  identifierMap.set(issue.identifier, issue.id);
135263
- const linearState = linear.states.find((s) => s.id === issue.stateId);
135264
- const blockedBy = yield* runCollect(linear.blockedBy(issue));
135265
- const state = linearStateToPrdState(linearState);
135275
+ const state = linearStateToPrdState(linear.states.find((s) => s.id === issue.stateId));
135276
+ const blockedBy = state === "todo" ? yield* runCollect(linear.blockedBy(issue)) : [];
135266
135277
  return new PrdIssue({
135267
135278
  id: issue.identifier,
135268
135279
  title: issue.title,
@@ -135333,7 +135344,8 @@ const LinearIssueSource = effect(IssueSource, gen(function* () {
135333
135344
  cancelIssue: fnUntraced(function* (issueId) {
135334
135345
  const linearIssueId = identifierMap.get(issueId);
135335
135346
  yield* linear.use((c) => c.updateIssue(linearIssueId, { stateId: canceledState.id }));
135336
- }, mapError$2((cause) => new IssueSourceError({ cause })))
135347
+ }, mapError$2((cause) => new IssueSourceError({ cause }))),
135348
+ ensureInProgress: () => void_$1
135337
135349
  });
135338
135350
  })).pipe(provide$3(Linear.layer));
135339
135351
  const resetLinear = gen(function* () {
@@ -141802,6 +141814,17 @@ const GithubIssueSource = effect(IssueSource, gen(function* () {
141802
141814
  issue_number: Number(issueId.slice(1)),
141803
141815
  state: "closed"
141804
141816
  });
141817
+ }, mapError$2((cause) => new IssueSourceError({ cause }))),
141818
+ ensureInProgress: fnUntraced(function* (issueId) {
141819
+ const issueNumber = Number(issueId.slice(1));
141820
+ yield* pipe(github.request((rest) => rest.issues.get({
141821
+ owner: cli.owner,
141822
+ repo: cli.repo,
141823
+ issue_number: issueNumber
141824
+ })), repeat({
141825
+ until: (r) => hasLabel(r.data.labels, "in-progress"),
141826
+ schedule: spaced("1 second")
141827
+ }));
141805
141828
  }, mapError$2((cause) => new IssueSourceError({ cause })))
141806
141829
  });
141807
141830
  })).pipe(provide$3([Github.layer, GithubCli.layer]));
@@ -142365,7 +142388,9 @@ const commandRoot = make$27("lalph", {
142365
142388
  const runConcurrency = Math.max(1, concurrency$1);
142366
142389
  const semaphore = makeSemaphoreUnsafe(runConcurrency);
142367
142390
  const fibers = yield* make$24();
142391
+ yield* resetInProgress.pipe(provide$1(source));
142368
142392
  yield* log$1(`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`);
142393
+ if (isSome(targetBranch$1)) yield* log$1(`Using target branch: ${targetBranch$1.value}`);
142369
142394
  let iteration = 0;
142370
142395
  let quit = false;
142371
142396
  while (true) {
@@ -142407,6 +142432,7 @@ const run = fnUntraced(function* (options) {
142407
142432
  const gh = yield* GithubCli;
142408
142433
  const cliAgent = yield* getOrSelectCliAgent;
142409
142434
  const prd = yield* Prd;
142435
+ const source = yield* IssueSource;
142410
142436
  const exec = (template, ...args$1) => make$21({ cwd: worktree.directory })(template, ...args$1).pipe(exitCode);
142411
142437
  const execWithStallTimeout = fnUntraced(function* (command) {
142412
142438
  let lastOutputAt = yield* now;
@@ -142443,8 +142469,15 @@ const run = fnUntraced(function* (options) {
142443
142469
  onTimeout: () => fail$4(new RunnerStalled())
142444
142470
  }));
142445
142471
  const taskJson = yield* fs.readFileString(pathService.join(worktree.directory, ".lalph", "task.json"));
142446
- const chosenTask = yield* decodeEffect(ChosenTask)(taskJson);
142472
+ const chosenTask = yield* decodeEffect(ChosenTask)(taskJson).pipe(mapError$2(() => new ChosenTaskNotFound()), tap(fnUntraced(function* (task$1) {
142473
+ if ((yield* prd.findById(task$1.id))?.state === "in-progress") return;
142474
+ return yield* new ChosenTaskNotFound();
142475
+ })));
142447
142476
  taskId = chosenTask.id;
142477
+ yield* source.ensureInProgress(taskId).pipe(timeoutOrElse({
142478
+ duration: "1 minute",
142479
+ onTimeout: () => fail$4(new RunnerStalled())
142480
+ }));
142448
142481
  yield* completeWith(options.startedDeferred, void_$1);
142449
142482
  if (chosenTask.githubPrNumber) {
142450
142483
  yield* exec`gh pr checkout ${chosenTask.githubPrNumber}`;
@@ -142489,6 +142522,9 @@ const run = fnUntraced(function* (options) {
142489
142522
  var RunnerStalled = class extends TaggedError("RunnerStalled") {
142490
142523
  message = "The runner has stalled due to inactivity.";
142491
142524
  };
142525
+ var ChosenTaskNotFound = class extends TaggedError("ChosenTaskNotFound") {
142526
+ message = "The AI agent failed to choose a task.";
142527
+ };
142492
142528
  const ChosenTask = fromJsonString(Struct({
142493
142529
  id: String$1,
142494
142530
  githubPrNumber: NullOr(Finite)
@@ -142624,7 +142660,7 @@ const commandSource = make$27("source").pipe(withDescription("Select the issue s
142624
142660
 
142625
142661
  //#endregion
142626
142662
  //#region package.json
142627
- var version = "0.1.77";
142663
+ var version = "0.1.78";
142628
142664
 
142629
142665
  //#endregion
142630
142666
  //#region src/cli.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.1.77",
4
+ "version": "0.1.78",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/Github.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  Option,
9
9
  pipe,
10
10
  RcMap,
11
+ Schedule,
11
12
  Schema,
12
13
  ServiceMap,
13
14
  Stream,
@@ -369,6 +370,25 @@ export const GithubIssueSource = Layer.effect(
369
370
  },
370
371
  Effect.mapError((cause) => new IssueSourceError({ cause })),
371
372
  ),
373
+ ensureInProgress: Effect.fnUntraced(
374
+ function* (issueId: string) {
375
+ const issueNumber = Number(issueId.slice(1))
376
+ yield* pipe(
377
+ github.request((rest) =>
378
+ rest.issues.get({
379
+ owner: cli.owner,
380
+ repo: cli.repo,
381
+ issue_number: issueNumber,
382
+ }),
383
+ ),
384
+ Effect.repeat({
385
+ until: (r) => hasLabel(r.data.labels, "in-progress"),
386
+ schedule: Schedule.spaced("1 second"),
387
+ }),
388
+ )
389
+ },
390
+ Effect.mapError((cause) => new IssueSourceError({ cause })),
391
+ ),
372
392
  })
373
393
  }),
374
394
  ).pipe(Layer.provide([Github.layer, GithubCli.layer]))
@@ -21,6 +21,10 @@ export class IssueSource extends ServiceMap.Service<
21
21
  readonly cancelIssue: (
22
22
  issueId: string,
23
23
  ) => Effect.Effect<void, IssueSourceError>
24
+
25
+ readonly ensureInProgress: (
26
+ issueId: string,
27
+ ) => Effect.Effect<void, IssueSourceError>
24
28
  }
25
29
  >()("lalph/IssueSource") {}
26
30
 
@@ -42,6 +46,25 @@ export const checkForWork = Effect.gen(function* () {
42
46
  }
43
47
  })
44
48
 
49
+ export const resetInProgress = Effect.gen(function* () {
50
+ const source = yield* IssueSource
51
+ const issues = yield* source.issues
52
+ const inProgress = issues.filter(
53
+ (issue): issue is PrdIssue & { id: string } =>
54
+ issue.state === "in-progress" && issue.id !== null,
55
+ )
56
+ if (inProgress.length === 0) return
57
+ yield* Effect.forEach(
58
+ inProgress,
59
+ (issue) =>
60
+ source.updateIssue({
61
+ issueId: issue.id,
62
+ state: "todo",
63
+ }),
64
+ { concurrency: 5, discard: true },
65
+ )
66
+ })
67
+
45
68
  export class NoMoreWork extends Schema.ErrorClass<NoMoreWork>(
46
69
  "lalph/Prd/NoMoreWork",
47
70
  )({
package/src/Linear.ts CHANGED
@@ -230,8 +230,11 @@ export const LinearIssueSource = Layer.effect(
230
230
  const linearState = linear.states.find(
231
231
  (s) => s.id === issue.stateId,
232
232
  )!
233
- const blockedBy = yield* Stream.runCollect(linear.blockedBy(issue))
234
233
  const state = linearStateToPrdState(linearState)
234
+ const blockedBy =
235
+ state === "todo"
236
+ ? yield* Stream.runCollect(linear.blockedBy(issue))
237
+ : []
235
238
  return new PrdIssue({
236
239
  id: issue.identifier,
237
240
  title: issue.title,
@@ -377,6 +380,8 @@ export const LinearIssueSource = Layer.effect(
377
380
  },
378
381
  Effect.mapError((cause) => new IssueSourceError({ cause })),
379
382
  ),
383
+ // linear api writes and reflected immediately in reads, so no-op
384
+ ensureInProgress: () => Effect.void,
380
385
  })
381
386
  }),
382
387
  ).pipe(Layer.provide(Linear.layer))
@@ -24,7 +24,7 @@ import { ChildProcess } from "effect/unstable/process"
24
24
  import { Worktree } from "../Worktree.ts"
25
25
  import { getCommandPrefix, getOrSelectCliAgent } from "./agent.ts"
26
26
  import { Flag, CliError, Command } from "effect/unstable/cli"
27
- import { checkForWork } from "../IssueSource.ts"
27
+ import { checkForWork, IssueSource, resetInProgress } from "../IssueSource.ts"
28
28
  import { CurrentIssueSource } from "../IssueSources.ts"
29
29
  import { GithubCli } from "../Github/Cli.ts"
30
30
 
@@ -124,9 +124,14 @@ export const commandRoot = Command.make("lalph", {
124
124
  const semaphore = Effect.makeSemaphoreUnsafe(runConcurrency)
125
125
  const fibers = yield* FiberSet.make()
126
126
 
127
+ yield* resetInProgress.pipe(Effect.provide(source))
128
+
127
129
  yield* Effect.log(
128
130
  `Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`,
129
131
  )
132
+ if (Option.isSome(targetBranch)) {
133
+ yield* Effect.log(`Using target branch: ${targetBranch.value}`)
134
+ }
130
135
 
131
136
  let iteration = 0
132
137
  let quit = false
@@ -217,6 +222,7 @@ const run = Effect.fnUntraced(
217
222
  const gh = yield* GithubCli
218
223
  const cliAgent = yield* getOrSelectCliAgent
219
224
  const prd = yield* Prd
225
+ const source = yield* IssueSource
220
226
 
221
227
  const exec = (
222
228
  template: TemplateStringsArray,
@@ -314,8 +320,23 @@ const run = Effect.fnUntraced(
314
320
  const taskJson = yield* fs.readFileString(
315
321
  pathService.join(worktree.directory, ".lalph", "task.json"),
316
322
  )
317
- const chosenTask = yield* Schema.decodeEffect(ChosenTask)(taskJson)
323
+ const chosenTask = yield* Schema.decodeEffect(ChosenTask)(taskJson).pipe(
324
+ Effect.mapError(() => new ChosenTaskNotFound()),
325
+ Effect.tap(
326
+ Effect.fnUntraced(function* (task) {
327
+ const prdTask = yield* prd.findById(task.id)
328
+ if (prdTask?.state === "in-progress") return
329
+ return yield* new ChosenTaskNotFound()
330
+ }),
331
+ ),
332
+ )
318
333
  taskId = chosenTask.id
334
+ yield* source.ensureInProgress(taskId).pipe(
335
+ Effect.timeoutOrElse({
336
+ duration: "1 minute",
337
+ onTimeout: () => Effect.fail(new RunnerStalled()),
338
+ }),
339
+ )
319
340
 
320
341
  yield* Deferred.completeWith(options.startedDeferred, Effect.void)
321
342
 
@@ -407,6 +428,10 @@ class RunnerStalled extends Data.TaggedError("RunnerStalled") {
407
428
  readonly message = "The runner has stalled due to inactivity."
408
429
  }
409
430
 
431
+ class ChosenTaskNotFound extends Data.TaggedError("ChosenTaskNotFound") {
432
+ readonly message = "The AI agent failed to choose a task."
433
+ }
434
+
410
435
  const ChosenTask = Schema.fromJsonString(
411
436
  Schema.Struct({
412
437
  id: Schema.String,