@qotaq/lalphgram 0.1.6 → 0.1.8

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.
Files changed (72) hide show
  1. package/dist/cjs/Main.js +7 -4
  2. package/dist/cjs/Main.js.map +1 -1
  3. package/dist/cjs/lib/StreamJsonParser.js +4 -3
  4. package/dist/cjs/lib/StreamJsonParser.js.map +1 -1
  5. package/dist/cjs/services/ChatMachine.js +9 -2
  6. package/dist/cjs/services/ChatMachine.js.map +1 -1
  7. package/dist/cjs/services/EventLoop.js +3 -1
  8. package/dist/cjs/services/EventLoop.js.map +1 -1
  9. package/dist/cjs/services/LalphConfig.js +18 -1
  10. package/dist/cjs/services/LalphConfig.js.map +1 -1
  11. package/dist/cjs/services/LinearSdkClient.js +13 -5
  12. package/dist/cjs/services/LinearSdkClient.js.map +1 -1
  13. package/dist/cjs/services/PlanSession.js +45 -6
  14. package/dist/cjs/services/PlanSession.js.map +1 -1
  15. package/dist/cjs/services/TaskTracker/GitHubIssueTracker.js +7 -3
  16. package/dist/cjs/services/TaskTracker/GitHubIssueTracker.js.map +1 -1
  17. package/dist/cjs/services/TaskTracker/LinearTracker.js +5 -1
  18. package/dist/cjs/services/TaskTracker/LinearTracker.js.map +1 -1
  19. package/dist/cjs/shim/main.js +4 -1
  20. package/dist/cjs/shim/main.js.map +1 -1
  21. package/dist/dts/lib/StreamJsonParser.d.ts +2 -1
  22. package/dist/dts/lib/StreamJsonParser.d.ts.map +1 -1
  23. package/dist/dts/services/AutoMerge.d.ts +1 -1
  24. package/dist/dts/services/ChatMachine.d.ts +1 -1
  25. package/dist/dts/services/ChatMachine.d.ts.map +1 -1
  26. package/dist/dts/services/EventLoop.d.ts +2 -2
  27. package/dist/dts/services/EventLoop.d.ts.map +1 -1
  28. package/dist/dts/services/LalphConfig.d.ts +1 -0
  29. package/dist/dts/services/LalphConfig.d.ts.map +1 -1
  30. package/dist/dts/services/LinearSdkClient.d.ts +1 -0
  31. package/dist/dts/services/LinearSdkClient.d.ts.map +1 -1
  32. package/dist/dts/services/PlanOverviewUploaderMap.d.ts +1 -1
  33. package/dist/dts/services/PlanSession.d.ts.map +1 -1
  34. package/dist/dts/services/PullRequestTracker.d.ts +1 -1
  35. package/dist/dts/services/TaskTracker/GitHubIssueTracker.d.ts +2 -1
  36. package/dist/dts/services/TaskTracker/GitHubIssueTracker.d.ts.map +1 -1
  37. package/dist/dts/services/TaskTracker/LinearTracker.d.ts +2 -1
  38. package/dist/dts/services/TaskTracker/LinearTracker.d.ts.map +1 -1
  39. package/dist/dts/services/TrackerLayerMap.d.ts +1 -1
  40. package/dist/dts/shim/main.d.ts +1 -1
  41. package/dist/dts/shim/main.d.ts.map +1 -1
  42. package/dist/esm/Main.js +7 -4
  43. package/dist/esm/Main.js.map +1 -1
  44. package/dist/esm/lib/StreamJsonParser.js +3 -2
  45. package/dist/esm/lib/StreamJsonParser.js.map +1 -1
  46. package/dist/esm/services/ChatMachine.js +9 -2
  47. package/dist/esm/services/ChatMachine.js.map +1 -1
  48. package/dist/esm/services/EventLoop.js +3 -1
  49. package/dist/esm/services/EventLoop.js.map +1 -1
  50. package/dist/esm/services/LalphConfig.js +19 -2
  51. package/dist/esm/services/LalphConfig.js.map +1 -1
  52. package/dist/esm/services/LinearSdkClient.js +13 -5
  53. package/dist/esm/services/LinearSdkClient.js.map +1 -1
  54. package/dist/esm/services/PlanSession.js +45 -6
  55. package/dist/esm/services/PlanSession.js.map +1 -1
  56. package/dist/esm/services/TaskTracker/GitHubIssueTracker.js +7 -3
  57. package/dist/esm/services/TaskTracker/GitHubIssueTracker.js.map +1 -1
  58. package/dist/esm/services/TaskTracker/LinearTracker.js +5 -1
  59. package/dist/esm/services/TaskTracker/LinearTracker.js.map +1 -1
  60. package/dist/esm/shim/main.js +4 -1
  61. package/dist/esm/shim/main.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/Main.ts +14 -10
  64. package/src/lib/StreamJsonParser.ts +6 -2
  65. package/src/services/ChatMachine.ts +11 -3
  66. package/src/services/EventLoop.ts +6 -4
  67. package/src/services/LalphConfig.ts +37 -2
  68. package/src/services/LinearSdkClient.ts +6 -1
  69. package/src/services/PlanSession.ts +60 -7
  70. package/src/services/TaskTracker/GitHubIssueTracker.ts +10 -3
  71. package/src/services/TaskTracker/LinearTracker.ts +4 -1
  72. package/src/shim/main.ts +4 -1
