lalph 0.3.92 → 0.3.94

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.92",
4
+ "version": "0.3.94",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,17 +37,17 @@
37
37
  "devDependencies": {
38
38
  "@changesets/changelog-github": "^0.6.0",
39
39
  "@changesets/cli": "^2.30.0",
40
- "@effect/ai-openai": "4.0.0-beta.35",
41
- "@effect/ai-openai-compat": "4.0.0-beta.35",
42
- "@effect/language-service": "^0.80.0",
43
- "@effect/platform-node": "4.0.0-beta.35",
40
+ "@effect/ai-openai": "4.0.0-beta.36",
41
+ "@effect/ai-openai-compat": "4.0.0-beta.36",
42
+ "@effect/language-service": "^0.81.0",
43
+ "@effect/platform-node": "4.0.0-beta.36",
44
44
  "@linear/sdk": "^78.0.0",
45
45
  "@octokit/plugin-rest-endpoint-methods": "^17.0.0",
46
46
  "@octokit/types": "^16.0.0",
47
- "@typescript/native-preview": "7.0.0-dev.20260319.1",
48
- "clanka": "^0.2.19",
47
+ "@typescript/native-preview": "7.0.0-dev.20260320.1",
48
+ "clanka": "^0.2.21",
49
49
  "concurrently": "^9.2.1",
50
- "effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@89c3e98",
50
+ "effect": "4.0.0-beta.36",
51
51
  "husky": "^9.1.7",
52
52
  "lint-staged": "^16.4.0",
53
53
  "octokit": "^5.0.5",
@@ -121,6 +121,11 @@ export class CurrentIssueSource extends ServiceMap.Service<
121
121
  Effect.retry(refreshSchedule),
122
122
  unlessRalph(projectId, Effect.succeed([])),
123
123
  ),
124
+ findById: (projectId, issueId) =>
125
+ ScopedRef.get(ref).pipe(
126
+ Effect.flatMap((source) => source.findById(projectId, issueId)),
127
+ unlessRalph(projectId, Effect.succeed(null)),
128
+ ),
124
129
  createIssue: (projectId, options) =>
125
130
  ScopedRef.get(ref).pipe(
126
131
  Effect.flatMap((source) => source.createIssue(projectId, options)),
package/src/Github.ts CHANGED
@@ -492,6 +492,11 @@ export const GithubIssueSource = Layer.effect(
492
492
  const settings = yield* Cache.get(projectSettings, projectId)
493
493
  return yield* issues(settings)
494
494
  }),
