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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.3.85",
4
+ "version": "0.3.87",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -39,7 +39,7 @@ export const agentChooser = Effect.fnUntraced(function* (options: {
39
39
  model: options.preset.extraArgs.join(" "),
40
40
  prompt: promptGen.promptChooseClanka({ gitFlow }),
41
41
  stallTimeout: options.stallTimeout,
42
- withChoose: true,
42
+ mode: "choose",
43
43
  }).pipe(
44
44
  Effect.provideService(ChosenTaskDeferred, deferred),
45
45
  Effect.flatMap(() => Effect.fail(new ChosenTaskNotFound())),
@@ -0,0 +1,57 @@
1
+ import { Data, Duration, Effect, FileSystem, Path, pipe } from "effect"
2
+ import { PromptGen } from "../PromptGen.ts"
3
+ import { ChildProcess } from "effect/unstable/process"
4
+ import { Worktree } from "../Worktree.ts"
5
+ import { RunnerStalled } from "../domain/Errors.ts"
6
+ import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
7
+ import { runClanka } from "../Clanka.ts"
8
+
9
+ export const agentChooserRalph = Effect.fnUntraced(function* (options: {
10
+ readonly stallTimeout: Duration.Duration
11
+ readonly preset: CliAgentPreset
12
+ readonly specFile: string
13
+ }) {
14
+ const fs = yield* FileSystem.FileSystem
15
+ const pathService = yield* Path.Path
16
+ const worktree = yield* Worktree
17
+ const promptGen = yield* PromptGen
18
+
19
+ // use clanka
20
+ if (!options.preset.cliAgent.command) {
21
+ yield* runClanka({
22
+ directory: worktree.directory,
23
+ model: options.preset.extraArgs.join(" "),
24
+ prompt: promptGen.promptChooseRalph({ specFile: options.specFile }),
25
+ stallTimeout: options.stallTimeout,
26
+ mode: "ralph",
27
+ })
28
+ } else {
29
+ yield* pipe(
30
+ options.preset.cliAgent.command({
31
+ prompt: promptGen.promptChooseRalph({ specFile: options.specFile }),
32
+ prdFilePath: undefined,
33
+ extraArgs: options.preset.extraArgs,
34
+ }),
35
+ ChildProcess.setCwd(worktree.directory),
36
+ options.preset.withCommandPrefix,
37
+ worktree.execWithWorkerOutput({
38
+ cliAgent: options.preset.cliAgent,
39
+ }),
40
+ Effect.timeoutOrElse({
41
+ duration: options.stallTimeout,
42
+ onTimeout: () => Effect.fail(new RunnerStalled()),
43
+ }),
44
+ )
45
+ }
46
+
47
+ return yield* pipe(
48
+ fs.readFileString(
49
+ pathService.join(worktree.directory, ".lalph", "task.md"),
50
+ ),
51
+ Effect.mapError((_) => new ChosenTaskNotFound()),
52
+ )
53
+ })
54
+
55
+ export class ChosenTaskNotFound extends Data.TaggedError("ChosenTaskNotFound") {
56
+ readonly message = "The AI agent failed to choose a task."
57
+ }
@@ -9,6 +9,7 @@ export const agentPlanner = Effect.fnUntraced(function* (options: {
9
9
  readonly specsDirectory: string
10
10
  readonly dangerous: boolean
11
11
  readonly preset: CliAgentPreset
12
+ readonly ralph: boolean
12
13
  }) {
13
14
  const pathService = yield* Path.Path
14
15
  const worktree = yield* Worktree
@@ -18,7 +19,9 @@ export const agentPlanner = Effect.fnUntraced(function* (options: {
18
19
  yield* pipe(
19
20
  options.preset.cliAgent.commandPlan({
20
21
  prompt: promptGen.planPrompt(options),
21
- prdFilePath: pathService.join(".lalph", "prd.yml"),
22
+ prdFilePath: options.ralph
23
+ ? undefined
24
+ : pathService.join(".lalph", "prd.yml"),
22
25
  dangerous: options.dangerous,
23
26
  }),
24
27
  ChildProcess.setCwd(worktree.directory),
@@ -12,6 +12,7 @@ export const agentReviewer = Effect.fnUntraced(function* (options: {
12
12
  readonly stallTimeout: Duration.Duration
13
13
  readonly preset: CliAgentPreset
14
14
  readonly instructions: string
15
+ readonly ralph: boolean
15
16
  }) {
16
17
  const fs = yield* FileSystem.FileSystem
17
18
  const pathService = yield* Path.Path
@@ -29,7 +30,7 @@ export const agentReviewer = Effect.fnUntraced(function* (options: {
29
30
  yield* runClanka({
30
31
  directory: worktree.directory,
31
32
  model: options.preset.extraArgs.join(" "),
32
- system: promptGen.systemClanka(options),
33
+ system: options.ralph ? undefined : promptGen.systemClanka(options),
33
34
  prompt: Option.match(customInstructions, {
34
35
  onNone: () =>
35
36
  promptGen.promptReview({
@@ -44,6 +45,7 @@ export const agentReviewer = Effect.fnUntraced(function* (options: {
44
45
  }),
45
46
  }),
46
47
  stallTimeout: options.stallTimeout,
48
+ mode: options.ralph ? "ralph" : "default",
47
49
  })
48
50
  return ExitCode(0)
49
51
  }
@@ -11,7 +11,16 @@ export const agentTimeout = Effect.fnUntraced(function* (options: {
11
11
  readonly specsDirectory: string
12
12
  readonly stallTimeout: Duration.Duration
13
13
  readonly preset: CliAgentPreset
14
- readonly task: PrdIssue
14
+ readonly task:
15
+ | {
16
+ readonly _tag: "task"
17
+ readonly task: PrdIssue
18
+ }
19
+ | {
20
+ readonly _tag: "ralph"
21
+ readonly task: string
22
+ readonly specFile: string
23
+ }
15
24
  }) {
16
25
  const pathService = yield* Path.Path
17
26
  const worktree = yield* Worktree
@@ -22,23 +31,42 @@ export const agentTimeout = Effect.fnUntraced(function* (options: {
22
31
  yield* runClanka({
23
32
  directory: worktree.directory,
24
33
  model: options.preset.extraArgs.join(" "),
25
- system: promptGen.systemClanka(options),
26
- prompt: promptGen.promptTimeoutClanka({
27
- taskId: options.task.id!,
28
- specsDirectory: options.specsDirectory,
29
- }),
34
+ system:
35
+ options.task._tag === "ralph"
36
+ ? undefined
37
+ : promptGen.systemClanka(options),
38
+ prompt:
39
+ options.task._tag === "ralph"
40
+ ? promptGen.promptTimeoutRalph({
41
+ task: options.task.task,
42
+ specFile: options.task.specFile,
43
+ })
44
+ : promptGen.promptTimeoutClanka({
45
+ taskId: options.task.task.id!,
46
+ specsDirectory: options.specsDirectory,
47
+ }),
30
48
  stallTimeout: options.stallTimeout,
49
+ mode: options.task._tag === "ralph" ? "ralph" : "default",
31
50
  })
32
51
  return ExitCode(0)
33
52
  }
34
53
 
35
54
  const timeoutCommand = pipe(
36
55
  options.preset.cliAgent.command({
37
- prompt: promptGen.promptTimeout({
38
- taskId: options.task.id!,
39
- specsDirectory: options.specsDirectory,
40
- }),
41
- prdFilePath: pathService.join(".lalph", "prd.yml"),
56
+ prompt:
57
+ options.task._tag === "ralph"
58
+ ? promptGen.promptTimeoutRalph({
59
+ task: options.task.task,
60
+ specFile: options.task.specFile,
61
+ })
62
+ : promptGen.promptTimeout({
63
+ taskId: options.task.task.id!,
64
+ specsDirectory: options.specsDirectory,
65
+ }),
66
+ prdFilePath:
67
+ options.task._tag === "ralph"
68
+ ? undefined
69
+ : pathService.join(".lalph", "prd.yml"),
42
70
  extraArgs: options.preset.extraArgs,
43
71
  }),
44
72
  ChildProcess.setCwd(worktree.directory),
@@ -13,6 +13,7 @@ export const agentWorker = Effect.fnUntraced(function* (options: {
13
13
  readonly prompt: string
14
14
  readonly research: Option.Option<string>
15
15
  readonly steer?: Stream.Stream<string>
16
+ readonly ralph: boolean
16
17
  }) {
17
18
  const pathService = yield* Path.Path
18
19
  const worktree = yield* Worktree
@@ -42,6 +43,7 @@ ${research}`,
42
43
  }),
43
44
  stallTimeout: options.stallTimeout,
44
45
  steer: options.steer,
46
+ mode: options.ralph ? "ralph" : "default",
45
47
  })
46
48
  return ExitCode(0)
47
49
  }
@@ -49,7 +51,9 @@ ${research}`,
49
51
  const cliCommand = pipe(
50
52
  options.preset.cliAgent.command({
51
53
  prompt: options.prompt,
52
- prdFilePath: pathService.join(".lalph", "prd.yml"),
54
+ prdFilePath: options.ralph
55
+ ? undefined
56
+ : pathService.join(".lalph", "prd.yml"),
53
57
  extraArgs: options.preset.extraArgs,
54
58
  }),
55
59
  ChildProcess.setCwd(worktree.directory),
package/src/Clanka.ts CHANGED
@@ -71,7 +71,7 @@ export const runClanka = Effect.fnUntraced(
71
71
  readonly system?: string | undefined
72
72
  readonly stallTimeout?: Duration.Input | undefined
73
73
  readonly steer?: Stream.Stream<string> | undefined
74
- readonly withChoose?: boolean | undefined
74
+ readonly mode?: "ralph" | "choose" | "default" | undefined
75
75
  }) {
76
76
  const muxer = yield* OutputFormatter.Muxer
77
77
  const agent = yield* Agent.Agent
@@ -112,7 +112,12 @@ export const runClanka = Effect.fnUntraced(
112
112
  effect,
113
113
  Agent.layerLocal({
114
114
  directory: options.directory,
115
- tools: options.withChoose ? TaskChooseTools : TaskTools,
115
+ tools:
116
+ options.mode === "ralph"
117
+ ? undefined
118
+ : options.mode === "choose"
119
+ ? TaskChooseTools
120
+ : TaskTools,
116
121
  }).pipe(Layer.merge(ClankaModels.get(options.model))),
117
122
  ),
118
123
  Effect.provide([NodeHttpClient.layerUndici, TaskToolsHandlers]),
@@ -9,7 +9,7 @@ import {
9
9
  ScopedRef,
10
10
  ServiceMap,
11
11
  } from "effect"
12
- import { CurrentProjectId, Setting, Settings } from "./Settings.ts"
12
+ import { allProjects, CurrentProjectId, Setting, Settings } from "./Settings.ts"
13
13
  import { LinearIssueSource } from "./Linear.ts"
14
14
  import { Prompt } from "effect/unstable/cli"
15
15
  import { GithubIssueSource } from "./Github.ts"
@@ -18,7 +18,7 @@ import { PlatformServices } from "./shared/platform.ts"
18
18
  import { atomRuntime } from "./shared/runtime.ts"
19
19
  import { Atom, Reactivity } from "effect/unstable/reactivity"
20
20
  import type { PrdIssue } from "./domain/PrdIssue.ts"
21
- import type { ProjectId } from "./domain/Project.ts"
21
+ import type { Project, ProjectId } from "./domain/Project.ts"
22
22
  import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
23
23
 
24
24
  const issueSources: ReadonlyArray<typeof CurrentIssueSource.Service> = [
@@ -76,6 +76,7 @@ export class CurrentIssueSource extends ServiceMap.Service<
76
76
  >()("lalph/CurrentIssueSource") {
77
77
  static layer = Layer.effectServices(
78
78
  Effect.gen(function* () {
79
+ const settings = yield* Settings
79
80
  const source = yield* getOrSelectIssueSource
80
81
  const build = Layer.build(source.layer).pipe(
81
82
  Effect.map(ServiceMap.get(IssueSource)),
@@ -88,6 +89,24 @@ export class CurrentIssueSource extends ServiceMap.Service<
88
89
  const refresh = ScopedRef.set(ref, build).pipe(
89
90
  Effect.provideServices(services),
90
91
  )
92
+ const unlessRalph =
93
+ <B>(projectId: ProjectId, orElse: Effect.Effect<B>) =>
94
+ <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A | B, E, R> =>
95
+ settings.get(allProjects).pipe(
96
+ Effect.map(
97
+ Option.filter((projects) =>
98
+ projects.some(
99
+ (p) => p.id === projectId && p.gitFlow === "ralph",
100
+ ),
101
+ ),
102
+ ),
103
+ Effect.flatMap(
104
+ Option.match({
105
+ onNone: (): Effect.Effect<A | B, E, R> => effect,
106
+ onSome: () => orElse,
107
+ }),
108
+ ),
109
+ )
91
110
 
92
111
  const proxy = IssueSource.of({
93
112
  issues: (projectId) =>
@@ -100,18 +119,22 @@ export class CurrentIssueSource extends ServiceMap.Service<
100
119
  ).pipe(Effect.andThen(Effect.ignore(refresh))),
101
120
  ),
102
121
  Effect.retry(refreshSchedule),
122
+ unlessRalph(projectId, Effect.succeed([])),
103
123
  ),
104
124
  createIssue: (projectId, options) =>
105
125
  ScopedRef.get(ref).pipe(
106
126
  Effect.flatMap((source) => source.createIssue(projectId, options)),
127
+ unlessRalph(projectId, Effect.interrupt),
107
128
  ),
108
129
  updateIssue: (options) =>
109
130
  ScopedRef.get(ref).pipe(
110
131
  Effect.flatMap((source) => source.updateIssue(options)),
132
+ unlessRalph(options.projectId, Effect.void),
111
133
  ),
112
134
  cancelIssue: (projectId, issueId) =>
113
135
  ScopedRef.get(ref).pipe(
114
136
  Effect.flatMap((source) => source.cancelIssue(projectId, issueId)),
137
+ unlessRalph(projectId, Effect.void),
115
138
  ),
116
139
  reset: ScopedRef.get(ref).pipe(
117
140
  Effect.flatMap((source) => source.reset),
@@ -119,10 +142,12 @@ export class CurrentIssueSource extends ServiceMap.Service<
119
142
  settings: (projectId) =>
120
143
  ScopedRef.get(ref).pipe(
121
144
  Effect.flatMap((source) => source.settings(projectId)),
145
+ unlessRalph(projectId, Effect.void),
122
146
  ),
123
147
  info: (projectId) =>
124
148
  ScopedRef.get(ref).pipe(
125
149
  Effect.flatMap((source) => source.info(projectId)),
150
+ unlessRalph(projectId, Effect.void),
126
151
  ),
127
152
  issueCliAgentPreset: (issue) =>
128
153
  ScopedRef.get(ref).pipe(
@@ -141,6 +166,7 @@ export class CurrentIssueSource extends ServiceMap.Service<
141
166
  Effect.flatMap((source) =>
142
167
  source.ensureInProgress(projectId, issueId),
143
168
  ),
169
+ unlessRalph(projectId, Effect.void),
144
170
  ),
145
171
  })
146
172
 
@@ -181,9 +207,9 @@ const getCurrentIssues = (projectId: ProjectId) =>
181
207
  suspendOnWaiting: true,
182
208
  })
183
209
 
184
- export const checkForWork = Effect.gen(function* () {
185
- const projectId = yield* CurrentProjectId
186
- const issues = yield* getCurrentIssues(projectId)
210
+ export const checkForWork = Effect.fnUntraced(function* (project: Project) {
211
+ if (project.gitFlow === "ralph") return
212
+ const issues = yield* getCurrentIssues(project.id)
187
213
  const hasIncomplete = issues.some(
188
214
  (issue) => issue.state === "todo" && issue.blockedBy.length === 0,
189
215
  )
package/src/GitFlow.ts CHANGED
@@ -199,6 +199,62 @@ But you **do not** need to git push your changes or switch branches.
199
199
  }),
200
200
  ).pipe(Layer.provide(AtomRegistry.layer))
201
201
 
202
+ export const GitFlowRalph = Layer.effect(
203
+ GitFlow,
204
+ Effect.gen(function* () {
205
+ const currentWorker = yield* CurrentWorkerState
206
+ const workerState = yield* Atom.get(currentWorker.state)
207
+
208
+ return GitFlow.of({
209
+ requiresGithubPr: false,
210
+ branch: `lalph/worker-${workerState.id}`,
211
+
212
+ setupInstructions: () =>
213
+ `You are already on a new branch for this task. You do not need to checkout any other branches.`,
214
+
215
+ commitInstructions:
216
+ () => `When you have completed your changes, **you must** commit them to the current local branch. Do not git push your changes or switch branches.
217
+ - **DO NOT** commit any of the files in the \`.lalph\` directory.`,
218
+
219
+ reviewInstructions: `You are already on the branch with their changes.
220
+ After making any changes, **you must** commit them to the same branch.
221
+ But you **do not** need to git push your changes or switch branches.
222
+
223
+ - **DO NOT** commit any of the files in the \`.lalph\` directory.
224
+ - You have full permission to create git commits.`,
225
+
226
+ postWork: Effect.fnUntraced(function* ({ worktree, targetBranch }) {
227
+ if (!targetBranch) {
228
+ return yield* Effect.logWarning(
229
+ "GitFlowRalph: No target branch specified, skipping postWork.",
230
+ )
231
+ }
232
+
233
+ const parsed = parseBranch(targetBranch)
234
+ yield* worktree.exec`git fetch ${parsed.remote}`
235
+
236
+ yield* worktree.exec`git restore --worktree .`
237
+ const rebaseResult =
238
+ yield* worktree.exec`git rebase ${parsed.branchWithRemote}`
239
+ if (rebaseResult !== 0) {
240
+ return yield* new GitFlowError({
241
+ message: `Failed to rebase onto ${parsed.branchWithRemote}. Aborting task.`,
242
+ })
243
+ }
244
+
245
+ const pushResult =
246
+ yield* worktree.exec`git push ${parsed.remote} ${`HEAD:${parsed.branch}`}`
247
+ if (pushResult !== 0) {
248
+ return yield* new GitFlowError({
249
+ message: `Failed to push changes to ${parsed.branchWithRemote}. Aborting task.`,
250
+ })
251
+ }
252
+ }),
253
+ autoMerge: () => Effect.void,
254
+ })
255
+ }),
256
+ ).pipe(Layer.provide(AtomRegistry.layer))
257
+
202
258
  // Errors
203
259
 
204
260
  export class GitFlowError extends Data.TaggedError("GitFlowError")<{
package/src/Prd.ts CHANGED
@@ -294,4 +294,13 @@ export class Prd extends ServiceMap.Service<
294
294
  CurrentIssueSource.layer,
295
295
  ]),
296
296
  )
297
+ static layerNoop = Layer.succeed(this, {
298
+ path: "",
299
+ maybeRevertIssue: () => Effect.void,
300
+ revertUpdatedIssues: Effect.void,
301
+ flagUnmergable: () => Effect.void,
302
+ findById: () => Effect.succeed(null),
303
+ setChosenIssueId: () => Effect.void,
304
+ setAutoMerge: () => Effect.void,
305
+ })
297
306
  }
package/src/Projects.ts CHANGED
@@ -1,15 +1,6 @@
1
- import {
2
- Array,
3
- Data,
4
- Effect,
5
- Layer,
6
- Option,
7
- pipe,
8
- Schema,
9
- String,
10
- } from "effect"
1
+ import { Array, Data, Effect, Layer, Option, pipe, String } from "effect"
11
2
  import { Project, ProjectId } from "./domain/Project.ts"
12
- import { CurrentProjectId, Setting, Settings } from "./Settings.ts"
3
+ import { allProjects, CurrentProjectId, Settings } from "./Settings.ts"
13
4
  import { Prompt } from "effect/unstable/cli"
14
5
  import { IssueSource } from "./IssueSource.ts"
15
6
  import { CurrentIssueSource } from "./CurrentIssueSource.ts"
@@ -22,8 +13,6 @@ export const layerProjectIdPrompt = Layer.effect(
22
13
  }),
23
14
  ).pipe(Layer.provide(Settings.layer), Layer.provide(CurrentIssueSource.layer))
24
15
 
25
- export const allProjects = new Setting("projects", Schema.Array(Project))
26
-
27
16
  export const getAllProjects = Settings.get(allProjects).pipe(
28
17
  Effect.map(Option.getOrElse((): ReadonlyArray<Project> => [])),
29
18
  )
@@ -76,6 +65,7 @@ export const welcomeWizard = Effect.gen(function* () {
76
65
 
77
66
  export const addOrUpdateProject = Effect.fnUntraced(function* (
78
67
  existing?: Project,
68
+ fromPlanMode = false,
79
69
  ) {
80
70
  const projects = yield* getAllProjects
81
71
  const id = existing
@@ -121,16 +111,29 @@ export const addOrUpdateProject = Effect.fnUntraced(function* (
121
111
  value: "commit",
122
112
  selected: existing ? existing.gitFlow === "commit" : false,
123
113
  },
114
+ {
115
+ title: "Ralph",
116
+ description: "Tasks are determined from a spec file",
117
+ value: "ralph",
118
+ selected: existing ? existing.gitFlow === "ralph" : false,
119
+ },
124
120
  ] as const,
125
121
  })
126
122
 
123
+ let ralphSpec = Option.none<string>()
124
+ if (gitFlow === "ralph" && !fromPlanMode) {
125
+ ralphSpec = yield* Prompt.file({
126
+ message: "Path to Ralph spec file",
127
+ }).pipe(Effect.fromYieldable, Effect.map(Option.some))
128
+ }
129
+
127
130
  const researchAgent = yield* Prompt.toggle({
128
131
  message: "Enable research agent?",
129
- initial: existing ? existing.researchAgent : true,
132
+ initial: existing ? existing.researchAgent : false,
130
133
  })
131
134
  const reviewAgent = yield* Prompt.toggle({
132
135
  message: "Enable review agent?",
133
- initial: existing ? existing.reviewAgent : true,
136
+ initial: existing ? existing.reviewAgent : false,
134
137
  })
135
138
 
136
139
  const project = new Project({
@@ -139,6 +142,7 @@ export const addOrUpdateProject = Effect.fnUntraced(function* (
139
142
  concurrency,
140
143
  targetBranch,
141
144
  gitFlow,
145
+ ralphSpec: Option.getOrUndefined(ralphSpec),
142
146
  researchAgent,
143
147
  reviewAgent,
144
148
  })
@@ -153,7 +157,9 @@ export const addOrUpdateProject = Effect.fnUntraced(function* (
153
157
 
154
158
  const source = yield* IssueSource
155
159
  yield* source.reset.pipe(Effect.provideService(CurrentProjectId, project.id))
156
- yield* source.settings(project.id)
160
+ if (gitFlow !== "ralph") {
161
+ yield* source.settings(project.id)
162
+ }
157
163
 
158
164
  return project
159
165
  })
package/src/PromptGen.ts CHANGED
@@ -112,18 +112,19 @@ ${
112
112
  ? `\n - Set \`githubPrNumber\` to the PR number if one exists, otherwise use \`null\`.`
113
113
  : "\n Leave `githubPrNumber` as null."
114
114
  }
115
+ `
116
+ const promptChooseRalph = (options: {
117
+ readonly specFile: string
118
+ }) => `- Read the spec file at \`${options.specFile}\` to understand the current project.
119
+ - Choose the next most important task to work on from the specification.
120
+ - If all of the tasks are complete then do nothing more. Otherwise, write the chosen task in a ".lalph/task.md" file.
121
+
122
+ Note: The task should be a specific, actionable item that can be completed in a reasonable amount of time.
115
123
  `
116
124
 
117
125
  const keyInformation = (options: {
118
126
  readonly specsDirectory: string
119
- }) => `## Important: Adding new tasks
120
-
121
- **If at any point** you discover something that needs fixing, or another task
122
- that needs doing, immediately add it to the prd.yml file as a new task.
123
-
124
- Read the "### Adding tasks" section below carefully for guidelines on creating tasks.
125
-
126
- ## Important: Recording key information
127
+ }) => `## Important: Recording key information
127
128
 
128
129
  This session will time out after a certain period, so make sure to record
129
130
  key information that could speed up future work on the task in the description.
@@ -146,12 +147,7 @@ ${prdNotes(options)}`
146
147
 
147
148
  const systemClanka = (options: {
148
149
  readonly specsDirectory: string
149
- }) => `## Important: Adding new tasks
150
-
151
- **If at any point** you discover something that needs fixing, or another task
152
- that needs doing, immediately add it as a new task.
153
-
154
- ## Important: Recording key information
150
+ }) => `## Important: Recording key information
155
151
 
156
152
  This session will time out after a certain period, so make sure to record
157
153
  key information that could speed up future work on the task in the description.
@@ -238,9 +234,6 @@ All steps must be done before the task can be considered complete.${
238
234
  2. Implement the task.
239
235
  - If this task is a research task, **do not** make any code changes yet.
240
236
  - If this task is a research task and you add follow-up tasks, include (at least) "${options.task.id}" in the new task's \`blockedBy\` field.
241
- - **If at any point** you discover something that needs fixing, or another task
242
- that needs doing, immediately add it as a new task unless you plan to fix it
243
- as part of this task.
244
237
  - Add important discoveries about the codebase, or challenges faced to the task's
245
238
  \`description\`. More details below.
246
239
  3. Run any checks / feedback loops, such as type checks, unit tests, or linting.
@@ -254,6 +247,34 @@ All steps must be done before the task can be considered complete.${
254
247
  - Rewrite the notes in the description to include only the key discoveries and information that could speed up future work on other tasks. Make sure to preserve important information such as specification file references.
255
248
  - If you believe the task is complete, update the \`state\` to "in-review".`
256
249
 
250
+ const promptRalph = (options: {
251
+ readonly task: string
252
+ readonly targetBranch: string | undefined
253
+ readonly specFile: string
254
+ readonly gitFlow: GitFlow["Service"]
255
+ }) => `${options.task}
256
+
257
+ ## Project specification
258
+
259
+ Make sure to review the project specification at \`${options.specFile}\` for any key information that may help you with this task.
260
+
261
+ ### Instructions
262
+
263
+ All steps must be done before the task can be considered complete.
264
+
265
+ 1. ${options.gitFlow.setupInstructions({ githubPrNumber: undefined })}
266
+ 2. Implement the task.
267
+ - Along the way, update the specification file with any important discoveries or issues found.
268
+ 3. Run any checks / feedback loops, such as type checks, unit tests, or linting.
269
+ 4. Update the specification implementation plan at \`${options.specFile}\` to reflect changes to task states.
270
+ 4. ${options.gitFlow.commitInstructions({
271
+ githubPrInstructions: sourceMeta.githubPrInstructions,
272
+ githubPrNumber: undefined,
273
+ taskId: "unknown",
274
+ targetBranch: options.targetBranch,
275
+ })}
276
+ `
277
+
257
278
  const promptResearch = (options: {
258
279
  readonly task: PrdIssue
259
280
  }) => `Your job is to gather all the necessary information and details to complete the task described below. Do not make any code changes yet, your job is just to research and gather information.
@@ -326,6 +347,22 @@ permission.
326
347
 
327
348
  ${prdNotes(options)}`
328
349
 
350
+ const promptTimeoutRalph = (options: {
351
+ readonly task: string
352
+ readonly specFile: string
353
+ }) => `Your earlier attempt to complete the following task took too
354
+ long and has timed out.
355
+
356
+ The following instructions should be done without interaction or asking for
357
+ permission.
358
+
359
+ 1. Investigate why you think the task took too long. Research the codebase
360
+ further to understand what is needed to complete the task.
361
+ 2. Update the specification file at \`${options.specFile}\` to break the task
362
+ down into smaller tasks, and include any important discoveries from your research.
363
+ 3. Commit the changes to the specification file without pushing.
364
+ `
365
+
329
366
  const promptTimeoutClanka = (options: {
330
367
  readonly taskId: string
331
368
  readonly specsDirectory: string
@@ -447,13 +484,16 @@ Make sure to setup dependencies between the tasks using the \`blockedBy\` field.
447
484
  return {
448
485
  promptChoose,
449
486
  promptChooseClanka,
487
+ promptChooseRalph,
450
488
  prompt,
489
+ promptRalph,
451
490
  promptClanka,
452
491
  promptResearch,
453
492
  promptReview,
454
493
  promptReviewCustom,
455
494
  promptTimeout,
456
495
  promptTimeoutClanka,
496
+ promptTimeoutRalph,
457
497
  planPrompt,
458
498
  promptPlanTasks,
459
499
  promptPlanTasksClanka,
package/src/Settings.ts CHANGED
@@ -3,7 +3,7 @@ import { Cache, Effect, Layer, Option, Schema, ServiceMap } from "effect"
3
3
  import { KeyValueStore } from "effect/unstable/persistence"
4
4
  import { layerKvs, ProjectsKvs } from "./Kvs.ts"
5
5
  import { allCliAgents } from "./domain/CliAgent.ts"
6
- import { ProjectId } from "./domain/Project.ts"
6
+ import { Project, ProjectId } from "./domain/Project.ts"
7
7
  import { Reactivity } from "effect/unstable/reactivity"
8
8
 
9
9
  export class Settings extends ServiceMap.Service<Settings>()("lalph/Settings", {
@@ -125,6 +125,17 @@ export class Settings extends ServiceMap.Service<Settings>()("lalph/Settings", {
125
125
  ) {
126
126
  return Settings.use((_) => _.set(setting, value))
127
127
  }
128
+ static update<Name extends string, S extends Schema.Codec<any, any>>(
129
+ setting: Setting<Name, S>,
130
+ f: (current: Option.Option<S["Type"]>) => Option.Option<S["Type"]>,
131
+ ) {
132
+ return Settings.use((_) =>
133
+ _.get(setting).pipe(
134
+ Effect.map(f),
135
+ Effect.flatMap((v) => _.set(setting, v)),
136
+ ),
137
+ )
138
+ }
128
139
 
129
140
  static getProject<Name extends string, S extends Schema.Codec<any, any>>(
130
141
  setting: ProjectSetting<Name, S>,
@@ -173,3 +184,5 @@ export const selectedCliAgentId = new Setting(
173
184
  "selectedCliAgentId",
174
185
  Schema.Literals(allCliAgents.map((a) => a.id)),
175
186
  )
187
+
188
+ export const allProjects = new Setting("projects", Schema.Array(Project))