taskplane 0.6.1 → 0.7.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 +6 -5
- package/dashboard/public/app.js +227 -0
- package/dashboard/public/index.html +19 -0
- package/dashboard/public/style.css +319 -0
- package/dashboard/server.cjs +219 -1
- package/extensions/taskplane/config-loader.ts +7 -1
- package/extensions/taskplane/config-schema.ts +19 -2
- package/extensions/taskplane/config.ts +23 -1
- package/extensions/taskplane/engine.ts +913 -46
- package/extensions/taskplane/execution.ts +1 -0
- package/extensions/taskplane/extension.ts +975 -54
- package/extensions/taskplane/formatting.ts +713 -712
- package/extensions/taskplane/index.ts +1 -0
- package/extensions/taskplane/merge.ts +236 -48
- package/extensions/taskplane/messages.ts +10 -0
- package/extensions/taskplane/persistence.ts +183 -3
- package/extensions/taskplane/resume.ts +243 -24
- package/extensions/taskplane/settings-tui.ts +10 -3
- package/extensions/taskplane/supervisor-primer.md +626 -0
- package/extensions/taskplane/supervisor.ts +3659 -0
- package/extensions/taskplane/types.ts +330 -3
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { executeOrchBatch } from "./engine.ts";
|
|
|
11
11
|
import { computeTransitiveDependents, execLog, executeWave, pollUntilTaskComplete, spawnLaneSession, tmuxHasSession } from "./execution.ts";
|
|
12
12
|
import type { MonitorUpdateCallback } from "./execution.ts";
|
|
13
13
|
import { getCurrentBranch, runGit } from "./git.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { mergeWaveByRepo } from "./merge.ts";
|
|
15
15
|
import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolicy, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
|
|
16
16
|
import type { CleanupGateRepoFailure } from "./messages.ts";
|
|
17
17
|
import { resolveOperatorId } from "./naming.ts";
|
|
@@ -285,6 +285,14 @@ export function checkResumeEligibility(state: PersistedBatchState, force: boolea
|
|
|
285
285
|
batchId,
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
+
case "launching":
|
|
289
|
+
return {
|
|
290
|
+
eligible: false,
|
|
291
|
+
reason: `Batch ${batchId} is currently launching. Wait for it to start or use /orch-abort.`,
|
|
292
|
+
phase,
|
|
293
|
+
batchId,
|
|
294
|
+
};
|
|
295
|
+
|
|
288
296
|
case "planning":
|
|
289
297
|
return {
|
|
290
298
|
eligible: false,
|
|
@@ -388,10 +396,14 @@ export function reconcileTaskStates(
|
|
|
388
396
|
};
|
|
389
397
|
}
|
|
390
398
|
|
|
391
|
-
// Precedence 5:
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
399
|
+
// Precedence 5: Pending task that was never started → remain pending
|
|
400
|
+
// Matches two cases:
|
|
401
|
+
// (a) No session assigned at all (future-wave task never allocated)
|
|
402
|
+
// (b) Session assigned from a prior failed resume, but session is dead
|
|
403
|
+
// and worktree doesn't exist — task was allocated but never actually
|
|
404
|
+
// started (TP-037 bug #102b fix)
|
|
405
|
+
// In both cases the task should be re-queued for execution, not failed.
|
|
406
|
+
if (task.status === "pending" && (!task.sessionName || (!sessionAlive && !worktreeExists))) {
|
|
395
407
|
return {
|
|
396
408
|
taskId: task.taskId,
|
|
397
409
|
persistedStatus: task.status,
|
|
@@ -417,11 +429,42 @@ export function reconcileTaskStates(
|
|
|
417
429
|
});
|
|
418
430
|
}
|
|
419
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Get the latest merge status for a specific wave index (0-based).
|
|
434
|
+
*
|
|
435
|
+
* Persisted merge results may contain multiple entries for the same wave
|
|
436
|
+
* (e.g., re-exec sentinel merges clamped to wave 0, or retry attempts).
|
|
437
|
+
* This helper returns the latest entry's status for the given wave,
|
|
438
|
+
* preferring the last entry in array order (which is the most recent).
|
|
439
|
+
*
|
|
440
|
+
* @param mergeResults - Persisted merge results array
|
|
441
|
+
* @param waveIndex - 0-based wave index to look up
|
|
442
|
+
* @returns The merge status ("succeeded" | "failed" | "partial") or null if no entry exists
|
|
443
|
+
*/
|
|
444
|
+
export function getMergeStatusForWave(
|
|
445
|
+
mergeResults: ReadonlyArray<{ waveIndex: number; status: "succeeded" | "failed" | "partial" }>,
|
|
446
|
+
waveIndex: number,
|
|
447
|
+
): "succeeded" | "failed" | "partial" | null {
|
|
448
|
+
// Walk in reverse to find the latest entry for this wave
|
|
449
|
+
for (let i = mergeResults.length - 1; i >= 0; i--) {
|
|
450
|
+
if (mergeResults[i].waveIndex === waveIndex) {
|
|
451
|
+
return mergeResults[i].status;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
420
457
|
/**
|
|
421
458
|
* Compute the resume point from reconciled task states and wave plan.
|
|
422
459
|
*
|
|
423
460
|
* Determines which wave to resume from by finding the first wave that
|
|
424
|
-
* has any incomplete tasks. Skips fully completed waves
|
|
461
|
+
* has any incomplete tasks. Skips fully completed waves only when
|
|
462
|
+
* their merge also succeeded.
|
|
463
|
+
*
|
|
464
|
+
* TP-037 (Bug #102): A wave where all tasks are terminal but the merge
|
|
465
|
+
* is missing or failed is NOT skipped — it is flagged for merge retry
|
|
466
|
+
* via `mergeRetryWaveIndexes`. The `resumeWaveIndex` is set to the
|
|
467
|
+
* earliest such wave so the resume loop can process it.
|
|
425
468
|
*
|
|
426
469
|
* Pure function — no process or filesystem access.
|
|
427
470
|
*
|
|
@@ -477,8 +520,11 @@ export function computeResumePoint(
|
|
|
477
520
|
}
|
|
478
521
|
}
|
|
479
522
|
|
|
480
|
-
// Find resume wave: first wave with any non-completed tasks
|
|
523
|
+
// Find resume wave: first wave with any non-completed tasks OR missing/failed merge.
|
|
524
|
+
// TP-037 (Bug #102): A wave where all tasks are terminal but the merge
|
|
525
|
+
// hasn't succeeded is flagged for merge retry, not skipped.
|
|
481
526
|
let resumeWaveIndex = persistedState.wavePlan.length; // default: past end = all done
|
|
527
|
+
const mergeRetryWaveIndexes: number[] = [];
|
|
482
528
|
|
|
483
529
|
for (let i = 0; i < persistedState.wavePlan.length; i++) {
|
|
484
530
|
const waveTasks = persistedState.wavePlan[i];
|
|
@@ -499,9 +545,36 @@ export function computeResumePoint(
|
|
|
499
545
|
});
|
|
500
546
|
|
|
501
547
|
if (!allDone) {
|
|
502
|
-
resumeWaveIndex
|
|
548
|
+
// Only set resumeWaveIndex if not already set by a merge retry
|
|
549
|
+
// (merge retry at an earlier wave takes precedence)
|
|
550
|
+
if (resumeWaveIndex === persistedState.wavePlan.length) {
|
|
551
|
+
resumeWaveIndex = i;
|
|
552
|
+
}
|
|
503
553
|
break;
|
|
504
554
|
}
|
|
555
|
+
|
|
556
|
+
// TP-037 (Bug #102): All tasks are terminal — but did the merge succeed?
|
|
557
|
+
// Only check merge status if the wave had any succeeded tasks (waves with
|
|
558
|
+
// only failures/skips don't produce merges and can be safely skipped).
|
|
559
|
+
const hasSucceededTasks = waveTasks.some((taskId) => {
|
|
560
|
+
const reconciled = reconciledMap.get(taskId);
|
|
561
|
+
if (!reconciled) return false;
|
|
562
|
+
if (reconciled.action === "mark-complete") return true;
|
|
563
|
+
if (reconciled.action === "skip" && (reconciled.liveStatus === "succeeded" || reconciled.persistedStatus === "succeeded")) return true;
|
|
564
|
+
return false;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (hasSucceededTasks && persistedState.mergeResults) {
|
|
568
|
+
const mergeStatus = getMergeStatusForWave(persistedState.mergeResults, i);
|
|
569
|
+
if (mergeStatus !== "succeeded") {
|
|
570
|
+
// Merge missing or failed — flag for retry, don't skip past this wave
|
|
571
|
+
mergeRetryWaveIndexes.push(i);
|
|
572
|
+
if (resumeWaveIndex === persistedState.wavePlan.length) {
|
|
573
|
+
// This is the first wave needing attention — set resume point here
|
|
574
|
+
resumeWaveIndex = i;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
505
578
|
}
|
|
506
579
|
|
|
507
580
|
// Determine pending tasks: tasks in resume wave and later that need execution
|
|
@@ -539,6 +612,7 @@ export function computeResumePoint(
|
|
|
539
612
|
failedTaskIds,
|
|
540
613
|
reconnectTaskIds,
|
|
541
614
|
reExecuteTaskIds,
|
|
615
|
+
mergeRetryWaveIndexes,
|
|
542
616
|
};
|
|
543
617
|
}
|
|
544
618
|
|
|
@@ -692,6 +766,11 @@ export async function resumeOrchBatch(
|
|
|
692
766
|
`❌ Cannot resume: ${err.message}`,
|
|
693
767
|
"error",
|
|
694
768
|
);
|
|
769
|
+
// ── TP-040 R006: Reset phase on pre-execution early return ──
|
|
770
|
+
// The caller may have set batchState.phase = "launching" before
|
|
771
|
+
// calling this function. Since we're returning without starting
|
|
772
|
+
// any work, reset to "idle" so the batch isn't stuck.
|
|
773
|
+
batchState.phase = "idle";
|
|
695
774
|
return;
|
|
696
775
|
}
|
|
697
776
|
throw err;
|
|
@@ -702,6 +781,8 @@ export async function resumeOrchBatch(
|
|
|
702
781
|
ORCH_MESSAGES.resumeNoState(),
|
|
703
782
|
"error",
|
|
704
783
|
);
|
|
784
|
+
// TP-040 R006: Reset phase on pre-execution early return
|
|
785
|
+
batchState.phase = "idle";
|
|
705
786
|
return;
|
|
706
787
|
}
|
|
707
788
|
|
|
@@ -712,6 +793,8 @@ export async function resumeOrchBatch(
|
|
|
712
793
|
ORCH_MESSAGES.resumePhaseNotResumable(persistedState.batchId, persistedState.phase, eligibility.reason),
|
|
713
794
|
"error",
|
|
714
795
|
);
|
|
796
|
+
// TP-040 R006: Reset phase on pre-execution early return
|
|
797
|
+
batchState.phase = "idle";
|
|
715
798
|
return;
|
|
716
799
|
}
|
|
717
800
|
|
|
@@ -732,6 +815,8 @@ export async function resumeOrchBatch(
|
|
|
732
815
|
ORCH_MESSAGES.forceResumeDiagnosticsFailed(persistedState.batchId),
|
|
733
816
|
"error",
|
|
734
817
|
);
|
|
818
|
+
// TP-040 R006: Reset phase on pre-execution early return
|
|
819
|
+
batchState.phase = "idle";
|
|
735
820
|
return;
|
|
736
821
|
}
|
|
737
822
|
|
|
@@ -780,6 +865,33 @@ export async function resumeOrchBatch(
|
|
|
780
865
|
// ── 4. Reconcile task states ─────────────────────────────────
|
|
781
866
|
const reconciledTasks = reconcileTaskStates(persistedState, aliveSessions, doneTaskIds, existingWorktreeTaskIds);
|
|
782
867
|
|
|
868
|
+
// ── 4b. Clear stale session allocation for tasks reconciled as pending ──
|
|
869
|
+
// TP-037 (Bug #102b): Pending tasks that had a sessionName from a prior
|
|
870
|
+
// failed resume but were never actually started need their allocation
|
|
871
|
+
// metadata cleared so they can be freshly assigned to new lanes.
|
|
872
|
+
// We also prune these tasks from persisted lane records so that
|
|
873
|
+
// serializeBatchState() doesn't reintroduce stale sessionName via the
|
|
874
|
+
// `outcome?.sessionName || lane?.tmuxSessionName` fallback path.
|
|
875
|
+
const stalePendingTaskIds = new Set<string>();
|
|
876
|
+
for (const reconciled of reconciledTasks) {
|
|
877
|
+
if (reconciled.action === "pending") {
|
|
878
|
+
const persistedTask = persistedState.tasks.find(t => t.taskId === reconciled.taskId);
|
|
879
|
+
if (persistedTask && persistedTask.sessionName) {
|
|
880
|
+
execLog("resume", persistedState.batchId, `clear-stale-session: ${reconciled.taskId} had stale session "${persistedTask.sessionName}" (lane ${persistedTask.laneNumber})`);
|
|
881
|
+
stalePendingTaskIds.add(reconciled.taskId);
|
|
882
|
+
persistedTask.sessionName = "";
|
|
883
|
+
persistedTask.laneNumber = 0;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// Prune stale-pending tasks from lane records so reconstructAllocatedLanes()
|
|
888
|
+
// (and subsequent serializeBatchState()) won't map them back to the old lane.
|
|
889
|
+
if (stalePendingTaskIds.size > 0) {
|
|
890
|
+
for (const lane of persistedState.lanes) {
|
|
891
|
+
lane.taskIds = lane.taskIds.filter(id => !stalePendingTaskIds.has(id));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
783
895
|
// ── 5. Compute resume point ──────────────────────────────────
|
|
784
896
|
const resumePoint = computeResumePoint(persistedState, reconciledTasks);
|
|
785
897
|
const completedTaskSet = new Set(resumePoint.completedTaskIds);
|
|
@@ -813,6 +925,13 @@ export async function resumeOrchBatch(
|
|
|
813
925
|
);
|
|
814
926
|
}
|
|
815
927
|
|
|
928
|
+
if (resumePoint.mergeRetryWaveIndexes.length > 0) {
|
|
929
|
+
onNotify(
|
|
930
|
+
`🔀 ${resumePoint.mergeRetryWaveIndexes.length} wave(s) need merge retry: ${resumePoint.mergeRetryWaveIndexes.map(i => `W${i + 1}`).join(", ")}`,
|
|
931
|
+
"warning",
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
816
935
|
// ── 6. Reconstruct runtime state ─────────────────────────────
|
|
817
936
|
|
|
818
937
|
// Guard: orchBranch must be present for routing. Persisted states from
|
|
@@ -826,6 +945,8 @@ export async function resumeOrchBatch(
|
|
|
826
945
|
`Use /orch-abort to clean up, then start a new batch.`,
|
|
827
946
|
"error",
|
|
828
947
|
);
|
|
948
|
+
// TP-040 R006: Reset phase on pre-execution early return
|
|
949
|
+
batchState.phase = "idle";
|
|
829
950
|
return;
|
|
830
951
|
}
|
|
831
952
|
|
|
@@ -836,7 +957,8 @@ export async function resumeOrchBatch(
|
|
|
836
957
|
|
|
837
958
|
batchState.mode = persistedState.mode;
|
|
838
959
|
batchState.startedAt = persistedState.startedAt;
|
|
839
|
-
|
|
960
|
+
// Preserve pauseSignal if already set during "launching" phase (TP-040)
|
|
961
|
+
if (!batchState.pauseSignal?.paused) batchState.pauseSignal = { paused: false };
|
|
840
962
|
batchState.totalWaves = persistedState.totalWaves;
|
|
841
963
|
batchState.totalTasks = persistedState.totalTasks;
|
|
842
964
|
batchState.succeededTasks = resumePoint.completedTaskIds.length;
|
|
@@ -1275,7 +1397,105 @@ export async function resumeOrchBatch(
|
|
|
1275
1397
|
}
|
|
1276
1398
|
|
|
1277
1399
|
if (waveTasks.length === 0) {
|
|
1278
|
-
|
|
1400
|
+
// TP-037 Bug #102: Check if this wave needs merge retry.
|
|
1401
|
+
// All tasks are terminal but the merge may have failed/been interrupted.
|
|
1402
|
+
if (resumePoint.mergeRetryWaveIndexes.includes(waveIdx)) {
|
|
1403
|
+
execLog("resume", batchState.batchId, `wave ${waveIdx + 1}: all tasks done but merge needs retry`);
|
|
1404
|
+
onNotify(`🔀 Wave ${waveIdx + 1}: retrying merge (tasks already complete, merge was missing/failed)`, "info");
|
|
1405
|
+
|
|
1406
|
+
// Reconstruct lanes for this wave from persisted state
|
|
1407
|
+
const waveTaskIds = new Set(persistedState.wavePlan[waveIdx]);
|
|
1408
|
+
const waveLaneRecords = persistedState.lanes.filter(
|
|
1409
|
+
lane => lane.taskIds.some(tid => waveTaskIds.has(tid)),
|
|
1410
|
+
);
|
|
1411
|
+
const mergeRetryLanes = reconstructAllocatedLanes(waveLaneRecords, persistedState.tasks);
|
|
1412
|
+
|
|
1413
|
+
// Build synthetic WaveExecutionResult with succeeded tasks
|
|
1414
|
+
const succeededTaskIds = persistedState.wavePlan[waveIdx].filter(
|
|
1415
|
+
taskId => completedTaskSet.has(taskId),
|
|
1416
|
+
);
|
|
1417
|
+
const syntheticLaneResults: LaneExecutionResult[] = mergeRetryLanes.map(lane => ({
|
|
1418
|
+
laneNumber: lane.laneNumber,
|
|
1419
|
+
laneId: lane.laneId,
|
|
1420
|
+
tasks: lane.tasks.map(t => ({
|
|
1421
|
+
taskId: t.taskId,
|
|
1422
|
+
status: (completedTaskSet.has(t.taskId) ? "succeeded" : "failed") as LaneTaskStatus,
|
|
1423
|
+
startTime: Date.now(),
|
|
1424
|
+
endTime: Date.now(),
|
|
1425
|
+
exitReason: completedTaskSet.has(t.taskId) ? "Task completed (merge retry)" : "Task failed (merge retry)",
|
|
1426
|
+
sessionName: lane.tmuxSessionName,
|
|
1427
|
+
doneFileFound: completedTaskSet.has(t.taskId),
|
|
1428
|
+
})),
|
|
1429
|
+
overallStatus: lane.tasks.every(t => completedTaskSet.has(t.taskId)) ? "succeeded" as const : "partial" as const,
|
|
1430
|
+
startTime: Date.now(),
|
|
1431
|
+
endTime: Date.now(),
|
|
1432
|
+
}));
|
|
1433
|
+
|
|
1434
|
+
const syntheticWaveResult: WaveExecutionResult = {
|
|
1435
|
+
waveIndex: waveIdx + 1,
|
|
1436
|
+
startedAt: Date.now(),
|
|
1437
|
+
endedAt: Date.now(),
|
|
1438
|
+
laneResults: syntheticLaneResults,
|
|
1439
|
+
policyApplied: orchConfig.failure.on_task_failure,
|
|
1440
|
+
stoppedEarly: false,
|
|
1441
|
+
failedTaskIds: [],
|
|
1442
|
+
skippedTaskIds: [],
|
|
1443
|
+
succeededTaskIds,
|
|
1444
|
+
blockedTaskIds: [],
|
|
1445
|
+
laneCount: mergeRetryLanes.length,
|
|
1446
|
+
overallStatus: "succeeded",
|
|
1447
|
+
finalMonitorState: null,
|
|
1448
|
+
allocatedLanes: mergeRetryLanes,
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
batchState.phase = "merging";
|
|
1452
|
+
persistRuntimeState("merge-retry-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1453
|
+
|
|
1454
|
+
const mergeRetryResult = mergeWaveByRepo(
|
|
1455
|
+
mergeRetryLanes,
|
|
1456
|
+
syntheticWaveResult,
|
|
1457
|
+
waveIdx + 1,
|
|
1458
|
+
orchConfig,
|
|
1459
|
+
repoRoot,
|
|
1460
|
+
batchState.batchId,
|
|
1461
|
+
batchState.orchBranch,
|
|
1462
|
+
workspaceConfig,
|
|
1463
|
+
stateRoot,
|
|
1464
|
+
agentRoot,
|
|
1465
|
+
runnerConfig.testing_commands,
|
|
1466
|
+
);
|
|
1467
|
+
batchState.mergeResults.push(mergeRetryResult);
|
|
1468
|
+
|
|
1469
|
+
if (mergeRetryResult.status === "succeeded") {
|
|
1470
|
+
onNotify(`✅ Wave ${waveIdx + 1} merge retry succeeded`, "info");
|
|
1471
|
+
// Clean up merged branches
|
|
1472
|
+
for (const lr of mergeRetryResult.laneResults) {
|
|
1473
|
+
if (!lr.error && (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED")) {
|
|
1474
|
+
const laneRepoRoot = resolveRepoRoot(lr.repoId, repoRoot, workspaceConfig);
|
|
1475
|
+
deleteBranchBestEffort(lr.sourceBranch, laneRepoRoot);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
} else {
|
|
1479
|
+
onNotify(
|
|
1480
|
+
`⚠️ Wave ${waveIdx + 1} merge retry ${mergeRetryResult.status}: ${mergeRetryResult.failureReason || "unknown"}`,
|
|
1481
|
+
"warning",
|
|
1482
|
+
);
|
|
1483
|
+
// Apply merge failure policy (same as normal wave merge failure)
|
|
1484
|
+
const policyResult = computeMergeFailurePolicy(mergeRetryResult, waveIdx, orchConfig);
|
|
1485
|
+
execLog("batch", batchState.batchId, `merge retry failure — applying ${policyResult.policy} policy`, policyResult.logDetails);
|
|
1486
|
+
batchState.phase = policyResult.targetPhase;
|
|
1487
|
+
batchState.errors.push(policyResult.errorMessage);
|
|
1488
|
+
persistRuntimeState(policyResult.persistTrigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1489
|
+
onNotify(policyResult.notifyMessage, policyResult.notifyLevel);
|
|
1490
|
+
preserveWorktreesForResume = true;
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
batchState.phase = "executing";
|
|
1495
|
+
persistRuntimeState("merge-retry-complete", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1496
|
+
} else {
|
|
1497
|
+
execLog("resume", batchState.batchId, `wave ${waveIdx + 1}: no tasks to execute (all completed/blocked)`);
|
|
1498
|
+
}
|
|
1279
1499
|
continue;
|
|
1280
1500
|
}
|
|
1281
1501
|
|
|
@@ -1891,21 +2111,18 @@ export async function resumeOrchBatch(
|
|
|
1891
2111
|
// Parity with engine.ts: auto-integrate if configured, else show manual guidance.
|
|
1892
2112
|
// Gate: only run for terminal phases (completed/failed). Paused/stopped batches
|
|
1893
2113
|
// are not yet done — integration would mutate refs prematurely.
|
|
1894
|
-
|
|
2114
|
+
//
|
|
2115
|
+
// TP-043: "supervised" and "auto" integration modes are now owned by the
|
|
2116
|
+
// supervisor agent. Legacy engine fast-forward is removed — supervisor
|
|
2117
|
+
// handles all non-manual integration after batch_complete event.
|
|
1895
2118
|
const mergedTaskCount = batchState.succeededTasks;
|
|
1896
2119
|
const isTerminalPhase = batchState.phase === "completed" || batchState.phase === "failed";
|
|
1897
2120
|
if (isTerminalPhase && !preserveWorktreesForResume && batchState.orchBranch && mergedTaskCount > 0) {
|
|
1898
|
-
if (orchConfig.orchestrator.integration === "auto") {
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
batchState.batchId,
|
|
1904
|
-
"resume",
|
|
1905
|
-
onNotify,
|
|
1906
|
-
);
|
|
1907
|
-
}
|
|
1908
|
-
if (!autoIntegrated) {
|
|
2121
|
+
if (orchConfig.orchestrator.integration === "supervised" || orchConfig.orchestrator.integration === "auto") {
|
|
2122
|
+
// TP-043: Supervisor-managed integration modes. Defer to supervisor.
|
|
2123
|
+
execLog("resume", batchState.batchId, `integration deferred to supervisor (mode: ${orchConfig.orchestrator.integration})`);
|
|
2124
|
+
} else {
|
|
2125
|
+
// Manual mode (default): show integration guidance
|
|
1909
2126
|
onNotify(
|
|
1910
2127
|
ORCH_MESSAGES.orchIntegrationManual(batchState.orchBranch, batchState.baseBranch, mergedTaskCount),
|
|
1911
2128
|
"info",
|
|
@@ -1946,6 +2163,8 @@ export async function resumeOrchBatch(
|
|
|
1946
2163
|
}
|
|
1947
2164
|
|
|
1948
2165
|
|
|
1949
|
-
// attemptAutoIntegration is
|
|
1950
|
-
//
|
|
2166
|
+
// TP-043: attemptAutoIntegration is no longer called from engine.ts or resume.ts.
|
|
2167
|
+
// Supervisor-managed integration ("supervised" and "auto" modes) is handled by
|
|
2168
|
+
// the supervisor agent after batch_complete. The helper remains in merge.ts for
|
|
2169
|
+
// use by the supervisor's integration flow.
|
|
1951
2170
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Settings TUI — interactive configuration viewer and editor.
|
|
3
3
|
*
|
|
4
4
|
* Provides a `/taskplane-settings` command that renders a two-level navigation:
|
|
5
|
-
* 1. Section selector (
|
|
5
|
+
* 1. Section selector (13 sections)
|
|
6
6
|
* 2. Per-section SettingsList with field display, source badges,
|
|
7
7
|
* and inline editing for enum/boolean/string/number fields
|
|
8
8
|
*
|
|
@@ -87,7 +87,7 @@ export interface SectionDef {
|
|
|
87
87
|
// ── Section & Field Definitions ──────────────────────────────────────
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Canonical navigation map —
|
|
90
|
+
* Canonical navigation map — 13 sections.
|
|
91
91
|
* Order matches the Step 1 design in STATUS.md.
|
|
92
92
|
*/
|
|
93
93
|
export const SECTIONS: SectionDef[] = [
|
|
@@ -102,7 +102,7 @@ export const SECTIONS: SectionDef[] = [
|
|
|
102
102
|
// The user-facing spawn mode setting is under Worker (controls /task behavior).
|
|
103
103
|
{ configPath: "orchestrator.orchestrator.tmuxPrefix", label: "Tmux Prefix", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "tmuxPrefix", description: "Prefix for orchestrator tmux sessions" },
|
|
104
104
|
{ configPath: "orchestrator.orchestrator.operatorId", label: "Operator ID", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "operatorId", description: "Operator identifier (empty = auto-detect)" },
|
|
105
|
-
{ configPath: "orchestrator.orchestrator.integration", label: "Integration", control: "toggle", layer: "L1", fieldType: "enum", values: ["manual", "auto"], description: "How completed batches are integrated. manual = user runs /orch-integrate. auto =
|
|
105
|
+
{ configPath: "orchestrator.orchestrator.integration", label: "Integration", control: "toggle", layer: "L1", fieldType: "enum", values: ["manual", "supervised", "auto"], description: "How completed batches are integrated. manual = user runs /orch-integrate. supervised = supervisor proposes plan, asks confirmation. auto = supervisor executes without asking." },
|
|
106
106
|
],
|
|
107
107
|
},
|
|
108
108
|
{
|
|
@@ -149,6 +149,13 @@ export const SECTIONS: SectionDef[] = [
|
|
|
149
149
|
{ configPath: "orchestrator.monitoring.pollInterval", label: "Poll Interval (sec)", control: "input", layer: "L1", fieldType: "number", description: "Poll interval for lane/task monitoring (seconds)" },
|
|
150
150
|
],
|
|
151
151
|
},
|
|
152
|
+
{
|
|
153
|
+
name: "Supervisor",
|
|
154
|
+
fields: [
|
|
155
|
+
{ configPath: "orchestrator.supervisor.model", label: "Supervisor Model", control: "input", layer: "L1+L2", fieldType: "string", prefsKey: "supervisorModel", description: "Supervisor model (empty = inherit session)" },
|
|
156
|
+
{ configPath: "orchestrator.supervisor.autonomy", label: "Autonomy Level", control: "toggle", layer: "L1", fieldType: "enum", values: ["interactive", "supervised", "autonomous"], description: "Recovery action confirmation behavior" },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
152
159
|
{
|
|
153
160
|
name: "Worker",
|
|
154
161
|
fields: [
|