lalph 0.3.97 → 0.3.99

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.3.97",
4
+ "version": "0.3.99",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -44,8 +44,8 @@
44
44
  "@linear/sdk": "^78.0.0",
45
45
  "@octokit/plugin-rest-endpoint-methods": "^17.0.0",
46
46
  "@octokit/types": "^16.0.0",
47
- "@typescript/native-preview": "7.0.0-dev.20260321.1",
48
- "clanka": "^0.2.26",
47
+ "@typescript/native-preview": "7.0.0-dev.20260322.1",
48
+ "clanka": "^0.2.29",
49
49
  "concurrently": "^9.2.1",
50
50
  "effect": "4.0.0-beta.36",
51
51
  "husky": "^9.1.7",
@@ -55,7 +55,7 @@
55
55
  "prettier": "^3.8.1",
56
56
  "tsdown": "^0.21.4",
57
57
  "typescript": "^5.9.3",
58
- "yaml": "^2.8.2"
58
+ "yaml": "^2.8.3"
59
59
  },
60
60
  "lint-staged": {
61
61
  "*.{ts,tsx}": [
@@ -3,6 +3,7 @@ import { PromptGen } from "../PromptGen.ts"
3
3
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
4
4
  import { Worktree } from "../Worktree.ts"
5
5
  import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
6
+ import { runClankaPlan } from "../Clanka.ts"
6
7
 
7
8
  export const agentPlanner = Effect.fnUntraced(function* (options: {
8
9
  readonly plan: string
@@ -16,6 +17,15 @@ export const agentPlanner = Effect.fnUntraced(function* (options: {
16
17
  const promptGen = yield* PromptGen
17
18
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
18
19
 
20
+ if (options.preset.cliAgent.id === "clanka") {
21
+ yield* runClankaPlan({
22
+ directory: worktree.directory,
23
+ model: options.preset.extraArgs.join(" "),
24
+ prompt: promptGen.planPrompt(options),
25
+ })
26
+ return
27
+ }
28
+
19
29
  yield* pipe(
20
30
  options.preset.cliAgent.commandPlan({
21
31
  prompt: promptGen.planPrompt(options),
package/src/Clanka.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  Stdio,
13
13
  Stream,
14
14
  } from "effect"
15
+ import { Prompt as CliPrompt } from "effect/unstable/cli"
15
16
  import { TaskChooseTools, TaskTools, TaskToolsHandlers } from "./TaskTools.ts"
16
17
  import { layerClankaModel, ModelServices } from "./ClankaModels.ts"
17
18
  import { withStallTimeout } from "./shared/stream.ts"
@@ -136,3 +137,52 @@ export const runClanka = Effect.fnUntraced(
136
137
  ),
137
138
  Effect.provide([ModelServices, TaskToolsHandlers]),
138
139
  )
140
+
141
+ export const runClankaPlan = Effect.fnUntraced(
142
+ function* (options: {
143
+ readonly directory: string
144
+ readonly model: string
145
+ readonly prompt: Prompt.RawInput
146
+ }) {
147
+ const stdio = yield* Stdio.Stdio
148
+ const agent = yield* Agent.Agent
149
+ let nextPrompt = options.prompt
150
+
151
+ while (true) {
152
+ const output = yield* agent.send({
153
+ prompt: nextPrompt,
154
+ system: `ONLY call taskComplete by itself. NEVER call taskComplete alongside other functions, to ensure you first read output before deciding a task is done.`,
155
+ })
156
+
157
+ yield* output.pipe(
158
+ OutputFormatter.pretty({
159
+ outputTruncation: 20,
160
+ }),
161
+ Stream.run(stdio.stdout()),
162
+ )
163
+
164
+ console.log("")
165
+ nextPrompt = yield* CliPrompt.text({
166
+ message: ">",
167
+ })
168
+ }
169
+ },
170
+ Effect.scoped,
171
+ (effect, options) =>
172
+ Effect.provide(
173
+ effect,
174
+ Agent.layerLocal({
175
+ directory: options.directory,
176
+ }).pipe(
177
+ Layer.provide(SemanticSearchLayer),
178
+ Layer.merge(layerClankaModel(options.model)),
179
+ ),
180
+ { local: true },
181
+ ),
182
+ Effect.provide([
183
+ ModelServices,
184
+ TaskToolsHandlers,
185
+ Agent.ConversationMode.layer(true),
186
+ ]),
187
+ Effect.ignore(),
188
+ )
@@ -17,7 +17,7 @@ import { GithubIssueSource } from "./Github.ts"
17
17
  import { IssuesChange, IssueSource } from "./IssueSource.ts"
18
18
  import { PlatformServices } from "./shared/platform.ts"
19
19
  import type { PrdIssue } from "./domain/PrdIssue.ts"
20
- import type { Project, ProjectId } from "./domain/Project.ts"
20
+ import type { ProjectId } from "./domain/Project.ts"
21
21
  import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
22
22
 
23
23
  const issueSources: ReadonlyArray<typeof CurrentIssueSource.Service> = [
@@ -202,17 +202,6 @@ const getCurrentIssues = (projectId: ProjectId) =>
202
202
  pipe(s.ref(projectId), Effect.flatMap(SubscriptionRef.get)),
203
203
  )
204
204
 
205
- export const checkForWork = Effect.fnUntraced(function* (project: Project) {
206
- if (project.gitFlow === "ralph") return
207
- const { issues } = yield* getCurrentIssues(project.id)
208
- const hasIncomplete = issues.some(
209
- (issue) => issue.state === "todo" && issue.blockedBy.length === 0,
210
- )
211
- if (!hasIncomplete) {
212
- return yield* new NoMoreWork({})
213
- }
214
- })
215
-
216
205
  export const resetInProgress = Effect.gen(function* () {
217
206
  const source = yield* IssueSource
218
207
  const projectId = yield* CurrentProjectId
@@ -233,11 +222,3 @@ export const resetInProgress = Effect.gen(function* () {
233
222
  { concurrency: 5, discard: true },
234
223
  )
235
224
  })
236
-
237
- export class NoMoreWork extends Schema.ErrorClass<NoMoreWork>(
238
- "lalph/Prd/NoMoreWork",
239
- )({
240
- _tag: Schema.tag("NoMoreWork"),
241
- }) {
242
- readonly message = "No more work to be done!"
243
- }
@@ -24,11 +24,7 @@ import { Prd } from "../Prd.ts"
24
24
  import { Worktree } from "../Worktree.ts"
25
25
  import { Flag, Command, Prompt } from "effect/unstable/cli"
26
26
  import { IssueSource, IssueSourceError } from "../IssueSource.ts"
27
- import {
28
- checkForWork,
29
- CurrentIssueSource,
30
- resetInProgress,
31
- } from "../CurrentIssueSource.ts"
27
+ import { CurrentIssueSource, resetInProgress } from "../CurrentIssueSource.ts"
32
28
  import { GithubCli } from "../Github/Cli.ts"
33
29
  import { agentWorker } from "../Agents/worker.ts"
34
30
  import { agentChooser, ChosenTaskNotFound } from "../Agents/chooser.ts"
@@ -542,6 +538,8 @@ const runProject = Effect.fnUntraced(
542
538
  const iterationsDisplay = isFinite ? options.iterations : "unlimited"
543
539
  const semaphore = Semaphore.makeUnsafe(options.project.concurrency)
544
540
  const fibers = yield* FiberSet.make()
541
+ const source = yield* IssueSource
542
+ const issuesRef = yield* source.ref(options.project.id)
545
543
 
546
544
  let executionMode: ProjectExecutionMode
547
545
  if (options.project.gitFlow === "ralph") {
@@ -594,28 +592,6 @@ const runProject = Effect.fnUntraced(
594
592
  })
595
593
  }
596
594
 
597
- const handleNoMoreWork = (
598
- currentIteration: number,
599
- setIterations: (iterations: number) => void,
600
- ) => {
601
- if (executionMode._tag === "ralph") {
602
- return Effect.void
603
- }
604
- if (isFinite) {
605
- // If we have a finite number of iterations, we exit when no more
606
- // work is found
607
- setIterations(currentIteration)
608
- return Effect.log(
609
- `No more work to process, ending after ${currentIteration} iteration(s).`,
610
- )
611
- }
612
- const log =
613
- Iterable.size(fibers) <= 1
614
- ? Effect.log("No more work to process, waiting 30 seconds...")
615
- : Effect.void
616
- return Effect.andThen(log, Effect.sleep(Duration.seconds(30)))
617
- }
618
-
619
595
  yield* resetInProgress.pipe(Effect.withSpan("Main.resetInProgress"))
620
596
 
621
597
  yield* Effect.log(
@@ -628,6 +604,33 @@ const runProject = Effect.fnUntraced(
628
604
 
629
605
  yield* Atom.mount(activeWorkerLoggingAtom)
630
606
 
607
+ const waitForWork =
608
+ executionMode._tag === "ralph"
609
+ ? Effect.void
610
+ : SubscriptionRef.changes(issuesRef).pipe(
611
+ Stream.takeUntilEffect(
612
+ Effect.fnUntraced(function* ({ issues }) {
613
+ const hasIncomplete = issues.some(
614
+ (issue) =>
615
+ issue.state === "todo" && issue.blockedBy.length === 0,
616
+ )
617
+ if (hasIncomplete) return true
618
+ if (isFinite) {
619
+ quit = true
620
+ yield* Effect.log(
621
+ `No more work to process, ending after ${iteration} iteration(s).`,
622
+ )
623
+ return yield* Effect.interrupt
624
+ }
625
+ if (Iterable.size(fibers) <= 1) {
626
+ yield* Effect.log("No more work to process")
627
+ }
628
+ return false
629
+ }),
630
+ ),
631
+ Stream.runDrain,
632
+ )
633
+
631
634
  while (true) {
632
635
  yield* semaphore.take(1)
633
636
  if (quit || (isFinite && iteration >= iterations)) {
@@ -640,7 +643,7 @@ const runProject = Effect.fnUntraced(
640
643
  let ralphDone = false
641
644
 
642
645
  const gitFlowLayer = resolveGitFlowLayer()
643
- const fiber = yield* checkForWork(options.project).pipe(
646
+ const fiber = yield* waitForWork.pipe(
644
647
  Effect.andThen(
645
648
  resolveRunEffect(startedDeferred).pipe(
646
649
  Effect.provide(gitFlowLayer, { local: true }),
@@ -657,11 +660,6 @@ const runProject = Effect.fnUntraced(
657
660
  `No more work to process for Ralph, ending after ${currentIteration + 1} iteration(s).`,
658
661
  )
659
662
  },
660
- NoMoreWork(_error) {
661
- return handleNoMoreWork(currentIteration, (newIterations) => {
662
- iterations = newIterations
663
- })
664
- },
665
663
  QuitError(_error) {
666
664
  quit = true
667
665
  return Effect.void
@@ -33,7 +33,7 @@ const clanka = new CliAgent({
33
33
  name: "clanka",
34
34
  commandPlan: ({ prompt, prdFilePath, dangerous }) =>
35
35
  ChildProcess.make(
36
- "opencode",
36
+ "clanka",
37
37
  [
38
38
  "--prompt",
39
39
  prdFilePath