package/src/Main.ts CHANGED
@@ -74,18 +74,22 @@ const lalphNotifyCommand = CliCommand.make(
74
74
  Effect.map((s) => s.trim())
75
75
  )
76
76
 
77
- // Resolve tsx binary for running the TypeScript shim
78
- const tsxPath = yield* PlatformCommand.make("which", "tsx").pipe(
79
- PlatformCommand.string,
80
- Effect.map((s) => s.trim())
81
- )
82
-
83
- // Resolve the SDK-based shim entry point relative to this file
84
- const shimMainTs = pathService.join(
77
+ // Resolve the SDK-based shim entry point relative to this file.
78
+ // In dev mode (tsx), the file is bin.ts; when built, it's bin.js.
79
+ const shimDir_ = pathService.join(
85
80
  pathService.dirname(fileURLToPath(import.meta.url)),
86
- "shim",
87
- "bin.ts"
81
+ "shim"
88
82
  )
83
+ const shimJsExists = yield* fs.exists(pathService.join(shimDir_, "bin.js"))
84
+ const shimMainTs = pathService.join(shimDir_, shimJsExists ? "bin.js" : "bin.ts")
85
+
86
+ // Use node for compiled JS, tsx for TypeScript source
87
+ const tsxPath = shimJsExists
88
+ ? process.execPath
89
+ : yield* PlatformCommand.make("which", "tsx").pipe(
90
+ PlatformCommand.string,
91
+ Effect.map((s) => s.trim())
92
+ )
89
93
 
90
94
  yield* Effect.log("Resolved shim paths").pipe(
91
95
  Effect.annotateLogs({ realClaudePath, tsxPath, shimMainTs })
@@ -74,7 +74,7 @@ export class StreamJsonInput extends Schema.Class<StreamJsonInput>("StreamJsonIn
74
74
  parent_tool_use_id: Schema.NullOr(Schema.String)
75
75
  }) {}
76
76
 
77
- const decodeJsonMessage = Schema.decodeUnknown(Schema.parseJson(StreamJsonMessage))
77
+ export const decodeJsonMessage = Schema.decodeUnknown(Schema.parseJson(StreamJsonMessage))
78
78
 
79
79
  /**
80
80
  * Splits a string stream into lines and parses each as a StreamJsonMessage,
@@ -89,7 +89,11 @@ export const parseNdjsonMessages = flow(
89
89
  decodeJsonMessage(line).pipe(
90
90
  Effect.tapError((err) =>
91
91
  Effect.logDebug("Non-JSON stdout line, skipping").pipe(
92
- Effect.annotateLogs({ line: line.slice(0, 300), error: err.message.slice(0, 100) })
92
+ Effect.annotateLogs({
93
+ line: line.slice(0, 300),
94
+ lineBytes: Array.from(line.slice(0, 100), (c) => c.charCodeAt(0).toString(16)).join(" "),
95
+ error: err.message
96
+ })
93
97
  )
94
98
  ),
95
99
  Effect.option
@@ -302,6 +302,15 @@ export const chatMachine = Machine.make(
302
302
  analysisFollowUpSent: boolean
303
303
  ): Effect.Effect<ChatState, never, never> =>
304
304
  Effect.gen(function*() {
305
+ yield* Effect.log("checkAllReady").pipe(
306
+ Effect.annotateLogs({
307
+ spec: String(flags.spec),
308
+ analysis: String(flags.analysis),
309
+ idle: String(flags.idle),
310
+ planType,
311
+ analysisFollowUpSent: String(analysisFollowUpSent)
312
+ })
313
+ )
305
314
  if (!flags.spec || !flags.analysis || !flags.idle) {
306
315
  return ChatState.SessionRunning({
307
316
  projectId,
@@ -660,11 +669,10 @@ export const chatMachine = Machine.make(
660
669
  case "SpecReady": {
661
670
  if (text === APPROVE_BUTTON_LABEL) {
662
671
  yield* Effect.log("User approved task creation")
663
- yield* planSession.approve.pipe(
664
- Effect.tapError((err) => Effect.logError(`Plan approve error: ${err.message}`)),
672
+ yield* planSession.approve
673
+ yield* notifier.sendMessage({ text: "Spec approved.", replyKeyboard: IDLE_KEYBOARD }).pipe(
665
674
  Effect.orElseSucceed(() => undefined)
666
675
  )
667
- yield* notifier.sendMessage({ text: "Spec approved.", replyKeyboard: IDLE_KEYBOARD })
668
676
  return reply(ChatState.Idle())
669
677
  }
670
678
  if (text === ABORT_BUTTON_LABEL) {
@@ -87,10 +87,12 @@ const autoMergeLayer = AutoMergeLive.pipe(
87
87
  Layer.provide(lalphConfigLayer)
88
88
  )
89
89
 
90
- const eventSourcesLayer = PullRequestTrackerLive.pipe(
91
- Layer.provide(servicesLayer),
92
- Layer.provide(lalphConfigLayer)
93
- )
90
+ const eventSourcesLayer = process.env["MOCK_GITHUB"] === "1"
91
+ ? Layer.succeed(PullRequestTracker, { eventStream: Stream.never })
92
+ : PullRequestTrackerLive.pipe(
93
+ Layer.provide(servicesLayer),
94
+ Layer.provide(lalphConfigLayer)
95
+ )
94
96
 
95
97
  const branchParserLayer = BranchParserLive
96
98
 
@@ -3,7 +3,7 @@
3
3
  * @since 1.0.0
4
4
  */
5
5
  import { Command, FileSystem, Path } from "@effect/platform"
6
- import { Context, Data, Effect, Layer, Ref, Schema, Stream } from "effect"
6
+ import { Array as Arr, Context, Data, Effect, Layer, Ref, Schema, Stream } from "effect"
7
7
  import { LalphGithubToken, LalphLinearToken } from "../schemas/CredentialSchemas.js"
8
8
  import { AppContext } from "./AppContext.js"
9
9
 
@@ -43,6 +43,7 @@ export interface LalphConfigService {
43
43
  readonly issueSource: "linear" | "github"
44
44
  readonly specUploader: "gist" | "telegraph"
45
45
  readonly repoFullName: string
46
+ readonly linearProjectIds: ReadonlyArray<string>
46
47
  }
47
48
 
48
49
  /**
@@ -160,6 +161,39 @@ export const LalphConfigLive = Layer.scoped(
160
161
  return parseRepoFullName(output.trim())
161
162
  })
162
163
 
164
+ const ProjectEntry = Schema.Struct({ id: Schema.String, enabled: Schema.Boolean })
165
+
166
+ const linearProjectIds: ReadonlyArray<string> = issueSource === "linear"
167
+ ? yield* Effect.gen(function*() {
168
+ const projectsJson = yield* readStringFile("settings.projects").pipe(
169
+ Effect.map((raw) => JSON.parse(raw)),
170
+ Effect.flatMap(Schema.decodeUnknown(Schema.Array(ProjectEntry))),
171
+ Effect.orElseSucceed((): ReadonlyArray<Schema.Schema.Type<typeof ProjectEntry>> => [])
172
+ )
173
+ const enabledIds = Arr.filter(projectsJson, (p) => p.enabled).map((p) => p.id)
174
+ const projectIds = yield* Effect.forEach(enabledIds, (id) => {
175
+ const filePath = pathService.join(
176
+ appContext.projectRoot,
177
+ ".lalph",
178
+ "projects",
179
+ encodeURIComponent(id),
180
+ "linear.selectedProjectId"
181
+ )
182
+ return fs.readFileString(filePath).pipe(
183
+ Effect.flatMap((content) =>
184
+ Effect.try({
185
+ try: () => JSON.parse(content),
186
+ catch: (err) => err
187
+ })
188
+ ),
189
+ Effect.flatMap(Schema.decodeUnknown(Schema.String)),
190
+ Effect.option
191
+ )
192
+ })
193
+ return Arr.filterMap(projectIds, (opt) => opt)
194
+ })
195
+ : []
196
+
163
197
  const githubTokenRef = yield* Ref.make(githubTokenData.token)
164
198
  const linearTokenRef = yield* Ref.make(linearAccessToken)
165
199
 
@@ -212,7 +246,8 @@ export const LalphConfigLive = Layer.scoped(
212
246
  ),
213
247
  issueSource,
214
248
  specUploader,
215
- repoFullName
249
+ repoFullName,
250
+ linearProjectIds
216
251
  })
217
252
  })
218
253
  )
@@ -46,6 +46,7 @@ export interface LinearSdkWorkflowState {
46
46
  export interface LinearSdkClientService {
47
47
  readonly listIssues: (params: {
48
48
  readonly since: string
49
+ readonly projectIds?: ReadonlyArray<string>
49
50
  }) => Effect.Effect<ReadonlyArray<LinearSdkIssue>, LinearSdkClientError>
50
51
  readonly getIssue: (params: {
51
52
  readonly id: string
@@ -99,7 +100,11 @@ export const LinearSdkClientLive = Layer.effect(
99
100
  const c = yield* getClient
100
101
  return yield* Effect.tryPromise({
101
102
  try: async () => {
102
- const connection = await c.issues({ filter: { updatedAt: { gte: new Date(params.since) } } })
103
+ const filter: Record<string, unknown> = { updatedAt: { gte: new Date(params.since) } }
104
+ if (params.projectIds && params.projectIds.length > 0) {
105
+ filter.project = { id: { in: params.projectIds } }
106
+ }
107
+ const connection = await c.issues({ filter })
103
108
  const results: Array<LinearSdkIssue> = []
104
109
  for (const node of connection.nodes) {
105
110
  const state = await node.state
@@ -5,7 +5,7 @@
5
5
  import { Command, CommandExecutor, FileSystem, Path } from "@effect/platform"
6
6
  import { Context, Data, Effect, Exit, Layer, Option, Queue, Ref, Schema, Scope, Stream } from "effect"
7
7
  import type { ContentBlock, StreamJsonMessage } from "../lib/StreamJsonParser.js"
8
- import { AskUserQuestionInput, parseNdjsonMessages } from "../lib/StreamJsonParser.js"
8
+ import { AskUserQuestionInput, decodeJsonMessage } from "../lib/StreamJsonParser.js"
9
9
  import { AppContext } from "./AppContext.js"
10
10
 
11
11
  /**
@@ -98,10 +98,13 @@ export type PlanEvent =
98
98
  | PlanAnalysisReady
99
99
  | PlanAwaitingInput
100
100
 
101
+ const StdinEOF: unique symbol = Symbol.for("StdinEOF")
102
+ type StdinItem = Uint8Array | typeof StdinEOF
103
+
101
104
  interface ActiveSession {
102
105
  readonly process: CommandExecutor.Process
103
106
  readonly scope: Scope.CloseableScope
104
- readonly stdinQueue: Queue.Queue<Uint8Array>
107
+ readonly stdinQueue: Queue.Queue<StdinItem>
105
108
  }
106
109
 
107
110
  /**
@@ -152,7 +155,7 @@ export class PlanCommandBuilder extends Context.Tag("PlanCommandBuilder")<
152
155
 
153
156
  const stripAnsi = (text: string): string =>
154
157
  // eslint-disable-next-line no-control-regex
155
- text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
158
+ text.replace(/\x1b\[\??[0-9;]*[a-zA-Z]/g, "")
156
159
 
157
160
  const WriteToolInput = Schema.Struct({ file_path: Schema.String })
158
161
  const EditToolInput = Schema.Struct({ file_path: Schema.String })
@@ -242,14 +245,16 @@ export const PlanSessionLive = Layer.scoped(
242
245
  )
243
246
  )
244
247
 
245
- const stdinQueue = yield* Queue.unbounded<Uint8Array>()
248
+ const stdinQueue = yield* Queue.unbounded<StdinItem>()
246
249
  yield* Ref.set(sessionRef, Option.some({ process, scope: processScope, stdinQueue }))
247
250
  yield* Effect.log("Plan session process spawned").pipe(
248
251
  Effect.annotateLogs({ tempFile })
249
252
  )
250
253
 
251
254
  yield* Stream.fromQueue(stdinQueue).pipe(
255
+ Stream.takeWhile((item): item is Uint8Array => item !== StdinEOF),
252
256
  Stream.run(process.stdin),
257
+ Effect.tap(() => Effect.log("stdin stream closed")),
253
258
  Effect.catchAll((err) => Effect.logError(`stdin write error: ${String(err)}`)),
254
259
  Effect.forkDaemon
255
260
  )
@@ -266,6 +271,8 @@ export const PlanSessionLive = Layer.scoped(
266
271
  const pendingTextRef = yield* Ref.make<Option.Option<{ messageId: string; text: string }>>(
267
272
  Option.none()
268
273
  )
274
+ const stage2Ref = yield* Ref.make(false)
275
+ const stage2BufferRef = yield* Ref.make<ReadonlyArray<string>>([])
269
276
 
270
277
  const flushPendingText = Effect.gen(function*() {
271
278
  const pending = yield* Ref.get(pendingTextRef)
@@ -278,6 +285,14 @@ export const PlanSessionLive = Layer.scoped(
278
285
  }
279
286
  })
280
287
 
288
+ const flushStage2Buffer = Effect.gen(function*() {
289
+ const buf = yield* Ref.get(stage2BufferRef)
290
+ if (buf.length > 0) {
291
+ yield* Queue.offer(eventQueue, new PlanTextOutput({ text: buf.join("\n") }))
292
+ yield* Ref.set(stage2BufferRef, [])
293
+ }
294
+ })
295
+
281
296
  const detectFileEvent = (block: typeof ContentBlock.Type) =>
282
297
  Effect.gen(function*() {
283
298
  const filePathResult = yield* Effect.gen(function*() {
@@ -380,6 +395,7 @@ export const PlanSessionLive = Layer.scoped(
380
395
  if (msg.type === "result") {
381
396
  yield* flushPendingText
382
397
  yield* Ref.set(idleRef, true)
398
+ yield* Ref.set(stage2Ref, true)
383
399
  yield* Queue.offer(eventQueue, new PlanAwaitingInput({}))
384
400
  yield* Effect.log("Planner result received")
385
401
  return
@@ -398,9 +414,39 @@ export const PlanSessionLive = Layer.scoped(
398
414
  Effect.annotateLogs({ chunkLength: String(chunk.length), preview: chunk.slice(0, 200) })
399
415
  )
400
416
  ),
401
- parseNdjsonMessages,
402
- Stream.mapEffect(routeMessage),
417
+ Stream.map(stripAnsi),
418
+ Stream.map((text) => text.replace(/\r/g, "\n")),
419
+ Stream.splitLines,
420
+ Stream.filter((line) => line.trim().length > 0),
421
+ Stream.mapEffect((line) =>
422
+ Effect.gen(function*() {
423
+ const isStage2 = yield* Ref.get(stage2Ref)
424
+ const parsed = yield* decodeJsonMessage(line).pipe(
425
+ Effect.tapError((err) => {
426
+ if (isStage2) return Effect.void
427
+ return Effect.logDebug("Non-JSON stdout line, skipping").pipe(
428
+ Effect.annotateLogs({
429
+ line: line.slice(0, 300),
430
+ lineBytes: Array.from(line.slice(0, 100), (c) => c.charCodeAt(0).toString(16)).join(" "),
431
+ error: err.message
432
+ })
433
+ )
434
+ }),
435
+ Effect.option
436
+ )
437
+ if (Option.isSome(parsed)) {
438
+ yield* routeMessage(parsed.value)
439
+ } else if (isStage2) {
440
+ yield* Ref.update(stage2BufferRef, (buf) => [...buf, line])
441
+ const buf = yield* Ref.get(stage2BufferRef)
442
+ if (line.includes("\u2713") || line.includes("\u2717") || buf.length >= 20) {
443
+ yield* flushStage2Buffer
444
+ }
445
+ }
446
+ })
447
+ ),
403
448
  Stream.runDrain,
449
+ Effect.tap(() => flushStage2Buffer),
404
450
  Effect.tap(() => flushPendingText),
405
451
  Effect.tap(() => Effect.log("stdout stream completed")),
406
452
  Effect.tapError((err) =>
@@ -437,6 +483,7 @@ export const PlanSessionLive = Layer.scoped(
437
483
  )
438
484
 
439
485
  yield* Effect.gen(function*() {
486
+ yield* Effect.log("Waiting for process exit code...")
440
487
  const exitCode = yield* process.exitCode
441
488
  yield* Effect.log("Plan session process exited").pipe(
442
489
  Effect.annotateLogs({ exitCode: String(exitCode) })
@@ -517,7 +564,13 @@ export const PlanSessionLive = Layer.scoped(
517
564
  }
518
565
  const encoder = new TextEncoder()
519
566
  yield* Ref.set(idleRef, false)
520
- yield* Queue.offer(current.value.stdinQueue, encoder.encode(JSON.stringify({ type: "shim_approve" }) + "\n"))
567
+ const payload = JSON.stringify({ type: "shim_approve" }) + "\n"
568
+ yield* Effect.log("Sending shim_approve to stdin").pipe(
569
+ Effect.annotateLogs({ payload: payload.trim() })
570
+ )
571
+ yield* Queue.offer(current.value.stdinQueue, encoder.encode(payload))
572
+ yield* Queue.offer(current.value.stdinQueue, StdinEOF)
573
+ yield* Effect.log("shim_approve + StdinEOF queued to stdin")
521
574
  })
522
575
 
523
576
  const reject = Effect.gen(function*() {
@@ -7,6 +7,7 @@ import { TaskCreated, TaskUpdated } from "../../Events.js"
7
7
  import type { TaskTrackerEvent } from "../../Events.js"
8
8
  import { TrackerIssue, TrackerIssueEvent } from "../../schemas/TrackerSchemas.js"
9
9
  import { AppRuntimeConfig } from "../AppRuntimeConfig.js"
10
+ import { LalphConfig } from "../LalphConfig.js"
10
11
  import { OctokitClient } from "../OctokitClient.js"
11
12
  import { TaskTracker, TaskTrackerError } from "./TaskTracker.js"
12
13
 
@@ -29,7 +30,9 @@ export const GitHubIssueTrackerLive = Layer.effect(
29
30
  Effect.gen(function*() {
30
31
  const octokit = yield* OctokitClient
31
32
  const config = yield* AppRuntimeConfig
33
+ const lalphConfig = yield* LalphConfig
32
34
  const interval = Duration.seconds(config.pollIntervalSeconds)
35
+ const repoFullName = lalphConfig.repoFullName
33
36
 
34
37
  const fetchRecentEvents = (since: string) =>
35
38
  Effect.gen(function*() {
@@ -42,10 +45,14 @@ export const GitHubIssueTrackerLive = Layer.effect(
42
45
  new TaskTrackerError({ message: `GitHub API request failed: ${String(err)}`, cause: err })
43
46
  )
44
47
  )
45
- return Array.map(issues, (issue) => {
46
- const repoFullName = extractRepoFullName(issue.repositoryUrl)
48
+ const filteredIssues = Array.filter(
49
+ issues,
50
+ (issue) => extractRepoFullName(issue.repositoryUrl) === repoFullName
51
+ )
52
+ return Array.map(filteredIssues, (issue) => {
53
+ const issueRepoFullName = extractRepoFullName(issue.repositoryUrl)
47
54
  const trackerIssue = new TrackerIssue({
48
- id: `${repoFullName}#${issue.number}`,
55
+ id: `${issueRepoFullName}#${issue.number}`,
49
56
  title: issue.title,
50
57
  state: issue.state,
51
58
  url: issue.htmlUrl,
@@ -7,6 +7,7 @@ import { TaskCreated, TaskUpdated } from "../../Events.js"
7
7
  import type { TaskTrackerEvent } from "../../Events.js"
8
8
  import { TrackerIssue, TrackerIssueEvent } from "../../schemas/TrackerSchemas.js"
9
9
  import { AppRuntimeConfig } from "../AppRuntimeConfig.js"
10
+ import { LalphConfig } from "../LalphConfig.js"
10
11
  import { LinearSdkClient } from "../LinearSdkClient.js"
11
12
  import { TaskTracker, TaskTrackerError } from "./TaskTracker.js"
12
13
 
@@ -15,7 +16,9 @@ export const LinearTrackerLive = Layer.effect(
15
16
  Effect.gen(function*() {
16
17
  const linearClient = yield* LinearSdkClient
17
18
  const config = yield* AppRuntimeConfig
19
+ const lalphConfig = yield* LalphConfig
18
20
  const interval = Duration.seconds(config.pollIntervalSeconds)
21
+ const projectIds = lalphConfig.linearProjectIds
19
22
  const todoStateIdRef = yield* Ref.make<string | null>(null)
20
23
 
21
24
  const resolveTodoStateId = Effect.gen(function*() {
@@ -36,7 +39,7 @@ export const LinearTrackerLive = Layer.effect(
36
39
  })
37
40
 
38
41
  const fetchRecentEvents = (since: string) =>
39
- linearClient.listIssues({ since }).pipe(
42
+ linearClient.listIssues({ since, projectIds }).pipe(
40
43
  Effect.map((issues) =>
41
44
  issues.map((node) => {
42
45
  const issue = new TrackerIssue({
package/src/shim/main.ts CHANGED
@@ -162,7 +162,10 @@ export const shimProgram = Effect.gen(function*() {
162
162
  }
163
163
  case "shim_approve": {
164
164
  yield* writeDebug("shim_approve intercepted")
165
- yield* Queue.offer(followUpQueue, FollowUpStop)
165
+ // Exit immediately — scope cleanup hangs because the stdin
166
+ // reader fiber is blocked on a Node.js pipe read that
167
+ // doesn't respond to Effect's fiber interruption.
168
+ yield* Effect.sync(() => process.exit(0))
166
169
  return
167
170
  }
168
171
  case "shim_start": {