lalph 0.3.46 → 0.3.48
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 +534 -91
- package/package.json +2 -2
- package/src/Agents/worker.ts +3 -1
- package/src/Clanka.ts +34 -16
- package/src/Editor.ts +12 -1
- package/src/PromptGen.ts +5 -6
- package/src/TaskTools.ts +56 -38
- package/src/commands/issue.ts +13 -3
- package/src/commands/plan/tasks.ts +2 -0
- package/src/commands/plan.ts +60 -36
- package/src/commands/root.ts +55 -1
- 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.48",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
|
31
31
|
"@octokit/types": "^16.0.0",
|
|
32
32
|
"@typescript/native-preview": "7.0.0-dev.20260310.1",
|
|
33
|
-
"clanka": "^0.0.
|
|
33
|
+
"clanka": "^0.0.21",
|
|
34
34
|
"concurrently": "^9.2.1",
|
|
35
35
|
"effect": "4.0.0-beta.30",
|
|
36
36
|
"husky": "^9.1.7",
|
package/src/Agents/worker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Duration, Effect, Path, pipe } from "effect"
|
|
1
|
+
import { Duration, Effect, 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"
|
|
@@ -10,6 +10,7 @@ export const agentWorker = Effect.fnUntraced(function* (options: {
|
|
|
10
10
|
readonly preset: CliAgentPreset
|
|
11
11
|
readonly system?: string
|
|
12
12
|
readonly prompt: string
|
|
13
|
+
readonly steer?: Stream.Stream<string>
|
|
13
14
|
}) {
|
|
14
15
|
const pathService = yield* Path.Path
|
|
15
16
|
const worktree = yield* Worktree
|
|
@@ -22,6 +23,7 @@ export const agentWorker = Effect.fnUntraced(function* (options: {
|
|
|
22
23
|
system: options.system,
|
|
23
24
|
prompt: options.prompt,
|
|
24
25
|
stallTimeout: options.stallTimeout,
|
|
26
|
+
steer: options.steer,
|
|
25
27
|
})
|
|
26
28
|
return ExitCode(0)
|
|
27
29
|
}
|
package/src/Clanka.ts
CHANGED
|
@@ -1,47 +1,65 @@
|
|
|
1
1
|
import { Agent, OutputFormatter } from "clanka"
|
|
2
|
-
import { Duration, Effect, Stream } from "effect"
|
|
2
|
+
import { Duration, Effect, Layer, Stdio, Stream } from "effect"
|
|
3
3
|
import {
|
|
4
|
+
TaskChooseTools,
|
|
4
5
|
TaskTools,
|
|
5
6
|
TaskToolsHandlers,
|
|
6
7
|
TaskToolsWithChoose,
|
|
7
8
|
} from "./TaskTools.ts"
|
|
8
9
|
import { ClankaModels, clankaSubagent } from "./ClankaModels.ts"
|
|
9
10
|
import { withStallTimeout } from "./shared/stream.ts"
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
export const ClankaMuxerLayer = Layer.effectDiscard(
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const muxer = yield* OutputFormatter.Muxer
|
|
15
|
+
const stdio = yield* Stdio.Stdio
|
|
16
|
+
yield* muxer.output.pipe(Stream.run(stdio.stdout()), Effect.forkScoped)
|
|
17
|
+
}),
|
|
18
|
+
).pipe(Layer.provideMerge(OutputFormatter.layerMuxer(OutputFormatter.pretty)))
|
|
12
19
|
|
|
13
20
|
export const runClanka = Effect.fnUntraced(
|
|
14
|
-
/** The working directory to run the agent in */
|
|
15
21
|
function* (options: {
|
|
16
22
|
readonly directory: string
|
|
17
23
|
readonly model: string
|
|
18
24
|
readonly prompt: string
|
|
19
25
|
readonly system?: string | undefined
|
|
20
26
|
readonly stallTimeout?: Duration.Input | undefined
|
|
27
|
+
readonly steer?: Stream.Stream<string> | undefined
|
|
21
28
|
readonly withChoose?: boolean | undefined
|
|
22
29
|
}) {
|
|
23
30
|
const models = yield* ClankaModels
|
|
31
|
+
const muxer = yield* OutputFormatter.Muxer
|
|
32
|
+
|
|
24
33
|
const agent = yield* Agent.make({
|
|
25
34
|
...options,
|
|
26
|
-
tools: options.withChoose
|
|
27
|
-
?
|
|
28
|
-
:
|
|
35
|
+
tools: (options.withChoose
|
|
36
|
+
? TaskChooseTools
|
|
37
|
+
: TaskTools) as unknown as typeof TaskToolsWithChoose,
|
|
29
38
|
subagentModel: clankaSubagent(models, options.model),
|
|
30
39
|
}).pipe(Effect.provide(models.get(options.model)))
|
|
31
40
|
|
|
41
|
+
yield* muxer.add(agent.output)
|
|
42
|
+
|
|
32
43
|
let stream = options.stallTimeout
|
|
33
44
|
? withStallTimeout(options.stallTimeout)(agent.output)
|
|
34
45
|
: agent.output
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
if (options.steer) {
|
|
48
|
+
yield* options.steer.pipe(
|
|
49
|
+
Stream.switchMap(
|
|
50
|
+
Effect.fnUntraced(function* (message) {
|
|
51
|
+
yield* Effect.log(`Received steer message: ${message}`)
|
|
52
|
+
yield* agent.steer(message)
|
|
53
|
+
}, Stream.fromEffectDrain),
|
|
54
|
+
),
|
|
55
|
+
Stream.runDrain,
|
|
56
|
+
Effect.forkScoped,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
yield* stream.pipe(
|
|
61
|
+
Stream.runDrain,
|
|
62
|
+
Effect.catchTag("AgentFinished", () => Effect.void),
|
|
45
63
|
)
|
|
46
64
|
},
|
|
47
65
|
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
|
@@ -90,12 +90,11 @@ Set \`githubPrNumber\` to the PR number if one exists, otherwise use \`null\`.
|
|
|
90
90
|
|
|
91
91
|
const promptChooseClanka = (options: {
|
|
92
92
|
readonly gitFlow: GitFlow["Service"]
|
|
93
|
-
}) =>
|
|
94
|
-
**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- Decide which single task to work on next from "listEligibleTasks". This should
|
|
93
|
+
}) => `- Use the "listEligibleTasks" function to view the list of tasks that you can start working on.
|
|
94
|
+
- **NO NOT PARSE THE yaml OUTPUT IN ANY WAY**
|
|
95
|
+
- **DO NOT** implement the task yet.
|
|
96
|
+
- **DO NOT** use the "delegate" function for any step in this workflow
|
|
97
|
+
- After reading through the list of tasks, choose the task to work on. This should
|
|
99
98
|
be the task YOU decide as the most important to work on next, not just the
|
|
100
99
|
first task in the list.${
|
|
101
100
|
options.gitFlow.requiresGithubPr
|
package/src/TaskTools.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Deferred,
|
|
3
|
+
Effect,
|
|
4
|
+
MutableRef,
|
|
5
|
+
Option,
|
|
6
|
+
Random,
|
|
7
|
+
Schema,
|
|
8
|
+
ServiceMap,
|
|
9
|
+
Struct,
|
|
10
|
+
} from "effect"
|
|
2
11
|
import { Tool, Toolkit } from "effect/unstable/ai"
|
|
3
12
|
import { PrdIssue } from "./domain/PrdIssue.ts"
|
|
4
13
|
import { IssueSource } from "./IssueSource.ts"
|
|
5
14
|
import { CurrentProjectId } from "./Settings.ts"
|
|
15
|
+
import * as Yaml from "yaml"
|
|
6
16
|
|
|
7
17
|
export class ChosenTaskDeferred extends ServiceMap.Reference(
|
|
8
18
|
"lalph/TaskTools/ChosenTaskDeferred",
|
|
@@ -14,6 +24,17 @@ export class ChosenTaskDeferred extends ServiceMap.Reference(
|
|
|
14
24
|
},
|
|
15
25
|
) {}
|
|
16
26
|
|
|
27
|
+
export class CurrentTaskRef extends ServiceMap.Service<
|
|
28
|
+
CurrentTaskRef,
|
|
29
|
+
MutableRef.MutableRef<PrdIssue>
|
|
30
|
+
>()("lalph/TaskTools/CurrentTaskRef") {
|
|
31
|
+
static update(f: (prev: PrdIssue) => PrdIssue) {
|
|
32
|
+
return Effect.serviceOption(CurrentTaskRef).pipe(
|
|
33
|
+
Effect.map(Option.map((ref) => MutableRef.updateAndGet(ref, f))),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
const TaskList = Schema.Array(
|
|
18
39
|
Schema.Struct({
|
|
19
40
|
id: Schema.String.annotate({
|
|
@@ -24,12 +45,20 @@ const TaskList = Schema.Array(
|
|
|
24
45
|
"description",
|
|
25
46
|
"state",
|
|
26
47
|
"priority",
|
|
27
|
-
"estimate",
|
|
28
48
|
"blockedBy",
|
|
29
49
|
]),
|
|
30
50
|
}),
|
|
31
51
|
)
|
|
32
52
|
|
|
53
|
+
const toTaskListItem = (issue: PrdIssue) => ({
|
|
54
|
+
id: issue.id ?? "",
|
|
55
|
+
title: issue.title,
|
|
56
|
+
description: issue.description,
|
|
57
|
+
state: issue.state,
|
|
58
|
+
priority: issue.priority,
|
|
59
|
+
blockedBy: issue.blockedBy,
|
|
60
|
+
})
|
|
61
|
+
|
|
33
62
|
export class TaskTools extends Toolkit.make(
|
|
34
63
|
Tool.make("listTasks", {
|
|
35
64
|
description: "Returns the current list of tasks.",
|
|
@@ -43,7 +72,6 @@ export class TaskTools extends Toolkit.make(
|
|
|
43
72
|
description: PrdIssue.fields.description,
|
|
44
73
|
state: PrdIssue.fields.state,
|
|
45
74
|
priority: PrdIssue.fields.priority,
|
|
46
|
-
estimate: PrdIssue.fields.estimate,
|
|
47
75
|
blockedBy: PrdIssue.fields.blockedBy,
|
|
48
76
|
}),
|
|
49
77
|
success: Schema.String,
|
|
@@ -69,22 +97,25 @@ export class TaskTools extends Toolkit.make(
|
|
|
69
97
|
}),
|
|
70
98
|
) {}
|
|
71
99
|
|
|
100
|
+
export class TaskChooseTools extends Toolkit.make(
|
|
101
|
+
Tool.make("chooseTask", {
|
|
102
|
+
description: "Choose the task to work on",
|
|
103
|
+
parameters: Schema.Struct({
|
|
104
|
+
taskId: Schema.String,
|
|
105
|
+
githubPrNumber: Schema.optional(Schema.Number),
|
|
106
|
+
}),
|
|
107
|
+
}),
|
|
108
|
+
Tool.make("listEligibleTasks", {
|
|
109
|
+
description:
|
|
110
|
+
"List tasks eligible for being chosen with chooseTask in yaml format.",
|
|
111
|
+
success: Schema.String,
|
|
112
|
+
dependencies: [CurrentProjectId],
|
|
113
|
+
}),
|
|
114
|
+
) {}
|
|
115
|
+
|
|
72
116
|
export class TaskToolsWithChoose extends Toolkit.merge(
|
|
73
117
|
TaskTools,
|
|
74
|
-
|
|
75
|
-
Tool.make("chooseTask", {
|
|
76
|
-
description: "Choose the task to work on",
|
|
77
|
-
parameters: Schema.Struct({
|
|
78
|
-
taskId: Schema.String,
|
|
79
|
-
githubPrNumber: Schema.optional(Schema.Number),
|
|
80
|
-
}),
|
|
81
|
-
}),
|
|
82
|
-
Tool.make("listEligibleTasks", {
|
|
83
|
-
description: "List tasks eligible for being chosen with chooseTask.",
|
|
84
|
-
success: TaskList,
|
|
85
|
-
dependencies: [CurrentProjectId],
|
|
86
|
-
}),
|
|
87
|
-
),
|
|
118
|
+
TaskChooseTools,
|
|
88
119
|
) {}
|
|
89
120
|
|
|
90
121
|
export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
@@ -96,31 +127,16 @@ export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
|
96
127
|
yield* Effect.log(`Calling "listTasks"`)
|
|
97
128
|
const projectId = yield* CurrentProjectId
|
|
98
129
|
const tasks = yield* source.issues(projectId)
|
|
99
|
-
return tasks.map(
|
|
100
|
-
id: issue.id ?? "",
|
|
101
|
-
title: issue.title,
|
|
102
|
-
description: issue.description,
|
|
103
|
-
state: issue.state,
|
|
104
|
-
priority: issue.priority,
|
|
105
|
-
estimate: issue.estimate,
|
|
106
|
-
blockedBy: issue.blockedBy,
|
|
107
|
-
}))
|
|
130
|
+
return tasks.map(toTaskListItem)
|
|
108
131
|
}, Effect.orDie),
|
|
109
132
|
listEligibleTasks: Effect.fn("TaskTools.listEligibleTasks")(function* () {
|
|
110
133
|
yield* Effect.log(`Calling "listEligibleTasks"`)
|
|
111
134
|
const projectId = yield* CurrentProjectId
|
|
112
|
-
const tasks = yield* source.issues(projectId)
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
title: issue.title,
|
|
118
|
-
description: issue.description,
|
|
119
|
-
state: issue.state,
|
|
120
|
-
priority: issue.priority,
|
|
121
|
-
estimate: issue.estimate,
|
|
122
|
-
blockedBy: issue.blockedBy,
|
|
123
|
-
}))
|
|
135
|
+
const tasks = (yield* source.issues(projectId))
|
|
136
|
+
.filter((t) => t.state === "todo" && t.blockedBy.length === 0)
|
|
137
|
+
.map(toTaskListItem)
|
|
138
|
+
const shuffled = yield* Random.shuffle(tasks)
|
|
139
|
+
return Yaml.stringify(shuffled, null, 2)
|
|
124
140
|
}, Effect.orDie),
|
|
125
141
|
chooseTask: Effect.fn("TaskTools.chooseTask")(function* (options) {
|
|
126
142
|
yield* Effect.log(`Calling "chooseTask"`).pipe(
|
|
@@ -137,6 +153,7 @@ export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
|
137
153
|
new PrdIssue({
|
|
138
154
|
...options,
|
|
139
155
|
id: null,
|
|
156
|
+
estimate: null,
|
|
140
157
|
autoMerge: false,
|
|
141
158
|
}),
|
|
142
159
|
)
|
|
@@ -147,6 +164,7 @@ export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
|
147
164
|
Effect.annotateLogs({ taskId: options.taskId }),
|
|
148
165
|
)
|
|
149
166
|
const projectId = yield* CurrentProjectId
|
|
167
|
+
yield* CurrentTaskRef.update((prev) => prev.update(options))
|
|
150
168
|
yield* source.updateIssue({
|
|
151
169
|
projectId,
|
|
152
170
|
issueId: options.taskId,
|
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
|
)
|
|
@@ -11,6 +11,7 @@ import { selectCliAgentPreset } from "../../Presets.ts"
|
|
|
11
11
|
import { CurrentIssueSource } from "../../CurrentIssueSource.ts"
|
|
12
12
|
import type { CliAgentPreset } from "../../domain/CliAgentPreset.ts"
|
|
13
13
|
import { ClankaModels } from "../../ClankaModels.ts"
|
|
14
|
+
import { ClankaMuxerLayer } from "../../Clanka.ts"
|
|
14
15
|
|
|
15
16
|
const specificationPath = Argument.path("spec", {
|
|
16
17
|
pathType: "file",
|
|
@@ -72,6 +73,7 @@ const generateTasks = Effect.fnUntraced(
|
|
|
72
73
|
},
|
|
73
74
|
Effect.provide([
|
|
74
75
|
ClankaModels.layer,
|
|
76
|
+
ClankaMuxerLayer,
|
|
75
77
|
Settings.layer,
|
|
76
78
|
PromptGen.layer,
|
|
77
79
|
Prd.layerProvided.pipe(Layer.provideMerge(layerProjectIdPrompt)),
|
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"
|
|
@@ -16,6 +25,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|
|
16
25
|
import { parseBranch } from "../shared/git.ts"
|
|
17
26
|
import type { CliAgentPreset } from "../domain/CliAgentPreset.ts"
|
|
18
27
|
import { ClankaModels } from "../ClankaModels.ts"
|
|
28
|
+
import { ClankaMuxerLayer } from "../Clanka.ts"
|
|
19
29
|
|
|
20
30
|
const dangerous = Flag.boolean("dangerous").pipe(
|
|
21
31
|
Flag.withAlias("d"),
|
|
@@ -48,41 +58,55 @@ export const commandPlan = Command.make("plan", {
|
|
|
48
58
|
"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
59
|
),
|
|
50
60
|
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
|
-
|
|
61
|
+
Effect.fnUntraced(
|
|
62
|
+
function* ({ dangerous, withNewProject, file }) {
|
|
63
|
+
const editor = yield* Editor
|
|
64
|
+
const fs = yield* FileSystem.FileSystem
|
|
65
|
+
|
|
66
|
+
const thePlan = yield* Effect.matchEffect(file.asEffect(), {
|
|
67
|
+
onFailure: () => editor.editTemp({ suffix: ".md" }),
|
|
68
|
+
onSuccess: (path) => fs.readFileString(path).pipe(Effect.asSome),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (Option.isNone(thePlan)) return
|
|
72
|
+
|
|
73
|
+
yield* Effect.addFinalizer((exit) => {
|
|
74
|
+
if (Exit.isSuccess(exit)) return Effect.void
|
|
75
|
+
return pipe(
|
|
76
|
+
editor.saveTemp(thePlan.value, { suffix: ".md" }),
|
|
77
|
+
Effect.flatMap((file) => Effect.log(`Saved your plan to: ${file}`)),
|
|
78
|
+
Effect.ignore,
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// We nest this effect, so we can launch the editor first as fast as
|
|
83
|
+
// possible
|
|
84
|
+
yield* Effect.gen(function* () {
|
|
85
|
+
const project = withNewProject
|
|
86
|
+
? yield* addOrUpdateProject()
|
|
87
|
+
: yield* selectProject
|
|
88
|
+
const { specsDirectory } = yield* commandRoot
|
|
89
|
+
const preset = yield* selectCliAgentPreset
|
|
90
|
+
|
|
91
|
+
yield* plan({
|
|
92
|
+
plan: thePlan.value,
|
|
93
|
+
specsDirectory,
|
|
94
|
+
targetBranch: project.targetBranch,
|
|
95
|
+
dangerous,
|
|
96
|
+
preset,
|
|
97
|
+
}).pipe(Effect.provideService(CurrentProjectId, project.id))
|
|
98
|
+
}).pipe(
|
|
99
|
+
Effect.provide([
|
|
100
|
+
Settings.layer,
|
|
101
|
+
CurrentIssueSource.layer,
|
|
102
|
+
ClankaModels.layer,
|
|
103
|
+
ClankaMuxerLayer,
|
|
104
|
+
]),
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
Effect.scoped,
|
|
108
|
+
Effect.provide(Editor.layer),
|
|
109
|
+
),
|
|
86
110
|
),
|
|
87
111
|
Command.withSubcommands([commandPlanTasks]),
|
|
88
112
|
)
|
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,10 @@ 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"
|
|
55
|
+
import { CurrentTaskRef } from "../TaskTools.ts"
|
|
56
|
+
import type { OutputFormatter } from "clanka"
|
|
57
|
+
import { ClankaMuxerLayer } from "../Clanka.ts"
|
|
51
58
|
|
|
52
59
|
// Main iteration run logic
|
|
53
60
|
|
|
@@ -84,6 +91,7 @@ const run = Effect.fnUntraced(
|
|
|
84
91
|
| Prd
|
|
85
92
|
| Worktree
|
|
86
93
|
| ClankaModels
|
|
94
|
+
| OutputFormatter.Muxer
|
|
87
95
|
| Scope.Scope
|
|
88
96
|
> {
|
|
89
97
|
const projectId = yield* CurrentProjectId
|
|
@@ -217,11 +225,26 @@ const run = Effect.fnUntraced(
|
|
|
217
225
|
gitFlow,
|
|
218
226
|
})
|
|
219
227
|
|
|
228
|
+
const issueRef = MutableRef.make(
|
|
229
|
+
chosenTask.prd.update({
|
|
230
|
+
state: "in-progress",
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
const steer = yield* taskUpdateSteer({
|
|
234
|
+
issueId: taskId,
|
|
235
|
+
current: issueRef,
|
|
236
|
+
})
|
|
237
|
+
|
|
220
238
|
const exitCode = yield* agentWorker({
|
|
221
239
|
stallTimeout: options.stallTimeout,
|
|
222
240
|
preset: taskPreset,
|
|
223
241
|
prompt: instructions,
|
|
224
|
-
|
|
242
|
+
steer,
|
|
243
|
+
}).pipe(
|
|
244
|
+
Effect.provideService(CurrentTaskRef, issueRef),
|
|
245
|
+
catchStallInReview,
|
|
246
|
+
Effect.withSpan("Main.agentWorker"),
|
|
247
|
+
)
|
|
225
248
|
yield* Effect.log(`Agent exited with code: ${exitCode}`)
|
|
226
249
|
|
|
227
250
|
// 3. Review task
|
|
@@ -469,6 +492,7 @@ export const commandRoot = Command.make("lalph", {
|
|
|
469
492
|
Effect.scoped,
|
|
470
493
|
Effect.provide([
|
|
471
494
|
ClankaModels.layer,
|
|
495
|
+
ClankaMuxerLayer,
|
|
472
496
|
PromptGen.layer,
|
|
473
497
|
GithubCli.layer,
|
|
474
498
|
Settings.layer,
|
|
@@ -515,3 +539,33 @@ const watchTaskState = Effect.fnUntraced(function* (options: {
|
|
|
515
539
|
Effect.withSpan("Main.watchTaskState"),
|
|
516
540
|
)
|
|
517
541
|
})
|
|
542
|
+
|
|
543
|
+
const taskUpdateSteer = Effect.fnUntraced(function* (options: {
|
|
544
|
+
readonly issueId: string
|
|
545
|
+
readonly current: MutableRef.MutableRef<PrdIssue>
|
|
546
|
+
}) {
|
|
547
|
+
const registry = yield* AtomRegistry.AtomRegistry
|
|
548
|
+
const projectId = yield* CurrentProjectId
|
|
549
|
+
|
|
550
|
+
return AtomRegistry.toStreamResult(
|
|
551
|
+
registry,
|
|
552
|
+
currentIssuesAtom(projectId),
|
|
553
|
+
).pipe(
|
|
554
|
+
Stream.drop(1),
|
|
555
|
+
Stream.retry(Schedule.forever),
|
|
556
|
+
Stream.orDie,
|
|
557
|
+
Stream.filterMap((issues) => {
|
|
558
|
+
const issue = issues.find((entry) => entry.id === options.issueId)
|
|
559
|
+
if (!issue) return Result.failVoid
|
|
560
|
+
if (!issue.isChangedComparedTo(options.current.current)) {
|
|
561
|
+
return Result.failVoid
|
|
562
|
+
}
|
|
563
|
+
MutableRef.set(options.current, issue)
|
|
564
|
+
return Result.succeed(`The task has been updated by the user. Here is the latest information:
|
|
565
|
+
|
|
566
|
+
# ${issue.title}
|
|
567
|
+
|
|
568
|
+
${issue.description}`)
|
|
569
|
+
}),
|
|
570
|
+
)
|
|
571
|
+
})
|
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
|
}
|