lalph 0.1.113 → 0.2.0
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 +4975 -3512
- package/package.json +1 -1
- package/src/GitFlow.ts +6 -3
- package/src/Github.ts +97 -40
- package/src/IssueSource.ts +41 -6
- package/src/IssueSources.ts +32 -34
- package/src/Kvs.ts +13 -1
- package/src/Linear.ts +138 -63
- package/src/Prd.ts +14 -6
- package/src/Projects.ts +134 -0
- package/src/Settings.ts +202 -14
- package/src/Workers.ts +24 -16
- package/src/cli.ts +9 -13
- package/src/commands/agent.ts +4 -4
- package/src/commands/edit.ts +27 -17
- package/src/commands/issue.ts +19 -4
- package/src/commands/plan.ts +6 -4
- package/src/commands/projects/add.ts +64 -0
- package/src/commands/projects/edit.ts +62 -0
- package/src/commands/projects/ls.ts +38 -0
- package/src/commands/projects/rm.ts +22 -0
- package/src/commands/projects/toggle.ts +37 -0
- package/src/commands/projects.ts +24 -0
- package/src/commands/root.ts +124 -152
- package/src/commands/{shell.ts → sh.ts} +10 -3
- package/src/domain/Project.ts +22 -0
- package/src/domain/WorkerState.ts +9 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Array, Effect, Option, pipe, String } from "effect"
|
|
2
|
+
import { Command, Prompt } from "effect/unstable/cli"
|
|
3
|
+
import { allProjects, getAllProjects, selectProject } from "../../Projects.ts"
|
|
4
|
+
import { CurrentProjectId, Settings } from "../../Settings.ts"
|
|
5
|
+
import { Project } from "../../domain/Project.ts"
|
|
6
|
+
import { IssueSource } from "../../IssueSource.ts"
|
|
7
|
+
import { CurrentIssueSource } from "../../IssueSources.ts"
|
|
8
|
+
|
|
9
|
+
export const commandProjectsEdit = Command.make("edit").pipe(
|
|
10
|
+
Command.withDescription("Modify a project"),
|
|
11
|
+
Command.withHandler(
|
|
12
|
+
Effect.fnUntraced(function* () {
|
|
13
|
+
const projects = yield* getAllProjects
|
|
14
|
+
const project = yield* selectProject
|
|
15
|
+
const concurrency = yield* Prompt.integer({
|
|
16
|
+
message: "Concurrency",
|
|
17
|
+
min: 1,
|
|
18
|
+
})
|
|
19
|
+
const targetBranch = pipe(
|
|
20
|
+
yield* Prompt.text({
|
|
21
|
+
message: "Target branch (leave empty to use HEAD)",
|
|
22
|
+
}),
|
|
23
|
+
String.trim,
|
|
24
|
+
Option.liftPredicate(String.isNonEmpty),
|
|
25
|
+
)
|
|
26
|
+
const gitFlow = yield* Prompt.select({
|
|
27
|
+
message: "Git flow",
|
|
28
|
+
choices: [
|
|
29
|
+
{ title: "Pull Request", value: "pr" },
|
|
30
|
+
{ title: "Commit", value: "commit" },
|
|
31
|
+
] as const,
|
|
32
|
+
})
|
|
33
|
+
const reviewAgent = yield* Prompt.toggle({
|
|
34
|
+
message: "Enable review agent?",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const nextProject = new Project({
|
|
38
|
+
...project,
|
|
39
|
+
concurrency,
|
|
40
|
+
targetBranch,
|
|
41
|
+
gitFlow,
|
|
42
|
+
reviewAgent,
|
|
43
|
+
})
|
|
44
|
+
yield* Settings.set(
|
|
45
|
+
allProjects,
|
|
46
|
+
Option.some(
|
|
47
|
+
Array.map(projects, (p) =>
|
|
48
|
+
p.id === nextProject.id ? nextProject : p,
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const source = yield* IssueSource
|
|
54
|
+
yield* source.reset.pipe(
|
|
55
|
+
Effect.provideService(CurrentProjectId, nextProject.id),
|
|
56
|
+
)
|
|
57
|
+
yield* source.settings(project.id)
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
Command.provide(Settings.layer),
|
|
61
|
+
Command.provide(CurrentIssueSource.layer),
|
|
62
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Option } from "effect"
|
|
2
|
+
import { Command } from "effect/unstable/cli"
|
|
3
|
+
import { IssueSource } from "../../IssueSource.ts"
|
|
4
|
+
import { CurrentIssueSource } from "../../IssueSources.ts"
|
|
5
|
+
import { getAllProjects } from "../../Projects.ts"
|
|
6
|
+
import { Settings } from "../../Settings.ts"
|
|
7
|
+
|
|
8
|
+
export const commandProjectsLs = Command.make("ls").pipe(
|
|
9
|
+
Command.withDescription("List all configured projects and their settings"),
|
|
10
|
+
Command.withHandler(
|
|
11
|
+
Effect.fnUntraced(function* () {
|
|
12
|
+
const meta = yield* CurrentIssueSource
|
|
13
|
+
const source = yield* IssueSource
|
|
14
|
+
console.log("Issue source:", meta.name)
|
|
15
|
+
console.log("")
|
|
16
|
+
|
|
17
|
+
const projects = yield* getAllProjects
|
|
18
|
+
|
|
19
|
+
for (const project of projects) {
|
|
20
|
+
console.log(`Project: ${project.id}`)
|
|
21
|
+
yield* source.info(project.id)
|
|
22
|
+
console.log(` Concurrency: ${project.concurrency}`)
|
|
23
|
+
if (Option.isSome(project.targetBranch)) {
|
|
24
|
+
console.log(` Target Branch: ${project.targetBranch.value}`)
|
|
25
|
+
}
|
|
26
|
+
console.log(
|
|
27
|
+
` Git flow: ${project.gitFlow === "pr" ? "Pull Request" : "Commit"}`,
|
|
28
|
+
)
|
|
29
|
+
console.log(
|
|
30
|
+
` Review agent: ${project.reviewAgent ? "Enabled" : "Disabled"}`,
|
|
31
|
+
)
|
|
32
|
+
console.log("")
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
Command.provide(Settings.layer),
|
|
37
|
+
Command.provide(CurrentIssueSource.layer),
|
|
38
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Array, Effect, Option } from "effect"
|
|
2
|
+
import { Command } from "effect/unstable/cli"
|
|
3
|
+
import { allProjects, getAllProjects, selectProject } from "../../Projects.ts"
|
|
4
|
+
import { Settings } from "../../Settings.ts"
|
|
5
|
+
|
|
6
|
+
export const commandProjectsRm = Command.make("rm").pipe(
|
|
7
|
+
Command.withDescription("Remove a project"),
|
|
8
|
+
Command.withHandler(
|
|
9
|
+
Effect.fnUntraced(function* () {
|
|
10
|
+
const projects = yield* getAllProjects
|
|
11
|
+
const project = yield* selectProject
|
|
12
|
+
const newProjects = projects.filter((p) => p.id !== project.id)
|
|
13
|
+
if (!Array.isArrayNonEmpty(newProjects)) {
|
|
14
|
+
return yield* Effect.log(
|
|
15
|
+
"You cannot remove the last remaining project.",
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
yield* Settings.set(allProjects, Option.some(newProjects))
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
Command.provide(Settings.layer),
|
|
22
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Array, Effect, Option } from "effect"
|
|
2
|
+
import { Command, Prompt } from "effect/unstable/cli"
|
|
3
|
+
import { allProjects, getAllProjects } from "../../Projects.ts"
|
|
4
|
+
import { Settings } from "../../Settings.ts"
|
|
5
|
+
import { Project } from "../../domain/Project.ts"
|
|
6
|
+
|
|
7
|
+
export const commandProjectsToggle = Command.make("toggle").pipe(
|
|
8
|
+
Command.withDescription("Enable or disable projects"),
|
|
9
|
+
Command.withHandler(
|
|
10
|
+
Effect.fnUntraced(function* () {
|
|
11
|
+
const projects = yield* getAllProjects
|
|
12
|
+
const enabled = yield* Prompt.multiSelect({
|
|
13
|
+
message: "Select projects to enable",
|
|
14
|
+
choices: projects.map((project) => ({
|
|
15
|
+
title: project.id,
|
|
16
|
+
value: project.id,
|
|
17
|
+
selected: project.enabled,
|
|
18
|
+
})),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
yield* Settings.set(
|
|
22
|
+
allProjects,
|
|
23
|
+
Option.some(
|
|
24
|
+
Array.map(
|
|
25
|
+
projects,
|
|
26
|
+
(p) =>
|
|
27
|
+
new Project({
|
|
28
|
+
...p,
|
|
29
|
+
enabled: enabled.includes(p.id),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
Command.provide(Settings.layer),
|
|
37
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from "effect/unstable/cli"
|
|
2
|
+
import { commandProjectsLs } from "./projects/ls.ts"
|
|
3
|
+
import { commandProjectsAdd } from "./projects/add.ts"
|
|
4
|
+
import { commandProjectsRm } from "./projects/rm.ts"
|
|
5
|
+
import { commandProjectsEdit } from "./projects/edit.ts"
|
|
6
|
+
import { commandProjectsToggle } from "./projects/toggle.ts"
|
|
7
|
+
|
|
8
|
+
const subcommands = Command.withSubcommands([
|
|
9
|
+
commandProjectsLs,
|
|
10
|
+
commandProjectsAdd,
|
|
11
|
+
commandProjectsEdit,
|
|
12
|
+
commandProjectsToggle,
|
|
13
|
+
commandProjectsRm,
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
export const commandProjects = Command.make("projects").pipe(
|
|
17
|
+
Command.withDescription("Manage projects"),
|
|
18
|
+
subcommands,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export const commandProjectsAlias = Command.make("p").pipe(
|
|
22
|
+
Command.withDescription("Alias for 'projects' command"),
|
|
23
|
+
subcommands,
|
|
24
|
+
)
|
package/src/commands/root.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Prd } from "../Prd.ts"
|
|
|
16
16
|
import { ChildProcess } from "effect/unstable/process"
|
|
17
17
|
import { Worktree } from "../Worktree.ts"
|
|
18
18
|
import { getCommandPrefix, getOrSelectCliAgent } from "./agent.ts"
|
|
19
|
-
import { Flag,
|
|
19
|
+
import { Flag, Command } from "effect/unstable/cli"
|
|
20
20
|
import { IssueSource } from "../IssueSource.ts"
|
|
21
21
|
import {
|
|
22
22
|
checkForWork,
|
|
@@ -29,7 +29,7 @@ import { agentChooser } from "../Agents/chooser.ts"
|
|
|
29
29
|
import { RunnerStalled } from "../domain/Errors.ts"
|
|
30
30
|
import { agentReviewer } from "../Agents/reviewer.ts"
|
|
31
31
|
import { agentTimeout } from "../Agents/timeout.ts"
|
|
32
|
-
import { Settings } from "../Settings.ts"
|
|
32
|
+
import { CurrentProjectId, Settings } from "../Settings.ts"
|
|
33
33
|
import { Atom, AtomRegistry, Reactivity } from "effect/unstable/reactivity"
|
|
34
34
|
import {
|
|
35
35
|
activeWorkerLoggingAtom,
|
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
import { WorkerStatus } from "../domain/WorkerState.ts"
|
|
40
40
|
import { GitFlow, GitFlowCommit, GitFlowPR } from "../GitFlow.ts"
|
|
41
41
|
import { parseBranch } from "../shared/git.ts"
|
|
42
|
+
import { getAllProjects } from "../Projects.ts"
|
|
43
|
+
import type { Project } from "../domain/Project.ts"
|
|
42
44
|
|
|
43
45
|
// Main iteration run logic
|
|
44
46
|
|
|
@@ -54,6 +56,7 @@ const run = Effect.fnUntraced(
|
|
|
54
56
|
) => ChildProcess.Command
|
|
55
57
|
readonly review: boolean
|
|
56
58
|
}) {
|
|
59
|
+
const projectId = yield* CurrentProjectId
|
|
57
60
|
const fs = yield* FileSystem.FileSystem
|
|
58
61
|
const pathService = yield* Path.Path
|
|
59
62
|
const worktree = yield* Worktree
|
|
@@ -127,7 +130,7 @@ const run = Effect.fnUntraced(
|
|
|
127
130
|
yield* prd.setChosenIssueId(taskId)
|
|
128
131
|
yield* prd.setAutoMerge(chosenTask.prd.autoMerge)
|
|
129
132
|
|
|
130
|
-
yield* source.ensureInProgress(taskId).pipe(
|
|
133
|
+
yield* source.ensureInProgress(projectId, taskId).pipe(
|
|
131
134
|
Effect.timeoutOrElse({
|
|
132
135
|
duration: "1 minute",
|
|
133
136
|
onTimeout: () => Effect.fail(new RunnerStalled()),
|
|
@@ -221,6 +224,111 @@ const run = Effect.fnUntraced(
|
|
|
221
224
|
Effect.provide(Prd.layer, { local: true }),
|
|
222
225
|
)
|
|
223
226
|
|
|
227
|
+
const runProject = Effect.fnUntraced(
|
|
228
|
+
function* (options: {
|
|
229
|
+
readonly iterations: number
|
|
230
|
+
readonly project: Project
|
|
231
|
+
readonly specsDirectory: string
|
|
232
|
+
readonly stallTimeout: Duration.Duration
|
|
233
|
+
readonly runTimeout: Duration.Duration
|
|
234
|
+
readonly commandPrefix: (
|
|
235
|
+
command: ChildProcess.Command,
|
|
236
|
+
) => ChildProcess.Command
|
|
237
|
+
}) {
|
|
238
|
+
const isFinite = Number.isFinite(options.iterations)
|
|
239
|
+
const iterationsDisplay = isFinite ? options.iterations : "unlimited"
|
|
240
|
+
const semaphore = Effect.makeSemaphoreUnsafe(options.project.concurrency)
|
|
241
|
+
const fibers = yield* FiberSet.make()
|
|
242
|
+
|
|
243
|
+
yield* resetInProgress.pipe(Effect.withSpan("Main.resetInProgress"))
|
|
244
|
+
|
|
245
|
+
yield* Effect.log(
|
|
246
|
+
`Executing ${iterationsDisplay} iteration(s) with concurrency ${options.project.concurrency}`,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
let iterations = options.iterations
|
|
250
|
+
let iteration = 0
|
|
251
|
+
let quit = false
|
|
252
|
+
|
|
253
|
+
yield* Atom.mount(activeWorkerLoggingAtom)
|
|
254
|
+
|
|
255
|
+
while (true) {
|
|
256
|
+
yield* semaphore.take(1)
|
|
257
|
+
if (quit || (isFinite && iteration >= iterations)) {
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const currentIteration = iteration
|
|
262
|
+
|
|
263
|
+
const startedDeferred = yield* Deferred.make<void>()
|
|
264
|
+
|
|
265
|
+
yield* checkForWork.pipe(
|
|
266
|
+
Effect.andThen(
|
|
267
|
+
run({
|
|
268
|
+
startedDeferred,
|
|
269
|
+
targetBranch: options.project.targetBranch,
|
|
270
|
+
specsDirectory: options.specsDirectory,
|
|
271
|
+
stallTimeout: options.stallTimeout,
|
|
272
|
+
runTimeout: options.runTimeout,
|
|
273
|
+
commandPrefix: options.commandPrefix,
|
|
274
|
+
review: options.project.reviewAgent,
|
|
275
|
+
}).pipe(
|
|
276
|
+
Effect.provide(
|
|
277
|
+
options.project.gitFlow === "commit" ? GitFlowCommit : GitFlowPR,
|
|
278
|
+
{ local: true },
|
|
279
|
+
),
|
|
280
|
+
withWorkerState(options.project.id),
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
Effect.catchFilter(
|
|
284
|
+
(e) =>
|
|
285
|
+
e._tag === "NoMoreWork" || e._tag === "QuitError"
|
|
286
|
+
? Filter.fail(e)
|
|
287
|
+
: e,
|
|
288
|
+
(e) =>
|
|
289
|
+
Effect.logWarning(Cause.fail(e)).pipe(
|
|
290
|
+
Effect.andThen(Effect.sleep(Duration.seconds(10))),
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
Effect.catchTags({
|
|
294
|
+
NoMoreWork(_) {
|
|
295
|
+
if (isFinite) {
|
|
296
|
+
// If we have a finite number of iterations, we exit when no more
|
|
297
|
+
// work is found
|
|
298
|
+
iterations = currentIteration
|
|
299
|
+
return Effect.log(
|
|
300
|
+
`No more work to process, ending after ${currentIteration} iteration(s).`,
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
const log =
|
|
304
|
+
Iterable.size(fibers) <= 1
|
|
305
|
+
? Effect.log("No more work to process, waiting 30 seconds...")
|
|
306
|
+
: Effect.void
|
|
307
|
+
return Effect.andThen(log, Effect.sleep(Duration.seconds(30)))
|
|
308
|
+
},
|
|
309
|
+
QuitError(_) {
|
|
310
|
+
quit = true
|
|
311
|
+
return Effect.void
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
Effect.ensuring(semaphore.release(1)),
|
|
315
|
+
Effect.ensuring(Deferred.completeWith(startedDeferred, Effect.void)),
|
|
316
|
+
FiberSet.run(fibers),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
yield* Deferred.await(startedDeferred)
|
|
320
|
+
|
|
321
|
+
iteration++
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
yield* FiberSet.awaitEmpty(fibers)
|
|
325
|
+
},
|
|
326
|
+
(effect, options) =>
|
|
327
|
+
Effect.annotateLogs(effect, {
|
|
328
|
+
project: options.project.id,
|
|
329
|
+
}),
|
|
330
|
+
)
|
|
331
|
+
|
|
224
332
|
// Command
|
|
225
333
|
|
|
226
334
|
const iterations = Flag.integer("iterations").pipe(
|
|
@@ -229,37 +337,6 @@ const iterations = Flag.integer("iterations").pipe(
|
|
|
229
337
|
Flag.withDefault(Number.POSITIVE_INFINITY),
|
|
230
338
|
)
|
|
231
339
|
|
|
232
|
-
const concurrency = Flag.integer("concurrency").pipe(
|
|
233
|
-
Flag.withDescription("Number of concurrent agents, defaults to 1"),
|
|
234
|
-
Flag.withAlias("c"),
|
|
235
|
-
Flag.withDefault(1),
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
const targetBranch = Flag.string("target-branch").pipe(
|
|
239
|
-
Flag.withDescription(
|
|
240
|
-
"Target branch for PRs. Defaults to current branch. Env variable: LALPH_TARGET_BRANCH",
|
|
241
|
-
),
|
|
242
|
-
Flag.withAlias("b"),
|
|
243
|
-
Flag.withFallbackConfig(Config.string("LALPH_TARGET_BRANCH")),
|
|
244
|
-
Flag.withDefault(
|
|
245
|
-
ChildProcess.make`git branch --show-current`.pipe(
|
|
246
|
-
ChildProcess.string,
|
|
247
|
-
Effect.orDie,
|
|
248
|
-
Effect.flatMap((output) => {
|
|
249
|
-
const branch = output.trim()
|
|
250
|
-
return branch === ""
|
|
251
|
-
? Effect.fail(
|
|
252
|
-
new CliError.MissingOption({
|
|
253
|
-
option: "--target-branch",
|
|
254
|
-
}),
|
|
255
|
-
)
|
|
256
|
-
: Effect.succeed(branch)
|
|
257
|
-
}),
|
|
258
|
-
),
|
|
259
|
-
),
|
|
260
|
-
Flag.optional,
|
|
261
|
-
)
|
|
262
|
-
|
|
263
340
|
const maxIterationMinutes = Flag.integer("max-minutes").pipe(
|
|
264
341
|
Flag.withDescription(
|
|
265
342
|
"Maximum number of minutes to allow an iteration to run. Defaults to 90 minutes. Env variable: LALPH_MAX_MINUTES",
|
|
@@ -285,36 +362,15 @@ const specsDirectory = Flag.directory("specs").pipe(
|
|
|
285
362
|
Flag.withDefault(".specs"),
|
|
286
363
|
)
|
|
287
364
|
|
|
288
|
-
const commitMode = Flag.boolean("commit").pipe(
|
|
289
|
-
Flag.withDescription("Commit to the target branch instead of creating PRs"),
|
|
290
|
-
)
|
|
291
|
-
|
|
292
365
|
const verbose = Flag.boolean("verbose").pipe(
|
|
293
366
|
Flag.withDescription("Enable verbose logging"),
|
|
294
367
|
Flag.withAlias("v"),
|
|
295
368
|
)
|
|
296
369
|
|
|
297
|
-
const review = Flag.boolean("review").pipe(
|
|
298
|
-
Flag.withDescription(
|
|
299
|
-
"Enable the AI peer-review step. Will use LALPH_REVIEW.md if present.",
|
|
300
|
-
),
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
// handled in cli.ts
|
|
304
|
-
const reset = Flag.boolean("reset").pipe(
|
|
305
|
-
Flag.withDescription("Reset the current issue source before running"),
|
|
306
|
-
Flag.withAlias("r"),
|
|
307
|
-
)
|
|
308
|
-
|
|
309
370
|
export const commandRoot = Command.make("lalph", {
|
|
310
371
|
iterations,
|
|
311
|
-
concurrency,
|
|
312
|
-
targetBranch,
|
|
313
372
|
maxIterationMinutes,
|
|
314
373
|
stallMinutes,
|
|
315
|
-
commitMode,
|
|
316
|
-
reset,
|
|
317
|
-
review,
|
|
318
374
|
specsDirectory,
|
|
319
375
|
verbose,
|
|
320
376
|
}).pipe(
|
|
@@ -322,110 +378,26 @@ export const commandRoot = Command.make("lalph", {
|
|
|
322
378
|
Effect.fnUntraced(
|
|
323
379
|
function* ({
|
|
324
380
|
iterations,
|
|
325
|
-
concurrency,
|
|
326
|
-
targetBranch,
|
|
327
381
|
maxIterationMinutes,
|
|
328
382
|
stallMinutes,
|
|
329
383
|
specsDirectory,
|
|
330
|
-
review,
|
|
331
|
-
commitMode,
|
|
332
384
|
}) {
|
|
333
385
|
const commandPrefix = yield* getCommandPrefix
|
|
334
386
|
yield* getOrSelectCliAgent
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
387
|
+
const projects = (yield* getAllProjects).filter((p) => p.enabled)
|
|
388
|
+
yield* Effect.forEach(
|
|
389
|
+
projects,
|
|
390
|
+
(project) =>
|
|
391
|
+
runProject({
|
|
392
|
+
iterations,
|
|
393
|
+
project,
|
|
394
|
+
specsDirectory,
|
|
395
|
+
stallTimeout: Duration.minutes(stallMinutes),
|
|
396
|
+
runTimeout: Duration.minutes(maxIterationMinutes),
|
|
397
|
+
commandPrefix,
|
|
398
|
+
}).pipe(Effect.provideService(CurrentProjectId, project.id)),
|
|
399
|
+
{ concurrency: "unbounded", discard: true },
|
|
346
400
|
)
|
|
347
|
-
if (Option.isSome(targetBranch)) {
|
|
348
|
-
yield* Effect.log(`Using target branch: ${targetBranch.value}`)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
let iteration = 0
|
|
352
|
-
let quit = false
|
|
353
|
-
|
|
354
|
-
yield* Atom.mount(activeWorkerLoggingAtom)
|
|
355
|
-
|
|
356
|
-
while (true) {
|
|
357
|
-
yield* semaphore.take(1)
|
|
358
|
-
if (quit || (isFinite && iteration >= iterations)) {
|
|
359
|
-
break
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const currentIteration = iteration
|
|
363
|
-
|
|
364
|
-
const startedDeferred = yield* Deferred.make<void>()
|
|
365
|
-
|
|
366
|
-
yield* checkForWork.pipe(
|
|
367
|
-
Effect.andThen(
|
|
368
|
-
run({
|
|
369
|
-
startedDeferred,
|
|
370
|
-
targetBranch,
|
|
371
|
-
specsDirectory,
|
|
372
|
-
stallTimeout: Duration.minutes(stallMinutes),
|
|
373
|
-
runTimeout: Duration.minutes(maxIterationMinutes),
|
|
374
|
-
commandPrefix,
|
|
375
|
-
review,
|
|
376
|
-
}).pipe(
|
|
377
|
-
Effect.provide(commitMode ? GitFlowCommit : GitFlowPR, {
|
|
378
|
-
local: true,
|
|
379
|
-
}),
|
|
380
|
-
withWorkerState(currentIteration),
|
|
381
|
-
),
|
|
382
|
-
),
|
|
383
|
-
Effect.catchFilter(
|
|
384
|
-
(e) =>
|
|
385
|
-
e._tag === "NoMoreWork" || e._tag === "QuitError"
|
|
386
|
-
? Filter.fail(e)
|
|
387
|
-
: e,
|
|
388
|
-
(e) => Effect.logWarning(Cause.fail(e)),
|
|
389
|
-
),
|
|
390
|
-
Effect.catchTags({
|
|
391
|
-
NoMoreWork(_) {
|
|
392
|
-
if (isFinite) {
|
|
393
|
-
// If we have a finite number of iterations, we exit when no more
|
|
394
|
-
// work is found
|
|
395
|
-
iterations = currentIteration
|
|
396
|
-
return Effect.log(
|
|
397
|
-
`No more work to process, ending after ${currentIteration} iteration(s).`,
|
|
398
|
-
)
|
|
399
|
-
}
|
|
400
|
-
const log =
|
|
401
|
-
Iterable.size(fibers) <= 1
|
|
402
|
-
? Effect.log(
|
|
403
|
-
"No more work to process, waiting 30 seconds...",
|
|
404
|
-
)
|
|
405
|
-
: Effect.void
|
|
406
|
-
return Effect.andThen(log, Effect.sleep(Duration.seconds(30)))
|
|
407
|
-
},
|
|
408
|
-
QuitError(_) {
|
|
409
|
-
quit = true
|
|
410
|
-
return Effect.void
|
|
411
|
-
},
|
|
412
|
-
}),
|
|
413
|
-
Effect.annotateLogs({
|
|
414
|
-
iteration: currentIteration,
|
|
415
|
-
}),
|
|
416
|
-
Effect.ensuring(semaphore.release(1)),
|
|
417
|
-
Effect.ensuring(
|
|
418
|
-
Deferred.completeWith(startedDeferred, Effect.void),
|
|
419
|
-
),
|
|
420
|
-
FiberSet.run(fibers),
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
yield* Deferred.await(startedDeferred)
|
|
424
|
-
|
|
425
|
-
iteration++
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
yield* FiberSet.awaitEmpty(fibers)
|
|
429
401
|
},
|
|
430
402
|
Effect.scoped,
|
|
431
403
|
Effect.provide([
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Command } from "effect/unstable/cli"
|
|
2
|
-
import { Effect, FileSystem, Path } from "effect"
|
|
2
|
+
import { Effect, FileSystem, Layer, Path } from "effect"
|
|
3
3
|
import { ChildProcess } from "effect/unstable/process"
|
|
4
4
|
import { Prd } from "../Prd.ts"
|
|
5
5
|
import { Worktree } from "../Worktree.ts"
|
|
6
|
+
import { layerProjectIdPrompt } from "../Projects.ts"
|
|
6
7
|
|
|
7
|
-
export const
|
|
8
|
+
export const commandSh = Command.make("sh").pipe(
|
|
8
9
|
Command.withDescription("Enter an interactive shell in the worktree"),
|
|
9
10
|
Command.withHandler(
|
|
10
11
|
Effect.fnUntraced(
|
|
@@ -18,6 +19,10 @@ export const commandShell = Command.make("shell").pipe(
|
|
|
18
19
|
pathService.resolve(pathService.join(".lalph", "config")),
|
|
19
20
|
pathService.join(worktree.directory, ".lalph", "config"),
|
|
20
21
|
)
|
|
22
|
+
yield* fs.symlink(
|
|
23
|
+
pathService.resolve(pathService.join(".lalph", "projects")),
|
|
24
|
+
pathService.join(worktree.directory, ".lalph", "projects"),
|
|
25
|
+
)
|
|
21
26
|
|
|
22
27
|
yield* ChildProcess.make(process.env.SHELL || "/bin/bash", [], {
|
|
23
28
|
cwd: worktree.directory,
|
|
@@ -27,7 +32,9 @@ export const commandShell = Command.make("shell").pipe(
|
|
|
27
32
|
}).pipe(ChildProcess.exitCode)
|
|
28
33
|
},
|
|
29
34
|
Effect.scoped,
|
|
30
|
-
Effect.provide(
|
|
35
|
+
Effect.provide(
|
|
36
|
+
Prd.layerProvided.pipe(Layer.provideMerge(layerProjectIdPrompt)),
|
|
37
|
+
),
|
|
31
38
|
),
|
|
32
39
|
),
|
|
33
40
|
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Option, Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
export const ProjectId = Schema.String.pipe(Schema.brand("lalph/ProjectId"))
|
|
4
|
+
export type ProjectId = typeof ProjectId.Type
|
|
5
|
+
|
|
6
|
+
export class Project extends Schema.Class<Project>("lalph/Project")({
|
|
7
|
+
id: ProjectId,
|
|
8
|
+
enabled: Schema.Boolean,
|
|
9
|
+
targetBranch: Schema.Option(Schema.String),
|
|
10
|
+
concurrency: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
11
|
+
gitFlow: Schema.Literals(["pr", "commit"]),
|
|
12
|
+
reviewAgent: Schema.Boolean,
|
|
13
|
+
}) {
|
|
14
|
+
static defaultProject = new Project({
|
|
15
|
+
id: ProjectId.makeUnsafe("default"),
|
|
16
|
+
enabled: true,
|
|
17
|
+
targetBranch: Option.none(),
|
|
18
|
+
concurrency: 1,
|
|
19
|
+
gitFlow: "pr",
|
|
20
|
+
reviewAgent: true,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { Data, DateTime, Exit } from "effect"
|
|
2
|
+
import type { ProjectId } from "./Project.ts"
|
|
2
3
|
|
|
3
4
|
export class WorkerState extends Data.Class<{
|
|
4
|
-
|
|
5
|
+
id: number
|
|
6
|
+
projectId: ProjectId
|
|
5
7
|
status: WorkerStatus
|
|
6
8
|
lastTransitionAt: DateTime.Utc
|
|
7
9
|
}> {
|
|
8
|
-
static initial(
|
|
10
|
+
static initial(options: {
|
|
11
|
+
readonly projectId: ProjectId
|
|
12
|
+
readonly id: number
|
|
13
|
+
}) {
|
|
9
14
|
return new WorkerState({
|
|
10
|
-
|
|
15
|
+
...options,
|
|
11
16
|
status: WorkerStatus.Booting(),
|
|
12
17
|
lastTransitionAt: DateTime.nowUnsafe(),
|
|
13
18
|
})
|
|
@@ -15,7 +20,7 @@ export class WorkerState extends Data.Class<{
|
|
|
15
20
|
|
|
16
21
|
transitionTo(status: WorkerStatus): WorkerState {
|
|
17
22
|
return new WorkerState({
|
|
18
|
-
|
|
23
|
+
...this,
|
|
19
24
|
status,
|
|
20
25
|
lastTransitionAt: DateTime.nowUnsafe(),
|
|
21
26
|
})
|