lalph 0.3.85 → 0.3.87

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.
@@ -14,7 +14,7 @@ import { Worktree } from "../Worktree.ts"
14
14
  import { Command, Flag } from "effect/unstable/cli"
15
15
  import { CurrentIssueSource } from "../CurrentIssueSource.ts"
16
16
  import { commandRoot } from "./root.ts"
17
- import { CurrentProjectId, Settings } from "../Settings.ts"
17
+ import { allProjects, CurrentProjectId, Settings } from "../Settings.ts"
18
18
  import { addOrUpdateProject, selectProject } from "../Projects.ts"
19
19
  import { agentPlanner } from "../Agents/planner.ts"
20
20
  import { agentTasker } from "../Agents/tasker.ts"
@@ -82,7 +82,7 @@ export const commandPlan = Command.make("plan", {
82
82
  // possible
83
83
  yield* Effect.gen(function* () {
84
84
  const project = withNewProject
85
- ? yield* addOrUpdateProject()
85
+ ? yield* addOrUpdateProject(undefined, true)
86
86
  : yield* selectProject
87
87
  const { specsDirectory } = yield* commandRoot
88
88
  const preset = yield* selectCliAgentPreset
@@ -93,6 +93,7 @@ export const commandPlan = Command.make("plan", {
93
93
  targetBranch: project.targetBranch,
94
94
  dangerous,
95
95
  preset,
96
+ ralph: project.gitFlow === "ralph",
96
97
  }).pipe(Effect.provideService(CurrentProjectId, project.id))
97
98
  }).pipe(
98
99
  Effect.provide([
@@ -116,16 +117,19 @@ const plan = Effect.fnUntraced(
116
117
  readonly targetBranch: Option.Option<string>
117
118
  readonly dangerous: boolean
118
119
  readonly preset: CliAgentPreset
120
+ readonly ralph: boolean
119
121
  }) {
120
122
  const fs = yield* FileSystem.FileSystem
121
123
  const pathService = yield* Path.Path
122
124
  const worktree = yield* Worktree
125
+ const projectId = yield* CurrentProjectId
123
126
 
124
127
  yield* agentPlanner({
125
128
  plan: options.plan,
126
129
  specsDirectory: options.specsDirectory,
127
130
  dangerous: options.dangerous,
128
131
  preset: options.preset,
132
+ ralph: options.ralph,
129
133
  })
130
134
 
131
135
  const planDetails = yield* pipe(
@@ -136,6 +140,19 @@ const plan = Effect.fnUntraced(
136
140
  Effect.mapError(() => new SpecNotFound()),
137
141
  )
138
142
 
143
+ if (options.ralph) {
144
+ yield* Settings.update(
145
+ allProjects,
146
+ Option.map((projects) =>
147
+ projects.map((p) =>
148
+ p.id === projectId
149
+ ? p.update({ ralphSpec: planDetails.specification })
150
+ : p,
151
+ ),
152
+ ),
153
+ )
154
+ }
155
+
139
156
  if (Option.isSome(options.targetBranch)) {
140
157
  yield* commitAndPushSpecification({
141
158
  specsDirectory: options.specsDirectory,
@@ -143,13 +160,15 @@ const plan = Effect.fnUntraced(
143
160
  })
144
161
  }
145
162
 
146
- yield* Effect.log("Converting specification into tasks")
163
+ if (!options.ralph) {
164
+ yield* Effect.log("Converting specification into tasks")
147
165
 
148
- yield* agentTasker({
149
- specificationPath: planDetails.specification,
150
- specsDirectory: options.specsDirectory,
151
- preset: options.preset,
152
- })
166
+ yield* agentTasker({
167
+ specificationPath: planDetails.specification,
168
+ specsDirectory: options.specsDirectory,
169
+ preset: options.preset,
170
+ })
171
+ }
153
172
 
154
173
  if (!worktree.inExisting) {
155
174
  yield* pipe(
@@ -163,13 +182,14 @@ const plan = Effect.fnUntraced(
163
182
  }
164
183
  },
165
184
  Effect.scoped,
166
- Effect.provide([
167
- PromptGen.layer,
168
- Prd.layerProvided,
169
- Worktree.layer,
170
- Settings.layer,
171
- CurrentIssueSource.layer,
172
- ]),
185
+ (effect, options) =>
186
+ Effect.provide(effect, [
187
+ PromptGen.layer,
188
+ options.ralph ? Prd.layerNoop : Prd.layerProvided,
189
+ Worktree.layer,
190
+ Settings.layer,
191
+ CurrentIssueSource.layer,
192
+ ]),
173
193
  )
174
194
 
175
195
  export class SpecNotFound extends Data.TaggedError("SpecNotFound") {
@@ -1,7 +1,7 @@
1
1
  import { Effect, FileSystem, Option, Path } from "effect"
2
2
  import { Command } from "effect/unstable/cli"
3
- import { allProjects, getAllProjects, selectProject } from "../../Projects.ts"
4
- import { Settings } from "../../Settings.ts"
3
+ import { getAllProjects, selectProject } from "../../Projects.ts"
4
+ import { allProjects, Settings } from "../../Settings.ts"
5
5
  import { CurrentIssueSource } from "../../CurrentIssueSource.ts"
6
6
 
7
7
  export const commandProjectsRm = Command.make("rm").pipe(
@@ -1,7 +1,7 @@
1
1
  import { Array, Effect, Option } from "effect"
2
2
  import { Command, Prompt } from "effect/unstable/cli"
3
- import { allProjects, getAllProjects } from "../../Projects.ts"
4
- import { Settings } from "../../Settings.ts"
3
+ import { getAllProjects } from "../../Projects.ts"
4
+ import { allProjects, Settings } from "../../Settings.ts"
5
5
  import { Project } from "../../domain/Project.ts"
6
6
 
7
7
  export const commandProjectsToggle = Command.make("toggle").pipe(
@@ -3,6 +3,7 @@ import {
3
3
  Deferred,
4
4
  Duration,
5
5
  Effect,
6
+ Fiber,
6
7
  FiberSet,
7
8
  FileSystem,
8
9
  Iterable,
@@ -17,6 +18,7 @@ import {
17
18
  Scope,
18
19
  Semaphore,
19
20
  Stream,
21
+ Unify,
20
22
  } from "effect"
21
23
  import { PromptGen } from "../PromptGen.ts"
22
24
  import { Prd } from "../Prd.ts"
@@ -35,7 +37,7 @@ import { agentChooser, ChosenTaskNotFound } from "../Agents/chooser.ts"
35
37
  import { RunnerStalled, TaskStateChanged } from "../domain/Errors.ts"
36
38
  import { agentReviewer } from "../Agents/reviewer.ts"
37
39
  import { agentTimeout } from "../Agents/timeout.ts"
38
- import { CurrentProjectId, Settings } from "../Settings.ts"
40
+ import { allProjects, CurrentProjectId, Settings } from "../Settings.ts"
39
41
  import { Atom, AtomRegistry, Reactivity } from "effect/unstable/reactivity"
40
42
  import {
41
43
  activeWorkerLoggingAtom,
@@ -43,7 +45,13 @@ import {
43
45
  withWorkerState,
44
46
  } from "../Workers.ts"
45
47
  import { WorkerStatus } from "../domain/WorkerState.ts"
46
- import { GitFlow, GitFlowCommit, GitFlowError, GitFlowPR } from "../GitFlow.ts"
48
+ import {
49
+ GitFlow,
50
+ GitFlowCommit,
51
+ GitFlowError,
52
+ GitFlowPR,
53
+ GitFlowRalph,
54
+ } from "../GitFlow.ts"
47
55
  import { getAllProjects, welcomeWizard } from "../Projects.ts"
48
56
  import type { Project } from "../domain/Project.ts"
49
57
  import { getDefaultCliAgentPreset } from "../Presets.ts"
@@ -57,6 +65,7 @@ import { CurrentTaskRef } from "../TaskTools.ts"
57
65
  import type { OutputFormatter } from "clanka"
58
66
  import { ClankaMuxerLayer, SemanticSearchLayer } from "../Clanka.ts"
59
67
  import { agentResearcher } from "../Agents/researcher.ts"
68
+ import { agentChooserRalph } from "../Agents/chooserRalph.ts"
60
69
 
61
70
  // Main iteration run logic
62
71
 
@@ -254,10 +263,12 @@ const run = Effect.fnUntraced(
254
263
 
255
264
  const exitCode = yield* agentWorker({
256
265
  stallTimeout: options.stallTimeout,
266
+ system: promptGen.systemClanka(options),
257
267
  preset: taskPreset,
258
268
  prompt: instructions,
259
269
  research: researchResult,
260
270
  steer,
271
+ ralph: false,
261
272
  }).pipe(
262
273
  Effect.provideService(CurrentTaskRef, issueRef),
263
274
  catchStallInReview,
@@ -284,6 +295,7 @@ const run = Effect.fnUntraced(
284
295
  stallTimeout: options.stallTimeout,
285
296
  preset: taskPreset,
286
297
  instructions,
298
+ ralph: false,
287
299
  }).pipe(catchStallInReview, Effect.withSpan("Main.agentReviewer"))
288
300
 
289
301
  yield* source.updateIssue({
@@ -299,7 +311,7 @@ const run = Effect.fnUntraced(
299
311
  specsDirectory: options.specsDirectory,
300
312
  stallTimeout: options.stallTimeout,
301
313
  preset: taskPreset,
302
- task: chosenTask.prd,
314
+ task: { _tag: "task", task: chosenTask.prd },
303
315
  }),
304
316
  ),
305
317
  Effect.raceFirst(watchTaskState({ issueId: taskId })),
@@ -336,6 +348,173 @@ const run = Effect.fnUntraced(
336
348
  }),
337
349
  )
338
350
 
351
+ const runRalph = Effect.fnUntraced(
352
+ function* (options: {
353
+ readonly targetBranch: Option.Option<string>
354
+ readonly stallTimeout: Duration.Duration
355
+ readonly runTimeout: Duration.Duration
356
+ readonly research: boolean
357
+ readonly review: boolean
358
+ readonly specFile: string
359
+ }): Effect.fn.Return<
360
+ void,
361
+ | PlatformError.PlatformError
362
+ | Schema.SchemaError
363
+ | IssueSourceError
364
+ | QuitError
365
+ | GitFlowError
366
+ | ChosenTaskNotFound
367
+ | RunnerStalled
368
+ | TimeoutError
369
+ | AiError,
370
+ | CurrentProjectId
371
+ | ChildProcessSpawner.ChildProcessSpawner
372
+ | Settings
373
+ | Reactivity.Reactivity
374
+ | GithubCli
375
+ | IssueSource
376
+ | Prompt.Environment
377
+ | AtomRegistry.AtomRegistry
378
+ | GitFlow
379
+ | CurrentWorkerState
380
+ | PromptGen
381
+ | Prd
382
+ | Worktree
383
+ | ClankaModels
384
+ | OutputFormatter.Muxer
385
+ | Scope.Scope
386
+ > {
387
+ const worktree = yield* Worktree
388
+ const gitFlow = yield* GitFlow
389
+ const currentWorker = yield* CurrentWorkerState
390
+ const registry = yield* AtomRegistry.AtomRegistry
391
+ const projectId = yield* CurrentProjectId
392
+
393
+ const preset = yield* getDefaultCliAgentPreset
394
+
395
+ // ensure cleanup of branch after run
396
+ yield* Effect.addFinalizer(
397
+ Effect.fnUntraced(function* () {
398
+ const currentBranchName = yield* worktree
399
+ .currentBranch(worktree.directory)
400
+ .pipe(Effect.option, Effect.map(Option.getOrUndefined))
401
+ if (!currentBranchName) return
402
+
403
+ // enter detached state
404
+ yield* worktree.exec`git checkout --detach ${currentBranchName}`
405
+ // delete the branch
406
+ yield* worktree.exec`git branch -D ${currentBranchName}`
407
+ }, Effect.ignore()),
408
+ )
409
+
410
+ // 1. Choose task
411
+ // --------------
412
+
413
+ registry.update(currentWorker.state, (s) =>
414
+ s.transitionTo(WorkerStatus.ChoosingTask()),
415
+ )
416
+
417
+ const chosenTask = yield* agentChooserRalph({
418
+ stallTimeout: options.stallTimeout,
419
+ preset,
420
+ specFile: options.specFile,
421
+ }).pipe(
422
+ Effect.tapErrorTag(
423
+ "ChosenTaskNotFound",
424
+ Effect.fnUntraced(function* () {
425
+ // Disable project when all tasks are done
426
+ yield* Settings.update(
427
+ allProjects,
428
+ Option.map((projects) =>
429
+ projects.map((p) =>
430
+ p.id === projectId ? p.update({ enabled: false }) : p,
431
+ ),
432
+ ),
433
+ )
434
+ }),
435
+ ),
436
+ Effect.withSpan("Main.chooser"),
437
+ )
438
+
439
+ yield* Effect.gen(function* () {
440
+ //
441
+ // 2. Work on task
442
+ // -----------------------
443
+
444
+ registry.update(currentWorker.state, (s) =>
445
+ s.transitionTo(WorkerStatus.Working({ issueId: "ralph" })),
446
+ )
447
+
448
+ let researchResult = Option.none<string>()
449
+ // if (options.research) {
450
+ // researchResult = yield* agentResearcher({
451
+ // task: chosenTask.prd,
452
+ // specsDirectory: options.specsDirectory,
453
+ // stallTimeout: options.stallTimeout,
454
+ // preset: taskPreset,
455
+ // })
456
+ // }
457
+
458
+ const promptGen = yield* PromptGen
459
+ const instructions = promptGen.promptRalph({
460
+ task: chosenTask,
461
+ specFile: options.specFile,
462
+ targetBranch: Option.getOrUndefined(options.targetBranch),
463
+ gitFlow,
464
+ })
465
+
466
+ const exitCode = yield* agentWorker({
467
+ stallTimeout: options.stallTimeout,
468
+ preset,
469
+ prompt: instructions,
470
+ research: researchResult,
471
+ ralph: true,
472
+ }).pipe(Effect.withSpan("Main.worker"))
473
+ yield* Effect.log(`Agent exited with code: ${exitCode}`)
474
+
475
+ // 3. Review task
476
+ // -----------------------
477
+
478
+ if (options.review) {
479
+ registry.update(currentWorker.state, (s) =>
480
+ s.transitionTo(WorkerStatus.Reviewing({ issueId: "ralph" })),
481
+ )
482
+
483
+ yield* agentReviewer({
484
+ specsDirectory: "",
485
+ stallTimeout: options.stallTimeout,
486
+ preset,
487
+ instructions,
488
+ ralph: true,
489
+ }).pipe(Effect.withSpan("Main.review"))
490
+ }
491
+ }).pipe(
492
+ Effect.timeout(options.runTimeout),
493
+ Effect.tapErrorTag("TimeoutError", () =>
494
+ agentTimeout({
495
+ specsDirectory: "",
496
+ stallTimeout: options.stallTimeout,
497
+ preset,
498
+ task: { _tag: "ralph", task: chosenTask, specFile: options.specFile },
499
+ }),
500
+ ),
501
+ )
502
+
503
+ yield* gitFlow.postWork({
504
+ worktree,
505
+ targetBranch: Option.getOrUndefined(options.targetBranch),
506
+ issueId: "",
507
+ })
508
+ },
509
+ Effect.scoped,
510
+ Effect.provide(
511
+ SemanticSearchLayer.pipe(
512
+ Layer.provideMerge([Prd.layerNoop, Worktree.layer]),
513
+ ),
514
+ { local: true },
515
+ ),
516
+ )
517
+
339
518
  const runProject = Effect.fnUntraced(
340
519
  function* (options: {
341
520
  readonly iterations: number
@@ -370,26 +549,52 @@ const runProject = Effect.fnUntraced(
370
549
  const currentIteration = iteration
371
550
 
372
551
  const startedDeferred = yield* Deferred.make<void>()
373
-
374
- yield* checkForWork.pipe(
552
+ let ralphDone = false
553
+
554
+ const gitFlow = options.project.gitFlow
555
+ const isRalph = gitFlow === "ralph"
556
+ const gitFlowLayer =
557
+ gitFlow === "commit"
558
+ ? GitFlowCommit
559
+ : gitFlow === "ralph"
560
+ ? GitFlowRalph
561
+ : GitFlowPR
562
+ const fiber = yield* checkForWork(options.project).pipe(
375
563
  Effect.andThen(
376
- run({
377
- startedDeferred,
378
- targetBranch: options.project.targetBranch,
379
- specsDirectory: options.specsDirectory,
380
- stallTimeout: options.stallTimeout,
381
- runTimeout: options.runTimeout,
382
- review: options.project.reviewAgent,
383
- research: options.project.researchAgent,
384
- }).pipe(
385
- Effect.provide(
386
- options.project.gitFlow === "commit" ? GitFlowCommit : GitFlowPR,
387
- { local: true },
388
- ),
564
+ Unify.unify(
565
+ isRalph
566
+ ? runRalph({
567
+ targetBranch: options.project.targetBranch,
568
+ stallTimeout: options.stallTimeout,
569
+ runTimeout: options.runTimeout,
570
+ review: options.project.reviewAgent,
571
+ research: options.project.researchAgent,
572
+ specFile: options.project.ralphSpec!,
573
+ })
574
+ : run({
575
+ startedDeferred,
576
+ targetBranch: options.project.targetBranch,
577
+ specsDirectory: options.specsDirectory,
578
+ stallTimeout: options.stallTimeout,
579
+ runTimeout: options.runTimeout,
580
+ review: options.project.reviewAgent,
581
+ research: options.project.researchAgent,
582
+ }),
583
+ ).pipe(
584
+ Effect.provide(gitFlowLayer, { local: true }),
389
585
  withWorkerState(options.project.id),
390
586
  ),
391
587
  ),
392
588
  Effect.catchTags({
589
+ ChosenTaskNotFound(_error) {
590
+ if (isRalph) {
591
+ ralphDone = true
592
+ return Effect.log(
593
+ `No more work to process for Ralph, ending after ${currentIteration + 1} iteration(s).`,
594
+ )
595
+ }
596
+ return Effect.void
597
+ },
393
598
  NoMoreWork(_error) {
394
599
  if (isFinite) {
395
600
  // If we have a finite number of iterations, we exit when no more
@@ -419,9 +624,12 @@ const runProject = Effect.fnUntraced(
419
624
  Effect.ensuring(Deferred.completeWith(startedDeferred, Effect.void)),
420
625
  FiberSet.run(fibers),
421
626
  )
422
-
423
- yield* Deferred.await(startedDeferred)
424
-
627
+ if (isRalph) {
628
+ yield* Fiber.await(fiber)
629
+ if (ralphDone) break
630
+ } else {
631
+ yield* Deferred.await(startedDeferred)
632
+ }
425
633
  iteration++
426
634
  }
427
635
 
@@ -14,12 +14,12 @@ export class CliAgent<const Id extends string> extends Data.Class<{
14
14
  outputTransformer?: OutputTransformer | undefined
15
15
  command?: (options: {
16
16
  readonly prompt: string
17
- readonly prdFilePath: string
17
+ readonly prdFilePath: string | undefined
18
18
  readonly extraArgs: ReadonlyArray<string>
19
19
  }) => ChildProcess.Command
20
20
  commandPlan: (options: {
21
21
  readonly prompt: string
22
- readonly prdFilePath: string
22
+ readonly prdFilePath: string | undefined
23
23
  readonly dangerous: boolean
24
24
  }) => ChildProcess.Command
25
25
  }> {}
@@ -36,9 +36,11 @@ const clanka = new CliAgent({
36
36
  "opencode",
37
37
  [
38
38
  "--prompt",
39
- `@${prdFilePath}
39
+ prdFilePath
40
+ ? `@${prdFilePath}
40
41
 
41
- ${prompt}`,
42
+ ${prompt}`
43
+ : prompt,
42
44
  ],
43
45
  {
44
46
  extendEnv: true,
@@ -62,7 +64,13 @@ const opencode = new CliAgent({
62
64
  command: ({ prompt, prdFilePath, extraArgs }) =>
63
65
  ChildProcess.make(
64
66
  "opencode",
65
- ["run", prompt, "--thinking", ...extraArgs, "-f", prdFilePath],
67
+ [
68
+ "run",
69
+ prompt,
70
+ "--thinking",
71
+ ...extraArgs,
72
+ ...(prdFilePath ? ["-f", prdFilePath] : []),
73
+ ],
66
74
  {
67
75
  extendEnv: true,
68
76
  env: {
@@ -78,9 +86,11 @@ const opencode = new CliAgent({
78
86
  "opencode",
79
87
  [
80
88
  "--prompt",
81
- `@${prdFilePath}
89
+ prdFilePath
90
+ ? `@${prdFilePath}
82
91
 
83
- ${prompt}`,
92
+ ${prompt}`
93
+ : prompt,
84
94
  ],
85
95
  {
86
96
  extendEnv: true,
@@ -113,9 +123,11 @@ const claude = new CliAgent({
113
123
  "AskUserQuestion",
114
124
  ...extraArgs,
115
125
  "--",
116
- `@${prdFilePath}
126
+ prdFilePath
127
+ ? `@${prdFilePath}
117
128
 
118
- ${prompt}`,
129
+ ${prompt}`
130
+ : prompt,
119
131
  ],
120
132
  {
121
133
  stdout: "pipe",
@@ -151,9 +163,11 @@ const codex = new CliAgent({
151
163
  "exec",
152
164
  "--dangerously-bypass-approvals-and-sandbox",
153
165
  ...extraArgs,
154
- `@${prdFilePath}
166
+ prdFilePath
167
+ ? `@${prdFilePath}
155
168
 
156
- ${prompt}`,
169
+ ${prompt}`
170
+ : prompt,
157
171
  ],
158
172
  {
159
173
  stdout: "pipe",
@@ -166,9 +180,11 @@ ${prompt}`,
166
180
  "codex",
167
181
  [
168
182
  ...(dangerous ? ["--dangerously-bypass-approvals-and-sandbox"] : []),
169
- `@${prdFilePath}
183
+ prdFilePath
184
+ ? `@${prdFilePath}
170
185
 
171
- ${prompt}`,
186
+ ${prompt}`
187
+ : prompt,
172
188
  ],
173
189
  {
174
190
  stdout: "inherit",
@@ -188,9 +204,11 @@ const amp = new CliAgent({
188
204
  "--dangerously-allow-all",
189
205
  "--stream-json-thinking",
190
206
  ...extraArgs,
191
- `@${prdFilePath}
207
+ prdFilePath
208
+ ? `@${prdFilePath}
192
209
 
193
- ${prompt}`,
210
+ ${prompt}`
211
+ : prompt,
194
212
  ],
195
213
  {
196
214
  stdout: "pipe",
@@ -8,7 +8,15 @@ export class Project extends Schema.Class<Project>("lalph/Project")({
8
8
  enabled: Schema.Boolean,
9
9
  targetBranch: Schema.Option(Schema.String),
10
10
  concurrency: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
11
- gitFlow: Schema.Literals(["pr", "commit"]),
11
+ gitFlow: Schema.Literals(["pr", "commit", "ralph"]),
12
+ ralphSpec: Schema.optional(Schema.String),
12
13
  researchAgent: Schema.Boolean.pipe(Schema.withDecodingDefault(() => false)),
13
14
  reviewAgent: Schema.Boolean,
14
- }) {}
15
+ }) {
16
+ update(updates: Partial<Project>): Project {
17
+ return new Project({
18
+ ...this,
19
+ ...updates,
20
+ })
21
+ }
22
+ }