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.
- package/dashboard/public/app.js +43 -18
- package/extensions/task-runner.ts +346 -127
- package/extensions/taskplane/config-loader.ts +1 -1
- package/extensions/taskplane/config-schema.ts +7 -5
- package/extensions/taskplane/engine.ts +6 -6
- package/extensions/taskplane/extension.ts +207 -5
- package/extensions/taskplane/merge.ts +21 -21
- package/extensions/taskplane/messages.ts +4 -4
- package/extensions/taskplane/resume.ts +6 -6
- package/extensions/taskplane/supervisor.ts +141 -9
- package/extensions/taskplane/types.ts +4 -4
- package/extensions/taskplane/worktree.ts +13 -0
- package/package.json +1 -1
- package/templates/agents/local/task-reviewer.md +1 -1
- package/templates/agents/local/task-worker.md +4 -3
- package/templates/agents/task-merger.md +215 -215
- package/templates/agents/task-reviewer.md +1 -1
- package/templates/agents/task-worker.md +18 -13
- package/templates/config/task-orchestrator.yaml +1 -1
- package/templates/config/task-runner.yaml +3 -3
|
@@ -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
|
|
1542
|
+
// TP-043: Generate summary before transition
|
|
1381
1543
|
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1382
|
-
|
|
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
|
|
1886
|
+
// TP-043: Generate summary before transition
|
|
1701
1887
|
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|