taskplane 0.7.1 → 0.8.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.
@@ -43,9 +43,11 @@ import {
43
43
  } from "./index.ts";
44
44
  import { buildExecutionContext } from "./workspace.ts";
45
45
  import { openSettingsTui } from "./settings-tui.ts";
46
+ import { loadProjectConfig } from "./config-loader.ts";
46
47
  import {
47
48
  activateSupervisor,
48
49
  deactivateSupervisor,
50
+ transitionToRoutingMode,
49
51
  freshSupervisorState,
50
52
  registerSupervisorPromptHook,
51
53
  checkSupervisorLockOnStartup,
@@ -55,8 +57,9 @@ import {
55
57
  DEFAULT_SUPERVISOR_CONFIG,
56
58
  triggerSupervisorIntegration,
57
59
  presentBatchSummary,
60
+ resolveModelFromString,
58
61
  } from "./supervisor.ts";
59
- import type { SupervisorConfig, IntegrationExecutor, CiDeps, SummaryDeps } from "./supervisor.ts";
62
+ import type { SupervisorConfig, SupervisorRoutingContext, IntegrationExecutor, CiDeps, SummaryDeps } from "./supervisor.ts";
60
63
  import type {
61
64
  AbortMode,
62
65
  ExecutionContext,
@@ -719,6 +722,128 @@ export function collectRepoCleanupFindings(
719
722
  * This prevents unhandled promise rejections from crashing the session
720
723
  * or leaving batch state inconsistent.
721
724
  */
725
+
726
+ // ── Model Availability Pre-Flight ───────────────────────────────────
727
+
728
+ /**
729
+ * A single model configuration to validate.
730
+ */
731
+ interface ModelCheckEntry {
732
+ /** Role label for display (e.g., "Worker", "Reviewer") */
733
+ role: string;
734
+ /** Model string from config (empty = inherit session model) */
735
+ modelStr: string;
736
+ }
737
+
738
+ /**
739
+ * Result of a single model availability check.
740
+ */
741
+ export interface ModelCheckResult {
742
+ role: string;
743
+ modelStr: string;
744
+ status: "inherit" | "found" | "not-found";
745
+ resolvedName?: string;
746
+ }
747
+
748
+ /**
749
+ * Validate that all configured agent models are available in the model registry.
750
+ *
751
+ * Checks worker, reviewer, merger, and supervisor model settings. Models set to
752
+ * empty string ("") or not configured inherit the session model and are always valid.
753
+ *
754
+ * Does NOT validate API keys (that would require side-effectful setModel calls).
755
+ * This catches the most common misconfiguration: specifying a model that isn't
756
+ * registered in pi (wrong name, missing provider, etc.).
757
+ *
758
+ * @param orchConfig - Orchestrator configuration
759
+ * @param runnerConfig - Task runner configuration
760
+ * @param supervisorConfig - Supervisor configuration
761
+ * @param ctx - Extension context with model registry
762
+ * @returns Array of check results (one per role)
763
+ *
764
+ * @since v0.7.2
765
+ */
766
+ export function validateModelAvailability(
767
+ orchConfig: OrchestratorConfig,
768
+ runnerConfig: TaskRunnerConfig,
769
+ supervisorConfig: SupervisorConfig,
770
+ ctx: ExtensionContext,
771
+ agentModels?: { workerModel?: string; reviewerModel?: string },
772
+ ): ModelCheckResult[] {
773
+ const entries: ModelCheckEntry[] = [
774
+ { role: "Worker", modelStr: agentModels?.workerModel ?? (runnerConfig as any).worker?.model ?? "" },
775
+ { role: "Reviewer", modelStr: agentModels?.reviewerModel ?? (runnerConfig as any).reviewer?.model ?? "" },
776
+ { role: "Merger", modelStr: orchConfig.merge?.model ?? "" },
777
+ { role: "Supervisor", modelStr: supervisorConfig.model ?? "" },
778
+ ];
779
+
780
+ const sessionModel = ctx.model;
781
+ const results: ModelCheckResult[] = [];
782
+
783
+ for (const entry of entries) {
784
+ if (!entry.modelStr) {
785
+ // Empty = inherit session model
786
+ results.push({
787
+ role: entry.role,
788
+ modelStr: "(inherit)",
789
+ status: "inherit",
790
+ resolvedName: sessionModel
791
+ ? `${(sessionModel as any).provider ?? ""}/${sessionModel.id}`.replace(/^\//, "")
792
+ : "session default",
793
+ });
794
+ continue;
795
+ }
796
+
797
+ const resolved = resolveModelFromString(entry.modelStr, ctx);
798
+ if (resolved) {
799
+ results.push({
800
+ role: entry.role,
801
+ modelStr: entry.modelStr,
802
+ status: "found",
803
+ resolvedName: `${(resolved as any).provider ?? ""}/${resolved.id}`.replace(/^\//, ""),
804
+ });
805
+ } else {
806
+ results.push({
807
+ role: entry.role,
808
+ modelStr: entry.modelStr,
809
+ status: "not-found",
810
+ });
811
+ }
812
+ }
813
+
814
+ return results;
815
+ }
816
+
817
+ /**
818
+ * Format model validation results for display.
819
+ *
820
+ * @param results - Model check results from validateModelAvailability
821
+ * @returns Formatted string for ctx.ui.notify
822
+ */
823
+ export function formatModelValidation(results: ModelCheckResult[]): string {
824
+ const lines: string[] = ["Model Configuration:"];
825
+ let hasFailure = false;
826
+
827
+ for (const r of results) {
828
+ if (r.status === "inherit") {
829
+ lines.push(` ✅ ${r.role.padEnd(12)} inherit → ${r.resolvedName}`);
830
+ } else if (r.status === "found") {
831
+ lines.push(` ✅ ${r.role.padEnd(12)} ${r.modelStr} → ${r.resolvedName}`);
832
+ } else {
833
+ lines.push(` ❌ ${r.role.padEnd(12)} ${r.modelStr} — NOT FOUND in model registry`);
834
+ hasFailure = true;
835
+ }
836
+ }
837
+
838
+ if (hasFailure) {
839
+ lines.push("");
840
+ lines.push(" Fix: update the model in .pi/taskplane-config.json or /taskplane-settings,");
841
+ lines.push(" or remove the override to inherit the session model.");
842
+ }
843
+
844
+ return lines.join("\n");
845
+ }
846
+
722
847
  export function startBatchAsync(
723
848
  engineFn: () => Promise<void>,
724
849
  batchState: import("./types.ts").OrchBatchRuntimeState,
@@ -1186,6 +1311,15 @@ export default function (pi: ExtensionAPI) {
1186
1311
 
1187
1312
  if (!requireExecCtx(ctx)) return;
1188
1313
 
1314
+ // ── TP-128: Transition from routing-mode supervisor to batch execution ──
1315
+ // If the supervisor is active in routing mode (conversational, no batch),
1316
+ // deactivate it so the batch can start fresh with monitoring-mode supervisor.
1317
+ // This enables the workflow: /orch → conversation → "run the tasks" → /orch all
1318
+ // without the operator needing to know about internal mode distinctions.
1319
+ if (supervisorState.active && supervisorState.routingContext) {
1320
+ await deactivateSupervisor(pi, supervisorState);
1321
+ }
1322
+
1189
1323
  // Prevent concurrent batch execution (merging is an active state)
1190
1324
  if (orchBatchState.phase !== "idle" && orchBatchState.phase !== "completed" && orchBatchState.phase !== "failed" && orchBatchState.phase !== "stopped") {
1191
1325
  ctx.ui.notify(
@@ -1265,6 +1399,34 @@ export default function (pi: ExtensionAPI) {
1265
1399
  break;
1266
1400
  }
1267
1401
 
1402
+ // ── Model availability pre-flight ────────────────────────
1403
+ // Validate that all configured agent models are resolvable in
1404
+ // the model registry before starting. Catches misconfigured
1405
+ // model names early instead of failing hours into a batch.
1406
+ // Note: runnerConfig (TaskRunnerConfig) is a stripped type without
1407
+ // worker/reviewer model fields. Load the full unified config to
1408
+ // get the actual agent model strings (including user preferences).
1409
+ let agentModels: { workerModel?: string; reviewerModel?: string } | undefined;
1410
+ try {
1411
+ const fullConfig = loadProjectConfig(execCtx!.repoRoot);
1412
+ agentModels = {
1413
+ workerModel: fullConfig.taskRunner.worker.model || "",
1414
+ reviewerModel: fullConfig.taskRunner.reviewer.model || "",
1415
+ };
1416
+ } catch { /* fall through — validateModelAvailability handles empty strings */ }
1417
+ const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx, agentModels);
1418
+ const modelFailures = modelResults.filter(r => r.status === "not-found");
1419
+ ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
1420
+ if (modelFailures.length > 0) {
1421
+ ctx.ui.notify(
1422
+ `❌ Cannot start batch — ${modelFailures.length} model(s) not found: ` +
1423
+ modelFailures.map(f => `${f.role} (${f.modelStr})`).join(", ") +
1424
+ `.\n\nFix the model configuration and try again.`,
1425
+ "error",
1426
+ );
1427
+ return;
1428
+ }
1429
+
1268
1430
  // Reset batch state for new execution
1269
1431
  orchBatchState = freshOrchBatchState();
1270
1432
  latestMonitorState = null;
@@ -1377,9 +1539,33 @@ export default function (pi: ExtensionAPI) {
1377
1539
  { triggerTurn: false },
1378
1540
  );
1379
1541
  }
1380
- // TP-043: Generate summary before deactivation (manual mode or non-completed)
1542
+ // TP-043: Generate summary before transition
1381
1543
  presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
1382
- deactivateSupervisor(pi, supervisorState);
1544
+ // TP-128: Transition to routing mode instead of deactivating.
1545
+ // The operator can continue the conversation (integrate, plan
1546
+ // next batch, create tasks) without re-invoking /orch.
1547
+ const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
1548
+ ? {
1549
+ routingState: "completed-batch",
1550
+ contextMessage:
1551
+ `Batch **${orchBatchState.batchId}** completed — ` +
1552
+ `${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
1553
+ `The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
1554
+ `Would you like me to integrate it, or would you prefer to review first?\n\n` +
1555
+ `You can also:\n` +
1556
+ `• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
1557
+ `• Create new tasks for the next batch\n` +
1558
+ `• Run a health check`,
1559
+ }
1560
+ : {
1561
+ routingState: "no-tasks",
1562
+ contextMessage:
1563
+ `Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
1564
+ `${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
1565
+ `${orchBatchState.skippedTasks} skipped.\n\n` +
1566
+ `What would you like to do next?`,
1567
+ };
1568
+ transitionToRoutingMode(pi, supervisorState, postBatchContext);
1383
1569
  },
1384
1570
  );
1385
1571
 
@@ -1697,9 +1883,25 @@ export default function (pi: ExtensionAPI) {
1697
1883
  { triggerTurn: false },
1698
1884
  );
1699
1885
  }
1700
- // TP-043: Generate summary before deactivation
1886
+ // TP-043: Generate summary before transition
1701
1887
  presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
1702
- deactivateSupervisor(pi, supervisorState);
1888
+ // TP-128: Transition to routing mode (same as /orch onTerminal)
1889
+ const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
1890
+ ? {
1891
+ routingState: "completed-batch",
1892
+ contextMessage:
1893
+ `Batch **${orchBatchState.batchId}** completed — ` +
1894
+ `${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
1895
+ `The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
1896
+ `Would you like me to integrate it, or would you prefer to review first?`,
1897
+ }
1898
+ : {
1899
+ routingState: "no-tasks",
1900
+ contextMessage:
1901
+ `Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
1902
+ `What would you like to do next?`,
1903
+ };
1904
+ transitionToRoutingMode(pi, supervisorState, postBatchContext);
1703
1905
  },
1704
1906
  );
1705
1907
 
@@ -11,7 +11,7 @@ import { resolveOperatorId } from "./naming.ts";
11
11
  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, MergeError, VALID_MERGE_STATUSES } from "./types.ts";
12
12
  import type { AllocatedLane, LaneExecutionResult, MergeLaneResult, MergeResult, MergeResultStatus, MergeWaveResult, OrchestratorConfig, RepoMergeOutcome, TaskRunnerConfig, TransactionRecord, TransactionStatus, VerificationBaselineResult, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
13
13
  import { resolveBaseBranch, resolveRepoRoot } from "./waves.ts";
14
- import { generateMergeWorktreePath, sleepSync } from "./worktree.ts";
14
+ import { generateMergeWorktreePath, sleepAsync, sleepSync } from "./worktree.ts";
15
15
  import { getCurrentBranch, runGit } from "./git.ts";
16
16
  import { ORCH_MESSAGES } from "./messages.ts";
17
17
  import { loadOrchestratorConfig } from "./config.ts";
@@ -353,7 +353,7 @@ export function buildMergeRequest(
353
353
  * @param agentRoot - Root for agent prompts. When pointer is resolved, this is the config repo's agent dir. Falls back to `<stateRoot>/.pi/agents/` or `<repoRoot>/.pi/agents/`.
354
354
  * @throws MergeError if spawn fails after retries
355
355
  */
356
- export function spawnMergeAgent(
356
+ export async function spawnMergeAgent(
357
357
  sessionName: string,
358
358
  repoRoot: string,
359
359
  mergeWorkDir: string,
@@ -361,7 +361,7 @@ export function spawnMergeAgent(
361
361
  config: OrchestratorConfig,
362
362
  stateRoot?: string,
363
363
  agentRoot?: string,
364
- ): void {
364
+ ): Promise<void> {
365
365
  execLog("merge", sessionName, "preparing to spawn merge agent", {
366
366
  mergeWorkDir,
367
367
  mergeRequestPath,
@@ -371,7 +371,7 @@ export function spawnMergeAgent(
371
371
  if (tmuxHasSession(sessionName)) {
372
372
  execLog("merge", sessionName, "killing stale merge session");
373
373
  tmuxKillSession(sessionName);
374
- sleepSync(500);
374
+ await sleepAsync(500);
375
375
  }
376
376
 
377
377
  // Build the pi command for the merge agent.
@@ -424,7 +424,7 @@ export function spawnMergeAgent(
424
424
  execLog("merge", sessionName, `merge spawn attempt ${attempt} failed: ${lastError}`);
425
425
 
426
426
  if (attempt <= MERGE_SPAWN_RETRY_MAX) {
427
- sleepSync(attempt * 1000);
427
+ await sleepAsync(attempt * 1000);
428
428
  }
429
429
  }
430
430
 
@@ -449,7 +449,7 @@ export function spawnMergeAgent(
449
449
  export function reloadMergeTimeoutMs(configRoot: string, pointerConfigRoot?: string): number {
450
450
  try {
451
451
  const freshConfig = loadOrchestratorConfig(configRoot, pointerConfigRoot);
452
- const minutes = freshConfig.merge.timeout_minutes ?? 10;
452
+ const minutes = freshConfig.merge.timeout_minutes ?? 90;
453
453
  return minutes * 60 * 1000;
454
454
  } catch (err: unknown) {
455
455
  // Config re-read is best-effort — fall back to default on failure
@@ -480,11 +480,11 @@ const SUCCESSFUL_MERGE_STATUSES = new Set<string>(["SUCCESS", "CONFLICT_RESOLVED
480
480
  * @returns Validated MergeResult
481
481
  * @throws MergeError on timeout, session death, or invalid result
482
482
  */
483
- export function waitForMergeResult(
483
+ export async function waitForMergeResult(
484
484
  resultPath: string,
485
485
  sessionName: string,
486
486
  timeoutMs: number = MERGE_TIMEOUT_MS,
487
- ): MergeResult {
487
+ ): Promise<MergeResult> {
488
488
  const startTime = Date.now();
489
489
  let sessionDiedAt: number | null = null;
490
490
 
@@ -559,7 +559,7 @@ export function waitForMergeResult(
559
559
  // parseMergeResult already retries, so if it throws, it's final.
560
560
  if (err instanceof MergeError && err.code === "MERGE_RESULT_INVALID") {
561
561
  // Wait a bit and try once more (file might still be in flight)
562
- sleepSync(MERGE_RESULT_READ_RETRY_DELAY_MS);
562
+ await sleepAsync(MERGE_RESULT_READ_RETRY_DELAY_MS);
563
563
  if (existsSync(resultPath)) {
564
564
  try {
565
565
  return parseMergeResult(resultPath);
@@ -605,7 +605,7 @@ export function waitForMergeResult(
605
605
  }
606
606
 
607
607
  // Poll interval
608
- sleepSync(MERGE_POLL_INTERVAL_MS);
608
+ await sleepAsync(MERGE_POLL_INTERVAL_MS);
609
609
  }
610
610
  }
611
611
 
@@ -931,7 +931,7 @@ function runPostMergeVerification(
931
931
  * @param baseBranch - Branch to merge into (captured at batch start)
932
932
  * @returns MergeWaveResult with per-lane outcomes
933
933
  */
934
- export function mergeWave(
934
+ export async function mergeWave(
935
935
  completedLanes: AllocatedLane[],
936
936
  waveResult: WaveExecutionResult,
937
937
  waveIndex: number,
@@ -943,7 +943,7 @@ export function mergeWave(
943
943
  agentRoot?: string,
944
944
  testingCommands?: Record<string, string>,
945
945
  repoId?: string,
946
- ): MergeWaveResult {
946
+ ): Promise<MergeWaveResult> {
947
947
  const startTime = Date.now();
948
948
  const tmuxPrefix = config.orchestrator.tmux_prefix;
949
949
  const opId = resolveOperatorId(config);
@@ -1008,7 +1008,7 @@ export function mergeWave(
1008
1008
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1009
1009
  if (existsSync(mergeWorkDir)) {
1010
1010
  // Force cleanup didn't fully remove — wait and retry once
1011
- sleepSync(500);
1011
+ await sleepAsync(500);
1012
1012
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1013
1013
  }
1014
1014
  try {
@@ -1242,14 +1242,14 @@ export function mergeWave(
1242
1242
  }
1243
1243
 
1244
1244
  // Re-spawn merge agent for the retry
1245
- spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1245
+ await spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1246
1246
  } else {
1247
1247
  // First attempt: spawn merge agent
1248
- spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1248
+ await spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
1249
1249
  }
1250
1250
 
1251
1251
  try {
1252
- mergeResult = waitForMergeResult(resultFilePath, sessionName, currentTimeoutMs);
1252
+ mergeResult = await waitForMergeResult(resultFilePath, sessionName, currentTimeoutMs);
1253
1253
  lastTimeoutError = null;
1254
1254
  break; // Success — exit retry loop
1255
1255
  } catch (waitErr: unknown) {
@@ -1732,7 +1732,7 @@ export function mergeWave(
1732
1732
  forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
1733
1733
  try {
1734
1734
  // Small delay to ensure worktree lock is released
1735
- sleepSync(500);
1735
+ await sleepAsync(500);
1736
1736
  spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
1737
1737
  } catch { /* best effort */ }
1738
1738
  }
@@ -1845,7 +1845,7 @@ export function groupLanesByRepo(
1845
1845
  * @param workspaceConfig - Workspace configuration (null in repo mode)
1846
1846
  * @returns MergeWaveResult with per-lane and per-repo outcomes
1847
1847
  */
1848
- export function mergeWaveByRepo(
1848
+ export async function mergeWaveByRepo(
1849
1849
  completedLanes: AllocatedLane[],
1850
1850
  waveResult: WaveExecutionResult,
1851
1851
  waveIndex: number,
@@ -1857,7 +1857,7 @@ export function mergeWaveByRepo(
1857
1857
  stateRoot?: string,
1858
1858
  agentRoot?: string,
1859
1859
  testingCommands?: Record<string, string>,
1860
- ): MergeWaveResult {
1860
+ ): Promise<MergeWaveResult> {
1861
1861
  const startTime = Date.now();
1862
1862
 
1863
1863
  // Build lane outcome lookup for merge eligibility (same logic as mergeWave).
@@ -1901,7 +1901,7 @@ export function mergeWaveByRepo(
1901
1901
  // In repo mode (single group with repoId=undefined), delegate directly
1902
1902
  // to mergeWave() for zero-overhead backward compatibility.
1903
1903
  if (repoGroups.length === 1 && repoGroups[0].repoId === undefined) {
1904
- const result = mergeWave(
1904
+ const result = await mergeWave(
1905
1905
  completedLanes,
1906
1906
  waveResult,
1907
1907
  waveIndex,
@@ -1956,7 +1956,7 @@ export function mergeWaveByRepo(
1956
1956
  allocatedLanes: waveResult.allocatedLanes.filter(l => groupLaneNumbers.has(l.laneNumber)),
1957
1957
  };
1958
1958
 
1959
- const groupResult = mergeWave(
1959
+ const groupResult = await mergeWave(
1960
1960
  group.lanes,
1961
1961
  filteredWaveResult,
1962
1962
  waveIndex,
@@ -698,12 +698,12 @@ export function extractFailedRepoId(mergeResult: MergeWaveResult): string | unde
698
698
  * @returns Outcome describing what happened during the retry cycle
699
699
  * @since TP-033 R006
700
700
  */
701
- export function applyMergeRetryLoop(
701
+ export async function applyMergeRetryLoop(
702
702
  mergeResult: MergeWaveResult,
703
703
  waveIdx: number,
704
704
  retryCountByScope: Record<string, number>,
705
705
  callbacks: MergeRetryCallbacks,
706
- ): MergeRetryLoopOutcome {
706
+ ): Promise<MergeRetryLoopOutcome> {
707
707
  let currentResult = mergeResult;
708
708
 
709
709
  // Classify the initial failure
@@ -766,12 +766,12 @@ export function applyMergeRetryLoop(
766
766
  );
767
767
 
768
768
  if (lastDecision.cooldownMs > 0) {
769
- callbacks.sleep(lastDecision.cooldownMs);
769
+ await callbacks.sleep(lastDecision.cooldownMs);
770
770
  }
771
771
 
772
772
  // Re-invoke merge
773
773
  callbacks.persist("merge-retry-start");
774
- currentResult = callbacks.performMerge();
774
+ currentResult = await callbacks.performMerge();
775
775
  callbacks.updateMergeResult(currentResult);
776
776
  callbacks.persist("merge-retry-complete");
777
777
 
@@ -1246,7 +1246,7 @@ export async function resumeOrchBatch(
1246
1246
  allocatedLanes: reExecAllocatedLanes,
1247
1247
  };
1248
1248
 
1249
- const reExecMergeResult = mergeWaveByRepo(
1249
+ const reExecMergeResult = await mergeWaveByRepo(
1250
1250
  reExecAllocatedLanes,
1251
1251
  syntheticWaveResult,
1252
1252
  RE_EXEC_WAVE_INDEX,
@@ -1451,7 +1451,7 @@ export async function resumeOrchBatch(
1451
1451
  batchState.phase = "merging";
1452
1452
  persistRuntimeState("merge-retry-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1453
1453
 
1454
- const mergeRetryResult = mergeWaveByRepo(
1454
+ const mergeRetryResult = await mergeWaveByRepo(
1455
1455
  mergeRetryLanes,
1456
1456
  syntheticWaveResult,
1457
1457
  waveIdx + 1,
@@ -1630,7 +1630,7 @@ export async function resumeOrchBatch(
1630
1630
  persistRuntimeState("merge-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
1631
1631
  onNotify(ORCH_MESSAGES.orchMergeStart(waveIdx + 1, mergeableLaneCount), "info");
1632
1632
 
1633
- mergeResult = mergeWaveByRepo(
1633
+ mergeResult = await mergeWaveByRepo(
1634
1634
  waveResult.allocatedLanes,
1635
1635
  waveResult,
1636
1636
  waveIdx + 1,
@@ -1762,14 +1762,14 @@ export async function resumeOrchBatch(
1762
1762
  batchState.resilience = defaultResilienceState();
1763
1763
  }
1764
1764
 
1765
- const retryOutcome = applyMergeRetryLoop(
1765
+ const retryOutcome = await applyMergeRetryLoop(
1766
1766
  mergeResult,
1767
1767
  waveIdx,
1768
1768
  batchState.resilience.retryCountByScope,
1769
1769
  {
1770
- performMerge: () => {
1770
+ performMerge: async () => {
1771
1771
  batchState.phase = "merging";
1772
- return mergeWaveByRepo(
1772
+ return await mergeWaveByRepo(
1773
1773
  waveResult.allocatedLanes,
1774
1774
  waveResult,
1775
1775
  waveIdx + 1,