495
+ findById: Effect.fnUntraced(function* (projectId, issueId) {
496
+ const settings = yield* Cache.get(projectSettings, projectId)
497
+ const projectIssues = yield* issues(settings)
498
+ return projectIssues.find((issue) => issue.id === issueId) ?? null
499
+ }),
495
500
  createIssue: Effect.fnUntraced(
496
501
  function* (projectId, issue) {
497
502
  const { labelFilter, autoMergeLabelName } = yield* Cache.get(
@@ -14,6 +14,11 @@ export class IssueSource extends ServiceMap.Service<
14
14
  projectId: ProjectId,
15
15
  ) => Effect.Effect<ReadonlyArray<PrdIssue>, IssueSourceError>
16
16
 
17
+ readonly findById: (
18
+ projectId: ProjectId,
19
+ issueId: string,
20
+ ) => Effect.Effect<PrdIssue | null, IssueSourceError>
21
+
17
22
  readonly createIssue: (
18
23
  projectId: ProjectId,
19
24
  issue: PrdIssue,
package/src/Linear.ts CHANGED
@@ -208,45 +208,48 @@ export const LinearIssueSource = Layer.effect(
208
208
  const identifierMap = new Map<string, string>()
209
209
  const presetMap = new Map<string, CliAgentPreset>()
210
210
 
211
- const backlogState =
212
- state.states.find(
213
- (s) => s.type === "backlog" && s.name.toLowerCase().includes("backlog"),
214
- ) || state.states.find((s) => s.type === "backlog")!
215
- const todoState =
216
- state.states.find(
217
- (s) =>
218
- s.type === "unstarted" &&
219
- (s.name.toLowerCase().includes("todo") ||
220
- s.name.toLowerCase().includes("unstarted")),
221
- ) || state.states.find((s) => s.type === "unstarted")!
222
- const inProgressState =
223
- state.states.find(
224
- (s) =>
225
- s.type === "started" &&
226
- (s.name.toLowerCase().includes("progress") ||
227
- s.name.toLowerCase().includes("started")),
228
- ) || state.states.find((s) => s.type === "started")!
229
- const inReviewState =
230
- state.states.find(
231
- (s) => s.type === "started" && s.name.toLowerCase().includes("review"),
232
- ) || state.states.find((s) => s.type === "completed")!
233
- const doneState = state.states.find((s) => s.type === "completed")!
234
-
235
- const canceledState = state.states.find(
236
- (state) => state.type === "canceled",
237
- )!
238
-
239
- const linearStateToPrdState = (state: State): PrdIssue["state"] => {
211
+ const findState = (
212
+ teamId: string,
213
+ type: string,
214
+ names: Array<string> = [],
215
+ fallbackType = type,
216
+ ) => {
217
+ const filtered = state.states.filter((s) => {
218
+ if (names.length === 0) return s.type === type
219
+ const name = s.name.toLowerCase()
220
+ return s.type === type && names.some((n) => name.includes(n))
221
+ })
222
+ const withTeamId = filtered.filter((s) => s.teamId === teamId)
223
+ if (withTeamId.length > 0) return withTeamId[0]!
224
+ const withoutTeamId = filtered.filter((s) => s.teamId === undefined)
225
+ if (withoutTeamId.length > 0) return withoutTeamId[0]!
226
+ return state.states.find((s) => s.type === fallbackType)!
227
+ }
228
+
229
+ const statesForTeamId = memoize((teamId: string) => ({
230
+ backlog: findState(teamId, "backlog", ["backlog"]),
231
+ todo: findState(teamId, "unstarted", ["todo", "unstarted"]),
232
+ inProgress: findState(teamId, "started", ["progress", "started"]),
233
+ inReview: findState(teamId, "started", ["review"], "completed"),
234
+ done: findState(teamId, "completed"),
235
+ canceled: findState(teamId, "canceled"),
236
+ }))
237
+
238
+ const linearStateToPrdState = (
239
+ state: State,
240
+ teamId: string,
241
+ ): PrdIssue["state"] => {
242
+ const states = statesForTeamId(teamId)
240
243
  switch (state.id) {
241
- case backlogState.id:
244
+ case states.backlog.id:
242
245
  return "backlog"
243
- case todoState.id:
246
+ case states.todo.id:
244
247
  return "todo"
245
- case inProgressState.id:
248
+ case states.inProgress.id:
246
249
  return "in-progress"
247
- case inReviewState.id:
250
+ case states.inReview.id:
248
251
  return "in-review"
249
- case doneState.id:
252
+ case states.done.id:
250
253
  return "done"
251
254
  default:
252
255
  if (state.type === "backlog") return "backlog"
@@ -256,28 +259,34 @@ export const LinearIssueSource = Layer.effect(
256
259
  return "backlog"
257
260
  }
258
261
  }
259
- const prdStateToLinearStateId = (state: PrdIssue["state"]): string => {
262
+ const prdStateToLinearStateId = (
263
+ state: PrdIssue["state"],
264
+ teamId: string,
265
+ ): string => {
266
+ const states = statesForTeamId(teamId)
260
267
  switch (state) {
261
268
  case "backlog":
262
- return backlogState.id
269
+ return states.backlog.id
263
270
  case "todo":
264
- return todoState.id
271
+ return states.todo.id
265
272
  case "in-progress":
266
- return inProgressState.id
273
+ return states.inProgress.id
267
274
  case "in-review":
268
- return inReviewState.id
275
+ return states.inReview.id
269
276
  case "done":
270
- return doneState.id
277
+ return states.done.id
271
278
  }
272
279
  }
273
280
 
274
281
  const issues = ({
275
282
  labelId,
276
283
  projectId,
284
+ teamId,
277
285
  autoMergeLabelId,
278
286
  }: {
279
287
  readonly labelId: Option.Option<string>
280
288
  readonly projectId: string
289
+ readonly teamId: string
281
290
  readonly autoMergeLabelId: Option.Option<string>
282
291
  }) =>
283
292
  linear.issues({ labelId, projectId }).pipe(
@@ -308,7 +317,7 @@ export const LinearIssueSource = Layer.effect(
308
317
  description: issue.description ?? "",
309
318
  priority: issue.priority,
310
319
  estimate: issue.estimate ?? null,
311
- state: linearStateToPrdState(issue.state),
320
+ state: linearStateToPrdState(issue.state, teamId),
312
321
  blockedBy: issue.blockedBy.map((r) => r.issue.identifier),
313
322
  autoMerge: autoMergeLabelId.pipe(
314
323
  Option.map((labelId) => issue.labelIds.includes(labelId)),
@@ -325,10 +334,21 @@ export const LinearIssueSource = Layer.effect(
325
334
  const settings = yield* Cache.get(projectSettings, projectId)
326
335
  return yield* issues({
327
336
  projectId: settings.project.id,
337
+ teamId: settings.teamId,
328
338
  labelId: settings.labelId,
329
339
  autoMergeLabelId: settings.autoMergeLabelId,
330
340
  })
331
341
  }),
342
+ findById: Effect.fnUntraced(function* (projectId, issueId) {
343
+ const settings = yield* Cache.get(projectSettings, projectId)
344
+ const projectIssues = yield* issues({
345
+ projectId: settings.project.id,
346
+ labelId: settings.labelId,
347
+ teamId: settings.teamId,
348
+ autoMergeLabelId: settings.autoMergeLabelId,
349
+ })
350
+ return projectIssues.find((issue) => issue.id === issueId) ?? null
351
+ }),
332
352
  createIssue: Effect.fnUntraced(
333
353
  function* (projectId, issue) {
334
354
  const { teamId, labelId, autoMergeLabelId, project } =
@@ -346,7 +366,7 @@ export const LinearIssueSource = Layer.effect(
346
366
  description: issue.description,
347
367
  priority: issue.priority,
348
368
  estimate: issue.estimate,
349
- stateId: prdStateToLinearStateId(issue.state),
369
+ stateId: prdStateToLinearStateId(issue.state, teamId),
350
370
  }),
351
371
  )
352
372
  const linearIssue = yield* linear.use(() => created.issue!)
@@ -382,7 +402,7 @@ export const LinearIssueSource = Layer.effect(
382
402
  ),
383
403
  updateIssue: Effect.fnUntraced(
384
404
  function* (options) {
385
- const { autoMergeLabelId } = yield* Cache.get(
405
+ const { autoMergeLabelId, teamId } = yield* Cache.get(
386
406
  projectSettings,
387
407
  options.projectId,
388
408
  )
@@ -403,7 +423,7 @@ export const LinearIssueSource = Layer.effect(
403
423
  update.description = options.description
404
424
  }
405
425
  if (options.state) {
406
- update.stateId = prdStateToLinearStateId(options.state)
426
+ update.stateId = prdStateToLinearStateId(options.state, teamId)
407
427
  }
408
428
  if (
409
429
  options.autoMerge !== undefined &&
@@ -466,11 +486,13 @@ export const LinearIssueSource = Layer.effect(
466
486
  Effect.mapError((cause) => new IssueSourceError({ cause })),
467
487
  ),
468
488
  cancelIssue: Effect.fnUntraced(
469
- function* (_project, issueId) {
489
+ function* (projectId, issueId) {
490
+ const { teamId } = yield* Cache.get(projectSettings, projectId)
491
+ const states = statesForTeamId(teamId)
470
492
  const linearIssueId = identifierMap.get(issueId)!
471
493
  yield* linear.use((c) =>
472
494
  c.updateIssue(linearIssueId, {
473
- stateId: canceledState.id,
495
+ stateId: states.canceled.id,
474
496
  }),
475
497
  )
476
498
  },
@@ -869,6 +891,7 @@ class LinearState extends Persistable.Class<{
869
891
  id: Schema.String,
870
892
  name: Schema.String,
871
893
  type: Schema.String,
894
+ teamId: Schema.optional(Schema.String),
872
895
  }),
873
896
  ),
874
897
  viewer: Schema.Struct({
@@ -876,3 +899,14 @@ class LinearState extends Persistable.Class<{
876
899
  }),
877
900
  }),
878
901
  }) {}
902
+
903
+ const memoize = <A, B>(f: (a: A) => B): ((a: A) => B) => {
904
+ const cache = new Map<A, B>()
905
+ return (a: A) => {
906
+ const cached = cache.get(a)
907
+ if (cached) return cached
908
+ const b = f(a)
909
+ cache.set(a, b)
910
+ return b
911
+ }
912
+ }
package/src/TaskTools.ts CHANGED
@@ -35,20 +35,20 @@ export class CurrentTaskRef extends ServiceMap.Service<
35
35
  }
36
36
  }
37
37
 
38
- const TaskList = Schema.Array(
39
- Schema.Struct({
40
- id: Schema.String.annotate({
41
- documentation: "The unique identifier of the task.",
42
- }),
43
- ...Struct.pick(PrdIssue.fields, [
44
- "title",
45
- "description",
46
- "state",
47
- "priority",
48
- "blockedBy",
49
- ]),
38
+ const Task = Schema.Struct({
39
+ id: Schema.String.annotate({
40
+ documentation: "The unique identifier of the task.",
50
41
  }),
51
- )
42
+ ...Struct.pick(PrdIssue.fields, [
43
+ "title",
44
+ "description",
45
+ "state",
46
+ "priority",
47
+ "blockedBy",
48
+ ]),
49
+ })
50
+
51
+ const TaskList = Schema.Array(Task)
52
52
 
53
53
  const toTaskListItem = (issue: PrdIssue) => ({
54
54
  id: issue.id ?? "",
@@ -77,6 +77,14 @@ export class TaskTools extends Toolkit.make(
77
77
  success: Schema.String,
78
78
  dependencies: [CurrentProjectId],
79
79
  }),
80
+ Tool.make("findTaskById", {
81
+ description: "Find a task by it's id. Returns null if not found.",
82
+ parameters: Schema.String.annotate({
83
+ identifier: "taskId",
84
+ }),
85
+ success: Schema.NullOr(Task),
86
+ dependencies: [CurrentProjectId],
87
+ }),
80
88
  Tool.make("updateTask", {
81
89
  description: "Update a task. Supports partial updates",
82
90
  parameters: Schema.Struct({
@@ -159,6 +167,14 @@ export const TaskToolsHandlers = TaskToolsWithChoose.toLayer(
159
167
  )
160
168
  return taskId.id
161
169
  }, Effect.orDie),
170
+ findTaskById: Effect.fn("TaskTools.findTaskById")(function* (taskId) {
171
+ yield* Effect.log(`Calling "findTaskById"`).pipe(
172
+ Effect.annotateLogs({ taskId }),
173
+ )
174
+ const projectId = yield* CurrentProjectId
175
+ const task = yield* source.findById(projectId, taskId)
176
+ return task ? toTaskListItem(task) : null
177
+ }, Effect.orDie),
162
178
  updateTask: Effect.fn("TaskTools.updateTask")(function* (options) {
163
179
  yield* Effect.log(`Calling "updateTask"`).pipe(
164
180
  Effect.annotateLogs({ taskId: options.taskId }),