@jmoyers/harness 0.1.11 → 0.1.20

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 (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -11,9 +11,10 @@ import { randomUUID } from 'node:crypto';
11
11
  import { tmpdir } from 'node:os';
12
12
  import { delimiter, isAbsolute, join, resolve } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
+ import { LinearClient } from '@linear/sdk';
14
15
  import { type CodexLiveEvent, type LiveSessionNotifyMode } from '../codex/live-session.ts';
15
16
  import type { PtyExit } from '../pty/pty_host.ts';
16
- import type { TerminalBufferTail, TerminalSnapshotFrame } from '../terminal/snapshot-oracle.ts';
17
+ import type { TerminalSnapshotFrame } from '../terminal/snapshot-oracle.ts';
17
18
  import {
18
19
  encodeStreamEnvelope,
19
20
  type StreamObservedEvent,
@@ -85,6 +86,11 @@ import {
85
86
  import { closeOwnedStateStore as closeOwnedStreamServerStateStore } from './stream-server-state-store.ts';
86
87
  import { SessionStatusEngine } from './status/session-status-engine.ts';
87
88
  import { SessionPromptEngine } from './prompt/session-prompt-engine.ts';
89
+ import {
90
+ StorageLifecycleCore,
91
+ type StorageLifecyclePolicy,
92
+ type StorageLifecycleTelemetryStore,
93
+ } from '../storage/storage-lifecycle-core.ts';
88
94
  import {
89
95
  appendThreadTitlePromptHistory,
90
96
  createAnthropicThreadTitleNamer,
@@ -98,32 +104,14 @@ import {
98
104
  eventIncludesTaskId as filterEventIncludesTaskId,
99
105
  matchesObservedFilter as matchesStreamObservedFilter,
100
106
  } from './stream-server-observed-filter.ts';
101
- import type { HarnessLifecycleHooksConfig } from '../config/config-core.ts';
107
+ import {
108
+ DEFAULT_HARNESS_CONFIG,
109
+ loadHarnessConfig,
110
+ type HarnessLifecycleHooksConfig,
111
+ } from '../config/config-core.ts';
102
112
  import { LifecycleHooksRuntime } from './lifecycle-hooks.ts';
103
113
  import { readGitDirectorySnapshot } from '../mux/live-mux/git-snapshot.ts';
104
-
105
- interface SessionDataEvent {
106
- cursor: number;
107
- chunk: Buffer;
108
- }
109
-
110
- interface SessionAttachHandlers {
111
- onData: (event: SessionDataEvent) => void;
112
- onExit: (exit: PtyExit) => void;
113
- }
114
-
115
- interface LiveSessionLike {
116
- attach(handlers: SessionAttachHandlers, sinceCursor?: number): string;
117
- detach(attachmentId: string): void;
118
- latestCursorValue(): number;
119
- processId(): number | null;
120
- write(data: string | Uint8Array): void;
121
- resize(cols: number, rows: number): void;
122
- snapshot(): TerminalSnapshotFrame;
123
- bufferTail?(tailLines?: number): TerminalBufferTail;
124
- close(): void;
125
- onEvent(listener: (event: CodexLiveEvent) => void): () => void;
126
- }
114
+ import type { LiveSessionLike, StartSessionRuntimeInput } from './stream-session-runtime-types.ts';
127
115
 
128
116
  export interface StartControlPlaneSessionInput {
129
117
  command?: string;
@@ -140,21 +128,6 @@ export interface StartControlPlaneSessionInput {
140
128
  terminalBackgroundHex?: string;
141
129
  }
142
130
 
143
- interface StartSessionRuntimeInput {
144
- readonly sessionId: string;
145
- readonly args: readonly string[];
146
- readonly initialCols: number;
147
- readonly initialRows: number;
148
- readonly env?: Record<string, string>;
149
- readonly cwd?: string;
150
- readonly tenantId?: string;
151
- readonly userId?: string;
152
- readonly workspaceId?: string;
153
- readonly worktreeId?: string;
154
- readonly terminalForegroundHex?: string;
155
- readonly terminalBackgroundHex?: string;
156
- }
157
-
158
131
  type StartControlPlaneSession = (input: StartControlPlaneSessionInput) => LiveSessionLike;
159
132
 
160
133
  interface CodexTelemetryServerConfig {
@@ -201,6 +174,11 @@ interface CritiqueConfig {
201
174
  readonly launch: CritiqueLaunchConfig;
202
175
  }
203
176
 
177
+ interface ClaudeLaunchConfig {
178
+ readonly defaultMode: 'yolo' | 'standard';
179
+ readonly directoryModes: Readonly<Record<string, 'yolo' | 'standard'>>;
180
+ }
181
+
204
182
  interface CursorLaunchConfig {
205
183
  readonly defaultMode: 'yolo' | 'standard';
206
184
  readonly directoryModes: Readonly<Record<string, 'yolo' | 'standard'>>;
@@ -230,6 +208,13 @@ interface GitHubIntegrationConfig {
230
208
  readonly viewerLogin: string | null;
231
209
  }
232
210
 
211
+ interface LinearIntegrationConfig {
212
+ readonly enabled: boolean;
213
+ readonly apiBaseUrl: string;
214
+ readonly tokenEnvVar: string;
215
+ readonly token: string | null;
216
+ }
217
+
233
218
  interface ThreadTitleConfig {
234
219
  readonly enabled: boolean;
235
220
  readonly apiKey: string | null;
@@ -248,6 +233,7 @@ interface GitHubRemotePullRequest {
248
233
  readonly baseBranch: string;
249
234
  readonly state: 'open' | 'closed';
250
235
  readonly isDraft: boolean;
236
+ readonly mergedAt: string | null;
251
237
  readonly updatedAt: string;
252
238
  readonly createdAt: string;
253
239
  readonly closedAt: string | null;
@@ -264,6 +250,48 @@ interface GitHubRemotePrJob {
264
250
  readonly completedAt: string | null;
265
251
  }
266
252
 
253
+ interface GitHubRemotePrReviewComment {
254
+ readonly commentId: string;
255
+ readonly authorLogin: string | null;
256
+ readonly body: string;
257
+ readonly url: string | null;
258
+ readonly createdAt: string;
259
+ readonly updatedAt: string;
260
+ }
261
+
262
+ interface GitHubRemotePrReviewThread {
263
+ readonly threadId: string;
264
+ readonly isResolved: boolean;
265
+ readonly isOutdated: boolean;
266
+ readonly resolvedByLogin: string | null;
267
+ readonly comments: readonly GitHubRemotePrReviewComment[];
268
+ }
269
+
270
+ interface GitHubProjectReviewCachePullRequest {
271
+ readonly number: number;
272
+ readonly title: string;
273
+ readonly url: string;
274
+ readonly authorLogin: string | null;
275
+ readonly headBranch: string;
276
+ readonly headSha: string;
277
+ readonly baseBranch: string;
278
+ readonly state: 'draft' | 'open' | 'merged' | 'closed';
279
+ readonly isDraft: boolean;
280
+ readonly mergedAt: string | null;
281
+ readonly closedAt: string | null;
282
+ readonly updatedAt: string;
283
+ readonly createdAt: string;
284
+ }
285
+
286
+ interface GitHubProjectReviewCacheEntry {
287
+ readonly repositoryId: string;
288
+ readonly branchName: string;
289
+ readonly pr: GitHubProjectReviewCachePullRequest | null;
290
+ readonly openThreads: readonly GitHubRemotePrReviewThread[];
291
+ readonly resolvedThreads: readonly GitHubRemotePrReviewThread[];
292
+ readonly fetchedAtMs: number;
293
+ }
294
+
267
295
  type GitDirectorySnapshot = Awaited<ReturnType<typeof readGitDirectorySnapshot>>;
268
296
  type GitDirectorySnapshotReader = (cwd: string) => Promise<GitDirectorySnapshot>;
269
297
  type GitHubTokenResolver = () => Promise<string | null>;
@@ -290,12 +318,14 @@ interface StartControlPlaneStreamServerOptions {
290
318
  codexTelemetry?: CodexTelemetryServerConfig;
291
319
  codexHistory?: CodexHistoryIngestConfig;
292
320
  codexLaunch?: CodexLaunchConfig;
321
+ claudeLaunch?: ClaudeLaunchConfig;
293
322
  critique?: CritiqueConfig;
294
323
  agentInstall?: Partial<Record<AgentToolType, AgentInstallCommandConfig>>;
295
324
  cursorLaunch?: CursorLaunchConfig;
296
325
  cursorHooks?: Partial<CursorHooksConfig>;
297
326
  gitStatus?: GitStatusMonitorConfig;
298
327
  github?: Partial<GitHubIntegrationConfig>;
328
+ linear?: Partial<LinearIntegrationConfig>;
299
329
  githubTokenResolver?: GitHubTokenResolver;
300
330
  githubExecFile?: GitHubExecFile;
301
331
  githubFetch?: typeof fetch;
@@ -303,6 +333,20 @@ interface StartControlPlaneStreamServerOptions {
303
333
  lifecycleHooks?: HarnessLifecycleHooksConfig;
304
334
  threadTitle?: Partial<ThreadTitleConfig>;
305
335
  threadTitleNamer?: ThreadTitleNamer;
336
+ storageLifecyclePolicy?: Partial<StorageLifecyclePolicy>;
337
+ storageLifecyclePolicyReload?: {
338
+ cwd: string;
339
+ filePath?: string;
340
+ env?: NodeJS.ProcessEnv;
341
+ pollMs?: number;
342
+ };
343
+ }
344
+
345
+ interface StorageLifecyclePolicyReloadConfig {
346
+ readonly cwd: string;
347
+ readonly filePath?: string;
348
+ readonly env?: NodeJS.ProcessEnv;
349
+ readonly pollMs: number;
306
350
  }
307
351
 
308
352
  interface ConnectionState {
@@ -427,6 +471,52 @@ interface OtlpEndpointTarget {
427
471
  readonly token: string;
428
472
  }
429
473
 
474
+ function asTelemetryLifecycleStore(value: unknown): StorageLifecycleTelemetryStore | null {
475
+ if (typeof value !== 'object' || value === null) {
476
+ return null;
477
+ }
478
+ const candidate = value as Record<string, unknown>;
479
+ const prune = candidate['pruneTelemetryOlderThan'];
480
+ const checkpoint = candidate['checkpointWal'];
481
+ const compact = candidate['compactFreelistPages'];
482
+ const copyForward = candidate['runOnlineCopyForwardCompactionStep'];
483
+ if (
484
+ typeof prune !== 'function' ||
485
+ typeof checkpoint !== 'function' ||
486
+ typeof compact !== 'function'
487
+ ) {
488
+ return null;
489
+ }
490
+ return {
491
+ pruneTelemetryOlderThan: (cutoffIngestedAt, limit) =>
492
+ (prune as (cutoffIngestedAt: string, limit: number) => number).call(
493
+ value,
494
+ cutoffIngestedAt,
495
+ limit,
496
+ ),
497
+ checkpointWal: (mode) => {
498
+ (checkpoint as (mode?: 'PASSIVE' | 'TRUNCATE') => void).call(value, mode);
499
+ },
500
+ compactFreelistPages: (maxPages) => {
501
+ (compact as (maxPages: number) => void).call(value, maxPages);
502
+ },
503
+ ...(typeof copyForward !== 'function'
504
+ ? {}
505
+ : {
506
+ runOnlineCopyForwardCompactionStep: (batchSize: number, finalizeTailRows: number) =>
507
+ (
508
+ copyForward as (
509
+ batchSize: number,
510
+ finalizeTailRows: number,
511
+ ) => {
512
+ readonly state: 'idle' | 'copying' | 'finalized';
513
+ readonly copiedRows: number;
514
+ }
515
+ ).call(value, batchSize, finalizeTailRows),
516
+ }),
517
+ };
518
+ }
519
+
430
520
  function isTelemetryRequestAbortError(error: unknown): boolean {
431
521
  const code = (error as NodeJS.ErrnoException).code;
432
522
  if (code === 'ECONNRESET' || code === 'ERR_STREAM_PREMATURE_CLOSE') {
@@ -441,6 +531,11 @@ const DEFAULT_SESSION_EXIT_TOMBSTONE_TTL_MS = 5 * 60 * 1000;
441
531
  const DEFAULT_MAX_STREAM_JOURNAL_ENTRIES = 10000;
442
532
  const DEFAULT_GIT_STATUS_POLL_MS = 1200;
443
533
  const DEFAULT_GITHUB_POLL_MS = 15_000;
534
+ const DEFAULT_STORAGE_LIFECYCLE_POLICY_RELOAD_POLL_MS = 5000;
535
+ const DEFAULT_GITHUB_PROJECT_REVIEW_PREWARM_INTERVAL_MS = 5 * 60 * 1000;
536
+ const DEFAULT_LINEAR_API_BASE_URL = 'https://api.linear.app/graphql';
537
+ const GITHUB_OAUTH_ACCESS_TOKEN_ENV_VAR = 'HARNESS_GITHUB_OAUTH_ACCESS_TOKEN';
538
+ const LINEAR_OAUTH_ACCESS_TOKEN_ENV_VAR = 'HARNESS_LINEAR_OAUTH_ACCESS_TOKEN';
444
539
  const HISTORY_POLL_JITTER_RATIO = 0.35;
445
540
  const SESSION_DIAGNOSTICS_BUCKET_MS = 10_000;
446
541
  const SESSION_DIAGNOSTICS_BUCKET_COUNT = 6;
@@ -597,6 +692,23 @@ function normalizeCodexHistoryConfig(
597
692
  };
598
693
  }
599
694
 
695
+ function normalizeStorageLifecyclePolicyReloadConfig(
696
+ input: StartControlPlaneStreamServerOptions['storageLifecyclePolicyReload'],
697
+ ): StorageLifecyclePolicyReloadConfig | null {
698
+ if (input === undefined) {
699
+ return null;
700
+ }
701
+ return {
702
+ cwd: input.cwd,
703
+ ...(input.filePath === undefined ? {} : { filePath: input.filePath }),
704
+ ...(input.env === undefined ? {} : { env: input.env }),
705
+ pollMs: Math.max(
706
+ 250,
707
+ Math.floor(input.pollMs ?? DEFAULT_STORAGE_LIFECYCLE_POLICY_RELOAD_POLL_MS),
708
+ ),
709
+ };
710
+ }
711
+
600
712
  function normalizeCodexLaunchConfig(input: CodexLaunchConfig | undefined): CodexLaunchConfig {
601
713
  return {
602
714
  defaultMode: input?.defaultMode ?? 'standard',
@@ -666,6 +778,13 @@ function normalizeAgentInstallConfig(
666
778
  };
667
779
  }
668
780
 
781
+ function normalizeClaudeLaunchConfig(input: ClaudeLaunchConfig | undefined): ClaudeLaunchConfig {
782
+ return {
783
+ defaultMode: input?.defaultMode ?? 'standard',
784
+ directoryModes: input?.directoryModes ?? {},
785
+ };
786
+ }
787
+
669
788
  function normalizeCursorLaunchConfig(input: CursorLaunchConfig | undefined): CursorLaunchConfig {
670
789
  return {
671
790
  defaultMode: input?.defaultMode ?? 'standard',
@@ -729,10 +848,15 @@ function normalizeGitHubIntegrationConfig(
729
848
  typeof tokenEnvVarRaw === 'string' && tokenEnvVarRaw.trim().length > 0
730
849
  ? tokenEnvVarRaw.trim()
731
850
  : 'GITHUB_TOKEN';
732
- const envToken = process.env[tokenEnvVar];
851
+ const manualEnvToken = process.env[tokenEnvVar];
852
+ const oauthEnvToken = process.env[GITHUB_OAUTH_ACCESS_TOKEN_ENV_VAR];
853
+ const envTokenRaw =
854
+ typeof manualEnvToken === 'string' && manualEnvToken.trim().length > 0
855
+ ? manualEnvToken
856
+ : oauthEnvToken;
733
857
  const tokenRaw =
734
858
  input?.token ??
735
- (typeof envToken === 'string' && envToken.trim().length > 0 ? envToken.trim() : null);
859
+ (typeof envTokenRaw === 'string' && envTokenRaw.trim().length > 0 ? envTokenRaw.trim() : null);
736
860
  const branchStrategyRaw = input?.branchStrategy;
737
861
  const branchStrategy =
738
862
  branchStrategyRaw === 'current-only' ||
@@ -772,6 +896,36 @@ function normalizeGitHubIntegrationConfig(
772
896
  };
773
897
  }
774
898
 
899
+ function normalizeLinearIntegrationConfig(
900
+ input: Partial<LinearIntegrationConfig> | undefined,
901
+ ): LinearIntegrationConfig {
902
+ const tokenEnvVarRaw = input?.tokenEnvVar;
903
+ const tokenEnvVar =
904
+ typeof tokenEnvVarRaw === 'string' && tokenEnvVarRaw.trim().length > 0
905
+ ? tokenEnvVarRaw.trim()
906
+ : 'LINEAR_API_KEY';
907
+ const manualEnvToken = process.env[tokenEnvVar];
908
+ const oauthEnvToken = process.env[LINEAR_OAUTH_ACCESS_TOKEN_ENV_VAR];
909
+ const envTokenRaw =
910
+ typeof manualEnvToken === 'string' && manualEnvToken.trim().length > 0
911
+ ? manualEnvToken
912
+ : oauthEnvToken;
913
+ const tokenRaw =
914
+ input?.token ??
915
+ (typeof envTokenRaw === 'string' && envTokenRaw.trim().length > 0 ? envTokenRaw.trim() : null);
916
+ const apiBaseUrlRaw = input?.apiBaseUrl;
917
+ const apiBaseUrl =
918
+ typeof apiBaseUrlRaw === 'string' && apiBaseUrlRaw.trim().length > 0
919
+ ? apiBaseUrlRaw.trim().replace(/\/+$/u, '')
920
+ : DEFAULT_LINEAR_API_BASE_URL;
921
+ return {
922
+ enabled: input?.enabled ?? false,
923
+ apiBaseUrl,
924
+ tokenEnvVar,
925
+ token: tokenRaw,
926
+ };
927
+ }
928
+
775
929
  function normalizeThreadTitleConfig(
776
930
  input: Partial<ThreadTitleConfig> | undefined,
777
931
  ): ThreadTitleConfig {
@@ -965,6 +1119,7 @@ const streamServerInternals = {
965
1119
  gitRepositorySnapshotEqual,
966
1120
  commandExists,
967
1121
  normalizeGitHubIntegrationConfig,
1122
+ normalizeLinearIntegrationConfig,
968
1123
  parseGitHubOwnerRepoFromRemote,
969
1124
  resolveTrackedBranchName,
970
1125
  summarizeGitHubCiRollup,
@@ -1092,12 +1247,14 @@ export class ControlPlaneStreamServer {
1092
1247
  private readonly codexTelemetry: CodexTelemetryServerConfig;
1093
1248
  private readonly codexHistory: CodexHistoryIngestConfig;
1094
1249
  private readonly codexLaunch: CodexLaunchConfig;
1250
+ private readonly claudeLaunch: ClaudeLaunchConfig;
1095
1251
  private readonly critique: CritiqueConfig;
1096
1252
  private readonly agentInstall: AgentInstallConfig;
1097
1253
  private readonly cursorLaunch: CursorLaunchConfig;
1098
1254
  private readonly cursorHooks: CursorHooksConfig;
1099
1255
  private readonly gitStatusMonitor: GitStatusMonitorConfig;
1100
1256
  private readonly github: GitHubIntegrationConfig;
1257
+ private readonly linear: LinearIntegrationConfig;
1101
1258
  private readonly githubTokenResolver: GitHubTokenResolver;
1102
1259
  private readonly githubExecFile: GitHubExecFile;
1103
1260
  private readonly githubFetch: typeof fetch;
@@ -1107,6 +1264,16 @@ export class ControlPlaneStreamServer {
1107
1264
  repo: string;
1108
1265
  headBranch: string;
1109
1266
  }): Promise<GitHubRemotePullRequest | null>;
1267
+ findPullRequestForBranch(input: {
1268
+ owner: string;
1269
+ repo: string;
1270
+ headBranch: string;
1271
+ }): Promise<GitHubRemotePullRequest | null>;
1272
+ listPullRequestReviewThreads(input: {
1273
+ owner: string;
1274
+ repo: string;
1275
+ pullNumber: number;
1276
+ }): Promise<readonly GitHubRemotePrReviewThread[]>;
1110
1277
  createPullRequest(input: {
1111
1278
  owner: string;
1112
1279
  repo: string;
@@ -1117,6 +1284,16 @@ export class ControlPlaneStreamServer {
1117
1284
  draft: boolean;
1118
1285
  }): Promise<GitHubRemotePullRequest>;
1119
1286
  };
1287
+ private readonly linearApi: {
1288
+ issueByIdentifier(input: { identifier: string }): Promise<{
1289
+ identifier: string;
1290
+ title: string;
1291
+ description: string | null;
1292
+ url: string | null;
1293
+ stateName: string | null;
1294
+ teamKey: string | null;
1295
+ } | null>;
1296
+ };
1120
1297
  private readonly readGitDirectorySnapshot: GitDirectorySnapshotReader;
1121
1298
  private readonly statusEngine = new SessionStatusEngine();
1122
1299
  private readonly promptEngine = new SessionPromptEngine();
@@ -1129,6 +1306,8 @@ export class ControlPlaneStreamServer {
1129
1306
  private telemetryAddress: AddressInfo | null = null;
1130
1307
  private readonly telemetryTokenToSessionId = new Map<string, string>();
1131
1308
  private readonly lifecycleHooks: LifecycleHooksRuntime;
1309
+ private readonly storageLifecycle: StorageLifecycleCore;
1310
+ private readonly storageLifecyclePolicyReload: StorageLifecyclePolicyReloadConfig | null;
1132
1311
  private historyPollTimer: NodeJS.Timeout | null = null;
1133
1312
  private historyPollInFlight = false;
1134
1313
  private historyIdleStreak = 0;
@@ -1142,9 +1321,18 @@ export class ControlPlaneStreamServer {
1142
1321
  private gitStatusPollInFlight = false;
1143
1322
  private githubPollInFlight = false;
1144
1323
  private githubPollPromise: Promise<void> | null = null;
1324
+ private storageLifecycleTimer: NodeJS.Timeout | null = null;
1325
+ private storageLifecyclePolicyReloadTimer: NodeJS.Timeout | null = null;
1326
+ private storageLifecyclePolicyLastKnownGood = DEFAULT_HARNESS_CONFIG;
1327
+ private storageLifecyclePolicyLastError: string | null = null;
1145
1328
  private readonly gitStatusRefreshInFlightDirectoryIds = new Set<string>();
1146
1329
  private readonly gitStatusByDirectoryId = new Map<string, DirectoryGitStatusCacheEntry>();
1147
1330
  private readonly gitStatusDirectoriesById = new Map<string, ControlPlaneDirectoryRecord>();
1331
+ private readonly githubProjectReviewCacheByKey = new Map<string, GitHubProjectReviewCacheEntry>();
1332
+ private readonly githubProjectReviewRefreshInFlightByKey = new Map<
1333
+ string,
1334
+ Promise<GitHubProjectReviewCacheEntry>
1335
+ >();
1148
1336
  private readonly connections = new Map<string, ConnectionState>();
1149
1337
  private readonly sessions = new Map<string, SessionState>();
1150
1338
  private readonly launchCommandBySessionId = new Map<string, string>();
@@ -1173,25 +1361,40 @@ export class ControlPlaneStreamServer {
1173
1361
  this.stateStore = options.stateStore;
1174
1362
  this.ownsStateStore = false;
1175
1363
  } else {
1176
- this.stateStore = new SqliteControlPlaneStore(options.stateStorePath ?? ':memory:');
1364
+ const busyTimeoutMs = options.storageLifecyclePolicy?.busyTimeoutMs;
1365
+ this.stateStore = new SqliteControlPlaneStore(
1366
+ options.stateStorePath ?? ':memory:',
1367
+ busyTimeoutMs === undefined
1368
+ ? undefined
1369
+ : {
1370
+ busyTimeoutMs,
1371
+ },
1372
+ );
1177
1373
  this.ownsStateStore = true;
1178
1374
  }
1179
1375
  this.codexTelemetry = normalizeCodexTelemetryConfig(options.codexTelemetry);
1180
1376
  this.codexHistory = normalizeCodexHistoryConfig(options.codexHistory);
1181
1377
  this.codexLaunch = normalizeCodexLaunchConfig(options.codexLaunch);
1378
+ this.claudeLaunch = normalizeClaudeLaunchConfig(options.claudeLaunch);
1182
1379
  this.critique = normalizeCritiqueConfig(options.critique);
1183
1380
  this.agentInstall = normalizeAgentInstallConfig(options.agentInstall);
1184
1381
  this.cursorLaunch = normalizeCursorLaunchConfig(options.cursorLaunch);
1185
1382
  this.cursorHooks = normalizeCursorHooksConfig(options.cursorHooks);
1186
1383
  this.gitStatusMonitor = normalizeGitStatusMonitorConfig(options.gitStatus);
1187
1384
  this.github = normalizeGitHubIntegrationConfig(options.github);
1385
+ this.linear = normalizeLinearIntegrationConfig(options.linear);
1188
1386
  this.githubExecFile = options.githubExecFile ?? execFile;
1189
1387
  this.githubTokenResolver =
1190
1388
  options.githubTokenResolver ?? (async () => await this.readGhAuthToken());
1191
1389
  this.githubFetch = options.githubFetch ?? fetch;
1192
1390
  this.githubApi = {
1193
- openPullRequestForBranch: async (input) => await this.openGitHubPullRequestForBranch(input),
1194
- createPullRequest: async (input) => await this.createGitHubPullRequest(input),
1391
+ openPullRequestForBranch: this.openGitHubPullRequestForBranch.bind(this),
1392
+ findPullRequestForBranch: this.findGitHubPullRequestForBranch.bind(this),
1393
+ listPullRequestReviewThreads: this.listGitHubPullRequestReviewThreads.bind(this),
1394
+ createPullRequest: this.createGitHubPullRequest.bind(this),
1395
+ };
1396
+ this.linearApi = {
1397
+ issueByIdentifier: this.fetchLinearIssueByIdentifier.bind(this),
1195
1398
  };
1196
1399
  this.readGitDirectorySnapshot =
1197
1400
  options.readGitDirectorySnapshot ??
@@ -1230,14 +1433,22 @@ export class ControlPlaneStreamServer {
1230
1433
  webhooks: [],
1231
1434
  },
1232
1435
  );
1233
- this.server = createServer((socket) => {
1234
- this.handleConnection(socket);
1235
- });
1436
+ this.server = createServer(this.handleConnection.bind(this));
1236
1437
  this.telemetryServer = this.codexTelemetry.enabled
1237
- ? createHttpServer((request, response) => {
1238
- this.handleTelemetryHttpRequest(request, response);
1239
- })
1438
+ ? createHttpServer(this.handleTelemetryHttpRequest.bind(this))
1240
1439
  : null;
1440
+ this.storageLifecycle = new StorageLifecycleCore({
1441
+ telemetryStore: asTelemetryLifecycleStore(this.stateStore),
1442
+ ...(options.storageLifecyclePolicy === undefined
1443
+ ? {}
1444
+ : {
1445
+ policy: options.storageLifecyclePolicy,
1446
+ }),
1447
+ writeStderr: (text) => process.stderr.write(text),
1448
+ });
1449
+ this.storageLifecyclePolicyReload = normalizeStorageLifecyclePolicyReloadConfig(
1450
+ options.storageLifecyclePolicyReload,
1451
+ );
1241
1452
  }
1242
1453
 
1243
1454
  async start(): Promise<void> {
@@ -1268,6 +1479,8 @@ export class ControlPlaneStreamServer {
1268
1479
  this.startHistoryPollingIfEnabled();
1269
1480
  this.startGitStatusPollingIfEnabled();
1270
1481
  this.startGitHubPollingIfEnabled();
1482
+ this.startStorageLifecyclePolling();
1483
+ this.startStorageLifecyclePolicyReloadPolling();
1271
1484
  }
1272
1485
 
1273
1486
  address(): AddressInfo {
@@ -1294,6 +1507,8 @@ export class ControlPlaneStreamServer {
1294
1507
  this.stopHistoryPolling();
1295
1508
  this.stopGitStatusPolling();
1296
1509
  this.stopGitHubPolling();
1510
+ this.stopStorageLifecyclePolling();
1511
+ this.stopStorageLifecyclePolicyReloadPolling();
1297
1512
  await this.waitForGitHubPollingToSettle();
1298
1513
 
1299
1514
  for (const sessionId of [...this.sessions.keys()]) {
@@ -1405,6 +1620,70 @@ export class ControlPlaneStreamServer {
1405
1620
  this.historyNextAllowedPollAtMs = 0;
1406
1621
  }
1407
1622
 
1623
+ updateStorageLifecyclePolicy(policy: Partial<StorageLifecyclePolicy>): void {
1624
+ this.storageLifecycle.updatePolicy(policy);
1625
+ }
1626
+
1627
+ private startStorageLifecyclePolling(): void {
1628
+ // Temporarily disabled while maintenance is moved out of interactive/server hot paths.
1629
+ }
1630
+
1631
+ private stopStorageLifecyclePolling(): void {
1632
+ if (this.storageLifecycleTimer === null) {
1633
+ return;
1634
+ }
1635
+ clearInterval(this.storageLifecycleTimer);
1636
+ this.storageLifecycleTimer = null;
1637
+ }
1638
+
1639
+ private startStorageLifecyclePolicyReloadPolling(): void {
1640
+ if (
1641
+ this.storageLifecyclePolicyReload === null ||
1642
+ this.storageLifecyclePolicyReloadTimer !== null
1643
+ ) {
1644
+ return;
1645
+ }
1646
+ const intervalMs = this.storageLifecyclePolicyReload.pollMs;
1647
+ this.storageLifecyclePolicyReloadTimer = setInterval(() => {
1648
+ this.reloadStorageLifecyclePolicyFromConfig();
1649
+ }, intervalMs);
1650
+ this.storageLifecyclePolicyReloadTimer.unref();
1651
+ }
1652
+
1653
+ private stopStorageLifecyclePolicyReloadPolling(): void {
1654
+ if (this.storageLifecyclePolicyReloadTimer === null) {
1655
+ return;
1656
+ }
1657
+ clearInterval(this.storageLifecyclePolicyReloadTimer);
1658
+ this.storageLifecyclePolicyReloadTimer = null;
1659
+ }
1660
+
1661
+ private reloadStorageLifecyclePolicyFromConfig(): void {
1662
+ if (this.storageLifecyclePolicyReload === null) {
1663
+ return;
1664
+ }
1665
+ const loaded = loadHarnessConfig({
1666
+ cwd: this.storageLifecyclePolicyReload.cwd,
1667
+ ...(this.storageLifecyclePolicyReload.filePath === undefined
1668
+ ? {}
1669
+ : { filePath: this.storageLifecyclePolicyReload.filePath }),
1670
+ ...(this.storageLifecyclePolicyReload.env === undefined
1671
+ ? {}
1672
+ : { env: this.storageLifecyclePolicyReload.env }),
1673
+ lastKnownGood: this.storageLifecyclePolicyLastKnownGood,
1674
+ });
1675
+ this.storageLifecyclePolicyLastKnownGood = loaded.config;
1676
+ if (loaded.fromLastKnownGood && loaded.error !== null) {
1677
+ if (loaded.error !== this.storageLifecyclePolicyLastError) {
1678
+ process.stderr.write(`[config] storage lifecycle policy reload skipped: ${loaded.error}\n`);
1679
+ this.storageLifecyclePolicyLastError = loaded.error;
1680
+ }
1681
+ return;
1682
+ }
1683
+ this.storageLifecyclePolicyLastError = null;
1684
+ this.updateStorageLifecyclePolicy(loaded.config.storage.lifecycle);
1685
+ }
1686
+
1408
1687
  private startGitStatusPollingIfEnabled(): void {
1409
1688
  if (!this.gitStatusMonitor.enabled || this.gitStatusPollTimer !== null) {
1410
1689
  return;
@@ -1551,6 +1830,7 @@ export class ControlPlaneStreamServer {
1551
1830
  this.stopGitHubPolling();
1552
1831
  this.stopGitStatusPolling();
1553
1832
  this.stopHistoryPolling();
1833
+ this.stopStorageLifecyclePolling();
1554
1834
  return true;
1555
1835
  }
1556
1836
 
@@ -1767,6 +2047,8 @@ export class ControlPlaneStreamServer {
1767
2047
  directoryPath: directory?.path ?? null,
1768
2048
  codexLaunchDefaultMode: this.codexLaunch.defaultMode,
1769
2049
  codexLaunchModeByDirectoryPath: this.codexLaunch.directoryModes,
2050
+ claudeLaunchDefaultMode: this.claudeLaunch.defaultMode,
2051
+ claudeLaunchModeByDirectoryPath: this.claudeLaunch.directoryModes,
1770
2052
  cursorLaunchDefaultMode: this.cursorLaunch.defaultMode,
1771
2053
  cursorLaunchModeByDirectoryPath: this.cursorLaunch.directoryModes,
1772
2054
  });
@@ -1959,6 +2241,39 @@ export class ControlPlaneStreamServer {
1959
2241
  }
1960
2242
 
1961
2243
  private handleTelemetryHttpRequest(request: IncomingMessage, response: ServerResponse): void {
2244
+ const requestWithEvents = request as unknown as {
2245
+ on?: (event: string, listener: (...args: unknown[]) => void) => void;
2246
+ once?: (event: string, listener: (...args: unknown[]) => void) => void;
2247
+ off?: (event: string, listener: (...args: unknown[]) => void) => void;
2248
+ };
2249
+ const responseWithEvents = response as unknown as {
2250
+ on?: (event: string, listener: (...args: unknown[]) => void) => void;
2251
+ once?: (event: string, listener: (...args: unknown[]) => void) => void;
2252
+ off?: (event: string, listener: (...args: unknown[]) => void) => void;
2253
+ };
2254
+ const onStreamError = (error: unknown): void => {
2255
+ if (isTelemetryRequestAbortError(error)) {
2256
+ return;
2257
+ }
2258
+ if (response.writableEnded) {
2259
+ return;
2260
+ }
2261
+ response.statusCode = 500;
2262
+ response.end();
2263
+ };
2264
+ const cleanupStreamErrorListeners = (): void => {
2265
+ requestWithEvents.off?.('error', onStreamError);
2266
+ responseWithEvents.off?.('error', onStreamError);
2267
+ requestWithEvents.off?.('close', cleanupStreamErrorListeners);
2268
+ responseWithEvents.off?.('close', cleanupStreamErrorListeners);
2269
+ responseWithEvents.off?.('finish', cleanupStreamErrorListeners);
2270
+ };
2271
+ requestWithEvents.on?.('error', onStreamError);
2272
+ responseWithEvents.on?.('error', onStreamError);
2273
+ requestWithEvents.once?.('close', cleanupStreamErrorListeners);
2274
+ responseWithEvents.once?.('close', cleanupStreamErrorListeners);
2275
+ responseWithEvents.once?.('finish', cleanupStreamErrorListeners);
2276
+
1962
2277
  void this.handleTelemetryHttpRequestAsync(request, response).catch((error: unknown) => {
1963
2278
  if (isTelemetryRequestAbortError(error)) {
1964
2279
  return;
@@ -2101,6 +2416,7 @@ export class ControlPlaneStreamServer {
2101
2416
  observedAt: event.observedAt,
2102
2417
  payload: event.payload,
2103
2418
  });
2419
+ const persistedPayload = this.storageLifecycle.prepareTelemetryPayload(event.payload);
2104
2420
 
2105
2421
  const inserted = this.stateStore.appendTelemetry({
2106
2422
  source: event.source,
@@ -2110,7 +2426,7 @@ export class ControlPlaneStreamServer {
2110
2426
  severity: event.severity,
2111
2427
  summary: event.summary,
2112
2428
  observedAt: event.observedAt,
2113
- payload: event.payload,
2429
+ payload: persistedPayload,
2114
2430
  fingerprint,
2115
2431
  });
2116
2432
  if (resolvedSessionId !== null) {
@@ -2385,6 +2701,200 @@ export class ControlPlaneStreamServer {
2385
2701
  }
2386
2702
  }
2387
2703
 
2704
+ private githubProjectReviewCacheKey(input: { repositoryId: string; branchName: string }): string {
2705
+ return `${input.repositoryId}:${input.branchName}`;
2706
+ }
2707
+
2708
+ private getGitHubProjectReviewCache(input: { repositoryId: string; branchName: string }): {
2709
+ repositoryId: string;
2710
+ branchName: string;
2711
+ pr: {
2712
+ number: number;
2713
+ title: string;
2714
+ url: string;
2715
+ authorLogin: string | null;
2716
+ headBranch: string;
2717
+ headSha: string;
2718
+ baseBranch: string;
2719
+ state: 'draft' | 'open' | 'merged' | 'closed';
2720
+ isDraft: boolean;
2721
+ mergedAt: string | null;
2722
+ closedAt: string | null;
2723
+ updatedAt: string;
2724
+ createdAt: string;
2725
+ } | null;
2726
+ openThreads: readonly {
2727
+ threadId: string;
2728
+ isResolved: boolean;
2729
+ isOutdated: boolean;
2730
+ resolvedByLogin: string | null;
2731
+ comments: readonly {
2732
+ commentId: string;
2733
+ authorLogin: string | null;
2734
+ body: string;
2735
+ url: string | null;
2736
+ createdAt: string;
2737
+ updatedAt: string;
2738
+ }[];
2739
+ }[];
2740
+ resolvedThreads: readonly {
2741
+ threadId: string;
2742
+ isResolved: boolean;
2743
+ isOutdated: boolean;
2744
+ resolvedByLogin: string | null;
2745
+ comments: readonly {
2746
+ commentId: string;
2747
+ authorLogin: string | null;
2748
+ body: string;
2749
+ url: string | null;
2750
+ createdAt: string;
2751
+ updatedAt: string;
2752
+ }[];
2753
+ }[];
2754
+ fetchedAtMs: number;
2755
+ } | null {
2756
+ const key = this.githubProjectReviewCacheKey(input);
2757
+ const cached = this.githubProjectReviewCacheByKey.get(key);
2758
+ return cached ?? null;
2759
+ }
2760
+
2761
+ private async refreshGitHubProjectReviewCache(input: {
2762
+ repositoryId: string;
2763
+ owner: string;
2764
+ repo: string;
2765
+ branchName: string;
2766
+ forceRefresh?: boolean;
2767
+ remotePr?: GitHubRemotePullRequest | null;
2768
+ }): Promise<{
2769
+ repositoryId: string;
2770
+ branchName: string;
2771
+ pr: {
2772
+ number: number;
2773
+ title: string;
2774
+ url: string;
2775
+ authorLogin: string | null;
2776
+ headBranch: string;
2777
+ headSha: string;
2778
+ baseBranch: string;
2779
+ state: 'draft' | 'open' | 'merged' | 'closed';
2780
+ isDraft: boolean;
2781
+ mergedAt: string | null;
2782
+ closedAt: string | null;
2783
+ updatedAt: string;
2784
+ createdAt: string;
2785
+ } | null;
2786
+ openThreads: readonly {
2787
+ threadId: string;
2788
+ isResolved: boolean;
2789
+ isOutdated: boolean;
2790
+ resolvedByLogin: string | null;
2791
+ comments: readonly {
2792
+ commentId: string;
2793
+ authorLogin: string | null;
2794
+ body: string;
2795
+ url: string | null;
2796
+ createdAt: string;
2797
+ updatedAt: string;
2798
+ }[];
2799
+ }[];
2800
+ resolvedThreads: readonly {
2801
+ threadId: string;
2802
+ isResolved: boolean;
2803
+ isOutdated: boolean;
2804
+ resolvedByLogin: string | null;
2805
+ comments: readonly {
2806
+ commentId: string;
2807
+ authorLogin: string | null;
2808
+ body: string;
2809
+ url: string | null;
2810
+ createdAt: string;
2811
+ updatedAt: string;
2812
+ }[];
2813
+ }[];
2814
+ fetchedAtMs: number;
2815
+ }> {
2816
+ const key = this.githubProjectReviewCacheKey(input);
2817
+ const forceRefresh = input.forceRefresh === true;
2818
+ const cached = this.githubProjectReviewCacheByKey.get(key);
2819
+ if (
2820
+ !forceRefresh &&
2821
+ cached !== undefined &&
2822
+ Date.now() - cached.fetchedAtMs < DEFAULT_GITHUB_PROJECT_REVIEW_PREWARM_INTERVAL_MS
2823
+ ) {
2824
+ return cached;
2825
+ }
2826
+ const inFlight = this.githubProjectReviewRefreshInFlightByKey.get(key);
2827
+ if (inFlight !== undefined) {
2828
+ return await inFlight;
2829
+ }
2830
+ const refreshPromise: Promise<GitHubProjectReviewCacheEntry> = (async () => {
2831
+ const remotePr =
2832
+ input.remotePr !== undefined
2833
+ ? input.remotePr
2834
+ : await this.openGitHubPullRequestForBranch({
2835
+ owner: input.owner,
2836
+ repo: input.repo,
2837
+ headBranch: input.branchName,
2838
+ });
2839
+ if (remotePr === null) {
2840
+ const next: GitHubProjectReviewCacheEntry = {
2841
+ repositoryId: input.repositoryId,
2842
+ branchName: input.branchName,
2843
+ pr: null,
2844
+ openThreads: [],
2845
+ resolvedThreads: [],
2846
+ fetchedAtMs: Date.now(),
2847
+ };
2848
+ this.githubProjectReviewCacheByKey.set(key, next);
2849
+ return next;
2850
+ }
2851
+ const reviewThreads = await this.listGitHubPullRequestReviewThreads({
2852
+ owner: input.owner,
2853
+ repo: input.repo,
2854
+ pullNumber: remotePr.number,
2855
+ });
2856
+ const next: GitHubProjectReviewCacheEntry = {
2857
+ repositoryId: input.repositoryId,
2858
+ branchName: input.branchName,
2859
+ pr: {
2860
+ number: remotePr.number,
2861
+ title: remotePr.title,
2862
+ url: remotePr.url,
2863
+ authorLogin: remotePr.authorLogin,
2864
+ headBranch: remotePr.headBranch,
2865
+ headSha: remotePr.headSha,
2866
+ baseBranch: remotePr.baseBranch,
2867
+ state:
2868
+ remotePr.state === 'open' && remotePr.isDraft
2869
+ ? 'draft'
2870
+ : remotePr.state === 'open'
2871
+ ? 'open'
2872
+ : remotePr.mergedAt !== null
2873
+ ? 'merged'
2874
+ : 'closed',
2875
+ isDraft: remotePr.isDraft,
2876
+ mergedAt: remotePr.mergedAt,
2877
+ closedAt: remotePr.closedAt,
2878
+ updatedAt: remotePr.updatedAt,
2879
+ createdAt: remotePr.createdAt,
2880
+ },
2881
+ openThreads: reviewThreads.filter((thread) => !thread.isResolved),
2882
+ resolvedThreads: reviewThreads.filter((thread) => thread.isResolved),
2883
+ fetchedAtMs: Date.now(),
2884
+ };
2885
+ this.githubProjectReviewCacheByKey.set(key, next);
2886
+ return next;
2887
+ })();
2888
+ this.githubProjectReviewRefreshInFlightByKey.set(key, refreshPromise);
2889
+ try {
2890
+ return await refreshPromise;
2891
+ } finally {
2892
+ if (this.githubProjectReviewRefreshInFlightByKey.get(key) === refreshPromise) {
2893
+ this.githubProjectReviewRefreshInFlightByKey.delete(key);
2894
+ }
2895
+ }
2896
+ }
2897
+
2388
2898
  private async syncGitHubBranch(input: {
2389
2899
  directory: ControlPlaneDirectoryRecord;
2390
2900
  repository: ControlPlaneRepositoryRecord;
@@ -2407,6 +2917,20 @@ export class ControlPlaneStreamServer {
2407
2917
  return;
2408
2918
  }
2409
2919
  if (remotePr === null) {
2920
+ this.githubProjectReviewCacheByKey.set(
2921
+ this.githubProjectReviewCacheKey({
2922
+ repositoryId: input.repository.repositoryId,
2923
+ branchName: input.branchName,
2924
+ }),
2925
+ {
2926
+ repositoryId: input.repository.repositoryId,
2927
+ branchName: input.branchName,
2928
+ pr: null,
2929
+ openThreads: [],
2930
+ resolvedThreads: [],
2931
+ fetchedAtMs: Date.now(),
2932
+ },
2933
+ );
2410
2934
  const staleOpen = this.stateStore.listGitHubPullRequests({
2411
2935
  repositoryId: input.repository.repositoryId,
2412
2936
  headBranch: input.branchName,
@@ -2487,6 +3011,13 @@ export class ControlPlaneStreamServer {
2487
3011
  isDraft: remotePr.isDraft,
2488
3012
  observedAt: remotePr.updatedAt || now,
2489
3013
  });
3014
+ void this.refreshGitHubProjectReviewCache({
3015
+ repositoryId: input.repository.repositoryId,
3016
+ owner: input.owner,
3017
+ repo: input.repo,
3018
+ branchName: input.branchName,
3019
+ remotePr,
3020
+ }).catch(() => {});
2490
3021
  const jobs = await this.listGitHubPrJobsForCommit({
2491
3022
  owner: input.owner,
2492
3023
  repo: input.repo,
@@ -2596,8 +3127,8 @@ export class ControlPlaneStreamServer {
2596
3127
  if (token === null) {
2597
3128
  const hint =
2598
3129
  this.githubTokenResolutionError === null
2599
- ? 'set GITHUB_TOKEN or run gh auth login'
2600
- : `${this.githubTokenResolutionError}; set GITHUB_TOKEN or run gh auth login`;
3130
+ ? `set ${this.github.tokenEnvVar} or ${GITHUB_OAUTH_ACCESS_TOKEN_ENV_VAR} or run gh auth login`
3131
+ : `${this.githubTokenResolutionError}; set ${this.github.tokenEnvVar} or ${GITHUB_OAUTH_ACCESS_TOKEN_ENV_VAR} or run gh auth login`;
2601
3132
  throw new Error(`github token not configured: ${hint}`);
2602
3133
  }
2603
3134
  const response = await this.githubFetch(`${this.github.apiBaseUrl}${path}`, {
@@ -2618,6 +3149,82 @@ export class ControlPlaneStreamServer {
2618
3149
  return await response.json();
2619
3150
  }
2620
3151
 
3152
+ private async fetchLinearIssueByIdentifier(input: { identifier: string }): Promise<{
3153
+ identifier: string;
3154
+ title: string;
3155
+ description: string | null;
3156
+ url: string | null;
3157
+ stateName: string | null;
3158
+ teamKey: string | null;
3159
+ } | null> {
3160
+ const token = this.linear.token;
3161
+ if (token === null || token.trim().length === 0) {
3162
+ throw new Error(
3163
+ `linear token not configured: set ${this.linear.tokenEnvVar} or ${LINEAR_OAUTH_ACCESS_TOKEN_ENV_VAR}`,
3164
+ );
3165
+ }
3166
+ const client = new LinearClient({
3167
+ apiKey: token,
3168
+ apiUrl: this.linear.apiBaseUrl,
3169
+ });
3170
+ const response = await client.client.rawRequest<
3171
+ {
3172
+ issues?: {
3173
+ nodes?: Array<{
3174
+ identifier?: string;
3175
+ title?: string;
3176
+ description?: string | null;
3177
+ url?: string | null;
3178
+ state?: {
3179
+ name?: string | null;
3180
+ } | null;
3181
+ team?: {
3182
+ key?: string | null;
3183
+ } | null;
3184
+ }>;
3185
+ };
3186
+ },
3187
+ { identifier: string }
3188
+ >(
3189
+ `
3190
+ query HarnessLinearIssueImport($identifier: String!) {
3191
+ issues(filter: { identifier: { eq: $identifier } }, first: 1) {
3192
+ nodes {
3193
+ identifier
3194
+ title
3195
+ description
3196
+ url
3197
+ state {
3198
+ name
3199
+ }
3200
+ team {
3201
+ key
3202
+ }
3203
+ }
3204
+ }
3205
+ }
3206
+ `,
3207
+ {
3208
+ identifier: input.identifier,
3209
+ },
3210
+ );
3211
+ const issue = response.data?.issues?.nodes?.[0];
3212
+ if (issue === undefined) {
3213
+ return null;
3214
+ }
3215
+ if (typeof issue.identifier !== 'string' || typeof issue.title !== 'string') {
3216
+ throw new Error('linear issue response malformed');
3217
+ }
3218
+ return {
3219
+ identifier: issue.identifier,
3220
+ title: issue.title,
3221
+ description: typeof issue.description === 'string' ? issue.description : null,
3222
+ url: typeof issue.url === 'string' ? issue.url : null,
3223
+ stateName: typeof issue.state?.name === 'string' ? issue.state.name : null,
3224
+ teamKey: typeof issue.team?.key === 'string' ? issue.team.key : null,
3225
+ };
3226
+ }
3227
+
2621
3228
  private parseGitHubPullRequest(value: unknown): GitHubRemotePullRequest | null {
2622
3229
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
2623
3230
  return null;
@@ -2634,6 +3241,8 @@ export class ControlPlaneStreamServer {
2634
3241
  const updatedAt = record['updated_at'];
2635
3242
  const createdAt = record['created_at'];
2636
3243
  const closedAt = record['closed_at'];
3244
+ const mergedAtRaw = record['merged_at'];
3245
+ const mergedAt = mergedAtRaw === undefined ? null : mergedAtRaw;
2637
3246
  if (
2638
3247
  typeof number !== 'number' ||
2639
3248
  typeof title !== 'string' ||
@@ -2643,6 +3252,7 @@ export class ControlPlaneStreamServer {
2643
3252
  typeof updatedAt !== 'string' ||
2644
3253
  typeof createdAt !== 'string' ||
2645
3254
  (closedAt !== null && typeof closedAt !== 'string') ||
3255
+ (mergedAt !== null && typeof mergedAt !== 'string') ||
2646
3256
  typeof head !== 'object' ||
2647
3257
  head === null ||
2648
3258
  Array.isArray(head) ||
@@ -2677,6 +3287,7 @@ export class ControlPlaneStreamServer {
2677
3287
  baseBranch: baseRef,
2678
3288
  state,
2679
3289
  isDraft: draft,
3290
+ mergedAt: mergedAt as string | null,
2680
3291
  updatedAt,
2681
3292
  createdAt,
2682
3293
  closedAt: closedAt as string | null,
@@ -2697,6 +3308,204 @@ export class ControlPlaneStreamServer {
2697
3308
  return this.parseGitHubPullRequest(payload[0]);
2698
3309
  }
2699
3310
 
3311
+ private async findGitHubPullRequestForBranch(input: {
3312
+ owner: string;
3313
+ repo: string;
3314
+ headBranch: string;
3315
+ }): Promise<GitHubRemotePullRequest | null> {
3316
+ const head = encodeURIComponent(`${input.owner}:${input.headBranch}`);
3317
+ const path = `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/pulls?state=all&head=${head}&per_page=10&sort=updated&direction=desc`;
3318
+ const payload = await this.githubJsonRequest(path);
3319
+ if (!Array.isArray(payload) || payload.length === 0) {
3320
+ return null;
3321
+ }
3322
+ for (const value of payload) {
3323
+ const parsed = this.parseGitHubPullRequest(value);
3324
+ if (parsed !== null) {
3325
+ return parsed;
3326
+ }
3327
+ }
3328
+ return null;
3329
+ }
3330
+
3331
+ private async listGitHubPullRequestReviewThreads(input: {
3332
+ owner: string;
3333
+ repo: string;
3334
+ pullNumber: number;
3335
+ }): Promise<readonly GitHubRemotePrReviewThread[]> {
3336
+ const payload = await this.githubJsonRequest('/graphql', {
3337
+ method: 'POST',
3338
+ headers: {
3339
+ 'Content-Type': 'application/json',
3340
+ },
3341
+ body: JSON.stringify({
3342
+ query: `
3343
+ query HarnessPullRequestReviewThreads($owner: String!, $repo: String!, $number: Int!) {
3344
+ repository(owner: $owner, name: $repo) {
3345
+ pullRequest(number: $number) {
3346
+ reviewThreads(first: 100) {
3347
+ nodes {
3348
+ id
3349
+ isResolved
3350
+ isOutdated
3351
+ resolvedBy {
3352
+ login
3353
+ }
3354
+ comments(first: 100) {
3355
+ nodes {
3356
+ id
3357
+ body
3358
+ bodyText
3359
+ url
3360
+ createdAt
3361
+ updatedAt
3362
+ author {
3363
+ login
3364
+ }
3365
+ }
3366
+ }
3367
+ }
3368
+ }
3369
+ }
3370
+ }
3371
+ }
3372
+ `,
3373
+ variables: {
3374
+ owner: input.owner,
3375
+ repo: input.repo,
3376
+ number: input.pullNumber,
3377
+ },
3378
+ }),
3379
+ });
3380
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
3381
+ throw new Error('github graphql review threads response malformed');
3382
+ }
3383
+ const payloadRecord = payload as Record<string, unknown>;
3384
+ const errors = payloadRecord['errors'];
3385
+ if (Array.isArray(errors) && errors.length > 0) {
3386
+ const firstError = errors[0];
3387
+ if (typeof firstError === 'object' && firstError !== null && !Array.isArray(firstError)) {
3388
+ const message = (firstError as Record<string, unknown>)['message'];
3389
+ if (typeof message === 'string' && message.trim().length > 0) {
3390
+ throw new Error(`github graphql review threads failed: ${message}`);
3391
+ }
3392
+ }
3393
+ throw new Error('github graphql review threads failed');
3394
+ }
3395
+ const data =
3396
+ typeof payloadRecord['data'] === 'object' &&
3397
+ payloadRecord['data'] !== null &&
3398
+ !Array.isArray(payloadRecord['data'])
3399
+ ? (payloadRecord['data'] as Record<string, unknown>)
3400
+ : null;
3401
+ const repository =
3402
+ data !== null &&
3403
+ typeof data['repository'] === 'object' &&
3404
+ data['repository'] !== null &&
3405
+ !Array.isArray(data['repository'])
3406
+ ? (data['repository'] as Record<string, unknown>)
3407
+ : null;
3408
+ const pullRequest =
3409
+ repository !== null &&
3410
+ typeof repository['pullRequest'] === 'object' &&
3411
+ repository['pullRequest'] !== null &&
3412
+ !Array.isArray(repository['pullRequest'])
3413
+ ? (repository['pullRequest'] as Record<string, unknown>)
3414
+ : null;
3415
+ const reviewThreads =
3416
+ pullRequest !== null &&
3417
+ typeof pullRequest['reviewThreads'] === 'object' &&
3418
+ pullRequest['reviewThreads'] !== null &&
3419
+ !Array.isArray(pullRequest['reviewThreads'])
3420
+ ? (pullRequest['reviewThreads'] as Record<string, unknown>)
3421
+ : null;
3422
+ const threadNodes = reviewThreads?.['nodes'];
3423
+ if (!Array.isArray(threadNodes)) {
3424
+ return [];
3425
+ }
3426
+ const threads: GitHubRemotePrReviewThread[] = [];
3427
+ for (const threadRaw of threadNodes) {
3428
+ if (typeof threadRaw !== 'object' || threadRaw === null || Array.isArray(threadRaw)) {
3429
+ continue;
3430
+ }
3431
+ const thread = threadRaw as Record<string, unknown>;
3432
+ const threadId = thread['id'];
3433
+ const isResolved = thread['isResolved'];
3434
+ const isOutdated = thread['isOutdated'];
3435
+ const resolvedBy =
3436
+ typeof thread['resolvedBy'] === 'object' &&
3437
+ thread['resolvedBy'] !== null &&
3438
+ !Array.isArray(thread['resolvedBy'])
3439
+ ? (thread['resolvedBy'] as Record<string, unknown>)
3440
+ : null;
3441
+ const resolvedByLogin =
3442
+ resolvedBy !== null && typeof resolvedBy['login'] === 'string' ? resolvedBy['login'] : null;
3443
+ const commentsRecord =
3444
+ typeof thread['comments'] === 'object' &&
3445
+ thread['comments'] !== null &&
3446
+ !Array.isArray(thread['comments'])
3447
+ ? (thread['comments'] as Record<string, unknown>)
3448
+ : null;
3449
+ const commentsNodes = commentsRecord?.['nodes'];
3450
+ if (
3451
+ typeof threadId !== 'string' ||
3452
+ typeof isResolved !== 'boolean' ||
3453
+ typeof isOutdated !== 'boolean' ||
3454
+ !Array.isArray(commentsNodes)
3455
+ ) {
3456
+ continue;
3457
+ }
3458
+ const comments: GitHubRemotePrReviewComment[] = [];
3459
+ for (const commentRaw of commentsNodes) {
3460
+ if (typeof commentRaw !== 'object' || commentRaw === null || Array.isArray(commentRaw)) {
3461
+ continue;
3462
+ }
3463
+ const comment = commentRaw as Record<string, unknown>;
3464
+ const commentId = comment['id'];
3465
+ const bodyText = comment['bodyText'];
3466
+ const body = comment['body'];
3467
+ const url = comment['url'];
3468
+ const createdAt = comment['createdAt'];
3469
+ const updatedAt = comment['updatedAt'];
3470
+ const author =
3471
+ typeof comment['author'] === 'object' &&
3472
+ comment['author'] !== null &&
3473
+ !Array.isArray(comment['author'])
3474
+ ? (comment['author'] as Record<string, unknown>)
3475
+ : null;
3476
+ const authorLogin =
3477
+ author !== null && typeof author['login'] === 'string' ? author['login'] : null;
3478
+ const normalizedBody =
3479
+ typeof bodyText === 'string' ? bodyText : typeof body === 'string' ? body : null;
3480
+ if (
3481
+ typeof commentId !== 'string' ||
3482
+ normalizedBody === null ||
3483
+ (url !== null && url !== undefined && typeof url !== 'string') ||
3484
+ typeof createdAt !== 'string' ||
3485
+ typeof updatedAt !== 'string'
3486
+ ) {
3487
+ continue;
3488
+ }
3489
+ comments.push({
3490
+ commentId,
3491
+ authorLogin,
3492
+ body: normalizedBody,
3493
+ url: typeof url === 'string' ? url : null,
3494
+ createdAt,
3495
+ updatedAt,
3496
+ });
3497
+ }
3498
+ threads.push({
3499
+ threadId,
3500
+ isResolved,
3501
+ isOutdated,
3502
+ resolvedByLogin,
3503
+ comments,
3504
+ });
3505
+ }
3506
+ return threads;
3507
+ }
3508
+
2700
3509
  private async createGitHubPullRequest(input: {
2701
3510
  owner: string;
2702
3511
  repo: string;
@@ -3387,7 +4196,7 @@ export class ControlPlaneStreamServer {
3387
4196
  scopeKind: task.scopeKind,
3388
4197
  projectId: task.projectId,
3389
4198
  title: task.title,
3390
- description: task.description,
4199
+ body: task.body,
3391
4200
  status: task.status,
3392
4201
  orderIndex: task.orderIndex,
3393
4202
  claimedByControllerId: task.claimedByControllerId,
@@ -3396,21 +4205,6 @@ export class ControlPlaneStreamServer {
3396
4205
  baseBranch: task.baseBranch,
3397
4206
  claimedAt: task.claimedAt,
3398
4207
  completedAt: task.completedAt,
3399
- linear: {
3400
- issueId: task.linear.issueId,
3401
- identifier: task.linear.identifier,
3402
- url: task.linear.url,
3403
- teamId: task.linear.teamId,
3404
- projectId: task.linear.projectId,
3405
- projectMilestoneId: task.linear.projectMilestoneId,
3406
- cycleId: task.linear.cycleId,
3407
- stateId: task.linear.stateId,
3408
- assigneeId: task.linear.assigneeId,
3409
- priority: task.linear.priority,
3410
- estimate: task.linear.estimate,
3411
- dueDate: task.linear.dueDate,
3412
- labelIds: [...task.linear.labelIds],
3413
- },
3414
4208
  createdAt: task.createdAt,
3415
4209
  updatedAt: task.updatedAt,
3416
4210
  };