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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lalph",
3
3
  "type": "module",
4
- "version": "0.3.37",
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.5.2",
24
- "@changesets/cli": "^2.29.8",
25
- "@effect/language-service": "^0.75.1",
26
- "@effect/platform-node": "4.0.0-beta.28",
27
- "@linear/sdk": "^75.0.0",
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.20260219.1",
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.28",
34
+ "effect": "4.0.0-beta.29",
33
35
  "husky": "^9.1.7",
34
- "lint-staged": "^16.2.7",
36
+ "lint-staged": "^16.3.2",
35
37
  "octokit": "^5.0.5",
36
- "oxlint": "^1.49.0",
38
+ "oxlint": "^1.51.0",
37
39
  "prettier": "^3.8.1",
38
- "tsdown": "^0.20.3",
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 (same folder as the prd.yml 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: (
@@ -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
- #### Task creation guidelines
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 to the prd.yml file for any work that could not be done,
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
+ }`
@@ -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)