taskplane 0.10.2 → 0.12.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.
- package/README.md +5 -5
- package/dashboard/server.cjs +4 -0
- package/extensions/task-runner.ts +58 -18
- package/extensions/taskplane/config-loader.ts +4 -0
- package/extensions/taskplane/config-schema.ts +24 -0
- package/extensions/taskplane/diagnostics.ts +75 -13
- package/extensions/taskplane/engine.ts +335 -13
- package/extensions/taskplane/execution.ts +6 -1
- package/extensions/taskplane/merge.ts +358 -2
- package/extensions/taskplane/supervisor-primer.md +30 -0
- package/extensions/taskplane/supervisor.ts +23 -0
- package/extensions/taskplane/types.ts +123 -1
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import type { MonitorUpdateCallback } from "./execution.ts";
|
|
|
11
11
|
// classifyExit no longer called directly — Tier 0 uses exitDiagnostic.classification
|
|
12
12
|
// from the diagnostic-reports pipeline (populated by assembleDiagnosticInput).
|
|
13
13
|
import { getCurrentBranch, runGit } from "./git.ts";
|
|
14
|
-
import { mergeWaveByRepo } from "./merge.ts";
|
|
14
|
+
import { mergeWaveByRepo, MergeHealthMonitor } from "./merge.ts";
|
|
15
15
|
import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolicy, extractFailedRepoId, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
|
|
16
16
|
import type { CleanupGateRepoFailure } from "./messages.ts";
|
|
17
17
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
@@ -85,6 +85,7 @@ async function attemptWorkerCrashRetry(
|
|
|
85
85
|
allTaskOutcomes: LaneTaskOutcome[],
|
|
86
86
|
onNotify: (message: string, level: "info" | "warning" | "error") => void,
|
|
87
87
|
stateRoot: string,
|
|
88
|
+
runnerConfig?: TaskRunnerConfig,
|
|
88
89
|
): Promise<{ retriedCount: number; succeededRetries: string[]; failedRetries: string[] }> {
|
|
89
90
|
if (!batchState.resilience) {
|
|
90
91
|
batchState.resilience = defaultResilienceState();
|
|
@@ -134,6 +135,14 @@ async function attemptWorkerCrashRetry(
|
|
|
134
135
|
continue;
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
// model_access_error is handled by attemptModelFallbackRetry() — skip here
|
|
139
|
+
if (classification === "model_access_error") {
|
|
140
|
+
execLog("batch", batchState.batchId,
|
|
141
|
+
`tier0: task ${taskId} classified as model_access_error — deferring to model fallback handler`,
|
|
142
|
+
);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
// Check retry budget
|
|
138
147
|
const scopeKey = tier0ScopeKey("worker_crash", taskId, waveIdx);
|
|
139
148
|
const currentCount = batchState.resilience.retryCountByScope[scopeKey] ?? 0;
|
|
@@ -333,6 +342,263 @@ async function attemptWorkerCrashRetry(
|
|
|
333
342
|
return { retriedCount, succeededRetries, failedRetries };
|
|
334
343
|
}
|
|
335
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Attempt model fallback retry for tasks that failed with `model_access_error`.
|
|
347
|
+
*
|
|
348
|
+
* When a configured agent model becomes unavailable mid-batch (API key expired,
|
|
349
|
+
* rate limit, model deprecated, provider outage), this function retries the task
|
|
350
|
+
* with the session model by setting `TASKPLANE_MODEL_FALLBACK=1` env var. The
|
|
351
|
+
* task-runner reads this var and omits the explicit `--model` flag, causing pi
|
|
352
|
+
* to use the session's default model.
|
|
353
|
+
*
|
|
354
|
+
* Only runs when `runnerConfig.model_fallback === "inherit"` (the default). When
|
|
355
|
+
* set to `"fail"`, model access errors fall through to normal failure handling.
|
|
356
|
+
*
|
|
357
|
+
* Separate from `attemptWorkerCrashRetry()` because:
|
|
358
|
+
* - Uses a different recovery pattern (`model_fallback` vs `worker_crash`)
|
|
359
|
+
* - Requires env var injection to change the model behavior
|
|
360
|
+
* - Has its own retry budget
|
|
361
|
+
*
|
|
362
|
+
* @since TP-055
|
|
363
|
+
*/
|
|
364
|
+
async function attemptModelFallbackRetry(
|
|
365
|
+
waveResult: WaveExecutionResult,
|
|
366
|
+
waveIdx: number,
|
|
367
|
+
batchState: OrchBatchRuntimeState,
|
|
368
|
+
orchConfig: OrchestratorConfig,
|
|
369
|
+
repoRoot: string,
|
|
370
|
+
workspaceConfig: WorkspaceConfig | null | undefined,
|
|
371
|
+
allTaskOutcomes: LaneTaskOutcome[],
|
|
372
|
+
onNotify: (message: string, level: "info" | "warning" | "error") => void,
|
|
373
|
+
stateRoot: string,
|
|
374
|
+
runnerConfig?: TaskRunnerConfig,
|
|
375
|
+
): Promise<{ retriedCount: number; succeededRetries: string[]; failedRetries: string[] }> {
|
|
376
|
+
// Short-circuit: if model fallback is disabled, skip entirely
|
|
377
|
+
const modelFallbackMode = runnerConfig?.model_fallback ?? "inherit";
|
|
378
|
+
if (modelFallbackMode !== "inherit") {
|
|
379
|
+
return { retriedCount: 0, succeededRetries: [], failedRetries: [] };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!batchState.resilience) {
|
|
383
|
+
batchState.resilience = defaultResilienceState();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const budget = TIER0_RETRY_BUDGETS.model_fallback;
|
|
387
|
+
const succeededRetries: string[] = [];
|
|
388
|
+
const failedRetries: string[] = [];
|
|
389
|
+
let retriedCount = 0;
|
|
390
|
+
|
|
391
|
+
// Build a map from taskId → lane for re-execution
|
|
392
|
+
const taskToLane = new Map<string, AllocatedLane>();
|
|
393
|
+
for (const lane of waveResult.allocatedLanes) {
|
|
394
|
+
for (const task of lane.tasks) {
|
|
395
|
+
taskToLane.set(task.taskId, lane);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Process only model_access_error tasks
|
|
400
|
+
for (const taskId of [...waveResult.failedTaskIds]) {
|
|
401
|
+
const lane = taskToLane.get(taskId);
|
|
402
|
+
if (!lane) continue;
|
|
403
|
+
|
|
404
|
+
const outcome = allTaskOutcomes.find(o => o.taskId === taskId);
|
|
405
|
+
if (!outcome) continue;
|
|
406
|
+
|
|
407
|
+
const classification = outcome.exitDiagnostic?.classification;
|
|
408
|
+
if (classification !== "model_access_error") continue;
|
|
409
|
+
|
|
410
|
+
// Check retry budget
|
|
411
|
+
const scopeKey = tier0ScopeKey("model_fallback", taskId, waveIdx);
|
|
412
|
+
const currentCount = batchState.resilience.retryCountByScope[scopeKey] ?? 0;
|
|
413
|
+
if (currentCount >= budget.maxRetries) {
|
|
414
|
+
execLog("batch", batchState.batchId,
|
|
415
|
+
`tier0: task ${taskId} model fallback retry budget exhausted (${currentCount}/${budget.maxRetries})`,
|
|
416
|
+
{ scopeKey },
|
|
417
|
+
);
|
|
418
|
+
emitTier0Event(stateRoot, {
|
|
419
|
+
...buildTier0EventBase("tier0_recovery_exhausted", batchState.batchId, waveIdx, "model_fallback", currentCount, budget.maxRetries),
|
|
420
|
+
taskId,
|
|
421
|
+
laneNumber: lane.laneNumber,
|
|
422
|
+
repoId: lane.repoId ?? null,
|
|
423
|
+
classification,
|
|
424
|
+
error: `Model fallback retry budget exhausted for task ${taskId}`,
|
|
425
|
+
scopeKey,
|
|
426
|
+
affectedTaskIds: [taskId],
|
|
427
|
+
suggestion: `Task ${taskId} failed with model_access_error and model fallback retry exhausted. Check API key validity and model availability.`,
|
|
428
|
+
});
|
|
429
|
+
emitTier0Escalation(stateRoot, batchState.batchId, waveIdx, "model_fallback", currentCount, budget.maxRetries,
|
|
430
|
+
`Model fallback retry budget exhausted for task ${taskId}`, [taskId],
|
|
431
|
+
`Task ${taskId} failed with model_access_error and model fallback retry exhausted. Check API key validity and model availability.`,
|
|
432
|
+
{ taskId, laneNumber: lane.laneNumber, repoId: lane.repoId ?? null, classification, scopeKey },
|
|
433
|
+
);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Increment retry counter
|
|
438
|
+
batchState.resilience.retryCountByScope[scopeKey] = currentCount + 1;
|
|
439
|
+
retriedCount++;
|
|
440
|
+
|
|
441
|
+
const failedModel = outcome.exitDiagnostic?.errorMessage || "configured model";
|
|
442
|
+
execLog("batch", batchState.batchId,
|
|
443
|
+
`tier0: model fallback — retrying task ${taskId} without explicit model (${failedModel} unavailable)`,
|
|
444
|
+
{ scopeKey, classification },
|
|
445
|
+
);
|
|
446
|
+
onNotify(
|
|
447
|
+
`🔄 Model fallback: Retrying task ${taskId} with session model (${failedModel} unavailable)`,
|
|
448
|
+
"info",
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Emit attempt event
|
|
452
|
+
emitTier0Event(stateRoot, {
|
|
453
|
+
...buildTier0EventBase("tier0_recovery_attempt", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
454
|
+
taskId,
|
|
455
|
+
laneNumber: lane.laneNumber,
|
|
456
|
+
repoId: lane.repoId ?? null,
|
|
457
|
+
classification,
|
|
458
|
+
cooldownMs: budget.cooldownMs,
|
|
459
|
+
scopeKey,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Cooldown before retry
|
|
463
|
+
if (budget.cooldownMs > 0) {
|
|
464
|
+
sleepSync(budget.cooldownMs);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Find the specific AllocatedTask
|
|
468
|
+
const allocatedTask = lane.tasks.find(t => t.taskId === taskId);
|
|
469
|
+
if (!allocatedTask) continue;
|
|
470
|
+
|
|
471
|
+
// Re-execute with model fallback env var
|
|
472
|
+
const retryLane: AllocatedLane = {
|
|
473
|
+
...lane,
|
|
474
|
+
tasks: [allocatedTask],
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const isWsMode = !!workspaceConfig;
|
|
478
|
+
const wsRoot = workspaceConfig
|
|
479
|
+
? resolve(workspaceConfig.configPath, "..", "..")
|
|
480
|
+
: undefined;
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const retryPauseSignal = { paused: false };
|
|
484
|
+
// Pass TASKPLANE_MODEL_FALLBACK=1 as extra env var to signal
|
|
485
|
+
// the task-runner to use the session model instead of configured model.
|
|
486
|
+
const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1" };
|
|
487
|
+
const retryResult = await executeLane(
|
|
488
|
+
retryLane,
|
|
489
|
+
orchConfig,
|
|
490
|
+
repoRoot,
|
|
491
|
+
retryPauseSignal,
|
|
492
|
+
wsRoot,
|
|
493
|
+
isWsMode,
|
|
494
|
+
modelFallbackEnv,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const retryOutcome = retryResult.tasks[0];
|
|
498
|
+
if (retryOutcome && retryOutcome.status === "succeeded") {
|
|
499
|
+
succeededRetries.push(taskId);
|
|
500
|
+
|
|
501
|
+
// Update waveResult: move from failed to succeeded
|
|
502
|
+
const failIdx = waveResult.failedTaskIds.indexOf(taskId);
|
|
503
|
+
if (failIdx !== -1) waveResult.failedTaskIds.splice(failIdx, 1);
|
|
504
|
+
waveResult.succeededTaskIds.push(taskId);
|
|
505
|
+
|
|
506
|
+
// Update lane results
|
|
507
|
+
for (const lr of waveResult.laneResults) {
|
|
508
|
+
const taskIdx = lr.tasks.findIndex(t => t.taskId === taskId);
|
|
509
|
+
if (taskIdx !== -1) {
|
|
510
|
+
lr.tasks[taskIdx] = retryOutcome;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
upsertTaskOutcome(allTaskOutcomes, retryOutcome);
|
|
516
|
+
|
|
517
|
+
execLog("batch", batchState.batchId,
|
|
518
|
+
`tier0: task ${taskId} model fallback retry succeeded`,
|
|
519
|
+
{ scopeKey },
|
|
520
|
+
);
|
|
521
|
+
onNotify(
|
|
522
|
+
`✅ Model fallback: Task ${taskId} succeeded with session model`,
|
|
523
|
+
"info",
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
emitTier0Event(stateRoot, {
|
|
527
|
+
...buildTier0EventBase("tier0_recovery_success", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
528
|
+
taskId,
|
|
529
|
+
laneNumber: lane.laneNumber,
|
|
530
|
+
repoId: lane.repoId ?? null,
|
|
531
|
+
classification,
|
|
532
|
+
resolution: `Task ${taskId} succeeded after falling back to session model`,
|
|
533
|
+
scopeKey,
|
|
534
|
+
});
|
|
535
|
+
} else {
|
|
536
|
+
failedRetries.push(taskId);
|
|
537
|
+
if (retryOutcome) {
|
|
538
|
+
upsertTaskOutcome(allTaskOutcomes, retryOutcome);
|
|
539
|
+
}
|
|
540
|
+
execLog("batch", batchState.batchId,
|
|
541
|
+
`tier0: task ${taskId} model fallback retry failed`,
|
|
542
|
+
{ scopeKey, exitReason: retryOutcome?.exitReason },
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const retryFailError = retryOutcome?.exitReason ?? `Task ${taskId} model fallback retry failed`;
|
|
546
|
+
emitTier0Event(stateRoot, {
|
|
547
|
+
...buildTier0EventBase("tier0_recovery_exhausted", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
548
|
+
taskId,
|
|
549
|
+
laneNumber: lane.laneNumber,
|
|
550
|
+
repoId: lane.repoId ?? null,
|
|
551
|
+
classification,
|
|
552
|
+
error: retryFailError,
|
|
553
|
+
scopeKey,
|
|
554
|
+
affectedTaskIds: [taskId],
|
|
555
|
+
suggestion: `Task ${taskId} failed even with session model fallback. Investigate task logs.`,
|
|
556
|
+
});
|
|
557
|
+
emitTier0Escalation(stateRoot, batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries,
|
|
558
|
+
retryFailError, [taskId],
|
|
559
|
+
`Task ${taskId} failed even with session model fallback. Investigate task logs.`,
|
|
560
|
+
{ taskId, laneNumber: lane.laneNumber, repoId: lane.repoId ?? null, classification, scopeKey },
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
} catch (err: unknown) {
|
|
564
|
+
failedRetries.push(taskId);
|
|
565
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
566
|
+
execLog("batch", batchState.batchId,
|
|
567
|
+
`tier0: task ${taskId} model fallback retry threw error: ${errMsg}`,
|
|
568
|
+
{ scopeKey },
|
|
569
|
+
);
|
|
570
|
+
emitTier0Event(stateRoot, {
|
|
571
|
+
...buildTier0EventBase("tier0_recovery_exhausted", batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries),
|
|
572
|
+
taskId,
|
|
573
|
+
laneNumber: lane.laneNumber,
|
|
574
|
+
repoId: lane.repoId ?? null,
|
|
575
|
+
classification,
|
|
576
|
+
error: errMsg,
|
|
577
|
+
scopeKey,
|
|
578
|
+
affectedTaskIds: [taskId],
|
|
579
|
+
suggestion: `Model fallback retry for task ${taskId} threw an exception: ${errMsg}`,
|
|
580
|
+
});
|
|
581
|
+
emitTier0Escalation(stateRoot, batchState.batchId, waveIdx, "model_fallback", currentCount + 1, budget.maxRetries,
|
|
582
|
+
errMsg, [taskId],
|
|
583
|
+
`Model fallback retry for task ${taskId} threw an exception: ${errMsg}`,
|
|
584
|
+
{ taskId, laneNumber: lane.laneNumber, repoId: lane.repoId ?? null, classification, scopeKey },
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Recalculate wave-level status if retries changed outcomes
|
|
590
|
+
if (succeededRetries.length > 0) {
|
|
591
|
+
if (waveResult.failedTaskIds.length === 0) {
|
|
592
|
+
waveResult.overallStatus = "succeeded";
|
|
593
|
+
waveResult.stoppedEarly = false;
|
|
594
|
+
} else if (waveResult.succeededTaskIds.length > 0) {
|
|
595
|
+
waveResult.overallStatus = "partial";
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return { retriedCount, succeededRetries, failedRetries };
|
|
600
|
+
}
|
|
601
|
+
|
|
336
602
|
/**
|
|
337
603
|
* Attempt stale worktree recovery when lane allocation fails with ALLOC_WORKTREE_FAILED.
|
|
338
604
|
*
|
|
@@ -885,6 +1151,40 @@ export async function executeOrchBatch(
|
|
|
885
1151
|
}
|
|
886
1152
|
}
|
|
887
1153
|
|
|
1154
|
+
// ── TP-055: Tier 0 — Model fallback retry ───────────────
|
|
1155
|
+
// Run model fallback BEFORE worker crash retry so that model_access_error
|
|
1156
|
+
// tasks are retried with session model first. Worker crash retry skips
|
|
1157
|
+
// model_access_error tasks (handled here instead).
|
|
1158
|
+
if (waveResult.failedTaskIds.length > 0) {
|
|
1159
|
+
const modelFallbackOutcome = await attemptModelFallbackRetry(
|
|
1160
|
+
waveResult,
|
|
1161
|
+
waveIdx,
|
|
1162
|
+
batchState,
|
|
1163
|
+
orchConfig,
|
|
1164
|
+
repoRoot,
|
|
1165
|
+
workspaceConfig,
|
|
1166
|
+
allTaskOutcomes,
|
|
1167
|
+
onNotify,
|
|
1168
|
+
stateRoot,
|
|
1169
|
+
runnerConfig,
|
|
1170
|
+
);
|
|
1171
|
+
if (modelFallbackOutcome.succeededRetries.length > 0) {
|
|
1172
|
+
// Recompute blocked tasks after model fallback successes
|
|
1173
|
+
if (waveResult.policyApplied === "skip-dependents" && waveResult.failedTaskIds.length > 0) {
|
|
1174
|
+
const recomputed = computeTransitiveDependents(
|
|
1175
|
+
new Set(waveResult.failedTaskIds),
|
|
1176
|
+
depGraph,
|
|
1177
|
+
);
|
|
1178
|
+
waveResult.blockedTaskIds = [...recomputed].sort();
|
|
1179
|
+
} else if (waveResult.failedTaskIds.length === 0) {
|
|
1180
|
+
waveResult.blockedTaskIds = [];
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (modelFallbackOutcome.retriedCount > 0) {
|
|
1184
|
+
persistRuntimeState("tier0-model-fallback", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discoveryRef, stateRoot);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
888
1188
|
// ── TP-039: Tier 0 — Worker crash retry ─────────────────
|
|
889
1189
|
// Run retry BEFORE accumulating counts and blocked tasks so that
|
|
890
1190
|
// successfully retried tasks don't inflate failedTasks count and
|
|
@@ -1058,19 +1358,41 @@ export async function executeOrchBatch(
|
|
|
1058
1358
|
laneCount: mergeableLaneCount,
|
|
1059
1359
|
}, onEngineEvent);
|
|
1060
1360
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
waveResult,
|
|
1064
|
-
waveIdx + 1,
|
|
1065
|
-
orchConfig,
|
|
1066
|
-
repoRoot,
|
|
1067
|
-
batchState.batchId,
|
|
1068
|
-
batchState.orchBranch,
|
|
1069
|
-
workspaceConfig,
|
|
1361
|
+
// TP-056: Start merge health monitor during merge phase
|
|
1362
|
+
const mergeHealthMonitor = new MergeHealthMonitor({
|
|
1070
1363
|
stateRoot,
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1364
|
+
batchId: batchState.batchId,
|
|
1365
|
+
waveIndex: waveIdx,
|
|
1366
|
+
phase: batchState.phase,
|
|
1367
|
+
onDeadSession: (sessionName, laneNumber) => {
|
|
1368
|
+
execLog("batch", batchState.batchId, `merge health monitor detected dead session`, {
|
|
1369
|
+
sessionName,
|
|
1370
|
+
laneNumber,
|
|
1371
|
+
waveIndex: waveIdx,
|
|
1372
|
+
});
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
mergeHealthMonitor.start();
|
|
1376
|
+
|
|
1377
|
+
try {
|
|
1378
|
+
mergeResult = await mergeWaveByRepo(
|
|
1379
|
+
waveResult.allocatedLanes,
|
|
1380
|
+
waveResult,
|
|
1381
|
+
waveIdx + 1,
|
|
1382
|
+
orchConfig,
|
|
1383
|
+
repoRoot,
|
|
1384
|
+
batchState.batchId,
|
|
1385
|
+
batchState.orchBranch,
|
|
1386
|
+
workspaceConfig,
|
|
1387
|
+
stateRoot,
|
|
1388
|
+
agentRoot,
|
|
1389
|
+
runnerConfig.testing_commands,
|
|
1390
|
+
mergeHealthMonitor,
|
|
1391
|
+
);
|
|
1392
|
+
} finally {
|
|
1393
|
+
// TP-056: Always stop the health monitor when merge phase ends
|
|
1394
|
+
mergeHealthMonitor.stop();
|
|
1395
|
+
}
|
|
1074
1396
|
allMergeResults.push(mergeResult);
|
|
1075
1397
|
batchState.mergeResults.push(mergeResult);
|
|
1076
1398
|
|
|
@@ -683,6 +683,7 @@ export function spawnLaneSession(
|
|
|
683
683
|
config: OrchestratorConfig,
|
|
684
684
|
repoRoot: string,
|
|
685
685
|
workspaceRoot?: string,
|
|
686
|
+
extraEnvVars?: Record<string, string>,
|
|
686
687
|
): void {
|
|
687
688
|
const sessionName = lane.tmuxSessionName;
|
|
688
689
|
const laneId = lane.laneId;
|
|
@@ -706,6 +707,9 @@ export function spawnLaneSession(
|
|
|
706
707
|
|
|
707
708
|
// Build env vars
|
|
708
709
|
const envVars = buildLaneEnvVars(lane, task.task.promptPath, repoRoot, workspaceRoot);
|
|
710
|
+
if (extraEnvVars) {
|
|
711
|
+
Object.assign(envVars, extraEnvVars);
|
|
712
|
+
}
|
|
709
713
|
|
|
710
714
|
// Prepare per-task lane log path for post-mortem diagnostics
|
|
711
715
|
const laneLogPath = resolveLaneLogPath(lane, task);
|
|
@@ -1017,6 +1021,7 @@ export async function executeLane(
|
|
|
1017
1021
|
pauseSignal: { paused: boolean },
|
|
1018
1022
|
workspaceRoot?: string,
|
|
1019
1023
|
isWorkspaceMode?: boolean,
|
|
1024
|
+
extraEnvVars?: Record<string, string>,
|
|
1020
1025
|
): Promise<LaneExecutionResult> {
|
|
1021
1026
|
const laneId = lane.laneId;
|
|
1022
1027
|
const laneStartTime = Date.now();
|
|
@@ -1053,7 +1058,7 @@ export async function executeLane(
|
|
|
1053
1058
|
|
|
1054
1059
|
try {
|
|
1055
1060
|
// Spawn TMUX session
|
|
1056
|
-
spawnLaneSession(lane, task, config, repoRoot, workspaceRoot);
|
|
1061
|
+
spawnLaneSession(lane, task, config, repoRoot, workspaceRoot, extraEnvVars);
|
|
1057
1062
|
|
|
1058
1063
|
// Poll until completion
|
|
1059
1064
|
const pollResult = await pollUntilTaskComplete(
|