@jmoyers/harness 0.1.10 → 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.
- package/README.md +31 -35
- package/package.json +31 -11
- package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
- package/packages/harness-ai/src/stream-text.ts +13 -91
- package/packages/harness-ui/src/frame-primitives.ts +158 -0
- package/packages/harness-ui/src/index.ts +18 -0
- package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
- package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
- package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
- package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
- package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
- package/packages/harness-ui/src/interaction/input.ts +420 -0
- package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
- package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
- package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
- package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
- package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
- package/packages/harness-ui/src/kit.ts +476 -0
- package/packages/harness-ui/src/layout.ts +238 -0
- package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
- package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
- package/packages/harness-ui/src/surface.ts +252 -0
- package/packages/harness-ui/src/text-layout.ts +210 -0
- package/packages/nim-core/src/contracts.ts +239 -0
- package/packages/nim-core/src/event-store.ts +299 -0
- package/packages/nim-core/src/events.ts +53 -0
- package/packages/nim-core/src/index.ts +9 -0
- package/packages/nim-core/src/provider-router.ts +129 -0
- package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
- package/packages/nim-core/src/runtime-factory.ts +49 -0
- package/packages/nim-core/src/runtime.ts +1797 -0
- package/packages/nim-core/src/session-store.ts +516 -0
- package/packages/nim-core/src/telemetry.ts +48 -0
- package/packages/nim-test-tui/src/index.ts +150 -0
- package/packages/nim-ui-core/src/index.ts +1 -0
- package/packages/nim-ui-core/src/projection.ts +87 -0
- package/scripts/codex-live-mux-runtime.ts +2 -3721
- package/scripts/control-plane-daemon.ts +24 -2
- package/scripts/harness-bin.js +5 -0
- package/scripts/harness-commands.ts +300 -0
- package/scripts/harness-runtime.ts +82 -0
- package/scripts/harness.ts +33 -3007
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/default-gateway-pointer.ts +193 -0
- package/src/cli/gateway/runtime.ts +1872 -0
- package/src/cli/parsing/flags.ts +23 -0
- package/src/cli/parsing/session.ts +42 -0
- package/src/cli/runtime/context.ts +193 -0
- package/src/cli/runtime-app/application.ts +392 -0
- package/src/cli/runtime-infra/gateway-control.ts +729 -0
- package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
- package/src/cli/workflows/runtime.ts +965 -0
- package/src/clients/tui/left-rail-interactions.ts +519 -0
- package/src/clients/tui/main-pane-interactions.ts +509 -0
- package/src/clients/tui/modal-input-routing.ts +71 -0
- package/src/clients/tui/render-snapshot-adapter.ts +88 -0
- package/src/clients/web/synced-selectors.ts +132 -0
- package/src/codex/live-session.ts +82 -29
- package/src/config/config-core.ts +361 -10
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/prompt/thread-title-namer.ts +49 -23
- package/src/control-plane/session-summary.ts +10 -81
- package/src/control-plane/status/reducer-base.ts +12 -12
- package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
- package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
- package/src/control-plane/stream-client.ts +12 -2
- package/src/control-plane/stream-command-parser.ts +83 -143
- package/src/control-plane/stream-protocol.ts +53 -37
- package/src/control-plane/stream-server-background.ts +18 -2
- package/src/control-plane/stream-server-command.ts +376 -69
- package/src/control-plane/stream-server-session-runtime.ts +3 -2
- package/src/control-plane/stream-server.ts +943 -80
- package/src/control-plane/stream-session-runtime-types.ts +41 -0
- package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
- package/src/core/state/observed-stream-cursor.ts +43 -0
- package/src/core/state/synced-observed-state.ts +273 -0
- package/src/core/store/harness-synced-store.ts +81 -0
- package/src/diff/budget.ts +136 -0
- package/src/diff/build.ts +289 -0
- package/src/diff/chunker.ts +146 -0
- package/src/diff/git-invoke.ts +315 -0
- package/src/diff/git-parse.ts +472 -0
- package/src/diff/hash.ts +70 -0
- package/src/diff/index.ts +24 -0
- package/src/diff/normalize.ts +134 -0
- package/src/diff/types.ts +178 -0
- package/src/diff-ui/args.ts +346 -0
- package/src/diff-ui/commands.ts +123 -0
- package/src/diff-ui/finder.ts +94 -0
- package/src/diff-ui/highlight.ts +127 -0
- package/src/diff-ui/index.ts +2 -0
- package/src/diff-ui/model.ts +141 -0
- package/src/diff-ui/pager.ts +412 -0
- package/src/diff-ui/render.ts +337 -0
- package/src/diff-ui/runtime.ts +379 -0
- package/src/diff-ui/state.ts +224 -0
- package/src/diff-ui/types.ts +236 -0
- package/src/domain/conversations.ts +11 -7
- package/src/domain/workspace.ts +76 -4
- package/src/mux/control-plane-op-queue.ts +93 -7
- package/src/mux/conversation-rail.ts +28 -71
- package/src/mux/dual-pane-core.ts +13 -13
- package/src/mux/harness-core-ui.ts +313 -42
- package/src/mux/input-shortcuts.ts +22 -112
- package/src/mux/keybinding-catalog.ts +340 -0
- package/src/mux/keybinding-registry.ts +103 -0
- package/src/mux/live-mux/command-menu-open-in.ts +280 -0
- package/src/mux/live-mux/command-menu.ts +167 -4
- package/src/mux/live-mux/conversation-state.ts +13 -0
- package/src/mux/live-mux/directory-resolution.ts +1 -1
- package/src/mux/live-mux/git-parsing.ts +16 -0
- package/src/mux/live-mux/git-snapshot.ts +33 -2
- package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
- package/src/mux/live-mux/home-pane-drop.ts +1 -1
- package/src/mux/live-mux/home-pane-pointer.ts +10 -0
- package/src/mux/live-mux/input-forwarding.ts +59 -2
- package/src/mux/live-mux/left-nav-activation.ts +124 -7
- package/src/mux/live-mux/left-nav.ts +35 -0
- package/src/mux/live-mux/link-click.ts +292 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
- package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
- package/src/mux/live-mux/modal-input-reducers.ts +106 -8
- package/src/mux/live-mux/modal-overlays.ts +210 -31
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
- package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
- package/src/mux/live-mux/pointer-routing.ts +5 -2
- package/src/mux/live-mux/project-pane-pointer.ts +8 -0
- package/src/mux/live-mux/rail-layout.ts +33 -30
- package/src/mux/live-mux/release-notes.ts +383 -0
- package/src/mux/live-mux/render-trace-analysis.ts +52 -7
- package/src/mux/live-mux/repository-folding.ts +3 -0
- package/src/mux/live-mux/selection.ts +0 -4
- package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
- package/src/mux/project-pane-github-review.ts +271 -0
- package/src/mux/render-frame.ts +4 -0
- package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
- package/src/mux/task-composer.ts +21 -14
- package/src/mux/task-focused-pane.ts +118 -117
- package/src/mux/task-screen-keybindings.ts +19 -82
- package/src/mux/workspace-rail-model.ts +270 -104
- package/src/mux/workspace-rail.ts +45 -22
- package/src/pty/session-broker.ts +1 -1
- package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
- package/src/services/control-plane.ts +50 -32
- package/src/services/conversation-lifecycle.ts +118 -87
- package/src/services/conversation-startup-hydration.ts +20 -12
- package/src/services/directory-hydration.ts +21 -16
- package/src/services/event-persistence.ts +7 -0
- package/src/services/left-rail-pointer-handler.ts +329 -0
- package/src/services/mux-ui-state-persistence.ts +5 -1
- package/src/services/recording.ts +34 -26
- package/src/services/runtime-command-menu-agent-tools.ts +1 -1
- package/src/services/runtime-control-actions.ts +79 -61
- package/src/services/runtime-control-plane-ops.ts +122 -83
- package/src/services/runtime-conversation-actions.ts +40 -26
- package/src/services/runtime-conversation-activation.ts +82 -30
- package/src/services/runtime-conversation-starter.ts +80 -48
- package/src/services/runtime-conversation-title-edit.ts +91 -80
- package/src/services/runtime-envelope-handler.ts +107 -105
- package/src/services/runtime-git-state.ts +42 -29
- package/src/services/runtime-layout-resize.ts +3 -1
- package/src/services/runtime-left-rail-render.ts +99 -63
- package/src/services/runtime-nim-cli-session.ts +438 -0
- package/src/services/runtime-nim-session.ts +705 -0
- package/src/services/runtime-nim-tool-bridge.ts +141 -0
- package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
- package/src/services/runtime-process-wiring.ts +29 -36
- package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
- package/src/services/runtime-render-flush.ts +63 -70
- package/src/services/runtime-render-lifecycle.ts +65 -64
- package/src/services/runtime-render-orchestrator.ts +55 -45
- package/src/services/runtime-render-pipeline.ts +106 -103
- package/src/services/runtime-render-state.ts +62 -49
- package/src/services/runtime-repository-actions.ts +97 -70
- package/src/services/runtime-right-pane-render.ts +80 -53
- package/src/services/runtime-shutdown.ts +38 -35
- package/src/services/runtime-stream-subscriptions.ts +35 -27
- package/src/services/runtime-task-composer-persistence.ts +71 -59
- package/src/services/runtime-task-composer-snapshot.ts +14 -0
- package/src/services/runtime-task-editor-actions.ts +46 -29
- package/src/services/runtime-task-pane-actions.ts +220 -134
- package/src/services/runtime-task-pane-shortcuts.ts +323 -123
- package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
- package/src/services/runtime-workspace-observed-events.ts +33 -184
- package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
- package/src/services/session-diagnostics-store.ts +217 -0
- package/src/services/startup-background-resume.ts +26 -21
- package/src/services/startup-orchestrator.ts +16 -13
- package/src/services/startup-paint-tracker.ts +29 -21
- package/src/services/startup-persisted-conversation-queue.ts +19 -13
- package/src/services/startup-settled-gate.ts +25 -15
- package/src/services/startup-shutdown.ts +18 -22
- package/src/services/startup-state-hydration.ts +44 -34
- package/src/services/startup-visibility.ts +12 -4
- package/src/services/task-pane-selection-actions.ts +89 -72
- package/src/services/task-planning-hydration.ts +24 -18
- package/src/services/task-planning-observed-events.ts +50 -52
- package/src/services/workspace-observed-events.ts +66 -63
- package/src/storage/storage-lifecycle-core.ts +438 -0
- package/src/store/control-plane-store-normalize.ts +33 -242
- package/src/store/control-plane-store-types.ts +1 -35
- package/src/store/control-plane-store.ts +396 -56
- package/src/store/event-store.ts +397 -3
- package/src/terminal/snapshot-oracle.ts +207 -94
- package/src/ui/mux-theme.ts +112 -8
- package/src/ui/panes/home-gridfire.ts +40 -31
- package/src/ui/panes/home.ts +10 -2
- package/src/ui/panes/nim.ts +315 -0
- package/src/mux/live-mux/actions-task.ts +0 -115
- package/src/mux/live-mux/left-rail-actions.ts +0 -118
- package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
- package/src/mux/live-mux/left-rail-pointer.ts +0 -74
- package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
- package/src/services/runtime-directory-actions.ts +0 -164
- package/src/services/runtime-input-pipeline.ts +0 -50
- package/src/services/runtime-input-router.ts +0 -189
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -119
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -278
- package/src/services/runtime-task-pane.ts +0 -62
- package/src/services/runtime-workspace-actions.ts +0 -158
- package/src/ui/conversation-input-forwarder.ts +0 -114
- package/src/ui/conversation-selection-input.ts +0 -103
- package/src/ui/global-shortcut-input.ts +0 -89
- package/src/ui/input.ts +0 -238
- package/src/ui/kit.ts +0 -509
- package/src/ui/left-nav-input.ts +0 -80
- package/src/ui/left-rail-pointer-input.ts +0 -148
- package/src/ui/repository-fold-input.ts +0 -91
- 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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
1194
|
-
|
|
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((
|
|
1234
|
-
this.handleConnection(socket);
|
|
1235
|
-
});
|
|
1436
|
+
this.server = createServer(this.handleConnection.bind(this));
|
|
1236
1437
|
this.telemetryServer = this.codexTelemetry.enabled
|
|
1237
|
-
? createHttpServer((
|
|
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()]) {
|
|
@@ -1381,7 +1596,18 @@ export class ControlPlaneStreamServer {
|
|
|
1381
1596
|
}
|
|
1382
1597
|
|
|
1383
1598
|
private pollHistoryTimerTick(): void {
|
|
1384
|
-
void this.pollHistoryFile()
|
|
1599
|
+
void this.pollHistoryFile().catch((error: unknown) => {
|
|
1600
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (this.shouldSkipStateStoreWork()) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1607
|
+
recordPerfEvent('control-plane.history.poll.failed', {
|
|
1608
|
+
error: message,
|
|
1609
|
+
});
|
|
1610
|
+
});
|
|
1385
1611
|
}
|
|
1386
1612
|
|
|
1387
1613
|
private stopHistoryPolling(): void {
|
|
@@ -1394,14 +1620,78 @@ export class ControlPlaneStreamServer {
|
|
|
1394
1620
|
this.historyNextAllowedPollAtMs = 0;
|
|
1395
1621
|
}
|
|
1396
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
|
+
|
|
1397
1687
|
private startGitStatusPollingIfEnabled(): void {
|
|
1398
1688
|
if (!this.gitStatusMonitor.enabled || this.gitStatusPollTimer !== null) {
|
|
1399
1689
|
return;
|
|
1400
1690
|
}
|
|
1401
1691
|
this.reloadGitStatusDirectoriesFromStore();
|
|
1402
|
-
|
|
1692
|
+
this.triggerGitStatusPoll();
|
|
1403
1693
|
this.gitStatusPollTimer = setInterval(() => {
|
|
1404
|
-
|
|
1694
|
+
this.triggerGitStatusPoll();
|
|
1405
1695
|
}, this.gitStatusMonitor.pollMs);
|
|
1406
1696
|
this.gitStatusPollTimer.unref();
|
|
1407
1697
|
}
|
|
@@ -1494,6 +1784,9 @@ export class ControlPlaneStreamServer {
|
|
|
1494
1784
|
|
|
1495
1785
|
private triggerGitHubPoll(): void {
|
|
1496
1786
|
void this.pollGitHub().catch((error: unknown) => {
|
|
1787
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1497
1790
|
if (this.shouldIgnoreGitHubPollError(error)) {
|
|
1498
1791
|
return;
|
|
1499
1792
|
}
|
|
@@ -1504,10 +1797,41 @@ export class ControlPlaneStreamServer {
|
|
|
1504
1797
|
});
|
|
1505
1798
|
}
|
|
1506
1799
|
|
|
1800
|
+
private triggerGitStatusPoll(): void {
|
|
1801
|
+
void this.pollGitStatus().catch((error: unknown) => {
|
|
1802
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (this.shouldSkipStateStoreWork()) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1809
|
+
recordPerfEvent('control-plane.git-status.poll.failed', {
|
|
1810
|
+
error: message,
|
|
1811
|
+
});
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1507
1815
|
private isStateStoreClosedError(error: unknown): boolean {
|
|
1508
1816
|
const message = error instanceof Error ? error.message : String(error);
|
|
1509
1817
|
const normalized = message.trim().toLowerCase();
|
|
1510
|
-
return
|
|
1818
|
+
return (
|
|
1819
|
+
normalized.includes('database has closed') ||
|
|
1820
|
+
normalized.includes('database is closed') ||
|
|
1821
|
+
normalized.includes('cannot use a closed database')
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
private markStateStoreClosedIfDetected(error: unknown): boolean {
|
|
1826
|
+
if (!this.isStateStoreClosedError(error)) {
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
this.stateStoreClosed = true;
|
|
1830
|
+
this.stopGitHubPolling();
|
|
1831
|
+
this.stopGitStatusPolling();
|
|
1832
|
+
this.stopHistoryPolling();
|
|
1833
|
+
this.stopStorageLifecyclePolling();
|
|
1834
|
+
return true;
|
|
1511
1835
|
}
|
|
1512
1836
|
|
|
1513
1837
|
private shouldSkipStateStoreWork(): boolean {
|
|
@@ -1526,6 +1850,9 @@ export class ControlPlaneStreamServer {
|
|
|
1526
1850
|
try {
|
|
1527
1851
|
await pollPromise;
|
|
1528
1852
|
} catch (error: unknown) {
|
|
1853
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1529
1856
|
if (!this.shouldIgnoreGitHubPollError(error)) {
|
|
1530
1857
|
const message = error instanceof Error ? error.message : String(error);
|
|
1531
1858
|
recordPerfEvent('control-plane.github.poll.failed-on-close', {
|
|
@@ -1720,6 +2047,8 @@ export class ControlPlaneStreamServer {
|
|
|
1720
2047
|
directoryPath: directory?.path ?? null,
|
|
1721
2048
|
codexLaunchDefaultMode: this.codexLaunch.defaultMode,
|
|
1722
2049
|
codexLaunchModeByDirectoryPath: this.codexLaunch.directoryModes,
|
|
2050
|
+
claudeLaunchDefaultMode: this.claudeLaunch.defaultMode,
|
|
2051
|
+
claudeLaunchModeByDirectoryPath: this.claudeLaunch.directoryModes,
|
|
1723
2052
|
cursorLaunchDefaultMode: this.cursorLaunch.defaultMode,
|
|
1724
2053
|
cursorLaunchModeByDirectoryPath: this.cursorLaunch.directoryModes,
|
|
1725
2054
|
});
|
|
@@ -1912,6 +2241,39 @@ export class ControlPlaneStreamServer {
|
|
|
1912
2241
|
}
|
|
1913
2242
|
|
|
1914
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
|
+
|
|
1915
2277
|
void this.handleTelemetryHttpRequestAsync(request, response).catch((error: unknown) => {
|
|
1916
2278
|
if (isTelemetryRequestAbortError(error)) {
|
|
1917
2279
|
return;
|
|
@@ -2054,6 +2416,7 @@ export class ControlPlaneStreamServer {
|
|
|
2054
2416
|
observedAt: event.observedAt,
|
|
2055
2417
|
payload: event.payload,
|
|
2056
2418
|
});
|
|
2419
|
+
const persistedPayload = this.storageLifecycle.prepareTelemetryPayload(event.payload);
|
|
2057
2420
|
|
|
2058
2421
|
const inserted = this.stateStore.appendTelemetry({
|
|
2059
2422
|
source: event.source,
|
|
@@ -2063,7 +2426,7 @@ export class ControlPlaneStreamServer {
|
|
|
2063
2426
|
severity: event.severity,
|
|
2064
2427
|
summary: event.summary,
|
|
2065
2428
|
observedAt: event.observedAt,
|
|
2066
|
-
payload:
|
|
2429
|
+
payload: persistedPayload,
|
|
2067
2430
|
fingerprint,
|
|
2068
2431
|
});
|
|
2069
2432
|
if (resolvedSessionId !== null) {
|
|
@@ -2205,9 +2568,15 @@ export class ControlPlaneStreamServer {
|
|
|
2205
2568
|
}
|
|
2206
2569
|
|
|
2207
2570
|
private async pollHistoryFile(): Promise<void> {
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2571
|
+
try {
|
|
2572
|
+
await pollStreamServerHistoryFile(
|
|
2573
|
+
this as unknown as Parameters<typeof pollStreamServerHistoryFile>[0],
|
|
2574
|
+
);
|
|
2575
|
+
} catch (error: unknown) {
|
|
2576
|
+
if (!this.markStateStoreClosedIfDetected(error)) {
|
|
2577
|
+
throw error;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2211
2580
|
}
|
|
2212
2581
|
|
|
2213
2582
|
private async pollHistoryFileUnsafe(): Promise<boolean> {
|
|
@@ -2217,9 +2586,15 @@ export class ControlPlaneStreamServer {
|
|
|
2217
2586
|
}
|
|
2218
2587
|
|
|
2219
2588
|
private async pollGitStatus(): Promise<void> {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2589
|
+
try {
|
|
2590
|
+
await pollStreamServerGitStatus(
|
|
2591
|
+
this as unknown as Parameters<typeof pollStreamServerGitStatus>[0],
|
|
2592
|
+
);
|
|
2593
|
+
} catch (error: unknown) {
|
|
2594
|
+
if (!this.markStateStoreClosedIfDetected(error)) {
|
|
2595
|
+
throw error;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2223
2598
|
}
|
|
2224
2599
|
|
|
2225
2600
|
private async refreshGitStatusForDirectory(
|
|
@@ -2314,6 +2689,10 @@ export class ControlPlaneStreamServer {
|
|
|
2314
2689
|
this.githubPollPromise = pollPromise;
|
|
2315
2690
|
try {
|
|
2316
2691
|
await pollPromise;
|
|
2692
|
+
} catch (error: unknown) {
|
|
2693
|
+
if (!this.markStateStoreClosedIfDetected(error)) {
|
|
2694
|
+
throw error;
|
|
2695
|
+
}
|
|
2317
2696
|
} finally {
|
|
2318
2697
|
if (this.githubPollPromise === pollPromise) {
|
|
2319
2698
|
this.githubPollPromise = null;
|
|
@@ -2322,6 +2701,200 @@ export class ControlPlaneStreamServer {
|
|
|
2322
2701
|
}
|
|
2323
2702
|
}
|
|
2324
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
|
+
|
|
2325
2898
|
private async syncGitHubBranch(input: {
|
|
2326
2899
|
directory: ControlPlaneDirectoryRecord;
|
|
2327
2900
|
repository: ControlPlaneRepositoryRecord;
|
|
@@ -2344,6 +2917,20 @@ export class ControlPlaneStreamServer {
|
|
|
2344
2917
|
return;
|
|
2345
2918
|
}
|
|
2346
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
|
+
);
|
|
2347
2934
|
const staleOpen = this.stateStore.listGitHubPullRequests({
|
|
2348
2935
|
repositoryId: input.repository.repositoryId,
|
|
2349
2936
|
headBranch: input.branchName,
|
|
@@ -2424,6 +3011,13 @@ export class ControlPlaneStreamServer {
|
|
|
2424
3011
|
isDraft: remotePr.isDraft,
|
|
2425
3012
|
observedAt: remotePr.updatedAt || now,
|
|
2426
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(() => {});
|
|
2427
3021
|
const jobs = await this.listGitHubPrJobsForCommit({
|
|
2428
3022
|
owner: input.owner,
|
|
2429
3023
|
repo: input.repo,
|
|
@@ -2491,6 +3085,9 @@ export class ControlPlaneStreamServer {
|
|
|
2491
3085
|
lastErrorAt: null,
|
|
2492
3086
|
});
|
|
2493
3087
|
} catch (error: unknown) {
|
|
3088
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
2494
3091
|
if (this.shouldIgnoreGitHubPollError(error)) {
|
|
2495
3092
|
return;
|
|
2496
3093
|
}
|
|
@@ -2510,6 +3107,9 @@ export class ControlPlaneStreamServer {
|
|
|
2510
3107
|
lastErrorAt: now,
|
|
2511
3108
|
});
|
|
2512
3109
|
} catch (syncStateError: unknown) {
|
|
3110
|
+
if (this.markStateStoreClosedIfDetected(syncStateError)) {
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
2513
3113
|
if (!this.shouldIgnoreGitHubPollError(syncStateError)) {
|
|
2514
3114
|
throw syncStateError;
|
|
2515
3115
|
}
|
|
@@ -2527,8 +3127,8 @@ export class ControlPlaneStreamServer {
|
|
|
2527
3127
|
if (token === null) {
|
|
2528
3128
|
const hint =
|
|
2529
3129
|
this.githubTokenResolutionError === null
|
|
2530
|
-
?
|
|
2531
|
-
: `${this.githubTokenResolutionError}; set
|
|
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`;
|
|
2532
3132
|
throw new Error(`github token not configured: ${hint}`);
|
|
2533
3133
|
}
|
|
2534
3134
|
const response = await this.githubFetch(`${this.github.apiBaseUrl}${path}`, {
|
|
@@ -2549,6 +3149,82 @@ export class ControlPlaneStreamServer {
|
|
|
2549
3149
|
return await response.json();
|
|
2550
3150
|
}
|
|
2551
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
|
+
|
|
2552
3228
|
private parseGitHubPullRequest(value: unknown): GitHubRemotePullRequest | null {
|
|
2553
3229
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
2554
3230
|
return null;
|
|
@@ -2565,6 +3241,8 @@ export class ControlPlaneStreamServer {
|
|
|
2565
3241
|
const updatedAt = record['updated_at'];
|
|
2566
3242
|
const createdAt = record['created_at'];
|
|
2567
3243
|
const closedAt = record['closed_at'];
|
|
3244
|
+
const mergedAtRaw = record['merged_at'];
|
|
3245
|
+
const mergedAt = mergedAtRaw === undefined ? null : mergedAtRaw;
|
|
2568
3246
|
if (
|
|
2569
3247
|
typeof number !== 'number' ||
|
|
2570
3248
|
typeof title !== 'string' ||
|
|
@@ -2574,6 +3252,7 @@ export class ControlPlaneStreamServer {
|
|
|
2574
3252
|
typeof updatedAt !== 'string' ||
|
|
2575
3253
|
typeof createdAt !== 'string' ||
|
|
2576
3254
|
(closedAt !== null && typeof closedAt !== 'string') ||
|
|
3255
|
+
(mergedAt !== null && typeof mergedAt !== 'string') ||
|
|
2577
3256
|
typeof head !== 'object' ||
|
|
2578
3257
|
head === null ||
|
|
2579
3258
|
Array.isArray(head) ||
|
|
@@ -2608,6 +3287,7 @@ export class ControlPlaneStreamServer {
|
|
|
2608
3287
|
baseBranch: baseRef,
|
|
2609
3288
|
state,
|
|
2610
3289
|
isDraft: draft,
|
|
3290
|
+
mergedAt: mergedAt as string | null,
|
|
2611
3291
|
updatedAt,
|
|
2612
3292
|
createdAt,
|
|
2613
3293
|
closedAt: closedAt as string | null,
|
|
@@ -2628,6 +3308,204 @@ export class ControlPlaneStreamServer {
|
|
|
2628
3308
|
return this.parseGitHubPullRequest(payload[0]);
|
|
2629
3309
|
}
|
|
2630
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
|
+
|
|
2631
3509
|
private async createGitHubPullRequest(input: {
|
|
2632
3510
|
owner: string;
|
|
2633
3511
|
repo: string;
|
|
@@ -3318,7 +4196,7 @@ export class ControlPlaneStreamServer {
|
|
|
3318
4196
|
scopeKind: task.scopeKind,
|
|
3319
4197
|
projectId: task.projectId,
|
|
3320
4198
|
title: task.title,
|
|
3321
|
-
|
|
4199
|
+
body: task.body,
|
|
3322
4200
|
status: task.status,
|
|
3323
4201
|
orderIndex: task.orderIndex,
|
|
3324
4202
|
claimedByControllerId: task.claimedByControllerId,
|
|
@@ -3327,21 +4205,6 @@ export class ControlPlaneStreamServer {
|
|
|
3327
4205
|
baseBranch: task.baseBranch,
|
|
3328
4206
|
claimedAt: task.claimedAt,
|
|
3329
4207
|
completedAt: task.completedAt,
|
|
3330
|
-
linear: {
|
|
3331
|
-
issueId: task.linear.issueId,
|
|
3332
|
-
identifier: task.linear.identifier,
|
|
3333
|
-
url: task.linear.url,
|
|
3334
|
-
teamId: task.linear.teamId,
|
|
3335
|
-
projectId: task.linear.projectId,
|
|
3336
|
-
projectMilestoneId: task.linear.projectMilestoneId,
|
|
3337
|
-
cycleId: task.linear.cycleId,
|
|
3338
|
-
stateId: task.linear.stateId,
|
|
3339
|
-
assigneeId: task.linear.assigneeId,
|
|
3340
|
-
priority: task.linear.priority,
|
|
3341
|
-
estimate: task.linear.estimate,
|
|
3342
|
-
dueDate: task.linear.dueDate,
|
|
3343
|
-
labelIds: [...task.linear.labelIds],
|
|
3344
|
-
},
|
|
3345
4208
|
createdAt: task.createdAt,
|
|
3346
4209
|
updatedAt: task.updatedAt,
|
|
3347
4210
|
};
|