taskplane 0.29.2 → 0.30.0

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 (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. package/package.json +14 -2
@@ -2,31 +2,89 @@
2
2
  * Merge orchestration, merge agents, merge worktree
3
3
  * @module orch/merge
4
4
  */
5
- import { readFileSync, writeFileSync, existsSync, unlinkSync, copyFileSync, mkdirSync, rmSync, readdirSync } from "fs";
5
+ import {
6
+ readFileSync,
7
+ writeFileSync,
8
+ existsSync,
9
+ unlinkSync,
10
+ copyFileSync,
11
+ mkdirSync,
12
+ rmSync,
13
+ readdirSync,
14
+ type Dirent,
15
+ } from "fs";
6
16
  import { readFile as fsReadFile } from "fs/promises";
7
17
  import { execSync, spawnSync } from "child_process";
8
18
  import { join, dirname, resolve, relative } from "path";
9
19
 
10
20
  import { execLog, isV2AgentAlive, setV2LivenessRegistryCache } from "./execution.ts";
11
21
  import { resolveOperatorId } from "./naming.ts";
12
- import { MERGE_POLL_INTERVAL_MS, MERGE_RESULT_GRACE_MS, MERGE_RESULT_READ_RETRIES, MERGE_RESULT_READ_RETRY_DELAY_MS, MERGE_SPAWN_RETRY_MAX, MERGE_TIMEOUT_MAX_RETRIES, MERGE_TIMEOUT_MS, MERGE_HEALTH_POLL_INTERVAL_MS, MERGE_HEALTH_WARNING_THRESHOLD_MS, MERGE_HEALTH_STUCK_THRESHOLD_MS, MergeError, VALID_MERGE_STATUSES, buildEngineEventBase } from "./types.ts";
13
- import type { AllocatedLane, LaneExecutionResult, MergeLaneResult, MergeResult, MergeResultStatus, MergeWaveResult, OrchestratorConfig, RepoMergeOutcome, TaskRunnerConfig, TransactionRecord, TransactionStatus, VerificationBaselineResult, WaveExecutionResult, WorkspaceConfig, MergeHealthStatus, MergeHealthEventType, MergeSessionSnapshot, MergeSessionHealthState, EngineEvent, OrchBatchPhase, RuntimeMergeSnapshot, RuntimeAgentTelemetrySnapshot } from "./types.ts";
22
+ import {
23
+ MERGE_POLL_INTERVAL_MS,
24
+ MERGE_RESULT_GRACE_MS,
25
+ MERGE_RESULT_READ_RETRIES,
26
+ MERGE_RESULT_READ_RETRY_DELAY_MS,
27
+ MERGE_SPAWN_RETRY_MAX,
28
+ MERGE_TIMEOUT_MAX_RETRIES,
29
+ MERGE_TIMEOUT_MS,
30
+ MERGE_HEALTH_POLL_INTERVAL_MS,
31
+ MERGE_HEALTH_WARNING_THRESHOLD_MS,
32
+ MERGE_HEALTH_STUCK_THRESHOLD_MS,
33
+ MergeError,
34
+ VALID_MERGE_STATUSES,
35
+ buildEngineEventBase,
36
+ } from "./types.ts";
37
+ import type {
38
+ AllocatedLane,
39
+ LaneExecutionResult,
40
+ MergeLaneResult,
41
+ MergeResult,
42
+ MergeResultStatus,
43
+ MergeWaveResult,
44
+ OrchestratorConfig,
45
+ RepoMergeOutcome,
46
+ TaskRunnerConfig,
47
+ TransactionRecord,
48
+ TransactionStatus,
49
+ VerificationBaselineResult,
50
+ WaveExecutionResult,
51
+ WorkspaceConfig,
52
+ MergeHealthStatus,
53
+ MergeHealthEventType,
54
+ MergeSessionSnapshot,
55
+ MergeSessionHealthState,
56
+ EngineEvent,
57
+ OrchBatchPhase,
58
+ RuntimeMergeSnapshot,
59
+ RuntimeAgentTelemetrySnapshot,
60
+ } from "./types.ts";
14
61
  import { resolveBaseBranch, resolveRepoRoot } from "./waves.ts";
15
- import { readManifest, writeManifest, buildRegistrySnapshot, writeRegistrySnapshot, readRegistrySnapshot, writeMergeSnapshot } from "./process-registry.ts";
62
+ import {
63
+ readManifest,
64
+ writeManifest,
65
+ buildRegistrySnapshot,
66
+ writeRegistrySnapshot,
67
+ readRegistrySnapshot,
68
+ writeMergeSnapshot,
69
+ } from "./process-registry.ts";
16
70
  import { generateMergeWorktreePath, sleepAsync, sleepSync } from "./worktree.ts";
17
71
  import { getCurrentBranch, runGit } from "./git.ts";
18
72
  import { ORCH_MESSAGES } from "./messages.ts";
19
73
  import { emitEngineEvent } from "./persistence.ts";
20
74
  import { loadOrchestratorConfig } from "./config.ts";
21
- import { captureBaseline, diffFingerprints, runVerificationCommands, parseTestOutput, deduplicateFingerprints } from "./verification.ts";
75
+ import {
76
+ captureBaseline,
77
+ diffFingerprints,
78
+ runVerificationCommands,
79
+ parseTestOutput,
80
+ deduplicateFingerprints,
81
+ } from "./verification.ts";
22
82
  import { spawnAgent } from "./agent-host.ts";
23
83
  import type { AgentHostOptions, AgentHostResult, AgentTelemetryCallback } from "./agent-host.ts";
24
84
  import { loadPiSettingsPackages, filterExcludedExtensions } from "./settings-loader.ts";
25
85
  import type { RuntimeBackend } from "./execution.ts";
26
86
  import type { VerificationBaseline, FingerprintDiff, TestFingerprint } from "./verification.ts";
27
87
 
28
-
29
-
30
88
  // ── Merge Implementation ─────────────────────────────────────────────
31
89
 
32
90
  /**
@@ -47,10 +105,7 @@ import type { VerificationBaseline, FingerprintDiff, TestFingerprint } from "./v
47
105
  */
48
106
  export function parseMergeResult(resultPath: string): MergeResult {
49
107
  if (!existsSync(resultPath)) {
50
- throw new MergeError(
51
- "MERGE_RESULT_INVALID",
52
- `Merge result file not found: ${resultPath}`,
53
- );
108
+ throw new MergeError("MERGE_RESULT_INVALID", `Merge result file not found: ${resultPath}`);
54
109
  }
55
110
 
56
111
  const pickString = (obj: Record<string, unknown>, ...keys: string[]): string | null => {
@@ -64,50 +119,55 @@ export function parseMergeResult(resultPath: string): MergeResult {
64
119
  };
65
120
 
66
121
  const hasFlatVerification = (obj: Record<string, unknown>): boolean =>
67
- typeof obj.verification_passed === "boolean"
68
- || Array.isArray(obj.verification_commands)
69
- || typeof obj.verification_output === "string"
70
- || typeof obj.verification_exit_code === "number";
71
-
72
- const normalizeVerification = (obj: Record<string, unknown>): MergeResult["verification"] | null => {
73
- const nested = (obj.verification && typeof obj.verification === "object")
74
- ? obj.verification as Record<string, unknown>
75
- : null;
122
+ typeof obj.verification_passed === "boolean" ||
123
+ Array.isArray(obj.verification_commands) ||
124
+ typeof obj.verification_output === "string" ||
125
+ typeof obj.verification_exit_code === "number";
126
+
127
+ const normalizeVerification = (
128
+ obj: Record<string, unknown>,
129
+ ): MergeResult["verification"] | null => {
130
+ const nested =
131
+ obj.verification && typeof obj.verification === "object"
132
+ ? (obj.verification as Record<string, unknown>)
133
+ : null;
76
134
 
77
135
  if (!nested && !hasFlatVerification(obj)) {
78
136
  return null;
79
137
  }
80
138
 
81
139
  const passedFromBool =
82
- (nested && typeof nested.passed === "boolean" ? nested.passed : undefined)
83
- ?? (nested && typeof nested.all_passed === "boolean" ? nested.all_passed : undefined)
84
- ?? (typeof obj.verification_passed === "boolean" ? obj.verification_passed : undefined);
140
+ (nested && typeof nested.passed === "boolean" ? nested.passed : undefined) ??
141
+ (nested && typeof nested.all_passed === "boolean" ? nested.all_passed : undefined) ??
142
+ (typeof obj.verification_passed === "boolean" ? obj.verification_passed : undefined);
85
143
 
86
144
  const exitCode =
87
- (nested && typeof nested.exitCode === "number" ? nested.exitCode : undefined)
88
- ?? (nested && typeof nested.exit_code === "number" ? nested.exit_code : undefined)
89
- ?? (typeof obj.verification_exit_code === "number" ? obj.verification_exit_code : undefined);
90
-
91
- const passed = typeof passedFromBool === "boolean"
92
- ? passedFromBool
93
- : (typeof exitCode === "number" ? exitCode === 0 : false);
94
-
95
- const ran = (nested && typeof nested.ran === "boolean")
96
- ? nested.ran
97
- : (
98
- typeof passedFromBool === "boolean"
99
- || typeof exitCode === "number"
100
- || (nested && typeof nested.command === "string")
101
- || (nested && typeof nested.summary === "string")
102
- || typeof obj.verification_output === "string"
103
- || Array.isArray(obj.verification_commands)
104
- );
145
+ (nested && typeof nested.exitCode === "number" ? nested.exitCode : undefined) ??
146
+ (nested && typeof nested.exit_code === "number" ? nested.exit_code : undefined) ??
147
+ (typeof obj.verification_exit_code === "number" ? obj.verification_exit_code : undefined);
148
+
149
+ const passed =
150
+ typeof passedFromBool === "boolean"
151
+ ? passedFromBool
152
+ : typeof exitCode === "number"
153
+ ? exitCode === 0
154
+ : false;
155
+
156
+ const ran =
157
+ nested && typeof nested.ran === "boolean"
158
+ ? nested.ran
159
+ : typeof passedFromBool === "boolean" ||
160
+ typeof exitCode === "number" ||
161
+ (nested && typeof nested.command === "string") ||
162
+ (nested && typeof nested.summary === "string") ||
163
+ typeof obj.verification_output === "string" ||
164
+ Array.isArray(obj.verification_commands);
105
165
 
106
166
  const output = (
107
- (nested && typeof nested.output === "string" ? nested.output : undefined)
108
- ?? (nested && typeof nested.summary === "string" ? nested.summary : undefined)
109
- ?? (nested && typeof nested.notes === "string" ? nested.notes : undefined)
110
- ?? (typeof obj.verification_output === "string" ? obj.verification_output : "")
167
+ (nested && typeof nested.output === "string" ? nested.output : undefined) ??
168
+ (nested && typeof nested.summary === "string" ? nested.summary : undefined) ??
169
+ (nested && typeof nested.notes === "string" ? nested.notes : undefined) ??
170
+ (typeof obj.verification_output === "string" ? obj.verification_output : "")
111
171
  ).slice(0, 2000);
112
172
 
113
173
  return { ran, passed, output };
@@ -159,13 +219,25 @@ export function parseMergeResult(resultPath: string): MergeResult {
159
219
  }
160
220
 
161
221
  // Normalize status to uppercase (merge agents may write lowercase)
162
- parsed.status = String(parsed.status).toUpperCase();
222
+ // TP-195: hoist normalized value to a local string so the
223
+ // `VALID_MERGE_STATUSES.has()` call typechecks. `parsed.status`
224
+ // is `unknown` after JSON parse; assigning `String(...)` to a
225
+ // property of an `any` doesn't propagate `string` type back
226
+ // through `parsed.status`. Runtime evaluation order is
227
+ // unchanged.
228
+ const normalizedStatus = String(parsed.status).toUpperCase();
229
+ parsed.status = normalizedStatus;
163
230
 
164
231
  // Validate status value
165
- if (!VALID_MERGE_STATUSES.has(parsed.status)) {
166
- execLog("merge", "parse", `unknown merge status "${parsed.status}" — treating as BUILD_FAILURE`, {
167
- resultPath,
168
- });
232
+ if (!VALID_MERGE_STATUSES.has(normalizedStatus)) {
233
+ execLog(
234
+ "merge",
235
+ "parse",
236
+ `unknown merge status "${normalizedStatus}" — treating as BUILD_FAILURE`,
237
+ {
238
+ resultPath,
239
+ },
240
+ );
169
241
  parsed.status = "BUILD_FAILURE";
170
242
  }
171
243
 
@@ -173,19 +245,20 @@ export function parseMergeResult(resultPath: string): MergeResult {
173
245
  const mergeCommit = pickString(parsed, "merge_commit", "mergeCommit") ?? "";
174
246
  const conflicts = Array.isArray(parsed.conflicts)
175
247
  ? parsed.conflicts
176
- .filter((c): c is { file: string; type: string; resolved: boolean; resolution?: string } => (
177
- typeof c === "object"
178
- && c !== null
179
- && typeof (c as { file?: unknown }).file === "string"
180
- && typeof (c as { type?: unknown }).type === "string"
181
- && typeof (c as { resolved?: unknown }).resolved === "boolean"
182
- ))
183
- .map(c => ({
184
- file: c.file,
185
- type: c.type,
186
- resolved: c.resolved,
187
- ...(typeof c.resolution === "string" ? { resolution: c.resolution } : {}),
188
- }))
248
+ .filter(
249
+ (c): c is { file: string; type: string; resolved: boolean; resolution?: string } =>
250
+ typeof c === "object" &&
251
+ c !== null &&
252
+ typeof (c as { file?: unknown }).file === "string" &&
253
+ typeof (c as { type?: unknown }).type === "string" &&
254
+ typeof (c as { resolved?: unknown }).resolved === "boolean",
255
+ )
256
+ .map((c) => ({
257
+ file: c.file,
258
+ type: c.type,
259
+ resolved: c.resolved,
260
+ ...(typeof c.resolution === "string" ? { resolution: c.resolution } : {}),
261
+ }))
189
262
  : [];
190
263
 
191
264
  // Normalize optional fields with defaults
@@ -212,7 +285,7 @@ export function parseMergeResult(resultPath: string): MergeResult {
212
285
  throw new MergeError(
213
286
  "MERGE_RESULT_INVALID",
214
287
  `Failed to parse merge result JSON after ${MERGE_RESULT_READ_RETRIES} attempts. ` +
215
- `Last error: ${lastParseError}. File: ${resultPath}`,
288
+ `Last error: ${lastParseError}. File: ${resultPath}`,
216
289
  );
217
290
  }
218
291
 
@@ -232,10 +305,7 @@ export function parseMergeResult(resultPath: string): MergeResult {
232
305
  */
233
306
  export async function parseMergeResultAsync(resultPath: string): Promise<MergeResult> {
234
307
  if (!existsSync(resultPath)) {
235
- throw new MergeError(
236
- "MERGE_RESULT_INVALID",
237
- `Merge result file not found: ${resultPath}`,
238
- );
308
+ throw new MergeError("MERGE_RESULT_INVALID", `Merge result file not found: ${resultPath}`);
239
309
  }
240
310
 
241
311
  const pickString = (obj: Record<string, unknown>, ...keys: string[]): string | null => {
@@ -249,50 +319,55 @@ export async function parseMergeResultAsync(resultPath: string): Promise<MergeRe
249
319
  };
250
320
 
251
321
  const hasFlatVerification = (obj: Record<string, unknown>): boolean =>
252
- typeof obj.verification_passed === "boolean"
253
- || Array.isArray(obj.verification_commands)
254
- || typeof obj.verification_output === "string"
255
- || typeof obj.verification_exit_code === "number";
256
-
257
- const normalizeVerification = (obj: Record<string, unknown>): MergeResult["verification"] | null => {
258
- const nested = (obj.verification && typeof obj.verification === "object")
259
- ? obj.verification as Record<string, unknown>
260
- : null;
322
+ typeof obj.verification_passed === "boolean" ||
323
+ Array.isArray(obj.verification_commands) ||
324
+ typeof obj.verification_output === "string" ||
325
+ typeof obj.verification_exit_code === "number";
326
+
327
+ const normalizeVerification = (
328
+ obj: Record<string, unknown>,
329
+ ): MergeResult["verification"] | null => {
330
+ const nested =
331
+ obj.verification && typeof obj.verification === "object"
332
+ ? (obj.verification as Record<string, unknown>)
333
+ : null;
261
334
 
262
335
  if (!nested && !hasFlatVerification(obj)) {
263
336
  return null;
264
337
  }
265
338
 
266
339
  const passedFromBool =
267
- (nested && typeof nested.passed === "boolean" ? nested.passed : undefined)
268
- ?? (nested && typeof nested.all_passed === "boolean" ? nested.all_passed : undefined)
269
- ?? (typeof obj.verification_passed === "boolean" ? obj.verification_passed : undefined);
340
+ (nested && typeof nested.passed === "boolean" ? nested.passed : undefined) ??
341
+ (nested && typeof nested.all_passed === "boolean" ? nested.all_passed : undefined) ??
342
+ (typeof obj.verification_passed === "boolean" ? obj.verification_passed : undefined);
270
343
 
271
344
  const exitCode =
272
- (nested && typeof nested.exitCode === "number" ? nested.exitCode : undefined)
273
- ?? (nested && typeof nested.exit_code === "number" ? nested.exit_code : undefined)
274
- ?? (typeof obj.verification_exit_code === "number" ? obj.verification_exit_code : undefined);
275
-
276
- const passed = typeof passedFromBool === "boolean"
277
- ? passedFromBool
278
- : (typeof exitCode === "number" ? exitCode === 0 : false);
279
-
280
- const ran = (nested && typeof nested.ran === "boolean")
281
- ? nested.ran
282
- : (
283
- typeof passedFromBool === "boolean"
284
- || typeof exitCode === "number"
285
- || (nested && typeof nested.command === "string")
286
- || (nested && typeof nested.summary === "string")
287
- || typeof obj.verification_output === "string"
288
- || Array.isArray(obj.verification_commands)
289
- );
345
+ (nested && typeof nested.exitCode === "number" ? nested.exitCode : undefined) ??
346
+ (nested && typeof nested.exit_code === "number" ? nested.exit_code : undefined) ??
347
+ (typeof obj.verification_exit_code === "number" ? obj.verification_exit_code : undefined);
348
+
349
+ const passed =
350
+ typeof passedFromBool === "boolean"
351
+ ? passedFromBool
352
+ : typeof exitCode === "number"
353
+ ? exitCode === 0
354
+ : false;
355
+
356
+ const ran =
357
+ nested && typeof nested.ran === "boolean"
358
+ ? nested.ran
359
+ : typeof passedFromBool === "boolean" ||
360
+ typeof exitCode === "number" ||
361
+ (nested && typeof nested.command === "string") ||
362
+ (nested && typeof nested.summary === "string") ||
363
+ typeof obj.verification_output === "string" ||
364
+ Array.isArray(obj.verification_commands);
290
365
 
291
366
  const output = (
292
- (nested && typeof nested.output === "string" ? nested.output : undefined)
293
- ?? (nested && typeof nested.summary === "string" ? nested.summary : undefined)
294
- ?? (nested && typeof nested.notes === "string" ? nested.notes : undefined)
295
- ?? (typeof obj.verification_output === "string" ? obj.verification_output : "")
367
+ (nested && typeof nested.output === "string" ? nested.output : undefined) ??
368
+ (nested && typeof nested.summary === "string" ? nested.summary : undefined) ??
369
+ (nested && typeof nested.notes === "string" ? nested.notes : undefined) ??
370
+ (typeof obj.verification_output === "string" ? obj.verification_output : "")
296
371
  ).slice(0, 2000);
297
372
 
298
373
  return { ran, passed, output };
@@ -342,12 +417,20 @@ export async function parseMergeResultAsync(resultPath: string): Promise<MergeRe
342
417
  }
343
418
 
344
419
  // Normalize status to uppercase
345
- parsed.status = String(parsed.status).toUpperCase();
346
-
347
- if (!VALID_MERGE_STATUSES.has(parsed.status)) {
348
- execLog("merge", "parse", `unknown merge status "${parsed.status}" treating as BUILD_FAILURE`, {
349
- resultPath,
350
- });
420
+ // TP-195: hoist normalized value to a local string (same rationale
421
+ // as the parallel block at line ~225 above).
422
+ const normalizedStatus = String(parsed.status).toUpperCase();
423
+ parsed.status = normalizedStatus;
424
+
425
+ if (!VALID_MERGE_STATUSES.has(normalizedStatus)) {
426
+ execLog(
427
+ "merge",
428
+ "parse",
429
+ `unknown merge status "${normalizedStatus}" — treating as BUILD_FAILURE`,
430
+ {
431
+ resultPath,
432
+ },
433
+ );
351
434
  parsed.status = "BUILD_FAILURE";
352
435
  }
353
436
 
@@ -355,19 +438,20 @@ export async function parseMergeResultAsync(resultPath: string): Promise<MergeRe
355
438
  const mergeCommit = pickString(parsed, "merge_commit", "mergeCommit") ?? "";
356
439
  const conflicts = Array.isArray(parsed.conflicts)
357
440
  ? parsed.conflicts
358
- .filter((c): c is { file: string; type: string; resolved: boolean; resolution?: string } => (
359
- typeof c === "object"
360
- && c !== null
361
- && typeof (c as { file?: unknown }).file === "string"
362
- && typeof (c as { type?: unknown }).type === "string"
363
- && typeof (c as { resolved?: unknown }).resolved === "boolean"
364
- ))
365
- .map(c => ({
366
- file: c.file,
367
- type: c.type,
368
- resolved: c.resolved,
369
- ...(typeof c.resolution === "string" ? { resolution: c.resolution } : {}),
370
- }))
441
+ .filter(
442
+ (c): c is { file: string; type: string; resolved: boolean; resolution?: string } =>
443
+ typeof c === "object" &&
444
+ c !== null &&
445
+ typeof (c as { file?: unknown }).file === "string" &&
446
+ typeof (c as { type?: unknown }).type === "string" &&
447
+ typeof (c as { resolved?: unknown }).resolved === "boolean",
448
+ )
449
+ .map((c) => ({
450
+ file: c.file,
451
+ type: c.type,
452
+ resolved: c.resolved,
453
+ ...(typeof c.resolution === "string" ? { resolution: c.resolution } : {}),
454
+ }))
371
455
  : [];
372
456
 
373
457
  return {
@@ -392,7 +476,7 @@ export async function parseMergeResultAsync(resultPath: string): Promise<MergeRe
392
476
  throw new MergeError(
393
477
  "MERGE_RESULT_INVALID",
394
478
  `Failed to parse merge result JSON after ${MERGE_RESULT_READ_RETRIES} attempts. ` +
395
- `Last error: ${lastParseError}. File: ${resultPath}`,
479
+ `Last error: ${lastParseError}. File: ${resultPath}`,
396
480
  );
397
481
  }
398
482
 
@@ -424,10 +508,9 @@ function stageSkippedArtifactsToTargetBranch(
424
508
  const resolvedTmpPath = resolve(tmpWorktreePath);
425
509
 
426
510
  try {
427
- const addResult = spawnSync(
428
- "git", ["worktree", "add", resolvedTmpPath, targetBranch],
429
- { cwd: repoRoot },
430
- );
511
+ const addResult = spawnSync("git", ["worktree", "add", resolvedTmpPath, targetBranch], {
512
+ cwd: repoRoot,
513
+ });
431
514
  if (addResult.status !== 0) {
432
515
  execLog("merge", `W${waveIndex}`, `failed to create temp worktree for skipped artifacts`, {
433
516
  stderr: addResult.stderr?.toString().trim(),
@@ -459,7 +542,9 @@ function stageSkippedArtifactsToTargetBranch(
459
542
  copyFileSync(srcPath, destPath);
460
543
  spawnSync("git", ["add", "--", relPath], { cwd: resolvedTmpPath });
461
544
  staged++;
462
- } catch { /* best effort */ }
545
+ } catch {
546
+ /* best effort */
547
+ }
463
548
  }
464
549
 
465
550
  for (const dirName of ALLOWED_DIRS) {
@@ -469,9 +554,7 @@ function stageSkippedArtifactsToTargetBranch(
469
554
  const entries = readdirSync(laneDir, { recursive: true, withFileTypes: true });
470
555
  for (const entry of entries) {
471
556
  if (!entry.isFile()) continue;
472
- const entryPath = entry.parentPath
473
- ? join(entry.parentPath, entry.name)
474
- : entry.name;
557
+ const entryPath = entry.parentPath ? join(entry.parentPath, entry.name) : entry.name;
475
558
  const fileRel = relative(laneDir, entryPath).replace(/\\/g, "/");
476
559
  if (fileRel.startsWith("..")) continue;
477
560
  const relPath = `${relFolder}/${dirName}/${fileRel}`;
@@ -482,7 +565,9 @@ function stageSkippedArtifactsToTargetBranch(
482
565
  spawnSync("git", ["add", "--", relPath], { cwd: resolvedTmpPath });
483
566
  staged++;
484
567
  }
485
- } catch { /* best effort */ }
568
+ } catch {
569
+ /* best effort */
570
+ }
486
571
  }
487
572
  }
488
573
  }
@@ -495,7 +580,7 @@ function stageSkippedArtifactsToTargetBranch(
495
580
  );
496
581
  if (commitResult.status === 0) {
497
582
  execLog("merge", `W${waveIndex}`, `staged ${staged} artifact(s) from skipped-only lanes`, {
498
- lanes: lanes.map(l => l.laneNumber).join(","),
583
+ lanes: lanes.map((l) => l.laneNumber).join(","),
499
584
  });
500
585
  } else {
501
586
  execLog("merge", `W${waveIndex}`, `failed to commit skipped-task artifacts`, {
@@ -511,12 +596,16 @@ function stageSkippedArtifactsToTargetBranch(
511
596
  // Clean up the temporary worktree
512
597
  try {
513
598
  spawnSync("git", ["worktree", "remove", "--force", resolvedTmpPath], { cwd: repoRoot });
514
- } catch { /* best effort cleanup */ }
599
+ } catch {
600
+ /* best effort cleanup */
601
+ }
515
602
  try {
516
603
  if (existsSync(resolvedTmpPath)) {
517
604
  rmSync(resolvedTmpPath, { recursive: true, force: true });
518
605
  }
519
- } catch { /* best effort cleanup */ }
606
+ } catch {
607
+ /* best effort cleanup */
608
+ }
520
609
  }
521
610
  }
522
611
 
@@ -584,10 +673,10 @@ export function buildMergeRequest(
584
673
  verifyCommands: string[],
585
674
  resultFilePath: string,
586
675
  ): string {
587
- const taskIds = lane.tasks.map(t => t.taskId).join(", ");
676
+ const taskIds = lane.tasks.map((t) => t.taskId).join(", ");
588
677
  // TP-169: Guard against null task stubs from reconstructAllocatedLanes
589
678
  const fileScopes = lane.tasks
590
- .flatMap(t => t.task?.fileScope || [])
679
+ .flatMap((t) => t.task?.fileScope || [])
591
680
  .filter((f, i, arr) => arr.indexOf(f) === i); // deduplicate
592
681
 
593
682
  const mergeMessage = `merge: wave ${waveIndex} lane ${lane.laneNumber} — ${taskIds}`;
@@ -605,15 +694,13 @@ export function buildMergeRequest(
605
694
  `${mergeMessage}`,
606
695
  "",
607
696
  `## Tasks Completed`,
608
- ...lane.tasks.map(t => `- ${t.taskId}: ${t.task?.taskName ?? "(unknown)"}`),
697
+ ...lane.tasks.map((t) => `- ${t.taskId}: ${t.task?.taskName ?? "(unknown)"}`),
609
698
  "",
610
699
  `## File Scope`,
611
- ...(fileScopes.length > 0
612
- ? fileScopes.map(f => `- ${f}`)
613
- : ["- (no file scope declared)"]),
700
+ ...(fileScopes.length > 0 ? fileScopes.map((f) => `- ${f}`) : ["- (no file scope declared)"]),
614
701
  "",
615
702
  `## Verification Commands`,
616
- ...verifyCommands.map(cmd => `\`\`\`bash\n${cmd}\n\`\`\``),
703
+ ...verifyCommands.map((cmd) => `\`\`\`bash\n${cmd}\n\`\`\``),
617
704
  "",
618
705
  `## Result File`,
619
706
  `result_file: ${resultFilePath.split("\\").join("/")}`,
@@ -624,12 +711,12 @@ export function buildMergeRequest(
624
711
  "",
625
712
  "```json",
626
713
  "{",
627
- " \"status\": \"SUCCESS\" | \"CONFLICT_RESOLVED\" | \"CONFLICT_UNRESOLVED\" | \"BUILD_FAILURE\",",
628
- " \"source_branch\": \"<source branch name>\",",
629
- " \"target_branch\": \"<target branch name>\",",
630
- " \"merge_commit\": \"<merge commit sha or empty string>\",",
631
- " \"conflicts\": [{ \"file\": \"...\", \"type\": \"...\", \"resolved\": true|false }],",
632
- " \"verification\": { \"ran\": true|false, \"passed\": true|false, \"output\": \"...\" }",
714
+ ' "status": "SUCCESS" | "CONFLICT_RESOLVED" | "CONFLICT_UNRESOLVED" | "BUILD_FAILURE",',
715
+ ' "source_branch": "<source branch name>",',
716
+ ' "target_branch": "<target branch name>",',
717
+ ' "merge_commit": "<merge commit sha or empty string>",',
718
+ ' "conflicts": [{ "file": "...", "type": "...", "resolved": true|false }],',
719
+ ' "verification": { "ran": true|false, "passed": true|false, "output": "..." }',
633
720
  "}",
634
721
  "```",
635
722
  "",
@@ -648,8 +735,6 @@ export function buildMergeRequest(
648
735
  return lines.join("\n");
649
736
  }
650
737
 
651
-
652
-
653
738
  /**
654
739
  * Spawn a merge agent via Runtime V2 direct agent-host (no terminal multiplexer).
655
740
  *
@@ -674,6 +759,12 @@ export function buildMergeRequest(
674
759
  *
675
760
  * @since TP-108
676
761
  */
762
+ // TP-195: return type changed from `Promise<AgentHostResult>` to
763
+ // `Promise<void>` to match actual semantics. The function never returns a
764
+ // value — it spawns the merge agent, attaches `.then`/`.catch` handlers
765
+ // for fire-and-forget exit logging (line ~912 marker: "Fire-and-forget"),
766
+ // and exits. Both call sites (lines ~1929, ~1942) `await` the returned
767
+ // promise but do not consume its value.
677
768
  export async function spawnMergeAgentV2(
678
769
  sessionName: string,
679
770
  repoRoot: string,
@@ -684,7 +775,7 @@ export async function spawnMergeAgentV2(
684
775
  agentRoot?: string,
685
776
  batchId?: string,
686
777
  waveIndex?: number,
687
- ): Promise<AgentHostResult> {
778
+ ): Promise<void> {
688
779
  execLog("merge", sessionName, "spawning merge agent via Runtime V2 (direct agent-host)", {
689
780
  mergeWorkDir,
690
781
  mergeRequestPath,
@@ -698,17 +789,28 @@ export async function spawnMergeAgentV2(
698
789
  agentRoot ? join(agentRoot, "task-merger.md") : "",
699
790
  join(stateRoot ?? repoRoot, ".pi", "agents", "task-merger.md"),
700
791
  ].filter(Boolean);
701
- const systemPromptPath = systemPromptCandidates.find(p => existsSync(p)) || "";
792
+ const systemPromptPath = systemPromptCandidates.find((p) => existsSync(p)) || "";
702
793
  let systemPrompt: string | undefined;
703
794
  if (systemPromptPath) {
704
- try { systemPrompt = readFileSync(systemPromptPath, "utf-8"); } catch { /* use default */ }
795
+ try {
796
+ systemPrompt = readFileSync(systemPromptPath, "utf-8");
797
+ } catch {
798
+ /* use default */
799
+ }
705
800
  }
706
801
 
707
802
  // Resolve event/exit paths
708
803
  const sidecarRoot = join(stateRoot ?? repoRoot, ".pi");
709
804
  const bid = batchId || "unknown";
710
805
  const eventsPath = join(sidecarRoot, "runtime", bid, "agents", sessionName, "events.jsonl");
711
- const exitSummaryPath = join(sidecarRoot, "runtime", bid, "agents", sessionName, "exit-summary.json");
806
+ const exitSummaryPath = join(
807
+ sidecarRoot,
808
+ "runtime",
809
+ bid,
810
+ "agents",
811
+ sessionName,
812
+ "exit-summary.json",
813
+ );
712
814
 
713
815
  // Mailbox directory
714
816
  let mailboxDir: string | null = null;
@@ -752,16 +854,24 @@ export async function spawnMergeAgentV2(
752
854
  // (e.g. "orch-henry-merge-1" → 1, "orch-henry-merge-2" → 2).
753
855
  const mergeNumberMatch = sessionName.match(/-merge-(\d+)$/);
754
856
  if (!mergeNumberMatch) {
755
- execLog("merge", sessionName, "warning: could not parse merge number from session name — defaulting to 1", { sessionName });
857
+ execLog(
858
+ "merge",
859
+ sessionName,
860
+ "warning: could not parse merge number from session name — defaulting to 1",
861
+ { sessionName },
862
+ );
756
863
  }
757
864
  const mergeNumber = mergeNumberMatch ? parseInt(mergeNumberMatch[1], 10) : 1;
758
865
  const mergeStartedAt = Date.now();
759
866
 
760
867
  // Helper: build a RuntimeAgentTelemetrySnapshot from a partial AgentHostResult.
761
- const buildAgentSnap = (tel: Partial<AgentHostResult>, status: RuntimeAgentTelemetrySnapshot["status"]): RuntimeAgentTelemetrySnapshot => ({
868
+ const buildAgentSnap = (
869
+ tel: Partial<AgentHostResult>,
870
+ status: RuntimeAgentTelemetrySnapshot["status"],
871
+ ): RuntimeAgentTelemetrySnapshot => ({
762
872
  agentId: sessionName,
763
873
  status,
764
- elapsedMs: tel.durationMs ?? (Date.now() - mergeStartedAt),
874
+ elapsedMs: tel.durationMs ?? Date.now() - mergeStartedAt,
765
875
  toolCalls: tel.toolCalls ?? 0,
766
876
  contextPct: tel.contextUsage?.percent ?? 0,
767
877
  costUsd: tel.costUsd ?? 0,
@@ -786,7 +896,9 @@ export async function spawnMergeAgentV2(
786
896
  updatedAt: Date.now(),
787
897
  };
788
898
  writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
789
- } catch { /* non-fatal */ }
899
+ } catch {
900
+ /* non-fatal */
901
+ }
790
902
  };
791
903
 
792
904
  const { promise, kill } = spawnAgent(opts, undefined, onMergeTelemetry);
@@ -804,7 +916,9 @@ export async function spawnMergeAgentV2(
804
916
  updatedAt: Date.now(),
805
917
  };
806
918
  writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, initialSnap);
807
- } catch { /* non-fatal */ }
919
+ } catch {
920
+ /* non-fatal */
921
+ }
808
922
 
809
923
  // Store the kill handle for external cleanup (pause/abort).
810
924
  // The promise runs in background — caller uses waitForMergeResult()
@@ -813,65 +927,78 @@ export async function spawnMergeAgentV2(
813
927
 
814
928
  // Fire-and-forget: the background promise handles exit logging and
815
929
  // writes a terminal snapshot ("complete" or "failed") when the agent exits.
816
- promise.then(result => {
817
- activeMergeAgents.delete(sessionName);
818
- execLog("merge", sessionName, "merge agent exited (V2)", {
819
- exitCode: result.exitCode,
820
- durationMs: result.durationMs,
821
- costUsd: result.costUsd,
822
- killed: result.killed,
823
- });
824
- // Write terminal snapshot. Promise resolves for both successful and
825
- // failed exits, so derive status from result fields rather than
826
- // relying on .catch to handle failures.
827
- // Determine terminal status. A clean post-success kill sets registry
828
- // manifest to "exited" via killMergeAgentV2(name, true=cleanExit).
829
- // Check the registry first so a successful-then-killed agent is shown
830
- // as "complete" rather than "failed".
831
- let terminalStatus: RuntimeMergeSnapshot["status"] = "complete";
832
- try {
833
- const manifest = readManifest(mergeStateRoot, bid, sessionName as any);
834
- if (manifest?.status === "exited") {
835
- terminalStatus = "complete";
836
- } else if (result.exitCode !== 0 || !result.agentEnded) {
837
- terminalStatus = "failed";
930
+ promise
931
+ .then((result) => {
932
+ activeMergeAgents.delete(sessionName);
933
+ execLog("merge", sessionName, "merge agent exited (V2)", {
934
+ exitCode: result.exitCode,
935
+ durationMs: result.durationMs,
936
+ costUsd: result.costUsd,
937
+ killed: result.killed,
938
+ });
939
+ // Write terminal snapshot. Promise resolves for both successful and
940
+ // failed exits, so derive status from result fields rather than
941
+ // relying on .catch to handle failures.
942
+ // Determine terminal status. A clean post-success kill sets registry
943
+ // manifest to "exited" via killMergeAgentV2(name, true=cleanExit).
944
+ // Check the registry first so a successful-then-killed agent is shown
945
+ // as "complete" rather than "failed".
946
+ let terminalStatus: RuntimeMergeSnapshot["status"] = "complete";
947
+ try {
948
+ const manifest = readManifest(mergeStateRoot, bid, sessionName as any);
949
+ if (manifest?.status === "exited") {
950
+ terminalStatus = "complete";
951
+ } else if (result.exitCode !== 0 || !result.agentEnded) {
952
+ terminalStatus = "failed";
953
+ }
954
+ } catch {
955
+ if (result.exitCode !== 0 || !result.agentEnded) terminalStatus = "failed";
838
956
  }
839
- } catch {
840
- if (result.exitCode !== 0 || !result.agentEnded) terminalStatus = "failed";
841
- }
842
- try {
843
- const snap: RuntimeMergeSnapshot = {
844
- batchId: bid,
845
- mergeNumber,
846
- sessionName,
847
- waveIndex: waveIndex ?? 0,
848
- status: terminalStatus,
849
- agent: buildAgentSnap(result, terminalStatus === "complete" ? "exited" : "crashed"),
850
- updatedAt: Date.now(),
851
- };
852
- writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
853
- } catch { /* non-fatal */ }
854
- }).catch(err => {
855
- activeMergeAgents.delete(sessionName);
856
- execLog("merge", sessionName, `merge agent error (V2): ${err instanceof Error ? err.message : String(err)}`);
857
- // Write a failed terminal snapshot on unexpected rejection.
858
- try {
859
- const snap: RuntimeMergeSnapshot = {
860
- batchId: bid,
861
- mergeNumber,
957
+ try {
958
+ const snap: RuntimeMergeSnapshot = {
959
+ batchId: bid,
960
+ mergeNumber,
961
+ sessionName,
962
+ waveIndex: waveIndex ?? 0,
963
+ status: terminalStatus,
964
+ agent: buildAgentSnap(result, terminalStatus === "complete" ? "exited" : "crashed"),
965
+ updatedAt: Date.now(),
966
+ };
967
+ writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
968
+ } catch {
969
+ /* non-fatal */
970
+ }
971
+ })
972
+ .catch((err) => {
973
+ activeMergeAgents.delete(sessionName);
974
+ execLog(
975
+ "merge",
862
976
  sessionName,
863
- waveIndex: waveIndex ?? 0,
864
- status: "failed",
865
- agent: buildAgentSnap({}, "crashed"),
866
- updatedAt: Date.now(),
867
- };
868
- writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
869
- } catch { /* non-fatal */ }
870
- });
977
+ `merge agent error (V2): ${err instanceof Error ? err.message : String(err)}`,
978
+ );
979
+ // Write a failed terminal snapshot on unexpected rejection.
980
+ try {
981
+ const snap: RuntimeMergeSnapshot = {
982
+ batchId: bid,
983
+ mergeNumber,
984
+ sessionName,
985
+ waveIndex: waveIndex ?? 0,
986
+ status: "failed",
987
+ agent: buildAgentSnap({}, "crashed"),
988
+ updatedAt: Date.now(),
989
+ };
990
+ writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
991
+ } catch {
992
+ /* non-fatal */
993
+ }
994
+ });
871
995
  }
872
996
 
873
997
  /** Active V2 merge agent handles for cleanup/abort. @since TP-108 */
874
- const activeMergeAgents = new Map<string, { promise: Promise<AgentHostResult>; kill: () => void; stateRoot?: string; batchId?: string }>();
998
+ const activeMergeAgents = new Map<
999
+ string,
1000
+ { promise: Promise<AgentHostResult>; kill: () => void; stateRoot?: string; batchId?: string }
1001
+ >();
875
1002
 
876
1003
  /**
877
1004
  * Kill a V2 merge agent if it's still running.
@@ -893,7 +1020,9 @@ export function killMergeAgentV2(sessionName: string, cleanExit?: boolean): bool
893
1020
  const snapshot = buildRegistrySnapshot(handle.stateRoot, handle.batchId);
894
1021
  writeRegistrySnapshot(handle.stateRoot, snapshot);
895
1022
  }
896
- } catch { /* best effort */ }
1023
+ } catch {
1024
+ /* best effort */
1025
+ }
897
1026
  }
898
1027
  activeMergeAgents.delete(sessionName);
899
1028
  return true;
@@ -937,7 +1066,11 @@ export function reloadMergeTimeoutMs(configRoot: string, pointerConfigRoot?: str
937
1066
  } catch (err: unknown) {
938
1067
  // Config re-read is best-effort — fall back to default on failure
939
1068
  const errMsg = err instanceof Error ? err.message : String(err);
940
- execLog("merge", "config-reload", `failed to re-read merge timeout from config: ${errMsg} — using default`);
1069
+ execLog(
1070
+ "merge",
1071
+ "config-reload",
1072
+ `failed to re-read merge timeout from config: ${errMsg} — using default`,
1073
+ );
941
1074
  return MERGE_TIMEOUT_MS;
942
1075
  }
943
1076
  }
@@ -989,11 +1122,16 @@ export async function waitForMergeResult(
989
1122
  try {
990
1123
  const lateResult = await parseMergeResultAsync(resultPath);
991
1124
  if (SUCCESSFUL_MERGE_STATUSES.has(lateResult.status)) {
992
- execLog("merge", sessionName, "merge agent slow but succeeded — accepting result at timeout", {
993
- status: lateResult.status,
994
- elapsed,
995
- timeoutMs,
996
- });
1125
+ execLog(
1126
+ "merge",
1127
+ sessionName,
1128
+ "merge agent slow but succeeded — accepting result at timeout",
1129
+ {
1130
+ status: lateResult.status,
1131
+ elapsed,
1132
+ timeoutMs,
1133
+ },
1134
+ );
997
1135
  // Clean up agent (may still be running post-write)
998
1136
  killMergeAgentV2(sessionName, true);
999
1137
  return lateResult;
@@ -1012,8 +1150,8 @@ export async function waitForMergeResult(
1012
1150
  throw new MergeError(
1013
1151
  "MERGE_TIMEOUT",
1014
1152
  `Merge agent '${sessionName}' did not produce a result within ` +
1015
- `${Math.round(timeoutMs / 1000)}s. The agent has been killed. ` +
1016
- `Check the merge request and agent logs.`,
1153
+ `${Math.round(timeoutMs / 1000)}s. The agent has been killed. ` +
1154
+ `Check the merge request and agent logs.`,
1017
1155
  );
1018
1156
  }
1019
1157
 
@@ -1032,7 +1170,11 @@ export async function waitForMergeResult(
1032
1170
  if (err instanceof MergeError && err.code === "MERGE_RESULT_INVALID") {
1033
1171
  await sleepAsync(MERGE_RESULT_READ_RETRY_DELAY_MS);
1034
1172
  if (existsSync(resultPath)) {
1035
- try { return await parseMergeResultAsync(resultPath); } catch { /* give up */ }
1173
+ try {
1174
+ return await parseMergeResultAsync(resultPath);
1175
+ } catch {
1176
+ /* give up */
1177
+ }
1036
1178
  }
1037
1179
  }
1038
1180
  }
@@ -1051,14 +1193,18 @@ export async function waitForMergeResult(
1051
1193
  } else if (Date.now() - sessionDiedAt >= MERGE_RESULT_GRACE_MS) {
1052
1194
  // Grace period expired — one final check
1053
1195
  if (existsSync(resultPath)) {
1054
- try { return await parseMergeResultAsync(resultPath); } catch { /* fall through */ }
1196
+ try {
1197
+ return await parseMergeResultAsync(resultPath);
1198
+ } catch {
1199
+ /* fall through */
1200
+ }
1055
1201
  }
1056
1202
 
1057
1203
  throw new MergeError(
1058
1204
  "MERGE_SESSION_DIED",
1059
1205
  `Merge agent '${sessionName}' exited without writing ` +
1060
- `a result file to '${resultPath}'. The merge may have crashed. ` +
1061
- `Check agent logs for diagnostics.`,
1206
+ `a result file to '${resultPath}'. The merge may have crashed. ` +
1207
+ `Check agent logs for diagnostics.`,
1062
1208
  );
1063
1209
  }
1064
1210
  }
@@ -1081,25 +1227,28 @@ export async function waitForMergeResult(
1081
1227
  * @param repoRoot - Main repository root for git operations
1082
1228
  * @param context - Logging context (e.g., "W1" for wave 1)
1083
1229
  */
1084
- function forceRemoveMergeWorktree(
1085
- mergeWorkDir: string,
1086
- repoRoot: string,
1087
- context: string,
1088
- ): void {
1230
+ function forceRemoveMergeWorktree(mergeWorkDir: string, repoRoot: string, context: string): void {
1089
1231
  if (!existsSync(mergeWorkDir)) return;
1090
1232
 
1091
1233
  // Try git worktree remove --force first
1092
- const removeResult = spawnSync("git", ["worktree", "remove", mergeWorkDir, "--force"], { cwd: repoRoot });
1234
+ const removeResult = spawnSync("git", ["worktree", "remove", mergeWorkDir, "--force"], {
1235
+ cwd: repoRoot,
1236
+ });
1093
1237
  if (removeResult.status === 0) {
1094
1238
  return;
1095
1239
  }
1096
1240
 
1097
1241
  // Fallback: force-remove the directory and prune git worktree state
1098
1242
  const stderr = removeResult.stderr?.toString().trim() || "";
1099
- execLog("merge", context, `git worktree remove failed for merge worktree, applying force cleanup`, {
1100
- error: stderr.slice(0, 200),
1101
- path: mergeWorkDir,
1102
- });
1243
+ execLog(
1244
+ "merge",
1245
+ context,
1246
+ `git worktree remove failed for merge worktree, applying force cleanup`,
1247
+ {
1248
+ error: stderr.slice(0, 200),
1249
+ path: mergeWorkDir,
1250
+ },
1251
+ );
1103
1252
 
1104
1253
  try {
1105
1254
  rmSync(mergeWorkDir, { recursive: true, force: true });
@@ -1107,14 +1256,18 @@ function forceRemoveMergeWorktree(
1107
1256
  } catch (rmErr: unknown) {
1108
1257
  // Node's rmSync may fail on Windows reserved-name files — try OS-level removal
1109
1258
  const rmMsg = rmErr instanceof Error ? rmErr.message : String(rmErr);
1110
- execLog("merge", context, `rmSync failed for merge worktree, trying OS-level removal`, { error: rmMsg });
1259
+ execLog("merge", context, `rmSync failed for merge worktree, trying OS-level removal`, {
1260
+ error: rmMsg,
1261
+ });
1111
1262
  try {
1112
1263
  if (process.platform === "win32") {
1113
1264
  execSync(`rd /s /q "${mergeWorkDir}"`, { stdio: "pipe", timeout: 30_000 });
1114
1265
  } else {
1115
1266
  execSync(`rm -rf "${mergeWorkDir}"`, { stdio: "pipe", timeout: 30_000 });
1116
1267
  }
1117
- execLog("merge", context, `OS-level removal of merge worktree succeeded`, { path: mergeWorkDir });
1268
+ execLog("merge", context, `OS-level removal of merge worktree succeeded`, {
1269
+ path: mergeWorkDir,
1270
+ });
1118
1271
  } catch (osErr: unknown) {
1119
1272
  const osMsg = osErr instanceof Error ? osErr.message : String(osErr);
1120
1273
  execLog("merge", context, `OS-level removal also failed — manual cleanup needed`, {
@@ -1149,17 +1302,11 @@ function forceRemoveMergeWorktree(
1149
1302
  */
1150
1303
  function persistTransactionRecord(record: TransactionRecord, stateRoot: string): string | null {
1151
1304
  try {
1152
- const repoSlug = record.repoId
1153
- ? record.repoId.replace(/[^a-zA-Z0-9_-]/g, "_")
1154
- : "default";
1305
+ const repoSlug = record.repoId ? record.repoId.replace(/[^a-zA-Z0-9_-]/g, "_") : "default";
1155
1306
  const verifyDir = join(stateRoot, ".pi", "verification", record.opId);
1156
1307
  mkdirSync(verifyDir, { recursive: true });
1157
1308
  const fileName = `txn-b${record.batchId}-repo-${repoSlug}-wave-${record.waveIndex}-lane-${record.laneNumber}.json`;
1158
- writeFileSync(
1159
- join(verifyDir, fileName),
1160
- JSON.stringify(record, null, 2),
1161
- "utf-8",
1162
- );
1309
+ writeFileSync(join(verifyDir, fileName), JSON.stringify(record, null, 2), "utf-8");
1163
1310
  execLog("merge", `W${record.waveIndex}`, `transaction record persisted`, {
1164
1311
  file: fileName,
1165
1312
  status: record.status,
@@ -1229,11 +1376,7 @@ function runPostMergeVerification(
1229
1376
  // when mergeWaveByRepo() calls mergeWave() once per repo group.
1230
1377
  const repoSuffix = repoId ? `-repo-${repoId.replace(/[^a-zA-Z0-9_-]/g, "_")}` : "";
1231
1378
  const postFileName = `post-b${batchId}-w${waveIndex}${repoSuffix}-lane${laneNumber}.json`;
1232
- writeFileSync(
1233
- join(verifyDir, postFileName),
1234
- JSON.stringify(postMerge, null, 2),
1235
- "utf-8",
1236
- );
1379
+ writeFileSync(join(verifyDir, postFileName), JSON.stringify(postMerge, null, 2), "utf-8");
1237
1380
  } catch {
1238
1381
  // Best effort — persistence failure doesn't block verification
1239
1382
  }
@@ -1264,7 +1407,7 @@ function runPostMergeVerification(
1264
1407
  // Only when flakyReruns > 0 (0 = disabled — any new failure immediately blocks)
1265
1408
  if (flakyReruns > 0) {
1266
1409
  // Identify which commandIds produced new failures
1267
- const failedCommandIds = new Set(diff.newFailures.map(fp => fp.commandId));
1410
+ const failedCommandIds = new Set(diff.newFailures.map((fp) => fp.commandId));
1268
1411
  const rerunCommands: Record<string, string> = {};
1269
1412
  for (const cmdId of failedCommandIds) {
1270
1413
  if (testingCommands[cmdId]) {
@@ -1275,10 +1418,15 @@ function runPostMergeVerification(
1275
1418
  // Re-run up to flakyReruns times; break early if failures clear
1276
1419
  let clearedOnRerun = false;
1277
1420
  for (let attempt = 0; attempt < flakyReruns; attempt++) {
1278
- execLog("merge", sessionName, `new failures detected — running flaky re-run ${attempt + 1}/${flakyReruns}`, {
1279
- failedCommands: [...failedCommandIds].join(", "),
1280
- rerunCount: Object.keys(rerunCommands).length,
1281
- });
1421
+ execLog(
1422
+ "merge",
1423
+ sessionName,
1424
+ `new failures detected — running flaky re-run ${attempt + 1}/${flakyReruns}`,
1425
+ {
1426
+ failedCommands: [...failedCommandIds].join(", "),
1427
+ rerunCount: Object.keys(rerunCommands).length,
1428
+ },
1429
+ );
1282
1430
 
1283
1431
  const rerunResults = runVerificationCommands(rerunCommands, mergeWorkDir);
1284
1432
 
@@ -1292,12 +1440,18 @@ function runPostMergeVerification(
1292
1440
 
1293
1441
  // Re-diff: compare baseline against re-run results for the failed commands only
1294
1442
  // Filter baseline fingerprints to only the commands we re-ran
1295
- const baselineForRerun = baseline.fingerprints.filter(fp => failedCommandIds.has(fp.commandId));
1443
+ const baselineForRerun = baseline.fingerprints.filter((fp) =>
1444
+ failedCommandIds.has(fp.commandId),
1445
+ );
1296
1446
  const rerunDiff = diffFingerprints(baselineForRerun, dedupedRerun);
1297
1447
 
1298
1448
  if (rerunDiff.newFailures.length === 0) {
1299
1449
  // Failures disappeared on re-run — flaky suspected
1300
- execLog("merge", sessionName, `flaky re-run ${attempt + 1} cleared all new failures — classifying as flaky_suspected`);
1450
+ execLog(
1451
+ "merge",
1452
+ sessionName,
1453
+ `flaky re-run ${attempt + 1} cleared all new failures — classifying as flaky_suspected`,
1454
+ );
1301
1455
  clearedOnRerun = true;
1302
1456
  break;
1303
1457
  }
@@ -1306,11 +1460,10 @@ function runPostMergeVerification(
1306
1460
  if (attempt === flakyReruns - 1) {
1307
1461
  const summary = rerunDiff.newFailures
1308
1462
  .slice(0, 5)
1309
- .map(fp => `${fp.commandId}:${fp.file}:${fp.case} (${fp.kind})`)
1463
+ .map((fp) => `${fp.commandId}:${fp.file}:${fp.case} (${fp.kind})`)
1310
1464
  .join("; ");
1311
- const truncated = rerunDiff.newFailures.length > 5
1312
- ? ` ... and ${rerunDiff.newFailures.length - 5} more`
1313
- : "";
1465
+ const truncated =
1466
+ rerunDiff.newFailures.length > 5 ? ` ... and ${rerunDiff.newFailures.length - 5} more` : "";
1314
1467
 
1315
1468
  return {
1316
1469
  performed: true,
@@ -1340,11 +1493,10 @@ function runPostMergeVerification(
1340
1493
  // flakyReruns === 0 or fallthrough: new failures block immediately
1341
1494
  const summary = diff.newFailures
1342
1495
  .slice(0, 5)
1343
- .map(fp => `${fp.commandId}:${fp.file}:${fp.case} (${fp.kind})`)
1496
+ .map((fp) => `${fp.commandId}:${fp.file}:${fp.case} (${fp.kind})`)
1344
1497
  .join("; ");
1345
- const truncated = diff.newFailures.length > 5
1346
- ? ` ... and ${diff.newFailures.length - 5} more`
1347
- : "";
1498
+ const truncated =
1499
+ diff.newFailures.length > 5 ? ` ... and ${diff.newFailures.length - 5} more` : "";
1348
1500
 
1349
1501
  return {
1350
1502
  performed: true,
@@ -1427,14 +1579,12 @@ export async function mergeWave(
1427
1579
  // TP-078: When forceMixedOutcome is true, lanes with both succeeded and
1428
1580
  // failed/stalled tasks are also considered mergeable. This allows the
1429
1581
  // orch_force_merge tool to merge succeeded commits from mixed-outcome lanes.
1430
- const mergeableLanes = completedLanes.filter(lane => {
1582
+ const mergeableLanes = completedLanes.filter((lane) => {
1431
1583
  const outcome = laneOutcomeByNumber.get(lane.laneNumber);
1432
1584
  if (!outcome) return false;
1433
1585
 
1434
- const hasSucceeded = outcome.tasks.some(t => t.status === "succeeded");
1435
- const hasHardFailure = outcome.tasks.some(
1436
- t => t.status === "failed" || t.status === "stalled",
1437
- );
1586
+ const hasSucceeded = outcome.tasks.some((t) => t.status === "succeeded");
1587
+ const hasHardFailure = outcome.tasks.some((t) => t.status === "failed" || t.status === "stalled");
1438
1588
 
1439
1589
  if (forceMixedOutcome) {
1440
1590
  // In force mode, merge any lane with at least one succeeded task
@@ -1449,11 +1599,11 @@ export async function mergeWave(
1449
1599
  // partial progress (STATUS.md updates) that should be staged on the target
1450
1600
  // branch so it survives integration. Stage artifacts directly without
1451
1601
  // creating a full merge worktree.
1452
- const skippedOnlyLanes = completedLanes.filter(lane => {
1602
+ const skippedOnlyLanes = completedLanes.filter((lane) => {
1453
1603
  if (!lane.worktreePath) return false;
1454
1604
  const outcome = laneOutcomeByNumber.get(lane.laneNumber);
1455
1605
  if (!outcome) return false;
1456
- return outcome.tasks.some(t => t.status === "skipped");
1606
+ return outcome.tasks.some((t) => t.status === "skipped");
1457
1607
  });
1458
1608
  if (skippedOnlyLanes.length > 0) {
1459
1609
  stageSkippedArtifactsToTargetBranch(skippedOnlyLanes, waveIndex, repoRoot, targetBranch);
@@ -1477,21 +1627,22 @@ export async function mergeWave(
1477
1627
  // These lanes won't have their branches merged, but their task artifacts
1478
1628
  // (STATUS.md, .reviews) should still be staged so partial progress is preserved
1479
1629
  // through integration. Only lanes with worktree paths can contribute artifacts.
1480
- const mergeableLaneNumbers = new Set(mergeableLanes.map(l => l.laneNumber));
1481
- const skippedArtifactLanes = completedLanes.filter(lane => {
1630
+ const mergeableLaneNumbers = new Set(mergeableLanes.map((l) => l.laneNumber));
1631
+ const skippedArtifactLanes = completedLanes.filter((lane) => {
1482
1632
  if (mergeableLaneNumbers.has(lane.laneNumber)) return false;
1483
1633
  if (!lane.worktreePath) return false;
1484
1634
  const outcome = laneOutcomeByNumber.get(lane.laneNumber);
1485
1635
  if (!outcome) return false;
1486
- return outcome.tasks.some(t => t.status === "skipped");
1636
+ return outcome.tasks.some((t) => t.status === "skipped");
1487
1637
  });
1488
1638
 
1489
1639
  execLog("merge", `W${waveIndex}`, `merging ${orderedLanes.length} lane(s)`, {
1490
1640
  order: config.merge.order,
1491
- lanes: orderedLanes.map(l => l.laneNumber).join(","),
1492
- skippedArtifactLanes: skippedArtifactLanes.length > 0
1493
- ? skippedArtifactLanes.map(l => l.laneNumber).join(",")
1494
- : undefined,
1641
+ lanes: orderedLanes.map((l) => l.laneNumber).join(","),
1642
+ skippedArtifactLanes:
1643
+ skippedArtifactLanes.length > 0
1644
+ ? skippedArtifactLanes.map((l) => l.laneNumber).join(",")
1645
+ : undefined,
1495
1646
  });
1496
1647
 
1497
1648
  // ── Create isolated merge worktree ──────────────────────────────
@@ -1513,7 +1664,9 @@ export async function mergeWave(
1513
1664
  }
1514
1665
  try {
1515
1666
  spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
1516
- } catch { /* branch may not exist */ }
1667
+ } catch {
1668
+ /* branch may not exist */
1669
+ }
1517
1670
 
1518
1671
  // Create temp branch at target branch HEAD, then worktree
1519
1672
  const branchResult = spawnSync("git", ["branch", tempBranch, targetBranch], { cwd: repoRoot });
@@ -1521,20 +1674,28 @@ export async function mergeWave(
1521
1674
  const err = branchResult.stderr?.toString().trim() || "unknown error";
1522
1675
  execLog("merge", `W${waveIndex}`, `failed to create temp branch: ${err}`);
1523
1676
  return {
1524
- waveIndex, status: "failed", laneResults: [],
1525
- failedLane: null, failureReason: `Failed to create merge temp branch: ${err}`,
1677
+ waveIndex,
1678
+ status: "failed",
1679
+ laneResults: [],
1680
+ failedLane: null,
1681
+ failureReason: `Failed to create merge temp branch: ${err}`,
1526
1682
  totalDurationMs: Date.now() - startTime,
1527
1683
  };
1528
1684
  }
1529
1685
 
1530
- const wtResult = spawnSync("git", ["worktree", "add", mergeWorkDir, tempBranch], { cwd: repoRoot });
1686
+ const wtResult = spawnSync("git", ["worktree", "add", mergeWorkDir, tempBranch], {
1687
+ cwd: repoRoot,
1688
+ });
1531
1689
  if (wtResult.status !== 0) {
1532
1690
  const err = wtResult.stderr?.toString().trim() || "unknown error";
1533
1691
  execLog("merge", `W${waveIndex}`, `failed to create merge worktree: ${err}`);
1534
1692
  spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
1535
1693
  return {
1536
- waveIndex, status: "failed", laneResults: [],
1537
- failedLane: null, failureReason: `Failed to create merge worktree: ${err}`,
1694
+ waveIndex,
1695
+ status: "failed",
1696
+ laneResults: [],
1697
+ failedLane: null,
1698
+ failureReason: `Failed to create merge worktree: ${err}`,
1538
1699
  totalDurationMs: Date.now() - startTime,
1539
1700
  };
1540
1701
  }
@@ -1559,18 +1720,33 @@ export async function mergeWave(
1559
1720
  // Verification is enabled but no testing commands configured — treat as
1560
1721
  // baseline-unavailable. Strict/permissive handling below.
1561
1722
  if (verificationMode === "strict") {
1562
- execLog("merge", `W${waveIndex}`, "verification enabled but no testing commands configured — strict mode: failing merge");
1723
+ execLog(
1724
+ "merge",
1725
+ `W${waveIndex}`,
1726
+ "verification enabled but no testing commands configured — strict mode: failing merge",
1727
+ );
1563
1728
  // Clean up worktree and temp branch before returning failure
1564
1729
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1565
- try { spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot }); } catch { /* best effort */ }
1730
+ try {
1731
+ spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
1732
+ } catch {
1733
+ /* best effort */
1734
+ }
1566
1735
  return {
1567
- waveIndex, status: "failed", laneResults: [],
1736
+ waveIndex,
1737
+ status: "failed",
1738
+ laneResults: [],
1568
1739
  failedLane: null,
1569
- failureReason: "Verification enabled (strict mode) but no testing commands configured in taskRunner.testing.commands",
1740
+ failureReason:
1741
+ "Verification enabled (strict mode) but no testing commands configured in taskRunner.testing.commands",
1570
1742
  totalDurationMs: Date.now() - startTime,
1571
1743
  };
1572
1744
  } else {
1573
- execLog("merge", `W${waveIndex}`, "verification enabled but no testing commands configured — permissive mode: continuing without verification");
1745
+ execLog(
1746
+ "merge",
1747
+ `W${waveIndex}`,
1748
+ "verification enabled but no testing commands configured — permissive mode: continuing without verification",
1749
+ );
1574
1750
  }
1575
1751
  }
1576
1752
 
@@ -1591,11 +1767,7 @@ export async function mergeWave(
1591
1767
  // when mergeWaveByRepo() calls mergeWave() once per repo group.
1592
1768
  const repoSuffix = repoId ? `-repo-${repoId.replace(/[^a-zA-Z0-9_-]/g, "_")}` : "";
1593
1769
  const baselineFileName = `baseline-b${batchId}-w${waveIndex}${repoSuffix}.json`;
1594
- writeFileSync(
1595
- join(verifyDir, baselineFileName),
1596
- JSON.stringify(baseline, null, 2),
1597
- "utf-8",
1598
- );
1770
+ writeFileSync(join(verifyDir, baselineFileName), JSON.stringify(baseline, null, 2), "utf-8");
1599
1771
 
1600
1772
  execLog("merge", `W${waveIndex}`, "verification baseline captured", {
1601
1773
  fingerprints: baseline.fingerprints.length,
@@ -1610,17 +1782,28 @@ export async function mergeWave(
1610
1782
  });
1611
1783
  // Clean up worktree and temp branch before returning failure
1612
1784
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1613
- try { spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot }); } catch { /* best effort */ }
1785
+ try {
1786
+ spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
1787
+ } catch {
1788
+ /* best effort */
1789
+ }
1614
1790
  return {
1615
- waveIndex, status: "failed", laneResults: [],
1791
+ waveIndex,
1792
+ status: "failed",
1793
+ laneResults: [],
1616
1794
  failedLane: null,
1617
1795
  failureReason: `Verification baseline capture failed (strict mode): ${errMsg}`,
1618
1796
  totalDurationMs: Date.now() - startTime,
1619
1797
  };
1620
1798
  }
1621
- execLog("merge", `W${waveIndex}`, `baseline capture failed — permissive mode: continuing without baseline verification`, {
1622
- error: errMsg,
1623
- });
1799
+ execLog(
1800
+ "merge",
1801
+ `W${waveIndex}`,
1802
+ `baseline capture failed — permissive mode: continuing without baseline verification`,
1803
+ {
1804
+ error: errMsg,
1805
+ },
1806
+ );
1624
1807
  // Permissive: baseline capture failure is non-fatal — merge proceeds without
1625
1808
  // orchestrator-side verification. Merge-agent verification (merge.verify)
1626
1809
  // still applies independently.
@@ -1659,7 +1842,10 @@ export async function mergeWave(
1659
1842
  // This is the rollback target if verification detects new failures.
1660
1843
  let baseHEAD = "";
1661
1844
  {
1662
- const headResult = spawnSync("git", ["rev-parse", "HEAD"], { cwd: mergeWorkDir, encoding: "utf-8" });
1845
+ const headResult = spawnSync("git", ["rev-parse", "HEAD"], {
1846
+ cwd: mergeWorkDir,
1847
+ encoding: "utf-8",
1848
+ });
1663
1849
  if (headResult.status === 0) {
1664
1850
  baseHEAD = headResult.stdout.trim();
1665
1851
  }
@@ -1668,7 +1854,10 @@ export async function mergeWave(
1668
1854
  // ── TP-033: Capture laneHEAD (source branch tip being merged in) ──
1669
1855
  let laneHEAD = "";
1670
1856
  {
1671
- const laneRef = spawnSync("git", ["rev-parse", lane.branch], { cwd: repoRoot, encoding: "utf-8" });
1857
+ const laneRef = spawnSync("git", ["rev-parse", lane.branch], {
1858
+ cwd: repoRoot,
1859
+ encoding: "utf-8",
1860
+ });
1672
1861
  if (laneRef.status === 0) {
1673
1862
  laneHEAD = laneRef.stdout.trim();
1674
1863
  }
@@ -1730,28 +1919,62 @@ export async function mergeWave(
1730
1919
  // Apply 2× backoff: double the timeout for each retry attempt
1731
1920
  currentTimeoutMs = freshTimeoutMs * Math.pow(2, attempt);
1732
1921
 
1733
- execLog("merge", sessionName, `retry ${attempt}/${MERGE_TIMEOUT_MAX_RETRIES} after timeout — respawning merge agent`, {
1734
- newTimeoutMs: currentTimeoutMs,
1735
- newTimeoutMin: Math.round(currentTimeoutMs / 60_000),
1736
- attempt,
1737
- });
1922
+ execLog(
1923
+ "merge",
1924
+ sessionName,
1925
+ `retry ${attempt}/${MERGE_TIMEOUT_MAX_RETRIES} after timeout — respawning merge agent`,
1926
+ {
1927
+ newTimeoutMs: currentTimeoutMs,
1928
+ newTimeoutMin: Math.round(currentTimeoutMs / 60_000),
1929
+ attempt,
1930
+ },
1931
+ );
1738
1932
 
1739
1933
  // Clean up stale result file from prior attempt
1740
1934
  if (existsSync(resultFilePath)) {
1741
- try { unlinkSync(resultFilePath); } catch { /* best effort */ }
1935
+ try {
1936
+ unlinkSync(resultFilePath);
1937
+ } catch {
1938
+ /* best effort */
1939
+ }
1742
1940
  }
1743
1941
 
1744
1942
  // Re-spawn merge agent for the retry.
1745
1943
  // Kill previous V2 agent handle to prevent orphan/duplicate.
1746
1944
  killMergeAgentV2(sessionName);
1747
- await spawnMergeAgentV2(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot, batchId, waveIndex);
1945
+ await spawnMergeAgentV2(
1946
+ sessionName,
1947
+ repoRoot,
1948
+ mergeWorkDir,
1949
+ requestFilePath,
1950
+ config,
1951
+ stateRoot,
1952
+ agentRoot,
1953
+ batchId,
1954
+ waveIndex,
1955
+ );
1748
1956
  } else {
1749
1957
  // First attempt: spawn merge agent (Runtime V2)
1750
- await spawnMergeAgentV2(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot, batchId, waveIndex);
1958
+ await spawnMergeAgentV2(
1959
+ sessionName,
1960
+ repoRoot,
1961
+ mergeWorkDir,
1962
+ requestFilePath,
1963
+ config,
1964
+ stateRoot,
1965
+ agentRoot,
1966
+ batchId,
1967
+ waveIndex,
1968
+ );
1751
1969
  }
1752
1970
 
1753
1971
  try {
1754
- mergeResult = await waitForMergeResult(resultFilePath, sessionName, currentTimeoutMs, runtimeBackend);
1972
+ mergeResult = await waitForMergeResult(
1973
+ resultFilePath,
1974
+ sessionName,
1975
+ currentTimeoutMs,
1976
+ runtimeBackend,
1977
+ );
1755
1978
  // TP-056: Deregister session from health monitor on completion
1756
1979
  if (healthMonitor) healthMonitor.removeSession(sessionName);
1757
1980
  lastTimeoutError = null;
@@ -1820,11 +2043,12 @@ export async function mergeWave(
1820
2043
  case "CONFLICT_UNRESOLVED":
1821
2044
  execLog("merge", sessionName, "merge failed — unresolved conflicts", {
1822
2045
  conflictCount: mergeResult.conflicts.length,
1823
- files: mergeResult.conflicts.map(c => c.file).join(", "),
2046
+ files: mergeResult.conflicts.map((c) => c.file).join(", "),
1824
2047
  });
1825
2048
  failedLane = lane.laneNumber;
1826
- failureReason = `Unresolved merge conflicts in lane ${lane.laneNumber}: ` +
1827
- mergeResult.conflicts.map(c => c.file).join(", ");
2049
+ failureReason =
2050
+ `Unresolved merge conflicts in lane ${lane.laneNumber}: ` +
2051
+ mergeResult.conflicts.map((c) => c.file).join(", ");
1828
2052
  break;
1829
2053
 
1830
2054
  case "BUILD_FAILURE":
@@ -1838,7 +2062,8 @@ export async function mergeWave(
1838
2062
  baselineActive: !!baseline,
1839
2063
  });
1840
2064
  failedLane = lane.laneNumber;
1841
- failureReason = `Post-merge verification failed in lane ${lane.laneNumber}: ` +
2065
+ failureReason =
2066
+ `Post-merge verification failed in lane ${lane.laneNumber}: ` +
1842
2067
  mergeResult.verification.output.slice(0, 500);
1843
2068
  break;
1844
2069
  }
@@ -1846,7 +2071,10 @@ export async function mergeWave(
1846
2071
  // ── TP-033: Capture mergedHEAD after successful merge commit ──
1847
2072
  let mergedHEAD: string | null = null;
1848
2073
  if (mergeResult.status === "SUCCESS" || mergeResult.status === "CONFLICT_RESOLVED") {
1849
- const postMergeRef = spawnSync("git", ["rev-parse", "HEAD"], { cwd: mergeWorkDir, encoding: "utf-8" });
2074
+ const postMergeRef = spawnSync("git", ["rev-parse", "HEAD"], {
2075
+ cwd: mergeWorkDir,
2076
+ encoding: "utf-8",
2077
+ });
1850
2078
  if (postMergeRef.status === 0) {
1851
2079
  mergedHEAD = postMergeRef.stdout.trim();
1852
2080
  }
@@ -1916,7 +2144,8 @@ export async function mergeWave(
1916
2144
  // ref advancement MUST NOT proceed for ANY lane, because the temp
1917
2145
  // branch HEAD includes the unverified commit.
1918
2146
  const resetErr = resetResult.stderr?.toString().trim() || "unknown error";
1919
- laneResult.error = `verification_new_failure: rollback reset failed (${resetErr}) — ` +
2147
+ laneResult.error =
2148
+ `verification_new_failure: rollback reset failed (${resetErr}) — ` +
1920
2149
  `temp branch may contain failing merge commit, advancement blocked`;
1921
2150
  blockAdvancement = true;
1922
2151
  txnStatus = "rollback_failed";
@@ -1931,15 +2160,21 @@ export async function mergeWave(
1931
2160
  ];
1932
2161
  rollbackFailed = true;
1933
2162
 
1934
- execLog("merge", sessionName, `CRITICAL: rollback reset failed: ${resetErr} — safe-stop triggered`, {
1935
- preLaneHead: preLaneHead.slice(0, 8),
1936
- recoveryCommands: txnRecoveryCommands,
1937
- });
2163
+ execLog(
2164
+ "merge",
2165
+ sessionName,
2166
+ `CRITICAL: rollback reset failed: ${resetErr} — safe-stop triggered`,
2167
+ {
2168
+ preLaneHead: preLaneHead.slice(0, 8),
2169
+ recoveryCommands: txnRecoveryCommands,
2170
+ },
2171
+ );
1938
2172
  }
1939
2173
  } else {
1940
2174
  // TP-032 R006-2: No pre-lane HEAD captured — cannot roll back.
1941
2175
  // Block advancement since the bad commit cannot be removed.
1942
- laneResult.error = `verification_new_failure: no pre-lane HEAD available for rollback — ` +
2176
+ laneResult.error =
2177
+ `verification_new_failure: no pre-lane HEAD available for rollback — ` +
1943
2178
  `advancement blocked`;
1944
2179
  blockAdvancement = true;
1945
2180
  txnStatus = "rollback_failed";
@@ -1957,18 +2192,28 @@ export async function mergeWave(
1957
2192
  ];
1958
2193
  rollbackFailed = true;
1959
2194
 
1960
- execLog("merge", sessionName, "CRITICAL: no baseHEAD — cannot roll back, safe-stop triggered");
2195
+ execLog(
2196
+ "merge",
2197
+ sessionName,
2198
+ "CRITICAL: no baseHEAD — cannot roll back, safe-stop triggered",
2199
+ );
1961
2200
  }
1962
2201
 
1963
2202
  failedLane = lane.laneNumber;
1964
- failureReason = `Verification baseline comparison detected ${verificationResult.newFailureCount} new failure(s) ` +
2203
+ failureReason =
2204
+ `Verification baseline comparison detected ${verificationResult.newFailureCount} new failure(s) ` +
1965
2205
  `in lane ${lane.laneNumber} (${verificationResult.preExistingCount} pre-existing). ` +
1966
2206
  verificationResult.newFailureSummary.slice(0, 300);
1967
2207
  } else if (verificationResult.classification === "flaky_suspected") {
1968
- execLog("merge", sessionName, "flaky test suspected — failures disappeared on re-run (warning only)", {
1969
- newFailures: verificationResult.newFailureCount,
1970
- flakyRerun: true,
1971
- });
2208
+ execLog(
2209
+ "merge",
2210
+ sessionName,
2211
+ "flaky test suspected — failures disappeared on re-run (warning only)",
2212
+ {
2213
+ newFailures: verificationResult.newFailureCount,
2214
+ flakyRerun: true,
2215
+ },
2216
+ );
1972
2217
  // Warning only — does not block merge advancement
1973
2218
  } else {
1974
2219
  execLog("merge", sessionName, "orchestrator-side verification passed", {
@@ -2001,7 +2246,6 @@ export async function mergeWave(
2001
2246
 
2002
2247
  // Stop merging if this lane failed
2003
2248
  if (failedLane !== null) break;
2004
-
2005
2249
  } catch (err: unknown) {
2006
2250
  // Clean up request file on error
2007
2251
  try {
@@ -2080,7 +2324,7 @@ export async function mergeWave(
2080
2324
  if (!existsSync(rootDir)) return [];
2081
2325
  const files: string[] = [];
2082
2326
  const walk = (dir: string): void => {
2083
- let entries;
2327
+ let entries: Dirent[];
2084
2328
  try {
2085
2329
  entries = readdirSync(dir, { withFileTypes: true });
2086
2330
  } catch {
@@ -2106,7 +2350,7 @@ export async function mergeWave(
2106
2350
  // because their code was not merged; staging .DONE would create false
2107
2351
  // completion markers on the orch branch.
2108
2352
  const SKIPPED_ARTIFACT_NAMES = ["STATUS.md", "REVIEW_VERDICT.json"];
2109
- const skippedArtifactLaneNumbers = new Set(skippedArtifactLanes.map(l => l.laneNumber));
2353
+ const skippedArtifactLaneNumbers = new Set(skippedArtifactLanes.map((l) => l.laneNumber));
2110
2354
 
2111
2355
  // Include both merged lanes and skipped-artifact lanes in staging.
2112
2356
  const artifactStagingLanes = [...orderedLanes, ...skippedArtifactLanes];
@@ -2117,9 +2361,14 @@ export async function mergeWave(
2117
2361
 
2118
2362
  for (const allocTask of lane.tasks) {
2119
2363
  if (!allocTask.task?.taskFolder?.trim()) {
2120
- execLog("merge", `W${waveIndex}`, `skipping task with missing taskFolder (possibly dynamically expanded)`, {
2121
- taskId: allocTask.taskId,
2122
- });
2364
+ execLog(
2365
+ "merge",
2366
+ `W${waveIndex}`,
2367
+ `skipping task with missing taskFolder (possibly dynamically expanded)`,
2368
+ {
2369
+ taskId: allocTask.taskId,
2370
+ },
2371
+ );
2123
2372
  continue;
2124
2373
  }
2125
2374
  const absFolder = resolve(allocTask.task.taskFolder);
@@ -2190,7 +2439,10 @@ export async function mergeWave(
2190
2439
  const resolvedSrc = resolve(repoRootSrc);
2191
2440
  const srcRelToRepo = relative(resolvedRepoRoot, resolvedSrc).replace(/\\/g, "/");
2192
2441
  if (srcRelToRepo.startsWith("..") || srcRelToRepo.startsWith("/")) {
2193
- execLog("merge", `W${waveIndex}`, `skipping artifact source outside repo root`, { path: relPath, src: repoRootSrc });
2442
+ execLog("merge", `W${waveIndex}`, `skipping artifact source outside repo root`, {
2443
+ path: relPath,
2444
+ src: repoRootSrc,
2445
+ });
2194
2446
  continue;
2195
2447
  }
2196
2448
  srcPath = repoRootSrc;
@@ -2211,14 +2463,26 @@ export async function mergeWave(
2211
2463
  }
2212
2464
 
2213
2465
  if (staged > 0) {
2214
- spawnSync("git", ["commit", "-m", `checkpoint: wave ${waveIndex} task artifacts (.DONE, STATUS.md, REVIEW_VERDICT.json, .reviews/*)`], { cwd: mergeWorkDir });
2466
+ spawnSync(
2467
+ "git",
2468
+ [
2469
+ "commit",
2470
+ "-m",
2471
+ `checkpoint: wave ${waveIndex} task artifacts (.DONE, STATUS.md, REVIEW_VERDICT.json, .reviews/*)`,
2472
+ ],
2473
+ { cwd: mergeWorkDir },
2474
+ );
2215
2475
  execLog("merge", `W${waveIndex}`, `committed ${staged} task artifact(s) to merge worktree`, {
2216
2476
  skipped,
2217
2477
  preserved,
2218
2478
  allowedCandidates: allowedRelPaths.size,
2219
2479
  });
2220
2480
  } else {
2221
- execLog("merge", `W${waveIndex}`, `no task artifacts to stage (0 of ${allowedRelPaths.size} candidates present/changed, ${preserved} preserved from lane merge)`);
2481
+ execLog(
2482
+ "merge",
2483
+ `W${waveIndex}`,
2484
+ `no task artifacts to stage (0 of ${allowedRelPaths.size} candidates present/changed, ${preserved} preserved from lane merge)`,
2485
+ );
2222
2486
  }
2223
2487
 
2224
2488
  // Keep both .DONE and STATUS.md in develop's working tree:
@@ -2235,13 +2499,19 @@ export async function mergeWave(
2235
2499
  // that would be included in branch advancement — so we block entirely.
2236
2500
  // Also exclude verification_new_failure lanes (with successful rollback) from
2237
2501
  // success accounting: they have laneResult.error set, so !r.error filters them.
2238
- const anySuccess = !blockAdvancement && laneResults.some(
2239
- r => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED"),
2240
- );
2502
+ const anySuccess =
2503
+ !blockAdvancement &&
2504
+ laneResults.some(
2505
+ (r) => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED"),
2506
+ );
2241
2507
 
2242
2508
  if (blockAdvancement) {
2243
- execLog("merge", `W${waveIndex}`, "branch advancement BLOCKED due to verification rollback failure — " +
2244
- "temp branch may contain unverified merge commit");
2509
+ execLog(
2510
+ "merge",
2511
+ `W${waveIndex}`,
2512
+ "branch advancement BLOCKED due to verification rollback failure — " +
2513
+ "temp branch may contain unverified merge commit",
2514
+ );
2245
2515
  }
2246
2516
 
2247
2517
  if (anySuccess) {
@@ -2296,7 +2566,9 @@ export async function mergeWave(
2296
2566
  } else {
2297
2567
  // Not checked out — safe to use update-ref without touching the worktree.
2298
2568
  // Use compare-and-swap (3-arg form) to guard against concurrent branch movement.
2299
- const oldRefResult = spawnSync("git", ["rev-parse", `refs/heads/${targetBranch}`], { cwd: repoRoot });
2569
+ const oldRefResult = spawnSync("git", ["rev-parse", `refs/heads/${targetBranch}`], {
2570
+ cwd: repoRoot,
2571
+ });
2300
2572
  const oldRef = oldRefResult.status === 0 ? oldRefResult.stdout.toString().trim() : "";
2301
2573
 
2302
2574
  const updateRefArgs = oldRef
@@ -2328,10 +2600,15 @@ export async function mergeWave(
2328
2600
  // branch for manual recovery. The operator can use the recovery commands in
2329
2601
  // the transaction record to restore consistency.
2330
2602
  if (rollbackFailed) {
2331
- execLog("merge", `W${waveIndex}`, "SAFE-STOP: preserving merge worktree and temp branch for recovery", {
2332
- mergeWorkDir,
2333
- tempBranch,
2334
- });
2603
+ execLog(
2604
+ "merge",
2605
+ `W${waveIndex}`,
2606
+ "SAFE-STOP: preserving merge worktree and temp branch for recovery",
2607
+ {
2608
+ mergeWorkDir,
2609
+ tempBranch,
2610
+ },
2611
+ );
2335
2612
  } else {
2336
2613
  // TP-029: Apply forceRemoveMergeWorktree fallback so locked/corrupted
2337
2614
  // merge worktrees don't persist between attempts.
@@ -2340,7 +2617,9 @@ export async function mergeWave(
2340
2617
  // Small delay to ensure worktree lock is released
2341
2618
  await sleepAsync(500);
2342
2619
  spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
2343
- } catch { /* best effort */ }
2620
+ } catch {
2621
+ /* best effort */
2622
+ }
2344
2623
  }
2345
2624
 
2346
2625
  // Determine overall status
@@ -2356,7 +2635,9 @@ export async function mergeWave(
2356
2635
  const totalDurationMs = Date.now() - startTime;
2357
2636
 
2358
2637
  execLog("merge", `W${waveIndex}`, `wave merge complete: ${status}`, {
2359
- mergedLanes: laneResults.filter(r => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED")).length,
2638
+ mergedLanes: laneResults.filter(
2639
+ (r) => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED"),
2640
+ ).length,
2360
2641
  failedLane: failedLane ?? 0,
2361
2642
  duration: `${Math.round(totalDurationMs / 1000)}s`,
2362
2643
  });
@@ -2386,7 +2667,6 @@ export async function mergeWave(
2386
2667
  return result;
2387
2668
  }
2388
2669
 
2389
-
2390
2670
  // ── Repo-Scoped Merge ────────────────────────────────────────────────
2391
2671
 
2392
2672
  /**
@@ -2412,7 +2692,7 @@ export function groupLanesByRepo(
2412
2692
  }
2413
2693
 
2414
2694
  const sortedKeys = [...groupMap.keys()].sort();
2415
- return sortedKeys.map(key => ({
2695
+ return sortedKeys.map((key) => ({
2416
2696
  repoId: key || undefined,
2417
2697
  lanes: groupMap.get(key)!,
2418
2698
  }));
@@ -2477,13 +2757,11 @@ export async function mergeWaveByRepo(
2477
2757
 
2478
2758
  // Filter to mergeable lanes (same criteria as mergeWave).
2479
2759
  // TP-078: When forceMixedOutcome is true, lanes with mixed outcomes are also included.
2480
- const mergeableLanes = completedLanes.filter(lane => {
2760
+ const mergeableLanes = completedLanes.filter((lane) => {
2481
2761
  const outcome = laneOutcomeByNumber.get(lane.laneNumber);
2482
2762
  if (!outcome) return false;
2483
- const hasSucceeded = outcome.tasks.some(t => t.status === "succeeded");
2484
- const hasHardFailure = outcome.tasks.some(
2485
- t => t.status === "failed" || t.status === "stalled",
2486
- );
2763
+ const hasSucceeded = outcome.tasks.some((t) => t.status === "succeeded");
2764
+ const hasHardFailure = outcome.tasks.some((t) => t.status === "failed" || t.status === "stalled");
2487
2765
  if (forceMixedOutcome) return hasSucceeded;
2488
2766
  return hasSucceeded && !hasHardFailure;
2489
2767
  });
@@ -2491,11 +2769,11 @@ export async function mergeWaveByRepo(
2491
2769
  if (mergeableLanes.length === 0) {
2492
2770
  // TP-171: Even when no lanes are mergeable, skipped-task lanes may have
2493
2771
  // partial progress that should be staged on the target branch.
2494
- const skippedOnlyLanes = completedLanes.filter(lane => {
2772
+ const skippedOnlyLanes = completedLanes.filter((lane) => {
2495
2773
  if (!lane.worktreePath) return false;
2496
2774
  const outcome = laneOutcomeByNumber.get(lane.laneNumber);
2497
2775
  if (!outcome) return false;
2498
- return outcome.tasks.some(t => t.status === "skipped");
2776
+ return outcome.tasks.some((t) => t.status === "skipped");
2499
2777
  });
2500
2778
  if (skippedOnlyLanes.length > 0) {
2501
2779
  // In workspace mode, group skipped lanes by repo and stage per-repo.
@@ -2522,7 +2800,7 @@ export async function mergeWaveByRepo(
2522
2800
  const repoGroups = groupLanesByRepo(mergeableLanes);
2523
2801
 
2524
2802
  execLog("merge", `W${waveIndex}`, `merging across ${repoGroups.length} repo group(s)`, {
2525
- repos: repoGroups.map(g => g.repoId ?? "(default)").join(", "),
2803
+ repos: repoGroups.map((g) => g.repoId ?? "(default)").join(", "),
2526
2804
  totalLanes: mergeableLanes.length,
2527
2805
  });
2528
2806
 
@@ -2577,21 +2855,21 @@ export async function mergeWaveByRepo(
2577
2855
  repoRoot: groupRepoRoot,
2578
2856
  baseBranch: groupBaseBranch,
2579
2857
  laneCount: group.lanes.length,
2580
- lanes: group.lanes.map(l => l.laneNumber).join(","),
2858
+ lanes: group.lanes.map((l) => l.laneNumber).join(","),
2581
2859
  });
2582
2860
 
2583
2861
  // TP-171: Build allGroupLanes from all completed lanes for this repo
2584
2862
  // (not just mergeable) so mergeWave() can compute skippedArtifactLanes.
2585
2863
  const groupRepoId = group.repoId;
2586
- const allGroupLanes = completedLanes.filter(l => (l.repoId ?? undefined) === groupRepoId);
2587
- const allGroupLaneNumbers = new Set(allGroupLanes.map(l => l.laneNumber));
2864
+ const allGroupLanes = completedLanes.filter((l) => (l.repoId ?? undefined) === groupRepoId);
2865
+ const allGroupLaneNumbers = new Set(allGroupLanes.map((l) => l.laneNumber));
2588
2866
 
2589
2867
  // Build a filtered WaveExecutionResult containing all lanes for this repo
2590
2868
  // (including skipped-only lanes that aren't in the mergeable group).
2591
2869
  const filteredWaveResult: WaveExecutionResult = {
2592
2870
  ...waveResult,
2593
- laneResults: waveResult.laneResults.filter(lr => allGroupLaneNumbers.has(lr.laneNumber)),
2594
- allocatedLanes: waveResult.allocatedLanes.filter(l => allGroupLaneNumbers.has(l.laneNumber)),
2871
+ laneResults: waveResult.laneResults.filter((lr) => allGroupLaneNumbers.has(lr.laneNumber)),
2872
+ allocatedLanes: waveResult.allocatedLanes.filter((l) => allGroupLaneNumbers.has(l.laneNumber)),
2595
2873
  };
2596
2874
 
2597
2875
  const groupResult = await mergeWave(
@@ -2658,9 +2936,14 @@ export async function mergeWaveByRepo(
2658
2936
  const processedIndex = repoGroups.indexOf(group);
2659
2937
  const remainingGroups = repoGroups.slice(processedIndex + 1);
2660
2938
  if (remainingGroups.length > 0) {
2661
- execLog("merge", `W${waveIndex}`, `safe-stop: skipping ${remainingGroups.length} remaining repo group(s) after rollback failure`, {
2662
- skippedRepos: remainingGroups.map(g => g.repoId ?? "(default)").join(", "),
2663
- });
2939
+ execLog(
2940
+ "merge",
2941
+ `W${waveIndex}`,
2942
+ `safe-stop: skipping ${remainingGroups.length} remaining repo group(s) after rollback failure`,
2943
+ {
2944
+ skippedRepos: remainingGroups.map((g) => g.repoId ?? "(default)").join(", "),
2945
+ },
2946
+ );
2664
2947
  }
2665
2948
  break;
2666
2949
  }
@@ -2668,14 +2951,14 @@ export async function mergeWaveByRepo(
2668
2951
 
2669
2952
  // TP-171: Stage artifacts for repos that have only skipped lanes but were
2670
2953
  // not included in the mergeable repoGroups.
2671
- const processedRepoIds = new Set(repoGroups.map(g => g.repoId));
2672
- const skippedOnlyRepoLanes = completedLanes.filter(lane => {
2954
+ const processedRepoIds = new Set(repoGroups.map((g) => g.repoId));
2955
+ const skippedOnlyRepoLanes = completedLanes.filter((lane) => {
2673
2956
  if (!lane.worktreePath) return false;
2674
2957
  const laneRepoId = lane.repoId ?? undefined;
2675
2958
  if (processedRepoIds.has(laneRepoId)) return false; // already handled by mergeWave
2676
2959
  const outcome = laneOutcomeByNumber.get(lane.laneNumber);
2677
2960
  if (!outcome) return false;
2678
- return outcome.tasks.some(t => t.status === "skipped");
2961
+ return outcome.tasks.some((t) => t.status === "skipped");
2679
2962
  });
2680
2963
  // TP-171 R004: Gate artifact staging behind safe-stop — do not advance
2681
2964
  // any branch refs when a rollback failure has been detected.
@@ -2694,7 +2977,7 @@ export async function mergeWaveByRepo(
2694
2977
  // both lane-level failures AND repo setup failures with failedLane=null)
2695
2978
  // TP-032 R006-3: Exclude verification_new_failure lanes from success determination
2696
2979
  const anyLaneSucceeded = allLaneResults.some(
2697
- r => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED"),
2980
+ (r) => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED"),
2698
2981
  );
2699
2982
 
2700
2983
  let status: MergeWaveResult["status"];
@@ -2710,8 +2993,10 @@ export async function mergeWaveByRepo(
2710
2993
 
2711
2994
  execLog("merge", `W${waveIndex}`, `repo-scoped wave merge complete: ${status}`, {
2712
2995
  repoCount: repoOutcomes.length,
2713
- repoStatuses: repoOutcomes.map(r => `${r.repoId ?? "default"}:${r.status}`).join(", "),
2714
- mergedLanes: allLaneResults.filter(r => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED")).length,
2996
+ repoStatuses: repoOutcomes.map((r) => `${r.repoId ?? "default"}:${r.status}`).join(", "),
2997
+ mergedLanes: allLaneResults.filter(
2998
+ (r) => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED"),
2999
+ ).length,
2715
3000
  duration: `${Math.round(totalDurationMs / 1000)}s`,
2716
3001
  });
2717
3002
 
@@ -2740,8 +3025,6 @@ export async function mergeWaveByRepo(
2740
3025
  return aggregateResult;
2741
3026
  }
2742
3027
 
2743
-
2744
-
2745
3028
  // ── Auto-Integration ─────────────────────────────────────────────────
2746
3029
 
2747
3030
  /**
@@ -2839,7 +3122,9 @@ export function attemptAutoIntegration(
2839
3122
  }
2840
3123
  }
2841
3124
 
2842
- execLog(logCategory, batchId, `auto-integrated: ${baseBranch} advanced to ${orchBranch}`, { orchHead });
3125
+ execLog(logCategory, batchId, `auto-integrated: ${baseBranch} advanced to ${orchBranch}`, {
3126
+ orchHead,
3127
+ });
2843
3128
  onNotify(ORCH_MESSAGES.orchIntegrationAutoSuccess(orchBranch, baseBranch), "info");
2844
3129
  return true;
2845
3130
  }
@@ -3042,12 +3327,7 @@ export class MergeHealthMonitor {
3042
3327
  const resultPath = this._resultPaths.get(sessionName) ?? "";
3043
3328
  const hasResultFile = resultPath ? existsSync(resultPath) : false;
3044
3329
 
3045
- const newStatus = classifyMergeHealth(
3046
- sessionAlive,
3047
- hasResultFile,
3048
- state,
3049
- now,
3050
- );
3330
+ const newStatus = classifyMergeHealth(sessionAlive, hasResultFile, state, now);
3051
3331
 
3052
3332
  state.status = newStatus;
3053
3333
 
@@ -3132,4 +3412,3 @@ export class MergeHealthMonitor {
3132
3412
  return new Map(this.sessions);
3133
3413
  }
3134
3414
  }
3135
-