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/dist/cli.mjs +621 -95
- package/package.json +1 -1
- package/src/Agents/chooser.ts +1 -1
- package/src/Agents/chooserRalph.ts +57 -0
- package/src/Agents/planner.ts +4 -1
- package/src/Agents/reviewer.ts +3 -1
- package/src/Agents/timeout.ts +39 -11
- package/src/Agents/worker.ts +5 -1
- package/src/Clanka.ts +7 -2
- package/src/CurrentIssueSource.ts +31 -5
- package/src/GitFlow.ts +56 -0
- package/src/Prd.ts +9 -0
- package/src/Projects.ts +22 -16
- package/src/PromptGen.ts +57 -17
- package/src/Settings.ts +14 -1
- package/src/commands/plan.ts +35 -15
- package/src/commands/projects/rm.ts +2 -2
- package/src/commands/projects/toggle.ts +2 -2
- package/src/commands/root.ts +229 -21
- package/src/domain/CliAgent.ts +33 -15
- package/src/domain/Project.ts +10 -2
package/package.json
CHANGED
package/src/Agents/chooser.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/Agents/planner.ts
CHANGED
|
@@ -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:
|
|
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),
|
package/src/Agents/reviewer.ts
CHANGED
|
@@ -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
|
}
|
package/src/Agents/timeout.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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),
|
package/src/Agents/worker.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
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.
|
|
185
|
-
|
|
186
|
-
const issues = yield* getCurrentIssues(
|
|
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 {
|
|
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 :
|
|
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 :
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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))
|