lalph 0.3.37 → 0.3.39
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 +69743 -32260
- package/package.json +13 -11
- package/src/Clanka.ts +46 -0
- package/src/ClankaModels.ts +53 -0
- package/src/GitFlow.ts +1 -1
- package/src/IssueSource.ts +5 -5
- package/src/Presets.ts +18 -0
- package/src/PromptGen.ts +151 -24
- package/src/TaskTools.ts +142 -0
- package/src/Worktree.ts +2 -21
- package/src/commands/root.ts +311 -14
- package/src/domain/CliAgentPreset.ts +2 -0
- package/src/domain/PrdIssue.ts +7 -0
- package/src/shared/stream.ts +29 -1
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.39",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -20,22 +20,24 @@
|
|
|
20
20
|
"url": "https://github.com/tim-smart/lalph.git"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@changesets/changelog-github": "^0.
|
|
24
|
-
"@changesets/cli": "^2.
|
|
25
|
-
"@effect/
|
|
26
|
-
"@effect/
|
|
27
|
-
"@
|
|
23
|
+
"@changesets/changelog-github": "^0.6.0",
|
|
24
|
+
"@changesets/cli": "^2.30.0",
|
|
25
|
+
"@effect/ai-openai": "https://pkg.pr.new/Effect-TS/effect-smol/@effect/ai-openai@d440fd4",
|
|
26
|
+
"@effect/language-service": "^0.79.0",
|
|
27
|
+
"@effect/platform-node": "4.0.0-beta.29",
|
|
28
|
+
"@linear/sdk": "^76.0.0",
|
|
28
29
|
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
|
29
30
|
"@octokit/types": "^16.0.0",
|
|
30
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
31
|
+
"@typescript/native-preview": "7.0.0-dev.20260308.1",
|
|
32
|
+
"clanka": "^0.0.9",
|
|
31
33
|
"concurrently": "^9.2.1",
|
|
32
|
-
"effect": "4.0.0-beta.
|
|
34
|
+
"effect": "4.0.0-beta.29",
|
|
33
35
|
"husky": "^9.1.7",
|
|
34
|
-
"lint-staged": "^16.2
|
|
36
|
+
"lint-staged": "^16.3.2",
|
|
35
37
|
"octokit": "^5.0.5",
|
|
36
|
-
"oxlint": "^1.
|
|
38
|
+
"oxlint": "^1.51.0",
|
|
37
39
|
"prettier": "^3.8.1",
|
|
38
|
-
"tsdown": "^0.
|
|
40
|
+
"tsdown": "^0.21.1",
|
|
39
41
|
"typescript": "^5.9.3",
|
|
40
42
|
"yaml": "^2.8.2"
|
|
41
43
|
},
|
package/src/Clanka.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Agent, OutputFormatter } from "clanka"
|
|
2
|
+
import { Duration, Effect, Stream } from "effect"
|
|
3
|
+
import {
|
|
4
|
+
TaskTools,
|
|
5
|
+
TaskToolsHandlers,
|
|
6
|
+
TaskToolsWithChoose,
|
|
7
|
+
} from "./TaskTools.ts"
|
|
8
|
+
import { clankaSubagent } from "./ClankaModels.ts"
|
|
9
|
+
import { withStallTimeout } from "./shared/stream.ts"
|
|
10
|
+
import type { AiError } from "effect/unstable/ai"
|
|
11
|
+
import type { RunnerStalled } from "./domain/Errors.ts"
|
|
12
|
+
|
|
13
|
+
export const runClanka = Effect.fnUntraced(
|
|
14
|
+
/** The working directory to run the agent in */
|
|
15
|
+
function* (options: {
|
|
16
|
+
readonly directory: string
|
|
17
|
+
readonly prompt: string
|
|
18
|
+
readonly system?: string | undefined
|
|
19
|
+
readonly stallTimeout?: Duration.Input | undefined
|
|
20
|
+
readonly withChoose?: boolean | undefined
|
|
21
|
+
}) {
|
|
22
|
+
const agent = yield* Agent.make({
|
|
23
|
+
...options,
|
|
24
|
+
tools: options.withChoose
|
|
25
|
+
? TaskToolsWithChoose
|
|
26
|
+
: (TaskTools as unknown as typeof TaskToolsWithChoose),
|
|
27
|
+
subagentModel: clankaSubagent,
|
|
28
|
+
})
|
|
29
|
+
let stream = options.stallTimeout
|
|
30
|
+
? withStallTimeout(options.stallTimeout)(agent.output)
|
|
31
|
+
: agent.output
|
|
32
|
+
|
|
33
|
+
return yield* stream.pipe(
|
|
34
|
+
OutputFormatter.pretty,
|
|
35
|
+
Stream.runForEachArray((out) => {
|
|
36
|
+
for (const item of out) {
|
|
37
|
+
process.stdout.write(item)
|
|
38
|
+
}
|
|
39
|
+
return Effect.void
|
|
40
|
+
}),
|
|
41
|
+
(_) => _ as Effect.Effect<void, AiError.AiError | RunnerStalled>,
|
|
42
|
+
)
|
|
43
|
+
},
|
|
44
|
+
Effect.scoped,
|
|
45
|
+
Effect.provide([Agent.layerServices, TaskToolsHandlers]),
|
|
46
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NodeHttpClient } from "@effect/platform-node"
|
|
2
|
+
import { Codex } from "clanka"
|
|
3
|
+
import { Layer, LayerMap, PlatformError, Schema } from "effect"
|
|
4
|
+
import { layerKvs } from "./Kvs.ts"
|
|
5
|
+
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
|
|
6
|
+
|
|
7
|
+
export const CodexLayer: Layer.Layer<
|
|
8
|
+
OpenAiClient.OpenAiClient,
|
|
9
|
+
PlatformError.PlatformError
|
|
10
|
+
> = Codex.layer.pipe(
|
|
11
|
+
Layer.provide(NodeHttpClient.layerUndici),
|
|
12
|
+
Layer.provide(layerKvs),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export const clankaModels = {
|
|
16
|
+
"gpt-5.4-xhigh": OpenAiLanguageModel.model("gpt-5.4", {
|
|
17
|
+
reasoning: {
|
|
18
|
+
effort: "xhigh",
|
|
19
|
+
summary: "auto",
|
|
20
|
+
},
|
|
21
|
+
}).pipe(Layer.provideMerge(CodexLayer)),
|
|
22
|
+
"gpt-5.4-high": OpenAiLanguageModel.model("gpt-5.4", {
|
|
23
|
+
reasoning: {
|
|
24
|
+
effort: "high",
|
|
25
|
+
summary: "auto",
|
|
26
|
+
},
|
|
27
|
+
}).pipe(Layer.provideMerge(CodexLayer)),
|
|
28
|
+
"gpt-5.4-medium": OpenAiLanguageModel.model("gpt-5.4", {
|
|
29
|
+
reasoning: {
|
|
30
|
+
effort: "high",
|
|
31
|
+
summary: "auto",
|
|
32
|
+
},
|
|
33
|
+
}).pipe(Layer.provideMerge(CodexLayer)),
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
export type ClankaModel = keyof typeof clankaModels
|
|
37
|
+
export const ClankaModel = Schema.Literals(
|
|
38
|
+
Object.keys(clankaModels) as ClankaModel[],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export const clankaSubagent = OpenAiLanguageModel.model("gpt-5.4", {
|
|
42
|
+
reasoning: {
|
|
43
|
+
effort: "low",
|
|
44
|
+
summary: "auto",
|
|
45
|
+
},
|
|
46
|
+
}).pipe(Layer.provideMerge(CodexLayer))
|
|
47
|
+
|
|
48
|
+
export class ClankaModels extends LayerMap.Service<ClankaModels>()(
|
|
49
|
+
"lalph/ClankaModels",
|
|
50
|
+
{
|
|
51
|
+
layers: clankaModels,
|
|
52
|
+
},
|
|
53
|
+
) {}
|
package/src/GitFlow.ts
CHANGED
|
@@ -61,7 +61,7 @@ export const GitFlowPR = Layer.succeed(
|
|
|
61
61
|
setupInstructions: ({ githubPrNumber }) =>
|
|
62
62
|
githubPrNumber
|
|
63
63
|
? `The Github PR #${githubPrNumber} has been detected for this task and the branch has been checked out.
|
|
64
|
-
- Review feedback in the .lalph/feedback.md file
|
|
64
|
+
- Review feedback in the .lalph/feedback.md file.`
|
|
65
65
|
: `Create a new branch for the task using the format \`{task id}/description\`, using the current HEAD as the base (don't checkout any other branches first).`,
|
|
66
66
|
|
|
67
67
|
commitInstructions: (
|
package/src/IssueSource.ts
CHANGED
|
@@ -22,11 +22,11 @@ export class IssueSource extends ServiceMap.Service<
|
|
|
22
22
|
readonly updateIssue: (options: {
|
|
23
23
|
readonly projectId: ProjectId
|
|
24
24
|
readonly issueId: string
|
|
25
|
-
readonly title?: string
|
|
26
|
-
readonly description?: string
|
|
27
|
-
readonly state?: PrdIssue["state"]
|
|
28
|
-
readonly blockedBy?: ReadonlyArray<string>
|
|
29
|
-
readonly autoMerge?: boolean
|
|
25
|
+
readonly title?: string | undefined
|
|
26
|
+
readonly description?: string | undefined
|
|
27
|
+
readonly state?: PrdIssue["state"] | undefined
|
|
28
|
+
readonly blockedBy?: ReadonlyArray<string> | undefined
|
|
29
|
+
readonly autoMerge?: boolean | undefined
|
|
30
30
|
}) => Effect.Effect<void, IssueSourceError>
|
|
31
31
|
|
|
32
32
|
readonly cancelIssue: (
|
package/src/Presets.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Prompt } from "effect/unstable/cli"
|
|
|
5
5
|
import { allCliAgents, type AnyCliAgent } from "./domain/CliAgent.ts"
|
|
6
6
|
import { parseCommand } from "./shared/child-process.ts"
|
|
7
7
|
import { IssueSource } from "./IssueSource.ts"
|
|
8
|
+
import { ClankaModel, clankaModels } from "./ClankaModels.ts"
|
|
8
9
|
|
|
9
10
|
export const allCliAgentPresets = new Setting(
|
|
10
11
|
"cliAgentPresets",
|
|
@@ -119,12 +120,29 @@ export const addOrUpdatePreset = Effect.fnUntraced(function* (options?: {
|
|
|
119
120
|
options?.existing?.commandPrefix,
|
|
120
121
|
)
|
|
121
122
|
|
|
123
|
+
const clankaModel = yield* Prompt.select<ClankaModel | undefined>({
|
|
124
|
+
message: "clanka model?",
|
|
125
|
+
choices: [
|
|
126
|
+
{
|
|
127
|
+
title: "none",
|
|
128
|
+
value: undefined,
|
|
129
|
+
selected: options?.existing?.clankaModel === undefined,
|
|
130
|
+
},
|
|
131
|
+
...(Object.keys(clankaModels) as Array<ClankaModel>).map((key) => ({
|
|
132
|
+
title: key,
|
|
133
|
+
value: key,
|
|
134
|
+
selected: options?.existing?.clankaModel === key,
|
|
135
|
+
})),
|
|
136
|
+
],
|
|
137
|
+
})
|
|
138
|
+
|
|
122
139
|
let preset = new CliAgentPreset({
|
|
123
140
|
id,
|
|
124
141
|
cliAgent,
|
|
125
142
|
commandPrefix,
|
|
126
143
|
extraArgs,
|
|
127
144
|
sourceMetadata: {},
|
|
145
|
+
...(clankaModel ? { clankaModel } : {}),
|
|
128
146
|
})
|
|
129
147
|
|
|
130
148
|
if (id !== CliAgentPreset.defaultId) {
|
package/src/PromptGen.ts
CHANGED
|
@@ -39,28 +39,7 @@ prd.yml file with a new id for the task.
|
|
|
39
39
|
|
|
40
40
|
After adding a new task, you can setup dependencies using the \`blockedBy\` field
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
**Important**: When creating tasks, make sure each task is independently shippable
|
|
45
|
-
without failing validation checks (typechecks, linting, tests). If a task would only
|
|
46
|
-
pass validations when combined with another, combine the work into one task.
|
|
47
|
-
|
|
48
|
-
Each task should be an atomic, committable piece of work.
|
|
49
|
-
Instead of creating tasks like "Refactor the authentication system", create
|
|
50
|
-
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.${
|
|
51
|
-
options?.specsDirectory
|
|
52
|
-
? `
|
|
53
|
-
|
|
54
|
-
If you need to add a research task, mention in the description that it needs to:
|
|
55
|
-
- add a specification file in the \`${options.specsDirectory}\` directory with
|
|
56
|
-
an implementation plan based on the research findings.
|
|
57
|
-
- once the specification file is added, turn the implementation plan into tasks
|
|
58
|
-
in the prd.yml file. Each task should reference the specification file in its
|
|
59
|
-
description, and be small, atomic and independently shippable without failing
|
|
60
|
-
validation checks (typechecks, linting, tests).
|
|
61
|
-
- make sure the follow up tasks include a dependency on the research task.`
|
|
62
|
-
: ""
|
|
63
|
-
}
|
|
42
|
+
${taskGuidelines(options)}
|
|
64
43
|
|
|
65
44
|
### Removing tasks
|
|
66
45
|
|
|
@@ -103,6 +82,36 @@ The following instructions should be done without interaction or asking for perm
|
|
|
103
82
|
options.gitFlow.requiresGithubPr
|
|
104
83
|
? `
|
|
105
84
|
|
|
85
|
+
Set \`githubPrNumber\` to the PR number if one exists, otherwise use \`null\`.
|
|
86
|
+
`
|
|
87
|
+
: "\n\nLeave `githubPrNumber` as null."
|
|
88
|
+
}
|
|
89
|
+
`
|
|
90
|
+
|
|
91
|
+
const promptChooseClanka = (options: {
|
|
92
|
+
readonly gitFlow: GitFlow["Service"]
|
|
93
|
+
}) => `Your job is to choose the next task to work on from the current task list.
|
|
94
|
+
**DO NOT** implement the task yet.
|
|
95
|
+
|
|
96
|
+
The following instructions should be done without interaction or asking for permission.
|
|
97
|
+
|
|
98
|
+
- Decide which single task to work on next from the task list. This should
|
|
99
|
+
be the task YOU decide as the most important to work on next, not just the
|
|
100
|
+
first task in the list.
|
|
101
|
+
- Only start tasks that are in a "todo" state.
|
|
102
|
+
- You **cannot** start tasks unless they have an empty \`blockedBy\` field.${
|
|
103
|
+
options.gitFlow.requiresGithubPr
|
|
104
|
+
? `
|
|
105
|
+
- Check if there is an open Github PR for the chosen task. If there is, note the PR number for inclusion in the task.json file.
|
|
106
|
+
- Only include "open" PRs that are not yet merged.
|
|
107
|
+
- The pull request will contain the task id in the title or description.`
|
|
108
|
+
: ""
|
|
109
|
+
}
|
|
110
|
+
- Use the "chooseTask" function to select the task you have chosen.
|
|
111
|
+
\`\`\`${
|
|
112
|
+
options.gitFlow.requiresGithubPr
|
|
113
|
+
? `
|
|
114
|
+
|
|
106
115
|
Set \`githubPrNumber\` to the PR number if one exists, otherwise use \`null\`.
|
|
107
116
|
`
|
|
108
117
|
: "\n\nLeave `githubPrNumber` as null."
|
|
@@ -139,6 +148,34 @@ challenges faced.
|
|
|
139
148
|
|
|
140
149
|
${prdNotes(options)}`
|
|
141
150
|
|
|
151
|
+
const systemClanka = (options: {
|
|
152
|
+
readonly specsDirectory: string
|
|
153
|
+
}) => `## Important: Adding new tasks
|
|
154
|
+
|
|
155
|
+
**If at any point** you discover something that needs fixing, or another task
|
|
156
|
+
that needs doing, immediately add it as a new task.
|
|
157
|
+
|
|
158
|
+
## Important: Recording key information
|
|
159
|
+
|
|
160
|
+
This session will time out after a certain period, so make sure to record
|
|
161
|
+
key information that could speed up future work on the task in the description.
|
|
162
|
+
Record the information **in the moment** as you discover it,
|
|
163
|
+
do not wait until the end of the task. Things to record include:
|
|
164
|
+
|
|
165
|
+
- Important discoveries about the codebase.
|
|
166
|
+
- Any challenges faced and how you overcame them. For example:
|
|
167
|
+
- If it took multiple attempts to get something working, record what worked.
|
|
168
|
+
- If you found a library api was renamed or moved, record the new name.
|
|
169
|
+
- Any other information that could help future work on similar tasks.
|
|
170
|
+
|
|
171
|
+
## Handling blockers
|
|
172
|
+
|
|
173
|
+
If for any reason you get stuck on a task, mark the task back as "todo" by updating its
|
|
174
|
+
\`state\` and leaving some notes in the task's \`description\` field about the
|
|
175
|
+
challenges faced.
|
|
176
|
+
|
|
177
|
+
${taskGuidelines(options)}`
|
|
178
|
+
|
|
142
179
|
const prompt = (options: {
|
|
143
180
|
readonly task: PrdIssue
|
|
144
181
|
readonly targetBranch: string | undefined
|
|
@@ -184,6 +221,49 @@ Your job is to implement the task described above.
|
|
|
184
221
|
|
|
185
222
|
${keyInformation(options)}`
|
|
186
223
|
|
|
224
|
+
const promptClanka = (options: {
|
|
225
|
+
readonly task: PrdIssue
|
|
226
|
+
readonly targetBranch: string | undefined
|
|
227
|
+
readonly specsDirectory: string
|
|
228
|
+
readonly githubPrNumber: number | undefined
|
|
229
|
+
readonly gitFlow: GitFlow["Service"]
|
|
230
|
+
}) => `# The task
|
|
231
|
+
|
|
232
|
+
ID: ${options.task.id}
|
|
233
|
+
Task: ${options.task.title}
|
|
234
|
+
Description:
|
|
235
|
+
|
|
236
|
+
${options.task.description}
|
|
237
|
+
|
|
238
|
+
# Instructions
|
|
239
|
+
|
|
240
|
+
Your job is to implement the task described above.
|
|
241
|
+
|
|
242
|
+
1. Carefully study the current task list to understand the context of the task, and
|
|
243
|
+
discover any key learnings from previous work.
|
|
244
|
+
Also read the ${options.specsDirectory}/README.md file (if available), to see
|
|
245
|
+
if any previous specifications could assist you.
|
|
246
|
+
2. ${options.gitFlow.setupInstructions(options)}
|
|
247
|
+
3. Implement the task.
|
|
248
|
+
- If this task is a research task, **do not** make any code changes yet.
|
|
249
|
+
- 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.
|
|
250
|
+
- **If at any point** you discover something that needs fixing, or another task
|
|
251
|
+
that needs doing, immediately add it as a new task unless you plan to fix it
|
|
252
|
+
as part of this task.
|
|
253
|
+
- Add important discoveries about the codebase, or challenges faced to the task's
|
|
254
|
+
\`description\`. More details below.
|
|
255
|
+
4. Run any checks / feedback loops, such as type checks, unit tests, or linting.
|
|
256
|
+
5. ${options.gitFlow.commitInstructions({
|
|
257
|
+
githubPrInstructions: sourceMeta.githubPrInstructions,
|
|
258
|
+
githubPrNumber: options.githubPrNumber,
|
|
259
|
+
taskId: options.task.id ?? "unknown",
|
|
260
|
+
targetBranch: options.targetBranch,
|
|
261
|
+
})}
|
|
262
|
+
6. **After ${options.gitFlow.requiresGithubPr ? "pushing" : "committing"}**
|
|
263
|
+
your changes, update current task to reflect any changes in the task state.
|
|
264
|
+
- 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.
|
|
265
|
+
- If you believe the task is complete, update the \`state\` to "in-review".`
|
|
266
|
+
|
|
187
267
|
const promptReview = (options: {
|
|
188
268
|
readonly prompt: string
|
|
189
269
|
readonly gitFlow: GitFlow["Service"]
|
|
@@ -196,8 +276,7 @@ in your review, looking for any potential issues or improvements.
|
|
|
196
276
|
Once you have completed your review, you should:
|
|
197
277
|
|
|
198
278
|
- Make any code changes needed to fix issues you find.
|
|
199
|
-
- Add follow-up tasks
|
|
200
|
-
or for remaining issues that need addressing.
|
|
279
|
+
- Add follow-up tasks for any work that could not be done, or for remaining issues that need addressing.
|
|
201
280
|
|
|
202
281
|
${options.gitFlow.reviewInstructions}
|
|
203
282
|
|
|
@@ -235,6 +314,25 @@ permission.
|
|
|
235
314
|
|
|
236
315
|
${prdNotes(options)}`
|
|
237
316
|
|
|
317
|
+
const promptTimeoutClanka = (options: {
|
|
318
|
+
readonly taskId: string
|
|
319
|
+
readonly specsDirectory: string
|
|
320
|
+
}) => `Your earlier attempt to complete the task with id \`${options.taskId}\` took too
|
|
321
|
+
long and has timed out.
|
|
322
|
+
|
|
323
|
+
The following instructions should be done without interaction or asking for
|
|
324
|
+
permission.
|
|
325
|
+
|
|
326
|
+
1. Investigate why you think the task took too long. Research the codebase
|
|
327
|
+
further to understand what is needed to complete the task.
|
|
328
|
+
2. Mark the original task as "done" by updating its \`state\`.
|
|
329
|
+
3. Break down the task into smaller tasks and add them to the prd.yml file.
|
|
330
|
+
Read the "### Adding tasks" section below **extremely carefully** for guidelines on creating tasks.
|
|
331
|
+
4. Setup task dependencies using the \`blockedBy\` field as needed. You will need
|
|
332
|
+
to wait 5 seconds after adding tasks to the prd.yml file to allow the system
|
|
333
|
+
to assign ids to the new tasks before you can setup dependencies.
|
|
334
|
+
5. If any specifications need updating based on your new understanding, update them.`
|
|
335
|
+
|
|
238
336
|
const planPrompt = (options: {
|
|
239
337
|
readonly plan: string
|
|
240
338
|
readonly specsDirectory: string
|
|
@@ -321,12 +419,16 @@ ${prdNotes(options)}`
|
|
|
321
419
|
|
|
322
420
|
return {
|
|
323
421
|
promptChoose,
|
|
422
|
+
promptChooseClanka,
|
|
324
423
|
prompt,
|
|
424
|
+
promptClanka,
|
|
325
425
|
promptReview,
|
|
326
426
|
promptReviewCustom,
|
|
327
427
|
promptTimeout,
|
|
428
|
+
promptTimeoutClanka,
|
|
328
429
|
planPrompt,
|
|
329
430
|
promptPlanTasks,
|
|
431
|
+
systemClanka,
|
|
330
432
|
} as const
|
|
331
433
|
}),
|
|
332
434
|
},
|
|
@@ -335,3 +437,28 @@ ${prdNotes(options)}`
|
|
|
335
437
|
Layer.provide(CurrentIssueSource.layer),
|
|
336
438
|
)
|
|
337
439
|
}
|
|
440
|
+
|
|
441
|
+
const taskGuidelines = (options?: {
|
|
442
|
+
readonly specsDirectory?: string | undefined
|
|
443
|
+
}) => `#### Task creation guidelines
|
|
444
|
+
|
|
445
|
+
**Important**: When creating tasks, make sure each task is independently shippable
|
|
446
|
+
without failing validation checks (typechecks, linting, tests). If a task would only
|
|
447
|
+
pass validations when combined with another, combine the work into one task.
|
|
448
|
+
|
|
449
|
+
Each task should be an atomic, committable piece of work.
|
|
450
|
+
Instead of creating tasks like "Refactor the authentication system", create
|
|
451
|
+
smaller tasks like "Implement OAuth2 login endpoint", "Add JWT token refresh mechanism", etc.${
|
|
452
|
+
options?.specsDirectory
|
|
453
|
+
? `
|
|
454
|
+
|
|
455
|
+
If you need to add a research task, mention in the description that it needs to:
|
|
456
|
+
- add a specification file in the \`${options.specsDirectory}\` directory with
|
|
457
|
+
an implementation plan based on the research findings.
|
|
458
|
+
- once the specification file is added, turn the implementation plan into tasks
|
|
459
|
+
in the prd.yml file. Each task should reference the specification file in its
|
|
460
|
+
description, and be small, atomic and independently shippable without failing
|
|
461
|
+
validation checks (typechecks, linting, tests).
|
|
462
|
+
- make sure the follow up tasks include a dependency on the research task.`
|
|
463
|
+
: ""
|
|
464
|
+
}`
|
package/src/TaskTools.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Deferred, Effect, Schema, ServiceMap, Struct } from "effect"
|
|
2
|
+
import { Tool, Toolkit } from "effect/unstable/ai"
|
|
3
|
+
import { PrdIssue } from "./domain/PrdIssue.ts"
|
|
4
|
+
import { IssueSource } from "./IssueSource.ts"
|
|
5
|
+
import { CurrentProjectId } from "./Settings.ts"
|
|
6
|
+
|
|
7
|
+
export class ChosenTaskDeferred extends ServiceMap.Reference(
|
|
8
|
+
"lalph/TaskTools/ChosenTaskDeferred",
|
|
9
|
+
{
|
|
10
|
+
defaultValue: Deferred.makeUnsafe<{
|
|
11
|
+
readonly taskId: string
|
|
12
|
+
readonly githubPrNumber?: number | undefined
|
|
13
|
+
}>,
|
|
14
|
+
},
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
export class TaskTools extends Toolkit.make(
|
|
18
|
+
Tool.make("listTasks", {
|
|
19
|
+
description: "Returns the current list of tasks.",
|
|
20
|
+
success: Schema.Array(
|
|
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
|
+
),
|
|
35
|
+
dependencies: [CurrentProjectId],
|
|
36
|
+
}),
|
|
37
|
+
Tool.make("createTask", {
|
|
38
|
+
description: "Create a new task and return it's id.",
|
|
39
|
+
parameters: Schema.Struct({
|
|
40
|
+
title: Schema.String,
|
|
41
|
+
description: PrdIssue.fields.description,
|
|
42
|
+
state: PrdIssue.fields.state,
|
|
43
|
+
priority: PrdIssue.fields.priority,
|
|
44
|
+
estimate: PrdIssue.fields.estimate,
|
|
45
|
+
blockedBy: PrdIssue.fields.blockedBy,
|
|
46
|
+
}),
|
|
47
|
+
success: Schema.String,
|
|
48
|
+
dependencies: [CurrentProjectId],
|
|
49
|
+
}),
|
|
50
|
+
Tool.make("updateTask", {
|
|
51
|
+
description: "Update a task. Supports partial updates",
|
|
52
|
+
parameters: Schema.Struct({
|
|
53
|
+
taskId: Schema.String,
|
|
54
|
+
title: Schema.optional(PrdIssue.fields.title),
|
|
55
|
+
description: Schema.optional(PrdIssue.fields.description),
|
|
56
|
+
state: Schema.optional(PrdIssue.fields.state),
|
|
57
|
+
blockedBy: Schema.optional(PrdIssue.fields.blockedBy),
|
|
58
|
+
}),
|
|
59
|
+
dependencies: [CurrentProjectId],
|
|
60
|
+
}),
|
|
61
|
+
// Tool.make("removeTask", {
|
|
62
|
+
// description: "Remove a task by it's id.",
|
|
63
|
+
// parameters: Schema.String.annotate({
|
|
64
|
+
// identifier: "taskId",
|
|
65
|
+
// }),
|
|
66
|
+
// dependencies: [CurrentProjectId],
|
|
67
|
+
// }),
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
export class TaskToolsWithChoose extends Toolkit.merge(
|
|
71
|
+
TaskTools,
|
|
72
|
+
Toolkit.make(
|
|
73
|
+
Tool.make("chooseTask", {
|
|
74
|
+
description: "Choose the task to work on",
|
|
75
|
+
parameters: Schema.Struct({
|
|
76
|
+
taskId: Schema.String,
|
|
77
|
+
githubPrNumber: Schema.optional(Schema.Number),
|
|
78
|
+
}),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const source = yield* IssueSource
|
|
86
|
+
|
|
87
|
+
return TaskToolsWithChoose.of({
|
|
88
|
+
listTasks: Effect.fn("TaskTools.listTasks")(function* () {
|
|
89
|
+
yield* Effect.log(`Calling "listTasks"`)
|
|
90
|
+
const projectId = yield* CurrentProjectId
|
|
91
|
+
const tasks = yield* source.issues(projectId)
|
|
92
|
+
return tasks.map((issue) => ({
|
|
93
|
+
id: issue.id ?? "",
|
|
94
|
+
title: issue.title,
|
|
95
|
+
description: issue.description,
|
|
96
|
+
state: issue.state,
|
|
97
|
+
priority: issue.priority,
|
|
98
|
+
estimate: issue.estimate,
|
|
99
|
+
blockedBy: issue.blockedBy,
|
|
100
|
+
}))
|
|
101
|
+
}, Effect.orDie),
|
|
102
|
+
chooseTask: Effect.fn("TaskTools.chooseTask")(function* (options) {
|
|
103
|
+
yield* Effect.log(`Calling "chooseTask"`).pipe(
|
|
104
|
+
Effect.annotateLogs(options),
|
|
105
|
+
)
|
|
106
|
+
const deferred = yield* ChosenTaskDeferred
|
|
107
|
+
yield* Deferred.succeed(deferred, options)
|
|
108
|
+
}),
|
|
109
|
+
createTask: Effect.fn("TaskTools.createTask")(function* (options) {
|
|
110
|
+
yield* Effect.log(`Calling "createTask"`)
|
|
111
|
+
const projectId = yield* CurrentProjectId
|
|
112
|
+
const taskId = yield* source.createIssue(
|
|
113
|
+
projectId,
|
|
114
|
+
new PrdIssue({
|
|
115
|
+
...options,
|
|
116
|
+
id: null,
|
|
117
|
+
autoMerge: false,
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
return taskId.id
|
|
121
|
+
}, Effect.orDie),
|
|
122
|
+
updateTask: Effect.fn("TaskTools.updateTask")(function* (options) {
|
|
123
|
+
yield* Effect.log(`Calling "updateTask"`).pipe(
|
|
124
|
+
Effect.annotateLogs({ taskId: options.taskId }),
|
|
125
|
+
)
|
|
126
|
+
const projectId = yield* CurrentProjectId
|
|
127
|
+
yield* source.updateIssue({
|
|
128
|
+
projectId,
|
|
129
|
+
issueId: options.taskId,
|
|
130
|
+
...options,
|
|
131
|
+
})
|
|
132
|
+
}, Effect.orDie),
|
|
133
|
+
// removeTask: Effect.fn("TaskTools.removeTask")(function* (taskId) {
|
|
134
|
+
// yield* Effect.log(`Calling "removeTask"`).pipe(
|
|
135
|
+
// Effect.annotateLogs({ taskId }),
|
|
136
|
+
// )
|
|
137
|
+
// const projectId = yield* CurrentProjectId
|
|
138
|
+
// yield* source.cancelIssue(projectId, taskId)
|
|
139
|
+
// }, Effect.orDie),
|
|
140
|
+
})
|
|
141
|
+
}),
|
|
142
|
+
)
|
package/src/Worktree.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Chunk,
|
|
3
|
-
DateTime,
|
|
4
3
|
Duration,
|
|
5
4
|
Effect,
|
|
6
5
|
FileSystem,
|
|
@@ -15,7 +14,6 @@ import {
|
|
|
15
14
|
Stream,
|
|
16
15
|
} from "effect"
|
|
17
16
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|
18
|
-
import { RunnerStalled } from "./domain/Errors.ts"
|
|
19
17
|
import type { AnyCliAgent } from "./domain/CliAgent.ts"
|
|
20
18
|
import { constWorkerMaxOutputChunks, CurrentWorkerState } from "./Workers.ts"
|
|
21
19
|
import { AtomRegistry } from "effect/unstable/reactivity"
|
|
@@ -23,6 +21,7 @@ import { CurrentProjectId } from "./Settings.ts"
|
|
|
23
21
|
import { projectById } from "./Projects.ts"
|
|
24
22
|
import { parseBranch } from "./shared/git.ts"
|
|
25
23
|
import { resolveLalphDirectory } from "./shared/lalphDirectory.ts"
|
|
24
|
+
import { withStallTimeout } from "./shared/stream.ts"
|
|
26
25
|
|
|
27
26
|
export class Worktree extends ServiceMap.Service<Worktree>()("lalph/Worktree", {
|
|
28
27
|
make: Effect.gen(function* () {
|
|
@@ -255,23 +254,6 @@ const makeExecHelpers = Effect.fnUntraced(function* (options: {
|
|
|
255
254
|
Effect.fnUntraced(function* (command: ChildProcess.Command) {
|
|
256
255
|
const registry = yield* AtomRegistry.AtomRegistry
|
|
257
256
|
const worker = yield* CurrentWorkerState
|
|
258
|
-
let lastOutputAt = yield* DateTime.now
|
|
259
|
-
|
|
260
|
-
const stallTimeout = Effect.suspend(function loop(): Effect.Effect<
|
|
261
|
-
never,
|
|
262
|
-
RunnerStalled
|
|
263
|
-
> {
|
|
264
|
-
const now = DateTime.nowUnsafe()
|
|
265
|
-
const deadline = DateTime.addDuration(
|
|
266
|
-
lastOutputAt,
|
|
267
|
-
options.stallTimeout,
|
|
268
|
-
)
|
|
269
|
-
if (DateTime.isLessThan(deadline, now)) {
|
|
270
|
-
return Effect.fail(new RunnerStalled())
|
|
271
|
-
}
|
|
272
|
-
const timeUntilDeadline = DateTime.distance(deadline, now)
|
|
273
|
-
return Effect.flatMap(Effect.sleep(timeUntilDeadline), loop)
|
|
274
|
-
})
|
|
275
257
|
|
|
276
258
|
const handle = yield* provide(command.asEffect())
|
|
277
259
|
|
|
@@ -280,8 +262,8 @@ const makeExecHelpers = Effect.fnUntraced(function* (options: {
|
|
|
280
262
|
options.cliAgent.outputTransformer
|
|
281
263
|
? options.cliAgent.outputTransformer
|
|
282
264
|
: identity,
|
|
265
|
+
withStallTimeout(options.stallTimeout),
|
|
283
266
|
Stream.runForEachArray((output) => {
|
|
284
|
-
lastOutputAt = DateTime.nowUnsafe()
|
|
285
267
|
for (const chunk of output) {
|
|
286
268
|
process.stdout.write(chunk)
|
|
287
269
|
}
|
|
@@ -294,7 +276,6 @@ const makeExecHelpers = Effect.fnUntraced(function* (options: {
|
|
|
294
276
|
)
|
|
295
277
|
return Effect.void
|
|
296
278
|
}),
|
|
297
|
-
Effect.raceFirst(stallTimeout),
|
|
298
279
|
)
|
|
299
280
|
return yield* handle.exitCode
|
|
300
281
|
}, Effect.scoped)
|