lalph 0.3.37 → 0.3.38
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 +69750 -32260
- package/package.json +13 -11
- package/src/Clanka.ts +39 -0
- package/src/ClankaModels.ts +53 -0
- package/src/GitFlow.ts +1 -1
- package/src/IssueSource.ts +5 -5
- package/src/Presets.ts +18 -0
- package/src/PromptGen.ts +151 -24
- package/src/TaskTools.ts +136 -0
- package/src/Worktree.ts +2 -21
- package/src/commands/root.ts +310 -14
- package/src/domain/CliAgentPreset.ts +2 -0
- package/src/domain/PrdIssue.ts +7 -0
- package/src/shared/stream.ts +29 -1
package/src/commands/root.ts
CHANGED
|
@@ -5,17 +5,21 @@ import {
|
|
|
5
5
|
Effect,
|
|
6
6
|
FiberSet,
|
|
7
7
|
FileSystem,
|
|
8
|
+
identity,
|
|
8
9
|
Iterable,
|
|
9
10
|
Option,
|
|
10
11
|
Path,
|
|
12
|
+
PlatformError,
|
|
13
|
+
Schema,
|
|
14
|
+
Scope,
|
|
11
15
|
Semaphore,
|
|
12
16
|
Stream,
|
|
13
17
|
} from "effect"
|
|
14
18
|
import { PromptGen } from "../PromptGen.ts"
|
|
15
19
|
import { Prd } from "../Prd.ts"
|
|
16
20
|
import { Worktree } from "../Worktree.ts"
|
|
17
|
-
import { Flag, Command } from "effect/unstable/cli"
|
|
18
|
-
import { IssueSource } from "../IssueSource.ts"
|
|
21
|
+
import { Flag, Command, Prompt } from "effect/unstable/cli"
|
|
22
|
+
import { IssueSource, IssueSourceError } from "../IssueSource.ts"
|
|
19
23
|
import {
|
|
20
24
|
checkForWork,
|
|
21
25
|
CurrentIssueSource,
|
|
@@ -24,7 +28,7 @@ import {
|
|
|
24
28
|
} from "../CurrentIssueSource.ts"
|
|
25
29
|
import { GithubCli } from "../Github/Cli.ts"
|
|
26
30
|
import { agentWorker } from "../Agents/worker.ts"
|
|
27
|
-
import { agentChooser } from "../Agents/chooser.ts"
|
|
31
|
+
import { agentChooser, ChosenTaskNotFound } from "../Agents/chooser.ts"
|
|
28
32
|
import { RunnerStalled, TaskStateChanged } from "../domain/Errors.ts"
|
|
29
33
|
import { agentReviewer } from "../Agents/reviewer.ts"
|
|
30
34
|
import { agentTimeout } from "../Agents/timeout.ts"
|
|
@@ -36,10 +40,17 @@ import {
|
|
|
36
40
|
withWorkerState,
|
|
37
41
|
} from "../Workers.ts"
|
|
38
42
|
import { WorkerStatus } from "../domain/WorkerState.ts"
|
|
39
|
-
import { GitFlow, GitFlowCommit, GitFlowPR } from "../GitFlow.ts"
|
|
43
|
+
import { GitFlow, GitFlowCommit, GitFlowError, GitFlowPR } from "../GitFlow.ts"
|
|
40
44
|
import { getAllProjects, welcomeWizard } from "../Projects.ts"
|
|
41
45
|
import type { Project } from "../domain/Project.ts"
|
|
42
46
|
import { getDefaultCliAgentPreset } from "../Presets.ts"
|
|
47
|
+
import { ChosenTaskDeferred } from "../TaskTools.ts"
|
|
48
|
+
import { ClankaModels, type ClankaModel } from "../ClankaModels.ts"
|
|
49
|
+
import { runClanka } from "../Clanka.ts"
|
|
50
|
+
import type { QuitError } from "effect/Terminal"
|
|
51
|
+
import type { TimeoutError } from "effect/Cause"
|
|
52
|
+
import type { ChildProcessSpawner } from "effect/unstable/process"
|
|
53
|
+
import type { AiError } from "effect/unstable/ai"
|
|
43
54
|
|
|
44
55
|
// Main iteration run logic
|
|
45
56
|
|
|
@@ -51,7 +62,31 @@ const run = Effect.fnUntraced(
|
|
|
51
62
|
readonly stallTimeout: Duration.Duration
|
|
52
63
|
readonly runTimeout: Duration.Duration
|
|
53
64
|
readonly review: boolean
|
|
54
|
-
})
|
|
65
|
+
}): Effect.fn.Return<
|
|
66
|
+
void,
|
|
67
|
+
| PlatformError.PlatformError
|
|
68
|
+
| Schema.SchemaError
|
|
69
|
+
| IssueSourceError
|
|
70
|
+
| QuitError
|
|
71
|
+
| GitFlowError
|
|
72
|
+
| ChosenTaskNotFound
|
|
73
|
+
| RunnerStalled
|
|
74
|
+
| TimeoutError,
|
|
75
|
+
| CurrentProjectId
|
|
76
|
+
| ChildProcessSpawner.ChildProcessSpawner
|
|
77
|
+
| Settings
|
|
78
|
+
| Reactivity.Reactivity
|
|
79
|
+
| GithubCli
|
|
80
|
+
| IssueSource
|
|
81
|
+
| Prompt.Environment
|
|
82
|
+
| AtomRegistry.AtomRegistry
|
|
83
|
+
| GitFlow
|
|
84
|
+
| CurrentWorkerState
|
|
85
|
+
| PromptGen
|
|
86
|
+
| Prd
|
|
87
|
+
| Worktree
|
|
88
|
+
| Scope.Scope
|
|
89
|
+
> {
|
|
55
90
|
const projectId = yield* CurrentProjectId
|
|
56
91
|
const fs = yield* FileSystem.FileSystem
|
|
57
92
|
const pathService = yield* Path.Path
|
|
@@ -239,6 +274,251 @@ const run = Effect.fnUntraced(
|
|
|
239
274
|
Effect.provide(Prd.layer, { local: true }),
|
|
240
275
|
)
|
|
241
276
|
|
|
277
|
+
const runWithClanka = Effect.fnUntraced(
|
|
278
|
+
function* (options: {
|
|
279
|
+
readonly startedDeferred: Deferred.Deferred<void>
|
|
280
|
+
readonly targetBranch: Option.Option<string>
|
|
281
|
+
readonly specsDirectory: string
|
|
282
|
+
readonly stallTimeout: Duration.Duration
|
|
283
|
+
readonly runTimeout: Duration.Duration
|
|
284
|
+
readonly review: boolean
|
|
285
|
+
readonly clankaModel: ClankaModel
|
|
286
|
+
}): Effect.fn.Return<
|
|
287
|
+
void,
|
|
288
|
+
| PlatformError.PlatformError
|
|
289
|
+
| Schema.SchemaError
|
|
290
|
+
| IssueSourceError
|
|
291
|
+
| GitFlowError
|
|
292
|
+
| ChosenTaskNotFound
|
|
293
|
+
| RunnerStalled
|
|
294
|
+
| TimeoutError
|
|
295
|
+
| AiError.AiError,
|
|
296
|
+
| CurrentProjectId
|
|
297
|
+
| FileSystem.FileSystem
|
|
298
|
+
| Path.Path
|
|
299
|
+
| Worktree
|
|
300
|
+
| ChildProcessSpawner.ChildProcessSpawner
|
|
301
|
+
| GithubCli
|
|
302
|
+
| IssueSource
|
|
303
|
+
| AtomRegistry.AtomRegistry
|
|
304
|
+
| GitFlow
|
|
305
|
+
| CurrentWorkerState
|
|
306
|
+
| PromptGen
|
|
307
|
+
| ClankaModels
|
|
308
|
+
| Scope.Scope
|
|
309
|
+
| Prd
|
|
310
|
+
> {
|
|
311
|
+
const projectId = yield* CurrentProjectId
|
|
312
|
+
const fs = yield* FileSystem.FileSystem
|
|
313
|
+
const pathService = yield* Path.Path
|
|
314
|
+
const worktree = yield* Worktree
|
|
315
|
+
const gh = yield* GithubCli
|
|
316
|
+
const source = yield* IssueSource
|
|
317
|
+
const gitFlow = yield* GitFlow
|
|
318
|
+
const currentWorker = yield* CurrentWorkerState
|
|
319
|
+
const registry = yield* AtomRegistry.AtomRegistry
|
|
320
|
+
const promptGen = yield* PromptGen
|
|
321
|
+
const models = yield* ClankaModels
|
|
322
|
+
const model = models.get(options.clankaModel)
|
|
323
|
+
|
|
324
|
+
// ensure cleanup of branch after run
|
|
325
|
+
yield* Effect.addFinalizer(
|
|
326
|
+
Effect.fnUntraced(function* () {
|
|
327
|
+
const currentBranchName = yield* worktree
|
|
328
|
+
.currentBranch(worktree.directory)
|
|
329
|
+
.pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
|
330
|
+
if (!currentBranchName) return
|
|
331
|
+
|
|
332
|
+
// enter detached state
|
|
333
|
+
yield* worktree.exec`git checkout --detach ${currentBranchName}`
|
|
334
|
+
// delete the branch
|
|
335
|
+
yield* worktree.exec`git branch -D ${currentBranchName}`
|
|
336
|
+
}, Effect.ignore()),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
let taskId: string | undefined = undefined
|
|
340
|
+
|
|
341
|
+
// setup finalizer to revert issue if we fail
|
|
342
|
+
yield* Effect.addFinalizer(
|
|
343
|
+
Effect.fnUntraced(function* (exit) {
|
|
344
|
+
if (exit._tag === "Success") return
|
|
345
|
+
if (taskId) {
|
|
346
|
+
yield* source.updateIssue({
|
|
347
|
+
projectId,
|
|
348
|
+
issueId: taskId,
|
|
349
|
+
state: "todo",
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
}, Effect.ignore()),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
const taskById = (taskId: string) =>
|
|
356
|
+
AtomRegistry.getResult(registry, currentIssuesAtom(projectId)).pipe(
|
|
357
|
+
Effect.map((issues) => issues.find((entry) => entry.id === taskId)),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
// 1. Choose task
|
|
361
|
+
// --------------
|
|
362
|
+
|
|
363
|
+
registry.update(currentWorker.state, (s) =>
|
|
364
|
+
s.transitionTo(WorkerStatus.ChoosingTask()),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
const deferred = ChosenTaskDeferred.of(Deferred.makeUnsafe())
|
|
368
|
+
const chooseResult = yield* runClanka({
|
|
369
|
+
directory: worktree.directory,
|
|
370
|
+
prompt: promptGen.promptChooseClanka({ gitFlow }),
|
|
371
|
+
stallTimeout: options.stallTimeout,
|
|
372
|
+
}).pipe(
|
|
373
|
+
Effect.andThen(Effect.fail(new ChosenTaskNotFound())),
|
|
374
|
+
Effect.provideService(ChosenTaskDeferred, deferred),
|
|
375
|
+
Effect.provide(model),
|
|
376
|
+
Effect.raceFirst(Deferred.await(deferred)),
|
|
377
|
+
Effect.withSpan("Main.choose"),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
const chosenTask = yield* taskById(chooseResult.taskId)
|
|
381
|
+
if (!chosenTask) {
|
|
382
|
+
return yield* new ChosenTaskNotFound()
|
|
383
|
+
}
|
|
384
|
+
taskId = chooseResult.taskId
|
|
385
|
+
yield* source.updateIssue({
|
|
386
|
+
projectId,
|
|
387
|
+
issueId: taskId,
|
|
388
|
+
state: "in-progress",
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
yield* source.ensureInProgress(projectId, taskId).pipe(
|
|
392
|
+
Effect.timeoutOrElse({
|
|
393
|
+
duration: "1 minute",
|
|
394
|
+
onTimeout: () => Effect.fail(new RunnerStalled()),
|
|
395
|
+
}),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
yield* Deferred.completeWith(options.startedDeferred, Effect.void)
|
|
399
|
+
|
|
400
|
+
if (gitFlow.requiresGithubPr && chooseResult.githubPrNumber) {
|
|
401
|
+
yield* worktree.exec`gh pr checkout ${chooseResult.githubPrNumber}`
|
|
402
|
+
const feedback = yield* gh.prFeedbackMd(chooseResult.githubPrNumber)
|
|
403
|
+
yield* fs.writeFileString(
|
|
404
|
+
pathService.join(worktree.directory, ".lalph", "feedback.md"),
|
|
405
|
+
feedback,
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const catchStallInReview = <A, E, R>(
|
|
410
|
+
effect: Effect.Effect<A, E | RunnerStalled, R>,
|
|
411
|
+
) =>
|
|
412
|
+
Effect.catchIf(
|
|
413
|
+
effect,
|
|
414
|
+
(u): u is RunnerStalled => u instanceof RunnerStalled,
|
|
415
|
+
Effect.fnUntraced(function* (e) {
|
|
416
|
+
const issues = yield* AtomRegistry.getResult(
|
|
417
|
+
registry,
|
|
418
|
+
currentIssuesAtom(projectId),
|
|
419
|
+
)
|
|
420
|
+
const task = issues.find((entry) => entry.id === taskId)
|
|
421
|
+
const inReview = task?.state === "in-review"
|
|
422
|
+
if (inReview) return
|
|
423
|
+
return yield* e
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
const cancelled = yield* Effect.gen(function* () {
|
|
428
|
+
//
|
|
429
|
+
// 2. Work on task
|
|
430
|
+
// -----------------------
|
|
431
|
+
|
|
432
|
+
registry.update(currentWorker.state, (s) =>
|
|
433
|
+
s.transitionTo(WorkerStatus.Working({ issueId: taskId })),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
const instructions = promptGen.promptClanka({
|
|
437
|
+
specsDirectory: options.specsDirectory,
|
|
438
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
439
|
+
task: chosenTask,
|
|
440
|
+
githubPrNumber: chooseResult.githubPrNumber ?? undefined,
|
|
441
|
+
gitFlow,
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
yield* runClanka({
|
|
445
|
+
directory: worktree.directory,
|
|
446
|
+
system: promptGen.systemClanka(options),
|
|
447
|
+
prompt: instructions,
|
|
448
|
+
stallTimeout: options.stallTimeout,
|
|
449
|
+
}).pipe(Effect.provide(model), Effect.withSpan("Main.worker"))
|
|
450
|
+
|
|
451
|
+
// 3. Review task
|
|
452
|
+
// -----------------------
|
|
453
|
+
|
|
454
|
+
if (options.review) {
|
|
455
|
+
registry.update(currentWorker.state, (s) =>
|
|
456
|
+
s.transitionTo(WorkerStatus.Reviewing({ issueId: taskId })),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
yield* runClanka({
|
|
460
|
+
directory: worktree.directory,
|
|
461
|
+
system: promptGen.systemClanka(options),
|
|
462
|
+
prompt: promptGen.promptReview({
|
|
463
|
+
prompt: instructions,
|
|
464
|
+
gitFlow,
|
|
465
|
+
}),
|
|
466
|
+
}).pipe(
|
|
467
|
+
Effect.provide(model),
|
|
468
|
+
catchStallInReview,
|
|
469
|
+
Effect.withSpan("Main.review"),
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
}).pipe(
|
|
473
|
+
Effect.timeout(options.runTimeout),
|
|
474
|
+
Effect.tapErrorTag("TimeoutError", () =>
|
|
475
|
+
runClanka({
|
|
476
|
+
directory: worktree.directory,
|
|
477
|
+
system: promptGen.systemClanka(options),
|
|
478
|
+
prompt: promptGen.promptTimeoutClanka({
|
|
479
|
+
taskId,
|
|
480
|
+
specsDirectory: options.specsDirectory,
|
|
481
|
+
}),
|
|
482
|
+
stallTimeout: options.stallTimeout,
|
|
483
|
+
}).pipe(Effect.provide(model), Effect.withSpan("Main.timeout")),
|
|
484
|
+
),
|
|
485
|
+
Effect.raceFirst(watchTaskState({ issueId: taskId })),
|
|
486
|
+
Effect.as(false),
|
|
487
|
+
Effect.catchTag("TaskStateChanged", (error) =>
|
|
488
|
+
Effect.log(
|
|
489
|
+
`Task ${error.issueId} moved to ${error.state}; cancelling run.`,
|
|
490
|
+
).pipe(Effect.as(true)),
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if (cancelled) return
|
|
495
|
+
|
|
496
|
+
yield* gitFlow.postWork({
|
|
497
|
+
worktree,
|
|
498
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
499
|
+
issueId: taskId,
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const task = yield* taskById(taskId)
|
|
503
|
+
if (task?.autoMerge) {
|
|
504
|
+
yield* gitFlow.autoMerge({
|
|
505
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
506
|
+
issueId: taskId,
|
|
507
|
+
worktree,
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
Effect.scoped,
|
|
512
|
+
Effect.provide(Prd.layer, { local: true }),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
type RunEffects = ReturnType<typeof run> | ReturnType<typeof runWithClanka>
|
|
516
|
+
type RunEffect = Effect.Effect<
|
|
517
|
+
void,
|
|
518
|
+
Effect.Error<RunEffects>,
|
|
519
|
+
Effect.Services<RunEffects>
|
|
520
|
+
>
|
|
521
|
+
|
|
242
522
|
const runProject = Effect.fnUntraced(
|
|
243
523
|
function* (options: {
|
|
244
524
|
readonly iterations: number
|
|
@@ -246,6 +526,7 @@ const runProject = Effect.fnUntraced(
|
|
|
246
526
|
readonly specsDirectory: string
|
|
247
527
|
readonly stallTimeout: Duration.Duration
|
|
248
528
|
readonly runTimeout: Duration.Duration
|
|
529
|
+
readonly clankaModel: ClankaModel | undefined
|
|
249
530
|
}) {
|
|
250
531
|
const isFinite = Number.isFinite(options.iterations)
|
|
251
532
|
const iterationsDisplay = isFinite ? options.iterations : "unlimited"
|
|
@@ -276,14 +557,26 @@ const runProject = Effect.fnUntraced(
|
|
|
276
557
|
|
|
277
558
|
yield* checkForWork.pipe(
|
|
278
559
|
Effect.andThen(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
560
|
+
identity<RunEffect>(
|
|
561
|
+
options.clankaModel
|
|
562
|
+
? runWithClanka({
|
|
563
|
+
startedDeferred,
|
|
564
|
+
targetBranch: options.project.targetBranch,
|
|
565
|
+
specsDirectory: options.specsDirectory,
|
|
566
|
+
stallTimeout: options.stallTimeout,
|
|
567
|
+
runTimeout: options.runTimeout,
|
|
568
|
+
review: options.project.reviewAgent,
|
|
569
|
+
clankaModel: options.clankaModel,
|
|
570
|
+
})
|
|
571
|
+
: run({
|
|
572
|
+
startedDeferred,
|
|
573
|
+
targetBranch: options.project.targetBranch,
|
|
574
|
+
specsDirectory: options.specsDirectory,
|
|
575
|
+
stallTimeout: options.stallTimeout,
|
|
576
|
+
runTimeout: options.runTimeout,
|
|
577
|
+
review: options.project.reviewAgent,
|
|
578
|
+
}),
|
|
579
|
+
).pipe(
|
|
287
580
|
Effect.provide(
|
|
288
581
|
options.project.gitFlow === "commit" ? GitFlowCommit : GitFlowPR,
|
|
289
582
|
{ local: true },
|
|
@@ -291,6 +584,7 @@ const runProject = Effect.fnUntraced(
|
|
|
291
584
|
withWorkerState(options.project.id),
|
|
292
585
|
),
|
|
293
586
|
),
|
|
587
|
+
(_) => _,
|
|
294
588
|
Effect.catchTags({
|
|
295
589
|
NoMoreWork(_error) {
|
|
296
590
|
if (isFinite) {
|
|
@@ -397,7 +691,7 @@ export const commandRoot = Command.make("lalph", {
|
|
|
397
691
|
stallMinutes,
|
|
398
692
|
specsDirectory,
|
|
399
693
|
}) {
|
|
400
|
-
yield* getDefaultCliAgentPreset
|
|
694
|
+
const preset = yield* getDefaultCliAgentPreset
|
|
401
695
|
|
|
402
696
|
let allProjects = yield* getAllProjects
|
|
403
697
|
if (allProjects.length === 0) {
|
|
@@ -420,12 +714,14 @@ export const commandRoot = Command.make("lalph", {
|
|
|
420
714
|
specsDirectory,
|
|
421
715
|
stallTimeout: Duration.minutes(stallMinutes),
|
|
422
716
|
runTimeout: Duration.minutes(maxIterationMinutes),
|
|
717
|
+
clankaModel: preset.clankaModel,
|
|
423
718
|
}).pipe(Effect.provideService(CurrentProjectId, project.id)),
|
|
424
719
|
{ concurrency: "unbounded", discard: true },
|
|
425
720
|
)
|
|
426
721
|
},
|
|
427
722
|
Effect.scoped,
|
|
428
723
|
Effect.provide([
|
|
724
|
+
ClankaModels.layer,
|
|
429
725
|
PromptGen.layer,
|
|
430
726
|
GithubCli.layer,
|
|
431
727
|
Settings.layer,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Array, Effect, identity, Option, Schema } from "effect"
|
|
2
2
|
import { CliAgentFromId } from "./CliAgent.ts"
|
|
3
3
|
import { ChildProcess } from "effect/unstable/process"
|
|
4
|
+
import { ClankaModel } from "../ClankaModels.ts"
|
|
4
5
|
|
|
5
6
|
export const CliAgentPresetId = Schema.NonEmptyString.pipe(
|
|
6
7
|
Schema.brand("lalph/CliAgentPresetId"),
|
|
@@ -15,6 +16,7 @@ export class CliAgentPreset extends Schema.Class<CliAgentPreset>(
|
|
|
15
16
|
commandPrefix: Schema.Array(Schema.String),
|
|
16
17
|
extraArgs: Schema.Array(Schema.String),
|
|
17
18
|
sourceMetadata: Schema.Record(Schema.String, Schema.Any),
|
|
19
|
+
clankaModel: Schema.optionalKey(ClankaModel),
|
|
18
20
|
}) {
|
|
19
21
|
static defaultId = CliAgentPresetId.makeUnsafe("default")
|
|
20
22
|
|
package/src/domain/PrdIssue.ts
CHANGED
|
@@ -7,6 +7,7 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
7
7
|
.annotate({
|
|
8
8
|
description:
|
|
9
9
|
"The unique identifier of the issue. If null, it is considered a new issue.",
|
|
10
|
+
documentation: "The unique identifier of the issue.",
|
|
10
11
|
})
|
|
11
12
|
.pipe(withEncodeDefault(() => null)),
|
|
12
13
|
title: Schema.String.annotate({
|
|
@@ -18,11 +19,15 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
18
19
|
priority: Schema.Finite.annotate({
|
|
19
20
|
description:
|
|
20
21
|
"The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.",
|
|
22
|
+
documentation:
|
|
23
|
+
"The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.",
|
|
21
24
|
}).pipe(withEncodeDefault(() => 3)),
|
|
22
25
|
estimate: Schema.NullOr(Schema.Finite)
|
|
23
26
|
.annotate({
|
|
24
27
|
description:
|
|
25
28
|
"The estimate of the issue in points. Null if no estimate is set. Roughly 1 point = 1 hour of work.",
|
|
29
|
+
documentation:
|
|
30
|
+
"Null if no estimate is set. Roughly 1 point = 1 hour of work.",
|
|
26
31
|
})
|
|
27
32
|
.pipe(withEncodeDefault(() => null)),
|
|
28
33
|
state: Schema.Literals([
|
|
@@ -34,12 +39,14 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
34
39
|
])
|
|
35
40
|
.annotate({
|
|
36
41
|
description: "The state of the issue.",
|
|
42
|
+
documentation: "The state of the issue.",
|
|
37
43
|
})
|
|
38
44
|
.pipe(withEncodeDefault(() => "todo")),
|
|
39
45
|
blockedBy: Schema.Array(Schema.String)
|
|
40
46
|
.annotate({
|
|
41
47
|
description:
|
|
42
48
|
"An array of issue IDs that block this issue. These issues must be completed before this issue can be worked on.",
|
|
49
|
+
documentation: "An array of issue IDs that block this issue.",
|
|
43
50
|
})
|
|
44
51
|
.pipe(withEncodeDefault(() => [])),
|
|
45
52
|
autoMerge: Schema.Boolean.annotate({
|
package/src/shared/stream.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Effect, flow, Schema, Stream } from "effect"
|
|
1
|
+
import { DateTime, Duration, Effect, flow, Schema, Stream } from "effect"
|
|
2
|
+
import { RunnerStalled } from "../domain/Errors.ts"
|
|
2
3
|
|
|
3
4
|
export const streamFilterJson = <S extends Schema.Top>(schema: S) => {
|
|
4
5
|
const fromString = Schema.fromJsonString(schema)
|
|
@@ -8,3 +9,30 @@ export const streamFilterJson = <S extends Schema.Top>(schema: S) => {
|
|
|
8
9
|
Stream.filterMapEffect((line) => decode(line).pipe(Effect.result)),
|
|
9
10
|
)
|
|
10
11
|
}
|
|
12
|
+
|
|
13
|
+
export const withStallTimeout = (timeout: Duration.Input) => {
|
|
14
|
+
const duration = Duration.fromInputUnsafe(timeout)
|
|
15
|
+
return <A, E, R>(stream: Stream.Stream<A, E, R>) =>
|
|
16
|
+
Stream.suspend(() => {
|
|
17
|
+
let lastOutputAt = DateTime.nowUnsafe()
|
|
18
|
+
const stallTimeout = Effect.suspend(function loop(): Effect.Effect<
|
|
19
|
+
never,
|
|
20
|
+
RunnerStalled
|
|
21
|
+
> {
|
|
22
|
+
const now = DateTime.nowUnsafe()
|
|
23
|
+
const deadline = DateTime.addDuration(lastOutputAt, duration)
|
|
24
|
+
if (DateTime.isLessThan(deadline, now)) {
|
|
25
|
+
return Effect.fail(new RunnerStalled())
|
|
26
|
+
}
|
|
27
|
+
const timeUntilDeadline = DateTime.distance(deadline, now)
|
|
28
|
+
return Effect.flatMap(Effect.sleep(timeUntilDeadline), loop)
|
|
29
|
+
})
|
|
30
|
+
return stream.pipe(
|
|
31
|
+
Stream.tap(() => {
|
|
32
|
+
lastOutputAt = DateTime.nowUnsafe()
|
|
33
|
+
return Effect.void
|
|
34
|
+
}),
|
|
35
|
+
Stream.mergeLeft(Stream.fromEffectDrain(stallTimeout)),
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
}
|