@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.
- package/README.md +31 -39
- 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/packages/harness-ui/src/modal-manager.ts +222 -0
- 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 -3872
- package/scripts/control-plane-daemon.ts +11 -0
- 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 -3019
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -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 +348 -8
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/control-plane/agent-realtime-api.ts +82 -427
- 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-command.ts +376 -69
- package/src/control-plane/stream-server-session-runtime.ts +3 -2
- package/src/control-plane/stream-server.ts +864 -70
- 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/workspace.ts +68 -5
- 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 +13 -131
- 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-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 +77 -12
- package/src/mux/live-mux/modal-overlays.ts +168 -34
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
- 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 +10 -101
- 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 +73 -46
- package/src/services/runtime-conversation-starter.ts +53 -45
- 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 -72
- 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 +360 -56
- package/src/store/event-store.ts +366 -8
- 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 -85
- 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 -195
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -137
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -279
- 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 -269
- 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/modals/manager.ts +0 -218
- 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()]) {
|
|
@@ -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:
|
|
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
|
-
?
|
|
2600
|
-
: `${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`;
|
|
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
|
-
|
|
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
|
};
|