lalph 0.3.45 → 0.3.47
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 +579 -127
- package/package.json +4 -2
- package/src/Agents/reviewer.ts +2 -0
- package/src/Agents/worker.ts +10 -2
- package/src/Clanka.ts +14 -3
- package/src/Editor.ts +12 -1
- package/src/PromptGen.ts +5 -1
- package/src/TaskTools.ts +72 -32
- package/src/commands/issue.ts +13 -3
- package/src/commands/plan.ts +58 -36
- package/src/commands/root.ts +59 -6
- package/src/domain/PrdIssue.ts +15 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lalph",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.47",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -22,13 +22,15 @@
|
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@changesets/changelog-github": "^0.6.0",
|
|
24
24
|
"@changesets/cli": "^2.30.0",
|
|
25
|
+
"@effect/ai-openai": "4.0.0-beta.30",
|
|
26
|
+
"@effect/ai-openai-compat": "4.0.0-beta.30",
|
|
25
27
|
"@effect/language-service": "^0.79.0",
|
|
26
28
|
"@effect/platform-node": "4.0.0-beta.30",
|
|
27
29
|
"@linear/sdk": "^77.0.0",
|
|
28
30
|
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
|
29
31
|
"@octokit/types": "^16.0.0",
|
|
30
32
|
"@typescript/native-preview": "7.0.0-dev.20260310.1",
|
|
31
|
-
"clanka": "^0.0.
|
|
33
|
+
"clanka": "^0.0.17",
|
|
32
34
|
"concurrently": "^9.2.1",
|
|
33
35
|
"effect": "4.0.0-beta.30",
|
|
34
36
|
"husky": "^9.1.7",
|
package/src/Agents/reviewer.ts
CHANGED
|
@@ -40,6 +40,7 @@ export const agentReviewer = Effect.fnUntraced(function* (options: {
|
|
|
40
40
|
promptGen.promptReviewCustom({
|
|
41
41
|
prompt,
|
|
42
42
|
specsDirectory: options.specsDirectory,
|
|
43
|
+
removePrdNotes: true,
|
|
43
44
|
}),
|
|
44
45
|
}),
|
|
45
46
|
stallTimeout: options.stallTimeout,
|
|
@@ -59,6 +60,7 @@ export const agentReviewer = Effect.fnUntraced(function* (options: {
|
|
|
59
60
|
promptGen.promptReviewCustom({
|
|
60
61
|
prompt,
|
|
61
62
|
specsDirectory: options.specsDirectory,
|
|
63
|
+
removePrdNotes: false,
|
|
62
64
|
}),
|
|
63
65
|
}),
|
|
64
66
|
prdFilePath: pathService.join(".lalph", "prd.yml"),
|
package/src/Agents/worker.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { Duration, Effect, Path, pipe } from "effect"
|
|
1
|
+
import { Duration, Effect, identity, Path, pipe, Stream } from "effect"
|
|
2
2
|
import { ChildProcess } from "effect/unstable/process"
|
|
3
3
|
import { Worktree } from "../Worktree.ts"
|
|
4
4
|
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
5
5
|
import { runClanka } from "../Clanka.ts"
|
|
6
6
|
import { ExitCode } from "effect/unstable/process/ChildProcessSpawner"
|
|
7
|
+
import { CurrentTaskRef } from "../TaskTools.ts"
|
|
7
8
|
|
|
8
9
|
export const agentWorker = Effect.fnUntraced(function* (options: {
|
|
9
10
|
readonly stallTimeout: Duration.Duration
|
|
10
11
|
readonly preset: CliAgentPreset
|
|
11
12
|
readonly system?: string
|
|
12
13
|
readonly prompt: string
|
|
14
|
+
readonly steer?: Stream.Stream<string>
|
|
15
|
+
readonly taskRef?: CurrentTaskRef["Service"]
|
|
13
16
|
}) {
|
|
14
17
|
const pathService = yield* Path.Path
|
|
15
18
|
const worktree = yield* Worktree
|
|
@@ -22,7 +25,12 @@ export const agentWorker = Effect.fnUntraced(function* (options: {
|
|
|
22
25
|
system: options.system,
|
|
23
26
|
prompt: options.prompt,
|
|
24
27
|
stallTimeout: options.stallTimeout,
|
|
25
|
-
|
|
28
|
+
steer: options.steer,
|
|
29
|
+
}).pipe(
|
|
30
|
+
options.taskRef
|
|
31
|
+
? Effect.provideService(CurrentTaskRef, options.taskRef)
|
|
32
|
+
: identity,
|
|
33
|
+
)
|
|
26
34
|
return ExitCode(0)
|
|
27
35
|
}
|
|
28
36
|
|
package/src/Clanka.ts
CHANGED
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
} from "./TaskTools.ts"
|
|
8
8
|
import { ClankaModels, clankaSubagent } from "./ClankaModels.ts"
|
|
9
9
|
import { withStallTimeout } from "./shared/stream.ts"
|
|
10
|
-
import type { AiError } from "effect/unstable/ai"
|
|
11
|
-
import type { RunnerStalled } from "./domain/Errors.ts"
|
|
12
10
|
|
|
13
11
|
export const runClanka = Effect.fnUntraced(
|
|
14
12
|
/** The working directory to run the agent in */
|
|
@@ -18,6 +16,7 @@ export const runClanka = Effect.fnUntraced(
|
|
|
18
16
|
readonly prompt: string
|
|
19
17
|
readonly system?: string | undefined
|
|
20
18
|
readonly stallTimeout?: Duration.Input | undefined
|
|
19
|
+
readonly steer?: Stream.Stream<string> | undefined
|
|
21
20
|
readonly withChoose?: boolean | undefined
|
|
22
21
|
}) {
|
|
23
22
|
const models = yield* ClankaModels
|
|
@@ -33,6 +32,19 @@ export const runClanka = Effect.fnUntraced(
|
|
|
33
32
|
? withStallTimeout(options.stallTimeout)(agent.output)
|
|
34
33
|
: agent.output
|
|
35
34
|
|
|
35
|
+
if (options.steer) {
|
|
36
|
+
yield* options.steer.pipe(
|
|
37
|
+
Stream.switchMap(
|
|
38
|
+
Effect.fnUntraced(function* (message) {
|
|
39
|
+
yield* Effect.log(`Received steer message: ${message}`)
|
|
40
|
+
yield* agent.steer(message)
|
|
41
|
+
}, Stream.fromEffectDrain),
|
|
42
|
+
),
|
|
43
|
+
Stream.runDrain,
|
|
44
|
+
Effect.forkScoped,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
return yield* stream.pipe(
|
|
37
49
|
OutputFormatter.pretty,
|
|
38
50
|
Stream.runForEachArray((out) => {
|
|
@@ -41,7 +53,6 @@ export const runClanka = Effect.fnUntraced(
|
|
|
41
53
|
}
|
|
42
54
|
return Effect.void
|
|
43
55
|
}),
|
|
44
|
-
(_) => _ as Effect.Effect<void, AiError.AiError | RunnerStalled>,
|
|
45
56
|
)
|
|
46
57
|
},
|
|
47
58
|
Effect.scoped,
|
package/src/Editor.ts
CHANGED
|
@@ -57,7 +57,18 @@ export class Editor extends ServiceMap.Service<Editor>()("lalph/Editor", {
|
|
|
57
57
|
Effect.option,
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const saveTemp = Effect.fnUntraced(function* (
|
|
61
|
+
content: string,
|
|
62
|
+
options: { suffix?: string },
|
|
63
|
+
) {
|
|
64
|
+
const file = yield* fs.makeTempFile({
|
|
65
|
+
suffix: options.suffix ?? ".txt",
|
|
66
|
+
})
|
|
67
|
+
yield* fs.writeFileString(file, content)
|
|
68
|
+
return file
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return { edit, editTemp, saveTemp } as const
|
|
61
72
|
}),
|
|
62
73
|
}) {
|
|
63
74
|
static layer = Layer.effect(this, this.make).pipe(
|
package/src/PromptGen.ts
CHANGED
|
@@ -285,7 +285,11 @@ ${options.prompt}`
|
|
|
285
285
|
const promptReviewCustom = (options: {
|
|
286
286
|
readonly prompt: string
|
|
287
287
|
readonly specsDirectory: string
|
|
288
|
-
|
|
288
|
+
readonly removePrdNotes: boolean
|
|
289
|
+
}) =>
|
|
290
|
+
options.removePrdNotes
|
|
291
|
+
? options.prompt
|
|
292
|
+
: `${options.prompt}
|
|
289
293
|
|
|
290
294
|
${prdNotes(options)}`
|
|
291
295
|
|
package/src/TaskTools.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Deferred,
|
|
3
|
+
Effect,
|
|
4
|
+
MutableRef,
|
|
5
|
+
Option,
|
|
6
|
+
Schema,
|
|
7
|
+
ServiceMap,
|
|
8
|
+
Struct,
|
|
9
|
+
} from "effect"
|
|
2
10
|
import { Tool, Toolkit } from "effect/unstable/ai"
|
|
3
11
|
import { PrdIssue } from "./domain/PrdIssue.ts"
|
|
4
12
|
import { IssueSource } from "./IssueSource.ts"
|
|
@@ -14,24 +22,36 @@ export class ChosenTaskDeferred extends ServiceMap.Reference(
|
|
|
14
22
|
},
|
|
15
23
|
) {}
|
|
16
24
|
|
|
25
|
+
export class CurrentTaskRef extends ServiceMap.Service<
|
|
26
|
+
CurrentTaskRef,
|
|
27
|
+
MutableRef.MutableRef<PrdIssue>
|
|
28
|
+
>()("lalph/TaskTools/CurrentTaskRef") {
|
|
29
|
+
static update(f: (prev: PrdIssue) => PrdIssue) {
|
|
30
|
+
return Effect.serviceOption(CurrentTaskRef).pipe(
|
|
31
|
+
Effect.map(Option.map((ref) => MutableRef.updateAndGet(ref, f))),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TaskList = Schema.Array(
|
|
37
|
+
Schema.Struct({
|
|
38
|
+
id: Schema.String.annotate({
|
|
39
|
+
documentation: "The unique identifier of the task.",
|
|
40
|
+
}),
|
|
41
|
+
...Struct.pick(PrdIssue.fields, [
|
|
42
|
+
"title",
|
|
43
|
+
"description",
|
|
44
|
+
"state",
|
|
45
|
+
"priority",
|
|
46
|
+
"blockedBy",
|
|
47
|
+
]),
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
|
|
17
51
|
export class TaskTools extends Toolkit.make(
|
|
18
52
|
Tool.make("listTasks", {
|
|
19
53
|
description: "Returns the current list of tasks.",
|
|
20
|
-
success:
|
|
21
|
-
Schema.Struct({
|
|
22
|
-
id: Schema.String.annotate({
|
|
23
|
-
documentation: "The unique identifier of the task.",
|
|
24
|
-
}),
|
|
25
|
-
...Struct.pick(PrdIssue.fields, [
|
|
26
|
-
"title",
|
|
27
|
-
"description",
|
|
28
|
-
"state",
|
|
29
|
-
"priority",
|
|
30
|
-
"estimate",
|
|
31
|
-
"blockedBy",
|
|
32
|
-
]),
|
|
33
|
-
}),
|
|
34
|
-
),
|
|
54
|
+
success: TaskList,
|
|
35
55
|
dependencies: [CurrentProjectId],
|
|
36
56
|
}),
|
|
37
57
|
Tool.make("createTask", {
|
|
@@ -41,7 +61,6 @@ export class TaskTools extends Toolkit.make(
|
|
|
41
61
|
description: PrdIssue.fields.description,
|
|
42
62
|
state: PrdIssue.fields.state,
|
|
43
63
|
priority: PrdIssue.fields.priority,
|
|
44
|
-
estimate: PrdIssue.fields.estimate,
|
|
45
64
|
blockedBy: PrdIssue.fields.blockedBy,
|
|
46
65
|
}),
|
|
47
66
|
success: Schema.String,
|
|
@@ -58,13 +77,13 @@ export class TaskTools extends Toolkit.make(
|
|
|
58
77
|
}),
|
|
59
78
|
dependencies: [CurrentProjectId],
|
|
60
79
|
}),
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
Tool.make("removeTask", {
|
|
81
|
+
description: "Remove a task by it's id.",
|
|
82
|
+
parameters: Schema.String.annotate({
|
|
83
|
+
identifier: "taskId",
|
|
84
|
+
}),
|
|
85
|
+
dependencies: [CurrentProjectId],
|
|
86
|
+
}),
|
|
68
87
|
) {}
|
|
69
88
|
|
|
70
89
|
export class TaskToolsWithChoose extends Toolkit.merge(
|
|
@@ -77,6 +96,11 @@ export class TaskToolsWithChoose extends Toolkit.merge(
|
|
|
77
96
|
githubPrNumber: Schema.optional(Schema.Number),
|
|
78
97
|
}),
|
|
79
98
|
}),
|
|
99
|
+
Tool.make("listEligibleTasks", {
|
|
100
|
+
description: "List tasks eligible for being chosen with chooseTask.",
|
|
101
|
+
success: TaskList,
|
|
102
|
+
dependencies: [CurrentProjectId],
|
|
103
|
+
}),
|
|
80
104
|
),
|
|
81
105
|
) {}
|
|
82
106
|
|
|
@@ -95,10 +119,24 @@ export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
|
95
119
|
description: issue.description,
|
|
96
120
|
state: issue.state,
|
|
97
121
|
priority: issue.priority,
|
|
98
|
-
estimate: issue.estimate,
|
|
99
122
|
blockedBy: issue.blockedBy,
|
|
100
123
|
}))
|
|
101
124
|
}, Effect.orDie),
|
|
125
|
+
listEligibleTasks: Effect.fn("TaskTools.listEligibleTasks")(function* () {
|
|
126
|
+
yield* Effect.log(`Calling "listEligibleTasks"`)
|
|
127
|
+
const projectId = yield* CurrentProjectId
|
|
128
|
+
const tasks = yield* source.issues(projectId)
|
|
129
|
+
return tasks
|
|
130
|
+
.filter((t) => t.blockedBy.length === 0 && t.state === "todo")
|
|
131
|
+
.map((issue) => ({
|
|
132
|
+
id: issue.id ?? "",
|
|
133
|
+
title: issue.title,
|
|
134
|
+
description: issue.description,
|
|
135
|
+
state: issue.state,
|
|
136
|
+
priority: issue.priority,
|
|
137
|
+
blockedBy: issue.blockedBy,
|
|
138
|
+
}))
|
|
139
|
+
}, Effect.orDie),
|
|
102
140
|
chooseTask: Effect.fn("TaskTools.chooseTask")(function* (options) {
|
|
103
141
|
yield* Effect.log(`Calling "chooseTask"`).pipe(
|
|
104
142
|
Effect.annotateLogs(options),
|
|
@@ -114,6 +152,7 @@ export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
|
114
152
|
new PrdIssue({
|
|
115
153
|
...options,
|
|
116
154
|
id: null,
|
|
155
|
+
estimate: null,
|
|
117
156
|
autoMerge: false,
|
|
118
157
|
}),
|
|
119
158
|
)
|
|
@@ -124,19 +163,20 @@ export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
|
124
163
|
Effect.annotateLogs({ taskId: options.taskId }),
|
|
125
164
|
)
|
|
126
165
|
const projectId = yield* CurrentProjectId
|
|
166
|
+
yield* CurrentTaskRef.update((prev) => prev.update(options))
|
|
127
167
|
yield* source.updateIssue({
|
|
128
168
|
projectId,
|
|
129
169
|
issueId: options.taskId,
|
|
130
170
|
...options,
|
|
131
171
|
})
|
|
132
172
|
}, Effect.orDie),
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
removeTask: Effect.fn("TaskTools.removeTask")(function* (taskId) {
|
|
174
|
+
yield* Effect.log(`Calling "removeTask"`).pipe(
|
|
175
|
+
Effect.annotateLogs({ taskId }),
|
|
176
|
+
)
|
|
177
|
+
const projectId = yield* CurrentProjectId
|
|
178
|
+
yield* source.cancelIssue(projectId, taskId)
|
|
179
|
+
}, Effect.orDie),
|
|
140
180
|
})
|
|
141
181
|
}),
|
|
142
182
|
)
|
package/src/commands/issue.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "effect/unstable/cli"
|
|
2
2
|
import { CurrentIssueSource } from "../CurrentIssueSource.ts"
|
|
3
|
-
import { Effect, flow, Option, Schema } from "effect"
|
|
3
|
+
import { Effect, Exit, flow, Option, pipe, Schema } from "effect"
|
|
4
4
|
import { IssueSource } from "../IssueSource.ts"
|
|
5
5
|
import { PrdIssue } from "../domain/PrdIssue.ts"
|
|
6
6
|
import * as Yaml from "yaml"
|
|
@@ -40,8 +40,18 @@ const handler = flow(
|
|
|
40
40
|
if (Option.isNone(content)) {
|
|
41
41
|
return
|
|
42
42
|
}
|
|
43
|
+
const contentValue = content.value.trim()
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
yield* Effect.addFinalizer((exit) => {
|
|
46
|
+
if (Exit.isSuccess(exit)) return Effect.void
|
|
47
|
+
return pipe(
|
|
48
|
+
editor.saveTemp(contentValue, { suffix: ".md" }),
|
|
49
|
+
Effect.flatMap((file) => Effect.log(`Saved your issue to: ${file}`)),
|
|
50
|
+
Effect.ignore,
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const lines = contentValue.split("\n")
|
|
45
55
|
const yamlLines: string[] = []
|
|
46
56
|
let descriptionStartIndex = 0
|
|
47
57
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -81,7 +91,7 @@ const handler = flow(
|
|
|
81
91
|
console.log(`Created issue with ID: ${created.id}`)
|
|
82
92
|
console.log(`URL: ${created.url}`)
|
|
83
93
|
}).pipe(Effect.provide([layerProjectIdPrompt, CurrentIssueSource.layer]))
|
|
84
|
-
}),
|
|
94
|
+
}, Effect.scoped),
|
|
85
95
|
),
|
|
86
96
|
Command.provide(Editor.layer),
|
|
87
97
|
)
|
package/src/commands/plan.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Data,
|
|
3
|
+
Effect,
|
|
4
|
+
Exit,
|
|
5
|
+
FileSystem,
|
|
6
|
+
Option,
|
|
7
|
+
Path,
|
|
8
|
+
pipe,
|
|
9
|
+
Schema,
|
|
10
|
+
} from "effect"
|
|
2
11
|
import { PromptGen } from "../PromptGen.ts"
|
|
3
12
|
import { Prd } from "../Prd.ts"
|
|
4
13
|
import { Worktree } from "../Worktree.ts"
|
|
@@ -48,41 +57,54 @@ export const commandPlan = Command.make("plan", {
|
|
|
48
57
|
"Draft a plan in your editor (or use --file); then generate a specification under --specs and create PRD tasks from it. Use --new to create a project first, and --dangerous to skip permission prompts during spec generation.",
|
|
49
58
|
),
|
|
50
59
|
Command.withHandler(
|
|
51
|
-
Effect.fnUntraced(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
60
|
+
Effect.fnUntraced(
|
|
61
|
+
function* ({ dangerous, withNewProject, file }) {
|
|
62
|
+
const editor = yield* Editor
|
|
63
|
+
const fs = yield* FileSystem.FileSystem
|
|
64
|
+
|
|
65
|
+
const thePlan = yield* Effect.matchEffect(file.asEffect(), {
|
|
66
|
+
onFailure: () => editor.editTemp({ suffix: ".md" }),
|
|
67
|
+
onSuccess: (path) => fs.readFileString(path).pipe(Effect.asSome),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (Option.isNone(thePlan)) return
|
|
71
|
+
|
|
72
|
+
yield* Effect.addFinalizer((exit) => {
|
|
73
|
+
if (Exit.isSuccess(exit)) return Effect.void
|
|
74
|
+
return pipe(
|
|
75
|
+
editor.saveTemp(thePlan.value, { suffix: ".md" }),
|
|
76
|
+
Effect.flatMap((file) => Effect.log(`Saved your plan to: ${file}`)),
|
|
77
|
+
Effect.ignore,
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// We nest this effect, so we can launch the editor first as fast as
|
|
82
|
+
// possible
|
|
83
|
+
yield* Effect.gen(function* () {
|
|
84
|
+
const project = withNewProject
|
|
85
|
+
? yield* addOrUpdateProject()
|
|
86
|
+
: yield* selectProject
|
|
87
|
+
const { specsDirectory } = yield* commandRoot
|
|
88
|
+
const preset = yield* selectCliAgentPreset
|
|
89
|
+
|
|
90
|
+
yield* plan({
|
|
91
|
+
plan: thePlan.value,
|
|
92
|
+
specsDirectory,
|
|
93
|
+
targetBranch: project.targetBranch,
|
|
94
|
+
dangerous,
|
|
95
|
+
preset,
|
|
96
|
+
}).pipe(Effect.provideService(CurrentProjectId, project.id))
|
|
97
|
+
}).pipe(
|
|
98
|
+
Effect.provide([
|
|
99
|
+
Settings.layer,
|
|
100
|
+
CurrentIssueSource.layer,
|
|
101
|
+
ClankaModels.layer,
|
|
102
|
+
]),
|
|
103
|
+
)
|
|
104
|
+
},
|
|
105
|
+
Effect.scoped,
|
|
106
|
+
Effect.provide(Editor.layer),
|
|
107
|
+
),
|
|
86
108
|
),
|
|
87
109
|
Command.withSubcommands([commandPlanTasks]),
|
|
88
110
|
)
|
package/src/commands/root.ts
CHANGED
|
@@ -6,9 +6,12 @@ import {
|
|
|
6
6
|
FiberSet,
|
|
7
7
|
FileSystem,
|
|
8
8
|
Iterable,
|
|
9
|
+
MutableRef,
|
|
9
10
|
Option,
|
|
10
11
|
Path,
|
|
11
12
|
PlatformError,
|
|
13
|
+
Result,
|
|
14
|
+
Schedule,
|
|
12
15
|
Schema,
|
|
13
16
|
Scope,
|
|
14
17
|
Semaphore,
|
|
@@ -48,6 +51,7 @@ import type { TimeoutError } from "effect/Cause"
|
|
|
48
51
|
import type { ChildProcessSpawner } from "effect/unstable/process"
|
|
49
52
|
import { ClankaModels } from "../ClankaModels.ts"
|
|
50
53
|
import type { AiError } from "effect/unstable/ai/AiError"
|
|
54
|
+
import type { PrdIssue } from "../domain/PrdIssue.ts"
|
|
51
55
|
|
|
52
56
|
// Main iteration run logic
|
|
53
57
|
|
|
@@ -201,18 +205,37 @@ const run = Effect.fnUntraced(
|
|
|
201
205
|
)
|
|
202
206
|
|
|
203
207
|
const promptGen = yield* PromptGen
|
|
204
|
-
const instructions =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
const instructions = taskPreset.cliAgent.command
|
|
209
|
+
? promptGen.prompt({
|
|
210
|
+
specsDirectory: options.specsDirectory,
|
|
211
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
212
|
+
task: chosenTask.prd,
|
|
213
|
+
githubPrNumber: chosenTask.githubPrNumber ?? undefined,
|
|
214
|
+
gitFlow,
|
|
215
|
+
})
|
|
216
|
+
: promptGen.promptClanka({
|
|
217
|
+
specsDirectory: options.specsDirectory,
|
|
218
|
+
targetBranch: Option.getOrUndefined(options.targetBranch),
|
|
219
|
+
task: chosenTask.prd,
|
|
220
|
+
githubPrNumber: chosenTask.githubPrNumber ?? undefined,
|
|
221
|
+
gitFlow,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const issueRef = MutableRef.make(
|
|
225
|
+
chosenTask.prd.update({
|
|
226
|
+
state: "in-progress",
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
const steer = yield* taskUpdateSteer({
|
|
230
|
+
issueId: taskId,
|
|
231
|
+
current: issueRef,
|
|
210
232
|
})
|
|
211
233
|
|
|
212
234
|
const exitCode = yield* agentWorker({
|
|
213
235
|
stallTimeout: options.stallTimeout,
|
|
214
236
|
preset: taskPreset,
|
|
215
237
|
prompt: instructions,
|
|
238
|
+
steer,
|
|
216
239
|
}).pipe(catchStallInReview, Effect.withSpan("Main.agentWorker"))
|
|
217
240
|
yield* Effect.log(`Agent exited with code: ${exitCode}`)
|
|
218
241
|
|
|
@@ -507,3 +530,33 @@ const watchTaskState = Effect.fnUntraced(function* (options: {
|
|
|
507
530
|
Effect.withSpan("Main.watchTaskState"),
|
|
508
531
|
)
|
|
509
532
|
})
|
|
533
|
+
|
|
534
|
+
const taskUpdateSteer = Effect.fnUntraced(function* (options: {
|
|
535
|
+
readonly issueId: string
|
|
536
|
+
readonly current: MutableRef.MutableRef<PrdIssue>
|
|
537
|
+
}) {
|
|
538
|
+
const registry = yield* AtomRegistry.AtomRegistry
|
|
539
|
+
const projectId = yield* CurrentProjectId
|
|
540
|
+
|
|
541
|
+
return AtomRegistry.toStreamResult(
|
|
542
|
+
registry,
|
|
543
|
+
currentIssuesAtom(projectId),
|
|
544
|
+
).pipe(
|
|
545
|
+
Stream.drop(1),
|
|
546
|
+
Stream.retry(Schedule.forever),
|
|
547
|
+
Stream.orDie,
|
|
548
|
+
Stream.filterMap((issues) => {
|
|
549
|
+
const issue = issues.find((entry) => entry.id === options.issueId)
|
|
550
|
+
if (!issue) return Result.failVoid
|
|
551
|
+
if (!issue.isChangedComparedTo(options.current.current)) {
|
|
552
|
+
return Result.failVoid
|
|
553
|
+
}
|
|
554
|
+
MutableRef.set(options.current, issue)
|
|
555
|
+
return Result.succeed(`The task has been updated by the user. Here is the latest information:
|
|
556
|
+
|
|
557
|
+
# ${issue.title}
|
|
558
|
+
|
|
559
|
+
${issue.description}`)
|
|
560
|
+
}),
|
|
561
|
+
)
|
|
562
|
+
})
|
package/src/domain/PrdIssue.ts
CHANGED
|
@@ -105,4 +105,19 @@ export class PrdIssue extends Schema.Class<PrdIssue>("PrdIssue")({
|
|
|
105
105
|
autoMerge,
|
|
106
106
|
})
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
update(options: {
|
|
110
|
+
readonly title?: string | undefined
|
|
111
|
+
readonly description?: string | undefined
|
|
112
|
+
readonly state?: PrdIssue["state"] | undefined
|
|
113
|
+
readonly blockedBy?: ReadonlyArray<string> | undefined
|
|
114
|
+
}): PrdIssue {
|
|
115
|
+
return new PrdIssue({
|
|
116
|
+
...this,
|
|
117
|
+
title: options.title ?? this.title,
|
|
118
|
+
description: options.description ?? this.description,
|
|
119
|
+
state: options.state ?? this.state,
|
|
120
|
+
blockedBy: options.blockedBy ?? this.blockedBy,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
108
123
|
}
|