lalph 0.3.37 → 0.3.39
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 +69743 -32260
- package/package.json +13 -11
- package/src/Clanka.ts +46 -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 +142 -0
- package/src/Worktree.ts +2 -21
- package/src/commands/root.ts +311 -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,252 @@ 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
|
+
withChoose: true,
|
|
373
|
+
}).pipe(
|
|
374
|
+
Effect.andThen(Effect.fail(new ChosenTaskNotFound())),
|
|
375
|
+
Effect.provideService(ChosenTaskDeferred, deferred),
|
|
376
|
+
Effect.provide(model),
|
|
377
|
+
Effect.raceFirst(Deferred.await(deferred)),
|
|
378
|
+
Effect.withSpan("Main.choose"),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
const chosenTask = yield* taskById(chooseResult.taskId)
|
|
382
|
+
if (!chosenTask) {
|
|
383
|
+
return yield* new ChosenTaskNotFound()
|
|
384
|
+
}
|
|
385
|
+
taskId = chooseResult.taskId
|
|
386
|
+
yield* source.updateIssue({
|
|
387
|
+
projectId,
|
|
388
|
+
issueId: taskId,
|
|
389
|
+
state: "in-progress",
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
yield* source.ensureInProgress(projectId, taskId).pipe(
|
|
393
|
+
Effect.timeoutOrElse({
|
|
394
|
+
duration: "1 minute",
|
|
395
|
+
onTimeout: () => Effect.fail(new RunnerStalled()),
|
|
396
|
+
}),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
yield* Deferred.completeWith(options.startedDeferred, Effect.void)
|
|
400
|
+
|
|
401
|
+
if (gitFlow.requiresGithubPr && chooseResult.githubPrNumber) {
|
|
402
|
+
yield* worktree.exec`gh pr checkout ${chooseResult.githubPrNumber}`
|
|
403
|
+
const feedback = yield* gh.prFeedbackMd(chooseResult.githubPrNumber)
|
|
404
|
+
yield* fs.writeFileString(
|
|
405
|
+
pathService.join(worktree.directory, ".lalph", "feedback.md"),
|
|
406
|
+
feedback,
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const catchStallInReview = <A, E, R>(
|
|
411
|
+
effect: Effect.Effect<A, E | RunnerStalled, R>,
|
|
412
|
+
) =>
|
|
413
|
+
Effect.catchIf(
|
|
414
|
+
effect,
|
|
415
|
+
(u): u is RunnerStalled => u instanceof RunnerStalled,
|
|
416
|
+
Effect.fnUntraced(function* (e) {
|
|
417
|
+
const issues = yield* AtomRegistry.getResult(
|
|
418
|
+
registry,
|
|
419
|
+
currentIssuesAtom(projectId),
|
|
420
|
+
)
|
|
421
|
+
const task = issues.find((entry) => entry.id === taskId)
|
|
422
|
+
const inReview = task?.state === "in-review"
|
|
423
|
+
if (inReview) return
|
|
424
|
+
return yield* e
|
|
425
|
+
}),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
const cancelled = yield* Effect.gen(function* () {
|
|
429
|
+
//
|
|
430
|
+
// 2. Work on task
|
|
431
|
+
// -----------------------
|
|
432
|
+
|
|
433
|
+
registry.update(currentWorker.state, (s) =>
|
|
434
|
+
s.transitionTo(WorkerStatus.Working({ issueId: taskId })),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
const instructions = promptGen.promptClanka({
|
|
438
|
+
specsDirectory: options.specsDirectory,
|
|
439
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
440
|
+
task: chosenTask,
|
|
441
|
+
githubPrNumber: chooseResult.githubPrNumber ?? undefined,
|
|
442
|
+
gitFlow,
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
yield* runClanka({
|
|
446
|
+
directory: worktree.directory,
|
|
447
|
+
system: promptGen.systemClanka(options),
|
|
448
|
+
prompt: instructions,
|
|
449
|
+
stallTimeout: options.stallTimeout,
|
|
450
|
+
}).pipe(Effect.provide(model), Effect.withSpan("Main.worker"))
|
|
451
|
+
|
|
452
|
+
// 3. Review task
|
|
453
|
+
// -----------------------
|
|
454
|
+
|
|
455
|
+
if (options.review) {
|
|
456
|
+
registry.update(currentWorker.state, (s) =>
|
|
457
|
+
s.transitionTo(WorkerStatus.Reviewing({ issueId: taskId })),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
yield* runClanka({
|
|
461
|
+
directory: worktree.directory,
|
|
462
|
+
system: promptGen.systemClanka(options),
|
|
463
|
+
prompt: promptGen.promptReview({
|
|
464
|
+
prompt: instructions,
|
|
465
|
+
gitFlow,
|
|
466
|
+
}),
|
|
467
|
+
}).pipe(
|
|
468
|
+
Effect.provide(model),
|
|
469
|
+
catchStallInReview,
|
|
470
|
+
Effect.withSpan("Main.review"),
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
}).pipe(
|
|
474
|
+
Effect.timeout(options.runTimeout),
|
|
475
|
+
Effect.tapErrorTag("TimeoutError", () =>
|
|
476
|
+
runClanka({
|
|
477
|
+
directory: worktree.directory,
|
|
478
|
+
system: promptGen.systemClanka(options),
|
|
479
|
+
prompt: promptGen.promptTimeoutClanka({
|
|
480
|
+
taskId,
|
|
481
|
+
specsDirectory: options.specsDirectory,
|
|
482
|
+
}),
|
|
483
|
+
stallTimeout: options.stallTimeout,
|
|
484
|
+
}).pipe(Effect.provide(model), Effect.withSpan("Main.timeout")),
|
|
485
|
+
),
|
|
486
|
+
Effect.raceFirst(watchTaskState({ issueId: taskId })),
|
|
487
|
+
Effect.as(false),
|
|
488
|
+
Effect.catchTag("TaskStateChanged", (error) =>
|
|
489
|
+
Effect.log(
|
|
490
|
+
`Task ${error.issueId} moved to ${error.state}; cancelling run.`,
|
|
491
|
+
).pipe(Effect.as(true)),
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if (cancelled) return
|
|
496
|
+
|
|
497
|
+
yield* gitFlow.postWork({
|
|
498
|
+
worktree,
|
|
499
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
500
|
+
issueId: taskId,
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const task = yield* taskById(taskId)
|
|
504
|
+
if (task?.autoMerge) {
|
|
505
|
+
yield* gitFlow.autoMerge({
|
|
506
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
507
|
+
issueId: taskId,
|
|
508
|
+
worktree,
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
Effect.scoped,
|
|
513
|
+
Effect.provide(Prd.layer, { local: true }),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
type RunEffects = ReturnType<typeof run> | ReturnType<typeof runWithClanka>
|
|
517
|
+
type RunEffect = Effect.Effect<
|
|
518
|
+
void,
|
|
519
|
+
Effect.Error<RunEffects>,
|
|
520
|
+
Effect.Services<RunEffects>
|
|
521
|
+
>
|
|
522
|
+
|
|
242
523
|
const runProject = Effect.fnUntraced(
|
|
243
524
|
function* (options: {
|
|
244
525
|
readonly iterations: number
|
|
@@ -246,6 +527,7 @@ const runProject = Effect.fnUntraced(
|
|
|
246
527
|
readonly specsDirectory: string
|
|
247
528
|
readonly stallTimeout: Duration.Duration
|
|
248
529
|
readonly runTimeout: Duration.Duration
|
|
530
|
+
readonly clankaModel: ClankaModel | undefined
|
|
249
531
|
}) {
|
|
250
532
|
const isFinite = Number.isFinite(options.iterations)
|
|
251
533
|
const iterationsDisplay = isFinite ? options.iterations : "unlimited"
|
|
@@ -276,14 +558,26 @@ const runProject = Effect.fnUntraced(
|
|
|
276
558
|
|
|
277
559
|
yield* checkForWork.pipe(
|
|
278
560
|
Effect.andThen(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
561
|
+
identity<RunEffect>(
|
|
562
|
+
options.clankaModel
|
|
563
|
+
? runWithClanka({
|
|
564
|
+
startedDeferred,
|
|
565
|
+
targetBranch: options.project.targetBranch,
|
|
566
|
+
specsDirectory: options.specsDirectory,
|
|
567
|
+
stallTimeout: options.stallTimeout,
|
|
568
|
+
runTimeout: options.runTimeout,
|
|
569
|
+
review: options.project.reviewAgent,
|
|
570
|
+
clankaModel: options.clankaModel,
|
|
571
|
+
})
|
|
572
|
+
: run({
|
|
573
|
+
startedDeferred,
|
|
574
|
+
targetBranch: options.project.targetBranch,
|
|
575
|
+
specsDirectory: options.specsDirectory,
|
|
576
|
+
stallTimeout: options.stallTimeout,
|
|
577
|
+
runTimeout: options.runTimeout,
|
|
578
|
+
review: options.project.reviewAgent,
|
|
579
|
+
}),
|
|
580
|
+
).pipe(
|
|
287
581
|
Effect.provide(
|
|
288
582
|
options.project.gitFlow === "commit" ? GitFlowCommit : GitFlowPR,
|
|
289
583
|
{ local: true },
|
|
@@ -291,6 +585,7 @@ const runProject = Effect.fnUntraced(
|
|
|
291
585
|
withWorkerState(options.project.id),
|
|
292
586
|
),
|
|
293
587
|
),
|
|
588
|
+
(_) => _,
|
|
294
589
|
Effect.catchTags({
|
|
295
590
|
NoMoreWork(_error) {
|
|
296
591
|
if (isFinite) {
|
|
@@ -397,7 +692,7 @@ export const commandRoot = Command.make("lalph", {
|
|
|
397
692
|
stallMinutes,
|
|
398
693
|
specsDirectory,
|
|
399
694
|
}) {
|
|
400
|
-
yield* getDefaultCliAgentPreset
|
|
695
|
+
const preset = yield* getDefaultCliAgentPreset
|
|
401
696
|
|
|
402
697
|
let allProjects = yield* getAllProjects
|
|
403
698
|
if (allProjects.length === 0) {
|
|
@@ -420,12 +715,14 @@ export const commandRoot = Command.make("lalph", {
|
|
|
420
715
|
specsDirectory,
|
|
421
716
|
stallTimeout: Duration.minutes(stallMinutes),
|
|
422
717
|
runTimeout: Duration.minutes(maxIterationMinutes),
|
|
718
|
+
clankaModel: preset.clankaModel,
|
|
423
719
|
}).pipe(Effect.provideService(CurrentProjectId, project.id)),
|
|
424
720
|
{ concurrency: "unbounded", discard: true },
|
|
425
721
|
)
|
|
426
722
|
},
|
|
427
723
|
Effect.scoped,
|
|
428
724
|
Effect.provide([
|
|
725
|
+
ClankaModels.layer,
|
|
429
726
|
PromptGen.layer,
|
|
430
727
|
GithubCli.layer,
|
|
431
728
|
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
|
+
}
|