@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.
Files changed (239) hide show
  1. package/README.md +31 -35
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. package/src/ui/surface.ts +0 -224
@@ -0,0 +1,315 @@
1
+ import { spawn } from 'node:child_process';
2
+ import type { DiffMode } from './types.ts';
3
+
4
+ type GitDiffCommandKind = 'patch' | 'name-status' | 'numstat';
5
+
6
+ export interface GitDiffInvocationOptions {
7
+ readonly cwd: string;
8
+ readonly mode: DiffMode;
9
+ readonly baseRef: string | null;
10
+ readonly headRef: string | null;
11
+ readonly noRenames: boolean;
12
+ readonly renameLimit: number | null;
13
+ }
14
+
15
+ interface GitDiffPreflight {
16
+ readonly filesChanged: number;
17
+ readonly additions: number;
18
+ readonly deletions: number;
19
+ readonly binaryFiles: number;
20
+ }
21
+
22
+ interface ResolveRangeBaseRefOptions {
23
+ readonly cwd: string;
24
+ readonly headRef: string;
25
+ readonly timeoutMs: number;
26
+ readonly runCommand?: ResolveRangeBaseRefCommandRunner;
27
+ }
28
+
29
+ interface StreamGitLinesInput {
30
+ readonly cwd: string;
31
+ readonly args: readonly string[];
32
+ readonly timeoutMs: number;
33
+ readonly onLine?: (line: string) => boolean | void;
34
+ readonly onBytes?: (bytes: number) => boolean | void;
35
+ }
36
+
37
+ interface StreamGitLinesResult {
38
+ readonly exitCode: number;
39
+ readonly signal: NodeJS.Signals | null;
40
+ readonly aborted: boolean;
41
+ readonly timedOut: boolean;
42
+ readonly bytesRead: number;
43
+ readonly peakLineBufferBytes: number;
44
+ readonly stderr: string;
45
+ }
46
+
47
+ interface GitCommandOutput {
48
+ readonly exitCode: number;
49
+ readonly aborted: boolean;
50
+ readonly timedOut: boolean;
51
+ readonly stdout: string;
52
+ readonly stderr: string;
53
+ }
54
+
55
+ type ResolveRangeBaseRefCommandRunner = (
56
+ cwd: string,
57
+ args: readonly string[],
58
+ timeoutMs: number,
59
+ ) => Promise<GitCommandOutput>;
60
+
61
+ function trimLineEnding(line: string): string {
62
+ return line.endsWith('\r') ? line.slice(0, -1) : line;
63
+ }
64
+
65
+ function parseFiniteInteger(value: string): number {
66
+ const parsed = Number.parseInt(value, 10);
67
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
68
+ }
69
+
70
+ function firstNonEmptyLine(text: string): string | null {
71
+ for (const line of text.split('\n')) {
72
+ const trimmed = line.trim();
73
+ if (trimmed.length > 0) {
74
+ return trimmed;
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ export function buildGitDiffArgs(
81
+ options: GitDiffInvocationOptions,
82
+ kind: GitDiffCommandKind,
83
+ ): readonly string[] {
84
+ const args: string[] = ['diff', '--no-ext-diff', '--no-color'];
85
+ if (options.noRenames) {
86
+ args.push('--no-renames');
87
+ } else if (options.renameLimit !== null) {
88
+ args.push(`-l${String(options.renameLimit)}`);
89
+ }
90
+ if (kind === 'patch') {
91
+ args.push('--patch', '--binary');
92
+ } else if (kind === 'name-status') {
93
+ args.push('--name-status');
94
+ } else {
95
+ args.push('--numstat');
96
+ }
97
+ if (options.mode === 'staged') {
98
+ args.push('--cached');
99
+ } else if (options.mode === 'range') {
100
+ if (options.baseRef === null || options.headRef === null) {
101
+ throw new Error('range diff requires baseRef and headRef');
102
+ }
103
+ args.push(options.baseRef, options.headRef);
104
+ }
105
+ return args;
106
+ }
107
+
108
+ export async function streamGitLines(input: StreamGitLinesInput): Promise<StreamGitLinesResult> {
109
+ return await new Promise<StreamGitLinesResult>((resolve, reject) => {
110
+ const child = spawn('git', [...input.args], {
111
+ cwd: input.cwd,
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ env: {
114
+ ...process.env,
115
+ GIT_PAGER: 'cat',
116
+ },
117
+ });
118
+ let timedOut = false;
119
+ let aborted = false;
120
+ let bytesRead = 0;
121
+ let peakLineBufferBytes = 0;
122
+ let pending = '';
123
+ let stderr = '';
124
+
125
+ const abort = (markTimedOut = false): void => {
126
+ aborted = true;
127
+ if (markTimedOut) {
128
+ timedOut = true;
129
+ }
130
+ child.kill('SIGTERM');
131
+ };
132
+
133
+ const timeout = setTimeout(() => abort(true), Math.max(1, Math.floor(input.timeoutMs)));
134
+
135
+ child.once('error', (error) => {
136
+ clearTimeout(timeout);
137
+ reject(error);
138
+ });
139
+
140
+ child.stdout.on('data', (chunk: Buffer) => {
141
+ bytesRead += chunk.length;
142
+ if (input.onBytes !== undefined) {
143
+ const keepReading = input.onBytes(chunk.length);
144
+ if (keepReading === false) {
145
+ abort();
146
+ }
147
+ }
148
+ pending += chunk.toString('utf8');
149
+ const pendingBytes = Buffer.byteLength(pending);
150
+ if (pendingBytes > peakLineBufferBytes) {
151
+ peakLineBufferBytes = pendingBytes;
152
+ }
153
+ while (true) {
154
+ const newlineIndex = pending.indexOf('\n');
155
+ if (newlineIndex < 0) {
156
+ break;
157
+ }
158
+ const line = trimLineEnding(pending.slice(0, newlineIndex));
159
+ pending = pending.slice(newlineIndex + 1);
160
+ if (input.onLine !== undefined) {
161
+ const keepReading = input.onLine(line);
162
+ if (keepReading === false) {
163
+ abort();
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ });
169
+
170
+ child.stderr.on('data', (chunk: Buffer) => {
171
+ stderr += chunk.toString('utf8');
172
+ });
173
+
174
+ child.once('close', (code, signal) => {
175
+ if (pending.length > 0 && input.onLine !== undefined) {
176
+ const keepReading = input.onLine(trimLineEnding(pending));
177
+ if (keepReading === false) {
178
+ aborted = true;
179
+ }
180
+ }
181
+ clearTimeout(timeout);
182
+ resolve({
183
+ exitCode: code ?? 1,
184
+ signal,
185
+ aborted,
186
+ timedOut,
187
+ bytesRead,
188
+ peakLineBufferBytes,
189
+ stderr: stderr.trim(),
190
+ });
191
+ });
192
+ });
193
+ }
194
+
195
+ async function runGitCommandCapture(
196
+ cwd: string,
197
+ args: readonly string[],
198
+ timeoutMs: number,
199
+ ): Promise<GitCommandOutput> {
200
+ const lines: string[] = [];
201
+ const result = await streamGitLines({
202
+ cwd,
203
+ args,
204
+ timeoutMs,
205
+ onLine: (line) => {
206
+ lines.push(line);
207
+ },
208
+ });
209
+ return {
210
+ exitCode: result.exitCode,
211
+ aborted: result.aborted,
212
+ timedOut: result.timedOut,
213
+ stdout: lines.join('\n').trim(),
214
+ stderr: result.stderr,
215
+ };
216
+ }
217
+
218
+ async function resolveRangeBaseTargetRef(
219
+ cwd: string,
220
+ timeoutMs: number,
221
+ runCommand: ResolveRangeBaseRefCommandRunner,
222
+ ): Promise<string> {
223
+ const remoteHead = await runCommand(
224
+ cwd,
225
+ ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'],
226
+ timeoutMs,
227
+ );
228
+ if (remoteHead.exitCode === 0 && !remoteHead.aborted) {
229
+ const remoteHeadRef = firstNonEmptyLine(remoteHead.stdout);
230
+ if (remoteHeadRef !== null) {
231
+ return remoteHeadRef;
232
+ }
233
+ }
234
+
235
+ for (const candidate of ['origin/main', 'main', 'origin/master', 'master']) {
236
+ const exists = await runCommand(
237
+ cwd,
238
+ ['rev-parse', '--verify', '--quiet', candidate],
239
+ timeoutMs,
240
+ );
241
+ if (exists.exitCode === 0 && !exists.aborted && firstNonEmptyLine(exists.stdout) !== null) {
242
+ return candidate;
243
+ }
244
+ }
245
+
246
+ return 'HEAD';
247
+ }
248
+
249
+ export async function resolveRangeBaseRef(options: ResolveRangeBaseRefOptions): Promise<string> {
250
+ const runCommand = options.runCommand ?? runGitCommandCapture;
251
+ const targetRef = await resolveRangeBaseTargetRef(options.cwd, options.timeoutMs, runCommand);
252
+ const mergeBase = await runCommand(
253
+ options.cwd,
254
+ ['merge-base', targetRef, options.headRef],
255
+ options.timeoutMs,
256
+ );
257
+ if (mergeBase.exitCode !== 0 || mergeBase.aborted) {
258
+ const reason = mergeBase.timedOut
259
+ ? 'timed out'
260
+ : mergeBase.stderr.length > 0
261
+ ? mergeBase.stderr
262
+ : 'unknown error';
263
+ throw new Error(`git merge-base ${targetRef} ${options.headRef} failed: ${reason}`);
264
+ }
265
+ const resolved = firstNonEmptyLine(mergeBase.stdout);
266
+ if (resolved === null) {
267
+ throw new Error(`git merge-base ${targetRef} ${options.headRef} returned no output`);
268
+ }
269
+ return resolved;
270
+ }
271
+
272
+ export async function readGitDiffPreflight(
273
+ options: GitDiffInvocationOptions,
274
+ timeoutMs: number,
275
+ ): Promise<GitDiffPreflight> {
276
+ let filesChanged = 0;
277
+ let additions = 0;
278
+ let deletions = 0;
279
+ let binaryFiles = 0;
280
+
281
+ const nameStatus = await streamGitLines({
282
+ cwd: options.cwd,
283
+ args: buildGitDiffArgs(options, 'name-status'),
284
+ timeoutMs,
285
+ onLine: (_line) => {
286
+ filesChanged += 1;
287
+ },
288
+ });
289
+ if (nameStatus.exitCode !== 0 && !nameStatus.aborted) {
290
+ throw new Error(`git diff --name-status failed: ${nameStatus.stderr || 'unknown error'}`);
291
+ }
292
+
293
+ await streamGitLines({
294
+ cwd: options.cwd,
295
+ args: buildGitDiffArgs(options, 'numstat'),
296
+ timeoutMs,
297
+ onLine: (line) => {
298
+ const [addRaw = '', delRaw = ''] = line.split('\t');
299
+ if (addRaw === '-' || delRaw === '-') {
300
+ binaryFiles += 1;
301
+ return;
302
+ }
303
+ const add = parseFiniteInteger(addRaw);
304
+ const del = parseFiniteInteger(delRaw);
305
+ additions += add;
306
+ deletions += del;
307
+ },
308
+ });
309
+ return {
310
+ filesChanged,
311
+ additions,
312
+ deletions,
313
+ binaryFiles,
314
+ };
315
+ }