taskplane 0.5.12 → 0.6.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 +1 -1
- package/bin/rpc-wrapper.mjs +777 -0
- package/dashboard/public/app.js +45 -6
- package/dashboard/public/style.css +31 -0
- package/dashboard/server.cjs +326 -1
- package/extensions/task-runner.ts +1111 -30
- package/extensions/taskplane/config-loader.ts +31 -0
- package/extensions/taskplane/config-schema.ts +88 -0
- package/extensions/taskplane/diagnostic-reports.ts +463 -0
- package/extensions/taskplane/diagnostics.ts +323 -0
- package/extensions/taskplane/engine.ts +407 -62
- package/extensions/taskplane/extension.ts +259 -8
- package/extensions/taskplane/index.ts +1 -0
- package/extensions/taskplane/merge.ts +786 -66
- package/extensions/taskplane/messages.ts +594 -2
- package/extensions/taskplane/persistence.ts +342 -19
- package/extensions/taskplane/quality-gate.ts +1033 -0
- package/extensions/taskplane/resume.ts +505 -36
- package/extensions/taskplane/supervisor-primer.md +664 -0
- package/extensions/taskplane/types.ts +534 -6
- package/extensions/taskplane/verification.ts +537 -0
- package/extensions/taskplane/worktree.ts +329 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/references/prompt-template.md +0 -2
- package/templates/agents/task-reviewer.md +54 -2
- package/templates/agents/task-worker.md +8 -4
|
@@ -5,19 +5,21 @@
|
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
|
|
8
|
+
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
8
9
|
import { runDiscovery } from "./discovery.ts";
|
|
9
10
|
import { executeOrchBatch } from "./engine.ts";
|
|
10
11
|
import { computeTransitiveDependents, execLog, executeWave, pollUntilTaskComplete, spawnLaneSession, tmuxHasSession } from "./execution.ts";
|
|
11
12
|
import type { MonitorUpdateCallback } from "./execution.ts";
|
|
12
13
|
import { getCurrentBranch, runGit } from "./git.ts";
|
|
13
14
|
import { attemptAutoIntegration, mergeWaveByRepo } from "./merge.ts";
|
|
14
|
-
import { computeMergeFailurePolicy, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
|
|
15
|
+
import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolicy, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
|
|
16
|
+
import type { CleanupGateRepoFailure } from "./messages.ts";
|
|
15
17
|
import { resolveOperatorId } from "./naming.ts";
|
|
16
|
-
import { deleteBatchState, hasTaskDoneMarker, loadBatchState, persistRuntimeState, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
17
|
-
import { StateFileError } from "./types.ts";
|
|
18
|
+
import { applyPartialProgressToOutcomes, deleteBatchState, hasTaskDoneMarker, loadBatchState, persistRuntimeState, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
19
|
+
import { defaultResilienceState, StateFileError } from "./types.ts";
|
|
18
20
|
import type { AllocatedLane, AllocatedTask, LaneExecutionResult, LaneTaskOutcome, LaneTaskStatus, MergeWaveResult, OrchBatchPhase, OrchBatchRuntimeState, OrchestratorConfig, ParsedTask, PersistedBatchState, PersistedLaneRecord, ReconciledTaskState, ResumeEligibility, ResumePoint, TaskRunnerConfig, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
19
21
|
import { buildDependencyGraph, resolveBaseBranch, resolveRepoRoot } from "./waves.ts";
|
|
20
|
-
import { deleteBranchBestEffort, forceCleanupWorktree, listWorktrees, removeAllWorktrees, removeWorktree, safeResetWorktree } from "./worktree.ts";
|
|
22
|
+
import { deleteBranchBestEffort, forceCleanupWorktree, listWorktrees, preserveFailedLaneProgress, removeAllWorktrees, removeWorktree, safeResetWorktree, sleepSync } from "./worktree.ts";
|
|
21
23
|
|
|
22
24
|
// ── Resume Repo Helpers ──────────────────────────────────────────────
|
|
23
25
|
|
|
@@ -191,20 +193,23 @@ export function collectAllRepoRoots(
|
|
|
191
193
|
* Check whether a persisted batch state is eligible for resume.
|
|
192
194
|
*
|
|
193
195
|
* Resume eligibility matrix:
|
|
194
|
-
* | Phase |
|
|
195
|
-
*
|
|
196
|
-
* | paused | ✅ | Batch was paused (user/merge-failure) |
|
|
197
|
-
* | executing | ✅ | Batch was executing when orchestrator died |
|
|
198
|
-
* | merging | ✅ | Batch was merging when orchestrator died |
|
|
199
|
-
* | stopped | ❌ | Batch was stopped by policy |
|
|
200
|
-
* | failed | ❌ | Batch has terminal failure |
|
|
201
|
-
* | completed | ❌ | Batch already completed |
|
|
202
|
-
* | idle | ❌ | Batch never started execution |
|
|
203
|
-
* | planning | ❌ | Batch was still planning |
|
|
196
|
+
* | Phase | Normal | --force | Reason |
|
|
197
|
+
* |-----------|-----------|-----------|-------------------------------------------|
|
|
198
|
+
* | paused | ✅ | ✅ | Batch was paused (user/merge-failure) |
|
|
199
|
+
* | executing | ✅ | ✅ | Batch was executing when orchestrator died |
|
|
200
|
+
* | merging | ✅ | ✅ | Batch was merging when orchestrator died |
|
|
201
|
+
* | stopped | ❌ | ✅ | Batch was stopped by policy |
|
|
202
|
+
* | failed | ❌ | ✅ | Batch has terminal failure |
|
|
203
|
+
* | completed | ❌ | ❌ | Batch already completed |
|
|
204
|
+
* | idle | ❌ | ❌ | Batch never started execution |
|
|
205
|
+
* | planning | ❌ | ❌ | Batch was still planning |
|
|
204
206
|
*
|
|
205
207
|
* Pure function — no process or filesystem access.
|
|
208
|
+
*
|
|
209
|
+
* @param state - Persisted batch state to check
|
|
210
|
+
* @param force - When true, `stopped` and `failed` phases become eligible
|
|
206
211
|
*/
|
|
207
|
-
export function checkResumeEligibility(state: PersistedBatchState): ResumeEligibility {
|
|
212
|
+
export function checkResumeEligibility(state: PersistedBatchState, force: boolean = false): ResumeEligibility {
|
|
208
213
|
const { phase, batchId } = state;
|
|
209
214
|
|
|
210
215
|
switch (phase) {
|
|
@@ -233,17 +238,33 @@ export function checkResumeEligibility(state: PersistedBatchState): ResumeEligib
|
|
|
233
238
|
};
|
|
234
239
|
|
|
235
240
|
case "stopped":
|
|
241
|
+
if (force) {
|
|
242
|
+
return {
|
|
243
|
+
eligible: true,
|
|
244
|
+
reason: `Batch ${batchId} was stopped by failure policy. Force-resuming (--force).`,
|
|
245
|
+
phase,
|
|
246
|
+
batchId,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
236
249
|
return {
|
|
237
250
|
eligible: false,
|
|
238
|
-
reason: `Batch ${batchId} was stopped by failure policy. Use /orch-abort to clean up
|
|
251
|
+
reason: `Batch ${batchId} was stopped by failure policy. Use --force to resume, or /orch-abort to clean up.`,
|
|
239
252
|
phase,
|
|
240
253
|
batchId,
|
|
241
254
|
};
|
|
242
255
|
|
|
243
256
|
case "failed":
|
|
257
|
+
if (force) {
|
|
258
|
+
return {
|
|
259
|
+
eligible: true,
|
|
260
|
+
reason: `Batch ${batchId} has a terminal failure. Force-resuming (--force).`,
|
|
261
|
+
phase,
|
|
262
|
+
batchId,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
244
265
|
return {
|
|
245
266
|
eligible: false,
|
|
246
|
-
reason: `Batch ${batchId} has a terminal failure. Use /orch-abort to clean up
|
|
267
|
+
reason: `Batch ${batchId} has a terminal failure. Use --force to resume, or /orch-abort to clean up.`,
|
|
247
268
|
phase,
|
|
248
269
|
batchId,
|
|
249
270
|
};
|
|
@@ -251,7 +272,7 @@ export function checkResumeEligibility(state: PersistedBatchState): ResumeEligib
|
|
|
251
272
|
case "completed":
|
|
252
273
|
return {
|
|
253
274
|
eligible: false,
|
|
254
|
-
reason: `Batch ${batchId} already completed. Delete the state file or start a new batch.`,
|
|
275
|
+
reason: `Batch ${batchId} already completed. ${force ? "--force cannot resume a completed batch. " : ""}Delete the state file or start a new batch.`,
|
|
255
276
|
phase,
|
|
256
277
|
batchId,
|
|
257
278
|
};
|
|
@@ -522,6 +543,127 @@ export function computeResumePoint(
|
|
|
522
543
|
}
|
|
523
544
|
|
|
524
545
|
|
|
546
|
+
// ── Pre-Resume Diagnostics ───────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Result of a single diagnostic check.
|
|
550
|
+
*/
|
|
551
|
+
export interface DiagnosticCheckResult {
|
|
552
|
+
/** Short label for the check */
|
|
553
|
+
check: string;
|
|
554
|
+
/** Whether the check passed */
|
|
555
|
+
passed: boolean;
|
|
556
|
+
/** Human-readable detail (reason for failure or confirmation) */
|
|
557
|
+
detail: string;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Aggregate result of pre-resume diagnostics.
|
|
562
|
+
*/
|
|
563
|
+
export interface PreResumeDiagnosticsResult {
|
|
564
|
+
/** Whether all checks passed and resume can proceed */
|
|
565
|
+
passed: boolean;
|
|
566
|
+
/** Individual check results */
|
|
567
|
+
checks: DiagnosticCheckResult[];
|
|
568
|
+
/** Summary message for operator display */
|
|
569
|
+
summary: string;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Run pre-resume diagnostics before allowing a force-resume.
|
|
574
|
+
*
|
|
575
|
+
* Checks performed (per repo in workspace mode):
|
|
576
|
+
* 1. **State coherence:** batch-state.json exists and is loadable
|
|
577
|
+
* 2. **Branch consistency:** orch branch exists in each repo
|
|
578
|
+
* 3. **Worktree health:** persisted lane worktrees are accessible or cleanly absent
|
|
579
|
+
*
|
|
580
|
+
* Pure-ish function — reads filesystem/git state but does not mutate anything.
|
|
581
|
+
*
|
|
582
|
+
* @param persistedState - Loaded batch state
|
|
583
|
+
* @param repoRoot - Default repo root (cwd)
|
|
584
|
+
* @param stateRoot - Root for state files (.pi/)
|
|
585
|
+
* @param workspaceConfig - Workspace configuration (null in repo mode)
|
|
586
|
+
* @returns Diagnostics result with pass/fail and per-check details
|
|
587
|
+
*/
|
|
588
|
+
export function runPreResumeDiagnostics(
|
|
589
|
+
persistedState: PersistedBatchState,
|
|
590
|
+
repoRoot: string,
|
|
591
|
+
stateRoot: string,
|
|
592
|
+
workspaceConfig?: WorkspaceConfig | null,
|
|
593
|
+
): PreResumeDiagnosticsResult {
|
|
594
|
+
const checks: DiagnosticCheckResult[] = [];
|
|
595
|
+
|
|
596
|
+
// 1. State coherence — verify batch-state.json is well-formed
|
|
597
|
+
// (Already loaded by caller, so if we get here the state is valid.)
|
|
598
|
+
checks.push({
|
|
599
|
+
check: "state-coherence",
|
|
600
|
+
passed: true,
|
|
601
|
+
detail: `Batch state loaded successfully (batchId: ${persistedState.batchId}, phase: ${persistedState.phase})`,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// 2. Branch consistency — verify orch branch exists in each repo
|
|
605
|
+
const repoRoots = collectRepoRoots(persistedState, repoRoot, workspaceConfig);
|
|
606
|
+
for (const root of repoRoots) {
|
|
607
|
+
const repoId = resolveRepoIdFromRoot(root, workspaceConfig);
|
|
608
|
+
const label = repoId ? `repo:${repoId}` : "default-repo";
|
|
609
|
+
|
|
610
|
+
if (persistedState.orchBranch) {
|
|
611
|
+
const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${persistedState.orchBranch}`], root);
|
|
612
|
+
if (branchCheck.ok) {
|
|
613
|
+
checks.push({
|
|
614
|
+
check: `branch-consistency:${label}`,
|
|
615
|
+
passed: true,
|
|
616
|
+
detail: `Orch branch "${persistedState.orchBranch}" exists in ${label}`,
|
|
617
|
+
});
|
|
618
|
+
} else {
|
|
619
|
+
checks.push({
|
|
620
|
+
check: `branch-consistency:${label}`,
|
|
621
|
+
passed: false,
|
|
622
|
+
detail: `Orch branch "${persistedState.orchBranch}" not found in ${label}. ` +
|
|
623
|
+
`The branch may have been deleted or the repo is in an inconsistent state.`,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 3. Worktree health — check each persisted lane worktree
|
|
630
|
+
for (const lane of persistedState.lanes) {
|
|
631
|
+
if (!lane.worktreePath) continue;
|
|
632
|
+
|
|
633
|
+
const wtExists = existsSync(lane.worktreePath);
|
|
634
|
+
if (wtExists) {
|
|
635
|
+
// Verify it's a valid git worktree (has .git file/directory)
|
|
636
|
+
const gitMarker = join(lane.worktreePath, ".git");
|
|
637
|
+
const isValidWt = existsSync(gitMarker);
|
|
638
|
+
checks.push({
|
|
639
|
+
check: `worktree-health:lane-${lane.laneNumber}`,
|
|
640
|
+
passed: isValidWt,
|
|
641
|
+
detail: isValidWt
|
|
642
|
+
? `Lane ${lane.laneNumber} worktree exists and has valid .git marker`
|
|
643
|
+
: `Lane ${lane.laneNumber} worktree exists at ${lane.worktreePath} but lacks .git marker (corrupted)`,
|
|
644
|
+
});
|
|
645
|
+
} else {
|
|
646
|
+
// Absent worktree is OK — resume will re-create or skip
|
|
647
|
+
checks.push({
|
|
648
|
+
check: `worktree-health:lane-${lane.laneNumber}`,
|
|
649
|
+
passed: true,
|
|
650
|
+
detail: `Lane ${lane.laneNumber} worktree absent (will be re-created on resume)`,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const failed = checks.filter(c => !c.passed);
|
|
656
|
+
const passed = failed.length === 0;
|
|
657
|
+
|
|
658
|
+
const summary = passed
|
|
659
|
+
? `✅ Pre-resume diagnostics passed (${checks.length} checks)`
|
|
660
|
+
: `❌ Pre-resume diagnostics failed (${failed.length}/${checks.length} checks failed):\n` +
|
|
661
|
+
failed.map(c => ` • ${c.check}: ${c.detail}`).join("\n");
|
|
662
|
+
|
|
663
|
+
return { passed, checks, summary };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
|
|
525
667
|
export async function resumeOrchBatch(
|
|
526
668
|
orchConfig: OrchestratorConfig,
|
|
527
669
|
runnerConfig: TaskRunnerConfig,
|
|
@@ -532,6 +674,7 @@ export async function resumeOrchBatch(
|
|
|
532
674
|
workspaceConfig?: WorkspaceConfig | null,
|
|
533
675
|
workspaceRoot?: string,
|
|
534
676
|
agentRoot?: string,
|
|
677
|
+
force: boolean = false,
|
|
535
678
|
): Promise<void> {
|
|
536
679
|
const repoRoot = cwd;
|
|
537
680
|
// State files (.pi/batch-state.json, lane-state, etc.) belong in the workspace root,
|
|
@@ -563,7 +706,7 @@ export async function resumeOrchBatch(
|
|
|
563
706
|
}
|
|
564
707
|
|
|
565
708
|
// ── 2. Check eligibility ─────────────────────────────────────
|
|
566
|
-
const eligibility = checkResumeEligibility(persistedState);
|
|
709
|
+
const eligibility = checkResumeEligibility(persistedState, force);
|
|
567
710
|
if (!eligibility.eligible) {
|
|
568
711
|
onNotify(
|
|
569
712
|
ORCH_MESSAGES.resumePhaseNotResumable(persistedState.batchId, persistedState.phase, eligibility.reason),
|
|
@@ -572,6 +715,37 @@ export async function resumeOrchBatch(
|
|
|
572
715
|
return;
|
|
573
716
|
}
|
|
574
717
|
|
|
718
|
+
// ── 2b. Force-resume: pre-resume diagnostics & state mutation ──
|
|
719
|
+
const isForceResume = force && (persistedState.phase === "stopped" || persistedState.phase === "failed");
|
|
720
|
+
if (isForceResume) {
|
|
721
|
+
onNotify(
|
|
722
|
+
ORCH_MESSAGES.forceResumeStarting(persistedState.batchId, persistedState.phase),
|
|
723
|
+
"warning",
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
// Run pre-resume diagnostics before allowing force-resume
|
|
727
|
+
const diagnostics = runPreResumeDiagnostics(persistedState, repoRoot, stateRoot, workspaceConfig);
|
|
728
|
+
onNotify(diagnostics.summary, diagnostics.passed ? "info" : "error");
|
|
729
|
+
|
|
730
|
+
if (!diagnostics.passed) {
|
|
731
|
+
onNotify(
|
|
732
|
+
ORCH_MESSAGES.forceResumeDiagnosticsFailed(persistedState.batchId),
|
|
733
|
+
"error",
|
|
734
|
+
);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Record force intent in resilience state
|
|
739
|
+
persistedState.resilience.resumeForced = true;
|
|
740
|
+
|
|
741
|
+
// Reset phase to paused so normal resume flow can proceed
|
|
742
|
+
execLog("resume", persistedState.batchId, `force-resume: phase ${persistedState.phase} → paused`, {
|
|
743
|
+
diagnosticChecks: diagnostics.checks.length,
|
|
744
|
+
diagnosticsPassed: diagnostics.passed,
|
|
745
|
+
});
|
|
746
|
+
persistedState.phase = "paused";
|
|
747
|
+
}
|
|
748
|
+
|
|
575
749
|
onNotify(
|
|
576
750
|
ORCH_MESSAGES.resumeStarting(persistedState.batchId, persistedState.phase),
|
|
577
751
|
"info",
|
|
@@ -702,6 +876,14 @@ export async function resumeOrchBatch(
|
|
|
702
876
|
batchState.currentWaveIndex = resumePoint.resumeWaveIndex;
|
|
703
877
|
batchState.waveResults = [];
|
|
704
878
|
|
|
879
|
+
// v3: Carry forward resilience and diagnostics from persisted state
|
|
880
|
+
batchState.resilience = persistedState.resilience;
|
|
881
|
+
batchState.diagnostics = persistedState.diagnostics;
|
|
882
|
+
// Carry forward unknown fields for roundtrip preservation
|
|
883
|
+
if (persistedState._extraFields) {
|
|
884
|
+
batchState._extraFields = persistedState._extraFields;
|
|
885
|
+
}
|
|
886
|
+
|
|
705
887
|
// ── 7. Re-run discovery for ParsedTask metadata ──────────────
|
|
706
888
|
// We need fresh ParsedTask data (taskFolder, promptPath) for execution.
|
|
707
889
|
// Use "all" to discover all areas.
|
|
@@ -953,6 +1135,7 @@ export async function resumeOrchBatch(
|
|
|
953
1135
|
workspaceConfig,
|
|
954
1136
|
stateRoot,
|
|
955
1137
|
agentRoot,
|
|
1138
|
+
runnerConfig.testing_commands,
|
|
956
1139
|
);
|
|
957
1140
|
|
|
958
1141
|
if (reExecMergeResult.status === "succeeded") {
|
|
@@ -962,8 +1145,9 @@ export async function resumeOrchBatch(
|
|
|
962
1145
|
);
|
|
963
1146
|
|
|
964
1147
|
// Clean up merged branches (resolve per-lane repo root for workspace mode)
|
|
1148
|
+
// TP-032 R006-3: Exclude verification_new_failure lanes from branch cleanup
|
|
965
1149
|
for (const lr of reExecMergeResult.laneResults) {
|
|
966
|
-
if (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED") {
|
|
1150
|
+
if (!lr.error && (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED")) {
|
|
967
1151
|
const laneRepoRoot = resolveRepoRoot(lr.repoId, repoRoot, workspaceConfig);
|
|
968
1152
|
deleteBranchBestEffort(lr.sourceBranch, laneRepoRoot);
|
|
969
1153
|
}
|
|
@@ -1024,6 +1208,11 @@ export async function resumeOrchBatch(
|
|
|
1024
1208
|
: persistedTask?.exitReason ?? "",
|
|
1025
1209
|
sessionName: persistedTask?.sessionName ?? "",
|
|
1026
1210
|
doneFileFound: status === "succeeded" ? true : task.doneFileFound,
|
|
1211
|
+
// Carry forward partial progress from persisted state (TP-028)
|
|
1212
|
+
partialProgressCommits: persistedTask?.partialProgressCommits,
|
|
1213
|
+
partialProgressBranch: persistedTask?.partialProgressBranch,
|
|
1214
|
+
// v3: Carry forward exit diagnostic from persisted state (TP-030)
|
|
1215
|
+
exitDiagnostic: persistedTask?.exitDiagnostic,
|
|
1027
1216
|
});
|
|
1028
1217
|
}
|
|
1029
1218
|
|
|
@@ -1232,20 +1421,23 @@ export async function resumeOrchBatch(
|
|
|
1232
1421
|
workspaceConfig,
|
|
1233
1422
|
stateRoot,
|
|
1234
1423
|
agentRoot,
|
|
1424
|
+
runnerConfig.testing_commands,
|
|
1235
1425
|
);
|
|
1236
1426
|
batchState.mergeResults.push(mergeResult);
|
|
1237
1427
|
|
|
1238
1428
|
// Emit per-lane merge notifications
|
|
1239
1429
|
for (const lr of mergeResult.laneResults) {
|
|
1240
1430
|
const durationSec = Math.round(lr.durationMs / 1000);
|
|
1241
|
-
|
|
1431
|
+
// TP-032 R006-3: Check lr.error first — verification_new_failure lanes
|
|
1432
|
+
// have error set even though lr.result.status may be SUCCESS/CONFLICT_RESOLVED.
|
|
1433
|
+
if (lr.error) {
|
|
1434
|
+
onNotify(ORCH_MESSAGES.orchMergeLaneFailed(lr.laneNumber, lr.error), "error");
|
|
1435
|
+
} else if (lr.result?.status === "SUCCESS") {
|
|
1242
1436
|
onNotify(ORCH_MESSAGES.orchMergeLaneSuccess(lr.laneNumber, lr.result.merge_commit, durationSec), "info");
|
|
1243
1437
|
} else if (lr.result?.status === "CONFLICT_RESOLVED") {
|
|
1244
1438
|
onNotify(ORCH_MESSAGES.orchMergeLaneConflictResolved(lr.laneNumber, lr.result.conflicts.length, durationSec), "info");
|
|
1245
1439
|
} else if (lr.result?.status === "CONFLICT_UNRESOLVED" || lr.result?.status === "BUILD_FAILURE") {
|
|
1246
|
-
onNotify(ORCH_MESSAGES.orchMergeLaneFailed(lr.laneNumber, lr.
|
|
1247
|
-
} else if (lr.error) {
|
|
1248
|
-
onNotify(ORCH_MESSAGES.orchMergeLaneFailed(lr.laneNumber, lr.error), "error");
|
|
1440
|
+
onNotify(ORCH_MESSAGES.orchMergeLaneFailed(lr.laneNumber, lr.result.status), "error");
|
|
1249
1441
|
}
|
|
1250
1442
|
}
|
|
1251
1443
|
|
|
@@ -1257,8 +1449,9 @@ export async function resumeOrchBatch(
|
|
|
1257
1449
|
mergeResult = { ...mergeResult, status: "partial", failedLane: mixedOutcomeLanes[0].laneNumber, failureReason };
|
|
1258
1450
|
}
|
|
1259
1451
|
|
|
1452
|
+
// TP-032 R006-3: Exclude verification_new_failure lanes from success count
|
|
1260
1453
|
const mergedCount = mergeResult.laneResults.filter(
|
|
1261
|
-
r => r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED",
|
|
1454
|
+
r => !r.error && (r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED"),
|
|
1262
1455
|
).length;
|
|
1263
1456
|
const mergeTotalSec = Math.round(mergeResult.totalDurationMs / 1000);
|
|
1264
1457
|
|
|
@@ -1304,24 +1497,139 @@ export async function resumeOrchBatch(
|
|
|
1304
1497
|
onNotify(ORCH_MESSAGES.orchMergeSkipped(waveIdx + 1), "info");
|
|
1305
1498
|
}
|
|
1306
1499
|
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1500
|
+
// ── TP-033: Safe-stop on rollback failure ─────────────────
|
|
1501
|
+
// When a verification rollback failed, force paused regardless of
|
|
1502
|
+
// on_merge_failure policy. The merge worktree and temp branch are
|
|
1503
|
+
// preserved for manual recovery using commands in the transaction record.
|
|
1504
|
+
if (mergeResult?.rollbackFailed) {
|
|
1505
|
+
// TP-033 R004-2: Include persistence error warning when transaction
|
|
1506
|
+
// record files may be missing, so operator knows to inspect manually
|
|
1507
|
+
const hasPersistErrors = mergeResult.persistenceErrors && mergeResult.persistenceErrors.length > 0;
|
|
1508
|
+
const persistWarning = hasPersistErrors
|
|
1509
|
+
? ` WARNING: ${mergeResult.persistenceErrors!.length} transaction record(s) failed to persist — recovery file(s) may be missing.`
|
|
1510
|
+
: "";
|
|
1511
|
+
|
|
1512
|
+
execLog("batch", batchState.batchId, "SAFE-STOP: verification rollback failed — forcing paused regardless of policy", {
|
|
1513
|
+
waveIndex: waveIdx,
|
|
1514
|
+
configPolicy: orchConfig.failure.on_merge_failure,
|
|
1515
|
+
...(hasPersistErrors ? { persistenceErrors: mergeResult.persistenceErrors } : {}),
|
|
1516
|
+
});
|
|
1312
1517
|
|
|
1313
|
-
batchState.phase =
|
|
1314
|
-
batchState.errors.push(
|
|
1315
|
-
|
|
1316
|
-
|
|
1518
|
+
batchState.phase = "paused";
|
|
1519
|
+
batchState.errors.push(
|
|
1520
|
+
`Safe-stop at wave ${waveIdx + 1}: verification rollback failed. ` +
|
|
1521
|
+
`Merge worktree and temp branch preserved for recovery. ` +
|
|
1522
|
+
`Check transaction records in .pi/verification/ for recovery commands.` +
|
|
1523
|
+
persistWarning
|
|
1524
|
+
);
|
|
1525
|
+
persistRuntimeState("merge-rollback-safe-stop", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1526
|
+
onNotify(
|
|
1527
|
+
`🛑 Safe-stop: verification rollback failed at wave ${waveIdx + 1}. ` +
|
|
1528
|
+
`Batch force-paused. Merge worktree preserved for manual recovery. ` +
|
|
1529
|
+
`See .pi/verification/ transaction records for recovery commands.` +
|
|
1530
|
+
persistWarning,
|
|
1531
|
+
"error",
|
|
1532
|
+
);
|
|
1317
1533
|
preserveWorktreesForResume = true;
|
|
1318
1534
|
break;
|
|
1319
1535
|
}
|
|
1320
1536
|
|
|
1537
|
+
// Handle merge failure — TP-033 Step 2 (R006): Retry policy matrix via shared applyMergeRetryLoop.
|
|
1538
|
+
// Uses the same centralized loop as engine.ts for guaranteed parity.
|
|
1539
|
+
if (mergeResult && (mergeResult.status === "failed" || mergeResult.status === "partial")) {
|
|
1540
|
+
// Initialize resilience state if not yet present
|
|
1541
|
+
if (!batchState.resilience) {
|
|
1542
|
+
batchState.resilience = defaultResilienceState();
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const retryOutcome = applyMergeRetryLoop(
|
|
1546
|
+
mergeResult,
|
|
1547
|
+
waveIdx,
|
|
1548
|
+
batchState.resilience.retryCountByScope,
|
|
1549
|
+
{
|
|
1550
|
+
performMerge: () => {
|
|
1551
|
+
batchState.phase = "merging";
|
|
1552
|
+
return mergeWaveByRepo(
|
|
1553
|
+
waveResult.allocatedLanes,
|
|
1554
|
+
waveResult,
|
|
1555
|
+
waveIdx + 1,
|
|
1556
|
+
orchConfig,
|
|
1557
|
+
repoRoot,
|
|
1558
|
+
batchState.batchId,
|
|
1559
|
+
batchState.orchBranch,
|
|
1560
|
+
workspaceConfig,
|
|
1561
|
+
stateRoot,
|
|
1562
|
+
agentRoot,
|
|
1563
|
+
runnerConfig.testing_commands,
|
|
1564
|
+
);
|
|
1565
|
+
},
|
|
1566
|
+
persist: (trigger) => persistRuntimeState(trigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot),
|
|
1567
|
+
log: (message, details) => execLog("batch", batchState.batchId, message, details),
|
|
1568
|
+
notify: (message, level) => onNotify(message, level),
|
|
1569
|
+
updateMergeResult: (result) => {
|
|
1570
|
+
mergeResult = result;
|
|
1571
|
+
batchState.mergeResults[batchState.mergeResults.length - 1] = result;
|
|
1572
|
+
},
|
|
1573
|
+
sleep: sleepSync,
|
|
1574
|
+
},
|
|
1575
|
+
);
|
|
1576
|
+
|
|
1577
|
+
if (retryOutcome.kind === "retry_succeeded") {
|
|
1578
|
+
mergeResult = retryOutcome.mergeResult;
|
|
1579
|
+
batchState.phase = "executing";
|
|
1580
|
+
persistRuntimeState("merge-retry-succeeded", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1581
|
+
// Fall through to normal post-merge flow
|
|
1582
|
+
} else if (retryOutcome.kind === "safe_stop") {
|
|
1583
|
+
mergeResult = retryOutcome.mergeResult;
|
|
1584
|
+
batchState.phase = "paused";
|
|
1585
|
+
batchState.errors.push(retryOutcome.errorMessage);
|
|
1586
|
+
persistRuntimeState("merge-rollback-safe-stop", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1587
|
+
onNotify(retryOutcome.notifyMessage, "error");
|
|
1588
|
+
preserveWorktreesForResume = true;
|
|
1589
|
+
break;
|
|
1590
|
+
} else if (retryOutcome.kind === "exhausted") {
|
|
1591
|
+
// TP-033 R006-2: Force paused regardless of on_merge_failure config.
|
|
1592
|
+
mergeResult = retryOutcome.mergeResult;
|
|
1593
|
+
const exhaustionMsg = retryOutcome.errorMessage +
|
|
1594
|
+
` [${retryOutcome.classification ?? "unknown"} ${retryOutcome.lastDecision.currentAttempt}/${retryOutcome.lastDecision.maxAttempts}, scope=${retryOutcome.scopeKey}]`;
|
|
1595
|
+
|
|
1596
|
+
execLog("batch", batchState.batchId, `merge retry exhausted — forcing paused`, {
|
|
1597
|
+
classification: retryOutcome.classification,
|
|
1598
|
+
scopeKey: retryOutcome.scopeKey,
|
|
1599
|
+
attempts: retryOutcome.lastDecision.currentAttempt,
|
|
1600
|
+
maxAttempts: retryOutcome.lastDecision.maxAttempts,
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
batchState.phase = "paused";
|
|
1604
|
+
batchState.errors.push(exhaustionMsg);
|
|
1605
|
+
persistRuntimeState("merge-retry-exhausted", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1606
|
+
onNotify(retryOutcome.notifyMessage, "error");
|
|
1607
|
+
preserveWorktreesForResume = true;
|
|
1608
|
+
break;
|
|
1609
|
+
} else {
|
|
1610
|
+
// kind === "no_retry": fall through to standard on_merge_failure policy
|
|
1611
|
+
mergeResult = retryOutcome.mergeResult;
|
|
1612
|
+
const policyResult = computeMergeFailurePolicy(mergeResult, waveIdx, orchConfig);
|
|
1613
|
+
const classNote = retryOutcome.classification
|
|
1614
|
+
? ` [not retriable: ${retryOutcome.classification}, scope=${retryOutcome.scopeKey}]`
|
|
1615
|
+
: "";
|
|
1616
|
+
|
|
1617
|
+
execLog("batch", batchState.batchId, `merge failure — applying ${policyResult.policy} policy${classNote}`, policyResult.logDetails);
|
|
1618
|
+
|
|
1619
|
+
batchState.phase = policyResult.targetPhase;
|
|
1620
|
+
batchState.errors.push(policyResult.errorMessage + classNote);
|
|
1621
|
+
persistRuntimeState(policyResult.persistTrigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1622
|
+
onNotify(policyResult.notifyMessage + classNote, policyResult.notifyLevel);
|
|
1623
|
+
preserveWorktreesForResume = true;
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1321
1628
|
// Post-merge: reset worktrees for next wave
|
|
1629
|
+
// TP-032 R006-3: Exclude verification_new_failure lanes from branch cleanup
|
|
1322
1630
|
if (mergeResult && mergeResult.status === "succeeded") {
|
|
1323
1631
|
for (const lr of mergeResult.laneResults) {
|
|
1324
|
-
if (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED") {
|
|
1632
|
+
if (!lr.error && (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED")) {
|
|
1325
1633
|
const laneRepoRoot = resolveRepoRoot(lr.repoId, repoRoot, workspaceConfig);
|
|
1326
1634
|
const ancestorCheck = runGit(["merge-base", "--is-ancestor", lr.sourceBranch, lr.targetBranch], laneRepoRoot);
|
|
1327
1635
|
if (ancestorCheck.ok) {
|
|
@@ -1331,9 +1639,58 @@ export async function resumeOrchBatch(
|
|
|
1331
1639
|
}
|
|
1332
1640
|
}
|
|
1333
1641
|
|
|
1642
|
+
// ── TP-028: Preserve partial progress before inter-wave reset ──
|
|
1643
|
+
// Hoisted outside the if-block so unsafeBranches is accessible to the
|
|
1644
|
+
// reset loop below — both blocks share the same guard condition.
|
|
1645
|
+
let ppUnsafeBranches = new Set<string>();
|
|
1646
|
+
if (waveIdx < persistedState.wavePlan.length - 1 && !batchState.pauseSignal.paused) {
|
|
1647
|
+
const ppOpId = resolveOperatorId(orchConfig);
|
|
1648
|
+
const ppResult = preserveFailedLaneProgress(
|
|
1649
|
+
latestAllocatedLanes,
|
|
1650
|
+
allTaskOutcomes,
|
|
1651
|
+
ppOpId,
|
|
1652
|
+
batchState.batchId,
|
|
1653
|
+
(repoId) => {
|
|
1654
|
+
const perRepoRoot = resolveRepoRoot(repoId, repoRoot, workspaceConfig);
|
|
1655
|
+
let targetBranch = batchState.orchBranch;
|
|
1656
|
+
if (repoId && perRepoRoot !== repoRoot) {
|
|
1657
|
+
try {
|
|
1658
|
+
targetBranch = resolveBaseBranch(repoId, perRepoRoot, batchState.orchBranch, workspaceConfig);
|
|
1659
|
+
} catch { /* fall back to orchBranch */ }
|
|
1660
|
+
}
|
|
1661
|
+
return { repoRoot: perRepoRoot, targetBranch };
|
|
1662
|
+
},
|
|
1663
|
+
);
|
|
1664
|
+
ppUnsafeBranches = ppResult.unsafeBranches;
|
|
1665
|
+
if (ppResult.results.some(r => r.saved)) {
|
|
1666
|
+
execLog("batch", batchState.batchId,
|
|
1667
|
+
`preserved partial progress for ${ppResult.results.filter(r => r.saved).length} failed task(s) before inter-wave reset`);
|
|
1668
|
+
}
|
|
1669
|
+
// Log per-task warnings for failed preservation attempts
|
|
1670
|
+
for (const r of ppResult.results) {
|
|
1671
|
+
if (!r.saved && (r.commitCount > 0 || r.error)) {
|
|
1672
|
+
execLog("batch", batchState.batchId,
|
|
1673
|
+
`WARNING: Failed to preserve partial progress for task ${r.taskId} ` +
|
|
1674
|
+
`(${r.commitCount} commit(s) at risk on lane branch)`,
|
|
1675
|
+
{ taskId: r.taskId, commitCount: r.commitCount, error: r.error ?? "unknown" });
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (ppUnsafeBranches.size > 0) {
|
|
1679
|
+
execLog("batch", batchState.batchId,
|
|
1680
|
+
`WARNING: ${ppUnsafeBranches.size} lane branch(es) could not be preserved — skipping reset for those lanes to prevent commit loss`,
|
|
1681
|
+
{ unsafeBranches: [...ppUnsafeBranches] });
|
|
1682
|
+
}
|
|
1683
|
+
// TP-028: Stamp task outcomes with partial progress data for persistence
|
|
1684
|
+
applyPartialProgressToOutcomes(ppResult, allTaskOutcomes);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1334
1687
|
if (waveIdx < persistedState.wavePlan.length - 1 && !batchState.pauseSignal.paused) {
|
|
1335
1688
|
const wtPrefix = orchConfig.orchestrator.worktree_prefix;
|
|
1336
1689
|
const resetOpId = resolveOperatorId(orchConfig);
|
|
1690
|
+
// TP-029 R006: Track worktrees that failed reset AND removal
|
|
1691
|
+
// so the cleanup gate only fires on true stale state, not
|
|
1692
|
+
// successfully-reset reusable worktrees. (Parity with engine.ts)
|
|
1693
|
+
const failedRemovalWorktrees = new Map<string, { repoId: string | undefined; paths: string[] }>();
|
|
1337
1694
|
|
|
1338
1695
|
// Use encounteredRepoRoots which includes both persisted lanes
|
|
1339
1696
|
// AND newly allocated lanes from resumed waves, ensuring repos
|
|
@@ -1357,21 +1714,124 @@ export async function resumeOrchBatch(
|
|
|
1357
1714
|
}
|
|
1358
1715
|
}
|
|
1359
1716
|
for (const wt of existingWorktrees) {
|
|
1717
|
+
// TP-028: Skip reset for worktrees whose lane branch has
|
|
1718
|
+
// unsaved partial progress (preservation failed with commits)
|
|
1719
|
+
if (ppUnsafeBranches.has(wt.branch)) {
|
|
1720
|
+
execLog("batch", batchState.batchId,
|
|
1721
|
+
`skipping worktree reset for lane ${wt.laneNumber} — branch "${wt.branch}" has unsaved partial progress`,
|
|
1722
|
+
{ path: wt.path, branch: wt.branch });
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1360
1726
|
const resetResult = safeResetWorktree(wt, targetBranch, perRepoRoot);
|
|
1361
1727
|
if (!resetResult.success) {
|
|
1362
1728
|
try {
|
|
1363
1729
|
removeWorktree(wt, perRepoRoot);
|
|
1364
1730
|
} catch {
|
|
1365
1731
|
forceCleanupWorktree(wt, perRepoRoot, batchState.batchId);
|
|
1732
|
+
// Track this worktree for the cleanup gate — it may still be registered
|
|
1733
|
+
const perRepoId = perRepoRoot === repoRoot
|
|
1734
|
+
? undefined
|
|
1735
|
+
: resolveRepoIdFromRoot(perRepoRoot, workspaceConfig);
|
|
1736
|
+
if (!failedRemovalWorktrees.has(perRepoRoot)) {
|
|
1737
|
+
failedRemovalWorktrees.set(perRepoRoot, { repoId: perRepoId, paths: [] });
|
|
1738
|
+
}
|
|
1739
|
+
failedRemovalWorktrees.get(perRepoRoot)!.paths.push(wt.path);
|
|
1366
1740
|
}
|
|
1367
1741
|
}
|
|
1368
1742
|
}
|
|
1369
1743
|
}
|
|
1370
1744
|
}
|
|
1745
|
+
|
|
1746
|
+
// ── TP-029: Post-merge cleanup gate (parity with engine.ts) ──
|
|
1747
|
+
// Only gate on worktrees that the reset loop tried and failed
|
|
1748
|
+
// to remove. Successfully-reset reusable worktrees are expected
|
|
1749
|
+
// to remain registered — they will be reused in the next wave.
|
|
1750
|
+
// For each failed-removal worktree, verify it is still registered
|
|
1751
|
+
// before classifying it as truly stale.
|
|
1752
|
+
const cleanupGateFailures: CleanupGateRepoFailure[] = [];
|
|
1753
|
+
if (failedRemovalWorktrees.size > 0) {
|
|
1754
|
+
for (const [perRepoRoot, { repoId: perRepoId, paths: failedPaths }] of failedRemovalWorktrees) {
|
|
1755
|
+
const remaining = listWorktrees(wtPrefix, perRepoRoot, resetOpId, batchState.batchId);
|
|
1756
|
+
const remainingPaths = new Set(remaining.map(wt => wt.path));
|
|
1757
|
+
// Only report worktrees that were targeted for removal but are still registered
|
|
1758
|
+
const stale = failedPaths.filter(p => remainingPaths.has(p));
|
|
1759
|
+
if (stale.length > 0) {
|
|
1760
|
+
cleanupGateFailures.push({
|
|
1761
|
+
repoRoot: perRepoRoot,
|
|
1762
|
+
repoId: perRepoId,
|
|
1763
|
+
staleWorktrees: stale,
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (cleanupGateFailures.length > 0) {
|
|
1770
|
+
const gatePolicyResult = computeCleanupGatePolicy(waveIdx, cleanupGateFailures);
|
|
1771
|
+
|
|
1772
|
+
execLog("batch", batchState.batchId, `cleanup gate failed — pausing batch`, gatePolicyResult.logDetails);
|
|
1773
|
+
|
|
1774
|
+
batchState.phase = gatePolicyResult.targetPhase;
|
|
1775
|
+
batchState.errors.push(gatePolicyResult.errorMessage);
|
|
1776
|
+
persistRuntimeState(gatePolicyResult.persistTrigger, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1777
|
+
onNotify(gatePolicyResult.notifyMessage, gatePolicyResult.notifyLevel);
|
|
1778
|
+
preserveWorktreesForResume = true;
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1371
1781
|
}
|
|
1372
1782
|
}
|
|
1373
1783
|
|
|
1784
|
+
// ── Pre-cleanup: Determine if worktrees should be preserved ──
|
|
1785
|
+
// TP-031 (R006): Parity with engine.ts — this check MUST run before cleanup
|
|
1786
|
+
// so that worktrees survive when failedTasks > 0. Without this, cleanup
|
|
1787
|
+
// deletes worktrees before the batch is marked "paused", breaking resumability.
|
|
1788
|
+
if (!preserveWorktreesForResume &&
|
|
1789
|
+
((batchState.phase as OrchBatchPhase) === "executing" || (batchState.phase as OrchBatchPhase) === "merging") &&
|
|
1790
|
+
batchState.failedTasks > 0) {
|
|
1791
|
+
preserveWorktreesForResume = true;
|
|
1792
|
+
execLog("resume", batchState.batchId, "pre-cleanup: failedTasks > 0 detected, preserving worktrees for resume");
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1374
1795
|
// ── 11. Cleanup and terminal state ───────────────────────────
|
|
1796
|
+
|
|
1797
|
+
// ── TP-028: Preserve partial progress before terminal cleanup ──
|
|
1798
|
+
if (!preserveWorktreesForResume) {
|
|
1799
|
+
const ppOpId = resolveOperatorId(orchConfig);
|
|
1800
|
+
const ppResult = preserveFailedLaneProgress(
|
|
1801
|
+
latestAllocatedLanes,
|
|
1802
|
+
allTaskOutcomes,
|
|
1803
|
+
ppOpId,
|
|
1804
|
+
batchState.batchId,
|
|
1805
|
+
(repoId) => {
|
|
1806
|
+
const perRepoRoot = resolveRepoRoot(repoId, repoRoot, workspaceConfig);
|
|
1807
|
+
let targetBranch = batchState.orchBranch;
|
|
1808
|
+
if (repoId && perRepoRoot !== repoRoot) {
|
|
1809
|
+
try {
|
|
1810
|
+
targetBranch = resolveBaseBranch(repoId, perRepoRoot, batchState.orchBranch, workspaceConfig);
|
|
1811
|
+
} catch { /* fall back to orchBranch */ }
|
|
1812
|
+
}
|
|
1813
|
+
return { repoRoot: perRepoRoot, targetBranch };
|
|
1814
|
+
},
|
|
1815
|
+
);
|
|
1816
|
+
if (ppResult.results.some(r => r.saved)) {
|
|
1817
|
+
execLog("batch", batchState.batchId,
|
|
1818
|
+
`preserved partial progress for ${ppResult.results.filter(r => r.saved).length} failed task(s) before terminal cleanup`);
|
|
1819
|
+
}
|
|
1820
|
+
// Log warnings for failed preservation attempts — at terminal cleanup
|
|
1821
|
+
// we cannot skip deletion (batch is ending), but operators need to know
|
|
1822
|
+
// that commits may become unreachable via reflog only.
|
|
1823
|
+
for (const r of ppResult.results) {
|
|
1824
|
+
if (!r.saved && (r.commitCount > 0 || r.error)) {
|
|
1825
|
+
execLog("batch", batchState.batchId,
|
|
1826
|
+
`WARNING: Failed to preserve partial progress for task ${r.taskId} ` +
|
|
1827
|
+
`(${r.commitCount} commit(s) may become unreachable after cleanup)`,
|
|
1828
|
+
{ taskId: r.taskId, commitCount: r.commitCount, error: r.error ?? "unknown" });
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
// TP-028: Stamp task outcomes with partial progress data for persistence
|
|
1832
|
+
applyPartialProgressToOutcomes(ppResult, allTaskOutcomes);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1375
1835
|
if (!preserveWorktreesForResume) {
|
|
1376
1836
|
const wtPrefix = orchConfig.orchestrator.worktree_prefix;
|
|
1377
1837
|
const cleanupOpId = resolveOperatorId(orchConfig);
|
|
@@ -1416,7 +1876,12 @@ export async function resumeOrchBatch(
|
|
|
1416
1876
|
|
|
1417
1877
|
if ((batchState.phase as OrchBatchPhase) === "executing" || (batchState.phase as OrchBatchPhase) === "merging") {
|
|
1418
1878
|
if (batchState.failedTasks > 0) {
|
|
1419
|
-
|
|
1879
|
+
// TP-031: Parity with engine.ts — default to "paused" so the batch is
|
|
1880
|
+
// resumable without --force. "failed" is reserved for unrecoverable
|
|
1881
|
+
// invariant violations after retry exhaustion.
|
|
1882
|
+
// NOTE: preserveWorktreesForResume was already set pre-cleanup to ensure
|
|
1883
|
+
// worktrees survive; this just sets the phase for state persistence.
|
|
1884
|
+
batchState.phase = "paused";
|
|
1420
1885
|
} else {
|
|
1421
1886
|
batchState.phase = "completed";
|
|
1422
1887
|
}
|
|
@@ -1450,6 +1915,10 @@ export async function resumeOrchBatch(
|
|
|
1450
1915
|
|
|
1451
1916
|
persistRuntimeState("batch-terminal", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1452
1917
|
|
|
1918
|
+
// ── TP-031: Emit diagnostic reports (JSONL + markdown) ──
|
|
1919
|
+
// Non-fatal: errors are logged but never crash batch finalization.
|
|
1920
|
+
emitDiagnosticReports(assembleDiagnosticInput(orchConfig, batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, stateRoot));
|
|
1921
|
+
|
|
1453
1922
|
if (batchState.phase === "paused" || batchState.phase === "stopped") {
|
|
1454
1923
|
execLog("resume", batchState.batchId, "resumed batch ended in non-terminal state", { phase: batchState.phase });
|
|
1455
1924
|
} else {
|