gsd-pi 2.81.0-dev.3cddbbba2 → 2.81.0-dev.72a81bdf3
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +100 -95
- package/dist/resources/extensions/gsd/auto-recovery.js +6 -181
- package/dist/resources/extensions/gsd/auto.js +6 -3
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +2 -5
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +9 -0
- package/dist/resources/extensions/gsd/gsd-db.js +7 -23
- package/dist/resources/extensions/gsd/markdown-renderer.js +0 -95
- package/dist/resources/extensions/gsd/recovery-classification.js +15 -1
- package/dist/resources/extensions/gsd/session-lock.js +40 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +131 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +247 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +50 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +87 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +50 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +124 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +32 -0
- package/dist/resources/extensions/gsd/state-reconciliation/errors.js +41 -0
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +99 -0
- package/dist/resources/extensions/gsd/state-reconciliation/registry.js +24 -0
- package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +43 -0
- package/dist/resources/extensions/gsd/state-reconciliation/types.js +3 -0
- package/dist/resources/extensions/gsd/state-reconciliation.js +5 -26
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +25 -17
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -209
- package/src/resources/extensions/gsd/auto.ts +7 -3
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +2 -5
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +12 -0
- package/src/resources/extensions/gsd/gsd-db.ts +7 -23
- package/src/resources/extensions/gsd/markdown-renderer.ts +4 -95
- package/src/resources/extensions/gsd/recovery-classification.ts +18 -1
- package/src/resources/extensions/gsd/session-lock.ts +41 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +172 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +337 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +69 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +109 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +68 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +185 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +46 -0
- package/src/resources/extensions/gsd/state-reconciliation/errors.ts +67 -0
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +142 -0
- package/src/resources/extensions/gsd/state-reconciliation/registry.ts +27 -0
- package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +60 -0
- package/src/resources/extensions/gsd/state-reconciliation/types.ts +83 -0
- package/src/resources/extensions/gsd/state-reconciliation.ts +21 -53
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +81 -10
- package/src/resources/extensions/gsd/tests/integration/integration-proof.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +952 -0
- /package/dist/web/standalone/.next/static/{F5x9E6H9k_52fjqyql93y → rIkMv4YSNlfSeqmGqWVns}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{F5x9E6H9k_52fjqyql93y → rIkMv4YSNlfSeqmGqWVns}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
cec78fe13fe69412
|
|
@@ -31,6 +31,7 @@ import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "
|
|
|
31
31
|
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
|
|
32
32
|
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
|
|
33
33
|
import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
|
|
34
|
+
import { reconcileBeforeSpawn } from "../state-reconciliation.js";
|
|
34
35
|
import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
|
|
35
36
|
import { resolveUokFlags } from "../uok/flags.js";
|
|
36
37
|
import { UokGateRunner } from "../uok/gate-runner.js";
|
|
@@ -647,6 +648,13 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
647
648
|
eligibleSlices: eligible.map(e => e.id),
|
|
648
649
|
});
|
|
649
650
|
ctx.ui.notify(`Slice-parallel: dispatching ${eligible.length} eligible slices for ${mid}.`, "info");
|
|
651
|
+
// ADR-017 #5707: reconcile before spawning so each worker doesn't
|
|
652
|
+
// independently race on the same drift. Failure aborts the spawn.
|
|
653
|
+
const spawnGate = await reconcileBeforeSpawn(s.basePath);
|
|
654
|
+
if (!spawnGate.ok) {
|
|
655
|
+
ctx.ui.notify(`Slice-parallel: aborting spawn — ${spawnGate.reason}`, "error");
|
|
656
|
+
return { action: "break", reason: `slice-parallel-reconciliation-failed: ${spawnGate.reason}` };
|
|
657
|
+
}
|
|
650
658
|
const result = await startSliceParallel(s.basePath, mid, eligible, {
|
|
651
659
|
maxWorkers: prefs.slice_parallel.max_workers ?? 2,
|
|
652
660
|
useExecutionGraph: uokFlags.executionGraph,
|
|
@@ -971,118 +979,115 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
971
979
|
return worktreeSafetyBlock;
|
|
972
980
|
// ── Sliding-window stuck detection with graduated recovery ──
|
|
973
981
|
const derivedKey = `${unitType}/${unitId}`;
|
|
974
|
-
// Always record this dispatch in the sliding window
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
// (#2007). Only the *response* to a stuck signal is suppressed during retries.
|
|
982
|
+
// Always record this dispatch in the sliding window and run detection so
|
|
983
|
+
// Rules 1/3/4 can catch retry loops with repeated failure content (#5719).
|
|
984
|
+
// Rules 2/2b suppress legitimate retry backoff through the dispatch ledger.
|
|
978
985
|
loopState.recentUnits.push({ key: derivedKey });
|
|
979
986
|
if (loopState.recentUnits.length > STUCK_WINDOW_SIZE)
|
|
980
987
|
loopState.recentUnits.shift();
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
return { action: "break", reason: "complete-milestone-artifact-db-mismatch" };
|
|
1007
|
-
}
|
|
1008
|
-
debugLog("autoLoop", {
|
|
1009
|
-
phase: "stuck-recovery",
|
|
1010
|
-
level: 1,
|
|
1011
|
-
action: "artifact-found",
|
|
1012
|
-
});
|
|
1013
|
-
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
1014
|
-
if (!recoveryDb.ok) {
|
|
1015
|
-
ctx.ui.notify(recoveryDb.fatal
|
|
1016
|
-
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
1017
|
-
: `${recoveryDb.message} Keeping stuck state for retry.`, "warning");
|
|
1018
|
-
if (recoveryDb.fatal) {
|
|
1019
|
-
await deps.pauseAuto(ctx, pi);
|
|
1020
|
-
return { action: "break", reason: recoveryDb.reason };
|
|
1021
|
-
}
|
|
1022
|
-
return { action: "continue" };
|
|
1023
|
-
}
|
|
1024
|
-
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
1025
|
-
deps.invalidateAllCaches();
|
|
1026
|
-
loopState.recentUnits.length = 0;
|
|
1027
|
-
loopState.stuckRecoveryAttempts = 0;
|
|
1028
|
-
return { action: "continue" };
|
|
988
|
+
const stuckSignal = detectStuck(loopState.recentUnits);
|
|
989
|
+
if (stuckSignal) {
|
|
990
|
+
debugLog("autoLoop", {
|
|
991
|
+
phase: "stuck-check",
|
|
992
|
+
unitType,
|
|
993
|
+
unitId,
|
|
994
|
+
reason: stuckSignal.reason,
|
|
995
|
+
recoveryAttempts: loopState.stuckRecoveryAttempts,
|
|
996
|
+
});
|
|
997
|
+
if (loopState.stuckRecoveryAttempts === 0) {
|
|
998
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
999
|
+
loopState.stuckRecoveryAttempts++;
|
|
1000
|
+
const artifactExists = verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1001
|
+
if (artifactExists) {
|
|
1002
|
+
if (unitType === "complete-milestone") {
|
|
1003
|
+
const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
|
|
1004
|
+
const stuckParts = [
|
|
1005
|
+
`Detected ${unitType} ${unitId} output on disk, but the same unit is still being derived.`,
|
|
1006
|
+
"This usually means the milestone summary exists while the DB row still does not mark the milestone complete.",
|
|
1007
|
+
];
|
|
1008
|
+
if (stuckDiag)
|
|
1009
|
+
stuckParts.push(`Expected: ${stuckDiag}`);
|
|
1010
|
+
ctx.ui.notify(stuckParts.join(" "), "warning");
|
|
1011
|
+
await deps.pauseAuto(ctx, pi);
|
|
1012
|
+
return { action: "break", reason: "complete-milestone-artifact-db-mismatch" };
|
|
1029
1013
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (artifactExists && unitType !== "complete-milestone") {
|
|
1038
|
-
debugLog("autoLoop", {
|
|
1039
|
-
phase: "stuck-recovery",
|
|
1040
|
-
level: 2,
|
|
1041
|
-
action: "artifact-found",
|
|
1042
|
-
});
|
|
1043
|
-
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
1044
|
-
if (recoveryDb.ok) {
|
|
1045
|
-
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
|
|
1046
|
-
loopState.recentUnits.length = 0;
|
|
1047
|
-
loopState.stuckRecoveryAttempts = 0;
|
|
1048
|
-
return { action: "continue" };
|
|
1049
|
-
}
|
|
1014
|
+
debugLog("autoLoop", {
|
|
1015
|
+
phase: "stuck-recovery",
|
|
1016
|
+
level: 1,
|
|
1017
|
+
action: "artifact-found",
|
|
1018
|
+
});
|
|
1019
|
+
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
1020
|
+
if (!recoveryDb.ok) {
|
|
1050
1021
|
ctx.ui.notify(recoveryDb.fatal
|
|
1051
1022
|
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
1052
|
-
: `${recoveryDb.message}
|
|
1023
|
+
: `${recoveryDb.message} Keeping stuck state for retry.`, "warning");
|
|
1053
1024
|
if (recoveryDb.fatal) {
|
|
1054
1025
|
await deps.pauseAuto(ctx, pi);
|
|
1055
1026
|
return { action: "break", reason: recoveryDb.reason };
|
|
1056
1027
|
}
|
|
1028
|
+
return { action: "continue" };
|
|
1057
1029
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
});
|
|
1064
|
-
const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
|
|
1065
|
-
const stuckRemediation = buildLoopRemediationSteps(unitType, unitId, s.basePath);
|
|
1066
|
-
const stuckParts = [`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}.`];
|
|
1067
|
-
if (stuckDiag)
|
|
1068
|
-
stuckParts.push(`Expected: ${stuckDiag}`);
|
|
1069
|
-
if (stuckRemediation)
|
|
1070
|
-
stuckParts.push(`To recover:\n${stuckRemediation}`);
|
|
1071
|
-
ctx.ui.notify(stuckParts.join(" "), "error");
|
|
1072
|
-
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
1073
|
-
return { action: "break", reason: "stuck-detected" };
|
|
1030
|
+
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
1031
|
+
deps.invalidateAllCaches();
|
|
1032
|
+
loopState.recentUnits.length = 0;
|
|
1033
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
1034
|
+
return { action: "continue" };
|
|
1074
1035
|
}
|
|
1036
|
+
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
|
|
1037
|
+
deps.invalidateAllCaches();
|
|
1075
1038
|
}
|
|
1076
1039
|
else {
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1040
|
+
// Level 2: hard stop — genuinely stuck
|
|
1041
|
+
deps.invalidateAllCaches();
|
|
1042
|
+
const artifactExists = verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1043
|
+
if (artifactExists && unitType !== "complete-milestone") {
|
|
1079
1044
|
debugLog("autoLoop", {
|
|
1080
|
-
phase: "stuck-
|
|
1081
|
-
|
|
1082
|
-
|
|
1045
|
+
phase: "stuck-recovery",
|
|
1046
|
+
level: 2,
|
|
1047
|
+
action: "artifact-found",
|
|
1083
1048
|
});
|
|
1084
|
-
|
|
1049
|
+
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
1050
|
+
if (recoveryDb.ok) {
|
|
1051
|
+
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
|
|
1052
|
+
loopState.recentUnits.length = 0;
|
|
1053
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
1054
|
+
return { action: "continue" };
|
|
1055
|
+
}
|
|
1056
|
+
ctx.ui.notify(recoveryDb.fatal
|
|
1057
|
+
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
1058
|
+
: `${recoveryDb.message} Stopping for manual recovery.`, "warning");
|
|
1059
|
+
if (recoveryDb.fatal) {
|
|
1060
|
+
await deps.pauseAuto(ctx, pi);
|
|
1061
|
+
return { action: "break", reason: recoveryDb.reason };
|
|
1062
|
+
}
|
|
1085
1063
|
}
|
|
1064
|
+
debugLog("autoLoop", {
|
|
1065
|
+
phase: "stuck-detected",
|
|
1066
|
+
unitType,
|
|
1067
|
+
unitId,
|
|
1068
|
+
reason: stuckSignal.reason,
|
|
1069
|
+
});
|
|
1070
|
+
const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
|
|
1071
|
+
const stuckRemediation = buildLoopRemediationSteps(unitType, unitId, s.basePath);
|
|
1072
|
+
const stuckParts = [`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}.`];
|
|
1073
|
+
if (stuckDiag)
|
|
1074
|
+
stuckParts.push(`Expected: ${stuckDiag}`);
|
|
1075
|
+
if (stuckRemediation)
|
|
1076
|
+
stuckParts.push(`To recover:\n${stuckRemediation}`);
|
|
1077
|
+
ctx.ui.notify(stuckParts.join(" "), "error");
|
|
1078
|
+
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
1079
|
+
return { action: "break", reason: "stuck-detected" };
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
// Progress detected — reset recovery counter
|
|
1084
|
+
if (loopState.stuckRecoveryAttempts > 0) {
|
|
1085
|
+
debugLog("autoLoop", {
|
|
1086
|
+
phase: "stuck-counter-reset",
|
|
1087
|
+
from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
|
|
1088
|
+
to: derivedKey,
|
|
1089
|
+
});
|
|
1090
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
1086
1091
|
}
|
|
1087
1092
|
}
|
|
1088
1093
|
return {
|
|
@@ -14,13 +14,11 @@ import { clearParseCache } from "./files.js";
|
|
|
14
14
|
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
|
|
15
15
|
import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice, getMilestone, refreshOpenDatabaseFromDisk, getCompletedMilestoneTaskFileHints, getMilestoneCommitAttributionShas, recordMilestoneCommitAttribution } from "./gsd-db.js";
|
|
16
16
|
import { isValidationTerminal } from "./state.js";
|
|
17
|
-
import {
|
|
18
|
-
import { logWarning, logError } from "./workflow-logger.js";
|
|
17
|
+
import { logWarning } from "./workflow-logger.js";
|
|
19
18
|
import { readIntegrationBranch } from "./git-service.js";
|
|
20
19
|
import { isClosedStatus } from "./status-guards.js";
|
|
21
|
-
import { nativeConflictFiles, nativeCommit, nativeCheckoutTheirs, nativeAddPaths, nativeMergeAbort, nativeRebaseAbort, nativeResetHard, } from "./native-git-bridge.js";
|
|
22
20
|
import { resolveSlicePath, resolveSliceFile, resolveTasksDir, resolveTaskFiles, relMilestoneFile, relSliceFile, buildSliceFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
|
|
23
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync,
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
24
22
|
import { execFileSync } from "node:child_process";
|
|
25
23
|
import { dirname, join } from "node:path";
|
|
26
24
|
import { resolveExpectedArtifactPath, diagnoseExpectedArtifact, } from "./auto-artifact-paths.js";
|
|
@@ -932,183 +930,10 @@ export function writeBlockerPlaceholder(unitType, unitId, base, reason) {
|
|
|
932
930
|
return diagnoseExpectedArtifact(unitType, unitId, base);
|
|
933
931
|
}
|
|
934
932
|
// ─── Merge State Reconciliation ───────────────────────────────────────────────
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
function abortAndResetMerge(basePath, hasMergeHead, squashMsgPath) {
|
|
940
|
-
if (hasMergeHead) {
|
|
941
|
-
try {
|
|
942
|
-
nativeMergeAbort(basePath);
|
|
943
|
-
}
|
|
944
|
-
catch (err) {
|
|
945
|
-
/* best-effort */
|
|
946
|
-
logWarning("recovery", `git merge-abort failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
else if (squashMsgPath) {
|
|
950
|
-
try {
|
|
951
|
-
unlinkSync(squashMsgPath);
|
|
952
|
-
}
|
|
953
|
-
catch (err) {
|
|
954
|
-
/* best-effort */
|
|
955
|
-
logWarning("recovery", `file unlink failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
try {
|
|
959
|
-
nativeResetHard(basePath);
|
|
960
|
-
}
|
|
961
|
-
catch (err) {
|
|
962
|
-
/* best-effort */
|
|
963
|
-
logError("recovery", `git reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Detect and abort other in-progress git operations left behind by a SIGKILL'd
|
|
968
|
-
* worker (rebase, cherry-pick, revert). Without this, a killed worker mid-rebase
|
|
969
|
-
* leaves `.git/rebase-merge/` or `.git/CHERRY_PICK_HEAD` and the worktree is
|
|
970
|
-
* wedged until the user manually runs the matching `--abort`.
|
|
971
|
-
*
|
|
972
|
-
* Called before merge-state reconciliation because these states block any
|
|
973
|
-
* subsequent merge/commit operation. (Issue #4980 HIGH-7)
|
|
974
|
-
*/
|
|
975
|
-
function reconcileOtherInProgressGitOps(basePath, ctx) {
|
|
976
|
-
const gitDir = join(basePath, ".git");
|
|
977
|
-
const states = [
|
|
978
|
-
{
|
|
979
|
-
label: "rebase",
|
|
980
|
-
indicators: [join(gitDir, "rebase-merge"), join(gitDir, "rebase-apply")],
|
|
981
|
-
abort: () => nativeRebaseAbort(basePath),
|
|
982
|
-
},
|
|
983
|
-
{
|
|
984
|
-
label: "cherry-pick",
|
|
985
|
-
indicators: [join(gitDir, "CHERRY_PICK_HEAD")],
|
|
986
|
-
abort: () => {
|
|
987
|
-
// No native helper; fall back to git CLI.
|
|
988
|
-
try {
|
|
989
|
-
execFileSync("git", ["cherry-pick", "--abort"], {
|
|
990
|
-
cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8",
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
catch (err) {
|
|
994
|
-
logWarning("recovery", `cherry-pick --abort failed: ${getErrorMessage(err)}`);
|
|
995
|
-
}
|
|
996
|
-
},
|
|
997
|
-
},
|
|
998
|
-
{
|
|
999
|
-
label: "revert",
|
|
1000
|
-
indicators: [join(gitDir, "REVERT_HEAD")],
|
|
1001
|
-
abort: () => {
|
|
1002
|
-
try {
|
|
1003
|
-
execFileSync("git", ["revert", "--abort"], {
|
|
1004
|
-
cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8",
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
catch (err) {
|
|
1008
|
-
logWarning("recovery", `revert --abort failed: ${getErrorMessage(err)}`);
|
|
1009
|
-
}
|
|
1010
|
-
},
|
|
1011
|
-
},
|
|
1012
|
-
];
|
|
1013
|
-
let reconciled = false;
|
|
1014
|
-
for (const s of states) {
|
|
1015
|
-
const present = s.indicators.some((p) => existsSync(p));
|
|
1016
|
-
if (!present)
|
|
1017
|
-
continue;
|
|
1018
|
-
try {
|
|
1019
|
-
s.abort();
|
|
1020
|
-
ctx.ui.notify(`Detected leftover ${s.label} state from prior session — aborted.`, "warning");
|
|
1021
|
-
reconciled = true;
|
|
1022
|
-
}
|
|
1023
|
-
catch (err) {
|
|
1024
|
-
logError("recovery", `${s.label} abort failed: ${getErrorMessage(err)}`);
|
|
1025
|
-
ctx.ui.notify(`Detected leftover ${s.label} state but auto-abort failed. ` +
|
|
1026
|
-
`Run \`git ${s.label} --abort\` manually before retrying.`, "error");
|
|
1027
|
-
return "blocked";
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
return reconciled ? "reconciled" : "clean";
|
|
1031
|
-
}
|
|
1032
|
-
/**
|
|
1033
|
-
* Detect leftover merge state from a prior session and reconcile it.
|
|
1034
|
-
* If MERGE_HEAD or SQUASH_MSG exists, check whether conflicts are resolved.
|
|
1035
|
-
* If resolved: finalize the commit. If only .gsd conflicts remain: auto-resolve.
|
|
1036
|
-
* If code conflicts remain: fail safe without modifying the worktree.
|
|
1037
|
-
*/
|
|
1038
|
-
export function reconcileMergeState(basePath, ctx) {
|
|
1039
|
-
// First, abort any rebase/cherry-pick/revert left over from a SIGKILL'd
|
|
1040
|
-
// worker. Doing this before the merge-state check unblocks any merge that
|
|
1041
|
-
// would otherwise refuse with "you have unfinished operation". (HIGH-7)
|
|
1042
|
-
const otherOpsResult = reconcileOtherInProgressGitOps(basePath, ctx);
|
|
1043
|
-
if (otherOpsResult === "blocked")
|
|
1044
|
-
return "blocked";
|
|
1045
|
-
const mergeHeadPath = join(basePath, ".git", "MERGE_HEAD");
|
|
1046
|
-
const squashMsgPath = join(basePath, ".git", "SQUASH_MSG");
|
|
1047
|
-
const hasMergeHead = existsSync(mergeHeadPath);
|
|
1048
|
-
const hasSquashMsg = existsSync(squashMsgPath);
|
|
1049
|
-
if (!hasMergeHead && !hasSquashMsg) {
|
|
1050
|
-
// If we cleaned up another op type, return "reconciled" so the caller
|
|
1051
|
-
// re-derives state from a known-good baseline.
|
|
1052
|
-
return otherOpsResult === "reconciled" ? "reconciled" : "clean";
|
|
1053
|
-
}
|
|
1054
|
-
const conflictedFiles = nativeConflictFiles(basePath);
|
|
1055
|
-
if (conflictedFiles.length === 0) {
|
|
1056
|
-
// All conflicts resolved — finalize the merge/squash commit
|
|
1057
|
-
try {
|
|
1058
|
-
const commitSha = nativeCommit(basePath, "chore(gsd): reconcile merge state");
|
|
1059
|
-
if (commitSha) {
|
|
1060
|
-
const mode = hasMergeHead ? "merge" : "squash commit";
|
|
1061
|
-
ctx.ui.notify(`Finalized leftover ${mode} from prior session.`, "info");
|
|
1062
|
-
}
|
|
1063
|
-
else {
|
|
1064
|
-
ctx.ui.notify("No new commit needed for leftover merge/squash state — already committed.", "info");
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
catch (err) {
|
|
1068
|
-
const errorMessage = getErrorMessage(err);
|
|
1069
|
-
ctx.ui.notify(`Failed to finalize leftover merge/squash commit: ${errorMessage}`, "error");
|
|
1070
|
-
return "blocked";
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
// Still conflicted — try auto-resolving .gsd/ state file conflicts (#530)
|
|
1075
|
-
const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/"));
|
|
1076
|
-
const codeConflicts = conflictedFiles.filter((f) => !f.startsWith(".gsd/"));
|
|
1077
|
-
if (gsdConflicts.length > 0 && codeConflicts.length === 0) {
|
|
1078
|
-
// All conflicts are in .gsd/ state files — auto-resolve by accepting theirs
|
|
1079
|
-
let resolved = true;
|
|
1080
|
-
try {
|
|
1081
|
-
nativeCheckoutTheirs(basePath, gsdConflicts);
|
|
1082
|
-
nativeAddPaths(basePath, gsdConflicts);
|
|
1083
|
-
}
|
|
1084
|
-
catch (e) {
|
|
1085
|
-
logError("recovery", `auto-resolve .gsd/ conflicts failed: ${e.message}`);
|
|
1086
|
-
resolved = false;
|
|
1087
|
-
}
|
|
1088
|
-
if (resolved) {
|
|
1089
|
-
try {
|
|
1090
|
-
nativeCommit(basePath, "chore: auto-resolve .gsd/ state file conflicts");
|
|
1091
|
-
ctx.ui.notify(`Auto-resolved ${gsdConflicts.length} .gsd/ state file conflict(s) from prior merge.`, "info");
|
|
1092
|
-
}
|
|
1093
|
-
catch (e) {
|
|
1094
|
-
logError("recovery", `auto-commit .gsd/ conflict resolution failed: ${e.message}`);
|
|
1095
|
-
resolved = false;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
if (!resolved) {
|
|
1099
|
-
abortAndResetMerge(basePath, hasMergeHead, squashMsgPath);
|
|
1100
|
-
ctx.ui.notify("Detected leftover merge state — auto-resolve failed, cleaned up. Re-deriving state.", "warning");
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
else {
|
|
1104
|
-
// Code conflicts present — fail safe and preserve any manual resolution
|
|
1105
|
-
// work instead of discarding it with merge --abort/reset --hard.
|
|
1106
|
-
ctx.ui.notify("Detected leftover merge state with unresolved code conflicts. Auto-mode will pause without modifying the worktree so manual conflict resolution is preserved.", "error");
|
|
1107
|
-
return "blocked";
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return "reconciled";
|
|
1111
|
-
}
|
|
933
|
+
// Body relocated to state-reconciliation/drift/merge-state.ts (ADR-017 #5701).
|
|
934
|
+
// Re-exported here for backward compatibility with existing call sites:
|
|
935
|
+
// auto.ts, auto/loop-deps.ts, tests/integration/auto-recovery.test.ts.
|
|
936
|
+
export { reconcileMergeState, } from "./state-reconciliation/drift/merge-state.js";
|
|
1112
937
|
// ─── Loop Remediation ─────────────────────────────────────────────────────────
|
|
1113
938
|
/**
|
|
1114
939
|
* Build concrete, manual remediation steps for a loop-detected unit failure.
|
|
@@ -1399,16 +1399,19 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1399
1399
|
stateReconciliation: {
|
|
1400
1400
|
async reconcileBeforeDispatch() {
|
|
1401
1401
|
const result = await reconcileBeforeDispatch(dispatchBasePath);
|
|
1402
|
-
if (
|
|
1402
|
+
if (result.blockers.length > 0) {
|
|
1403
1403
|
return {
|
|
1404
1404
|
ok: false,
|
|
1405
|
-
reason: result.
|
|
1405
|
+
reason: result.blockers[0],
|
|
1406
1406
|
stateSnapshot: result.stateSnapshot,
|
|
1407
1407
|
};
|
|
1408
1408
|
}
|
|
1409
|
+
const repairedKinds = result.repaired.map((d) => d.kind);
|
|
1409
1410
|
return {
|
|
1410
1411
|
ok: true,
|
|
1411
|
-
reason:
|
|
1412
|
+
reason: repairedKinds.length > 0
|
|
1413
|
+
? `repaired: ${repairedKinds.join(", ")}`
|
|
1414
|
+
: "clean",
|
|
1412
1415
|
stateSnapshot: result.stateSnapshot,
|
|
1413
1416
|
};
|
|
1414
1417
|
},
|
|
@@ -116,8 +116,7 @@ export function isBareClaudeCodeStreamAbortPlaceholder(lastMsg) {
|
|
|
116
116
|
* Claude Code abort markers are intentionally ignored when the abort fires
|
|
117
117
|
* while the session-switch is in flight: the abort is the expected side-effect
|
|
118
118
|
* of the transition, not a user signal. Other branches (genuine `stopReason
|
|
119
|
-
* === "aborted"` with
|
|
120
|
-
* behavior.
|
|
119
|
+
* === "aborted"` with explicit errorMessage) preserve the prior behavior.
|
|
121
120
|
*/
|
|
122
121
|
export function _handleSessionSwitchAgentEnd(lastMsg, resolveCancelled) {
|
|
123
122
|
if (!lastMsg || typeof lastMsg !== "object")
|
|
@@ -136,10 +135,8 @@ export function _handleSessionSwitchAgentEnd(lastMsg, resolveCancelled) {
|
|
|
136
135
|
return;
|
|
137
136
|
}
|
|
138
137
|
if (m.stopReason === "aborted") {
|
|
139
|
-
const content = m.content;
|
|
140
|
-
const hasEmptyContent = Array.isArray(content) && content.length === 0;
|
|
141
138
|
const hasErrorMessage = !!m.errorMessage;
|
|
142
|
-
if (
|
|
139
|
+
if (hasErrorMessage) {
|
|
143
140
|
resolveCancelled(_buildAbortedPauseContext(m));
|
|
144
141
|
}
|
|
145
142
|
}
|
|
@@ -2,6 +2,7 @@ import { getOrchestratorState, getWorkerStatuses, isParallelActive, pauseWorker,
|
|
|
2
2
|
import { formatEligibilityReport } from "../../parallel-eligibility.js";
|
|
3
3
|
import { formatMergeResults, mergeAllCompleted, mergeCompletedMilestone } from "../../parallel-merge.js";
|
|
4
4
|
import { loadEffectiveGSDPreferences, resolveParallelConfig } from "../../preferences.js";
|
|
5
|
+
import { reconcileBeforeSpawn } from "../../state-reconciliation.js";
|
|
5
6
|
import { projectRoot } from "../context.js";
|
|
6
7
|
function emitParallelMessage(pi, content) {
|
|
7
8
|
pi.sendMessage({ customType: "gsd-parallel", content, display: true });
|
|
@@ -26,6 +27,14 @@ export async function handleParallelCommand(trimmed, _ctx, pi) {
|
|
|
26
27
|
emitParallelMessage(pi, `${report}\n\nNo milestones are eligible for parallel execution.`);
|
|
27
28
|
return true;
|
|
28
29
|
}
|
|
30
|
+
// ADR-017 #5707: reconcile before spawning so workers don't independently
|
|
31
|
+
// race on the same drift. Failures abort the spawn with an actionable
|
|
32
|
+
// user-visible message.
|
|
33
|
+
const gate = await reconcileBeforeSpawn(root);
|
|
34
|
+
if (!gate.ok) {
|
|
35
|
+
emitParallelMessage(pi, `${report}\n\nParallel orchestration aborted before spawn — ${gate.reason}`);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
29
38
|
const result = await startParallel(root, candidates.eligible.map((candidate) => candidate.milestoneId), loaded?.preferences);
|
|
30
39
|
const lines = ["Parallel orchestration started.", `Workers: ${result.started.join(", ")}`];
|
|
31
40
|
if (result.errors.length > 0) {
|
|
@@ -977,32 +977,16 @@ export function setSliceSketchFlag(milestoneId, sliceId, isSketch) {
|
|
|
977
977
|
currentDb.prepare(`UPDATE slices SET is_sketch = :is_sketch WHERE milestone_id = :mid AND id = :sid`).run({ ":is_sketch": isSketch ? 1 : 0, ":mid": milestoneId, ":sid": sliceId });
|
|
978
978
|
}
|
|
979
979
|
/**
|
|
980
|
-
* ADR-
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
* to keep path logic in one place — do not hand-roll the path inside the callback.
|
|
985
|
-
*
|
|
986
|
-
* Recovers from two scenarios:
|
|
987
|
-
* 1. Crash between `gsd_plan_slice` write and the sketch flag flip.
|
|
988
|
-
* 2. Flag-OFF downgrade path: when `progressive_planning` is off, the dispatch
|
|
989
|
-
* rule routes sketch slices to plan-slice, which writes PLAN.md but leaves
|
|
990
|
-
* `is_sketch=1` — the next state derivation auto-heals it to 0 here.
|
|
991
|
-
*
|
|
992
|
-
* Not aggressive in practice: PLAN.md is only written via the DB-backed
|
|
993
|
-
* `gsd_plan_slice` tool (which also inserts tasks), so a "stale PLAN.md with
|
|
994
|
-
* is_sketch=1" is extremely unlikely to indicate anything other than the two
|
|
995
|
-
* recovery scenarios above.
|
|
980
|
+
* ADR-017 raw primitive: returns slice IDs in a milestone whose is_sketch flag
|
|
981
|
+
* is still 1. The stale-sketch-flag drift handler at
|
|
982
|
+
* `state-reconciliation/drift/sketch-flag.ts` composes this with PLAN.md
|
|
983
|
+
* existence checks to detect drift, then writes via `setSliceSketchFlag`.
|
|
996
984
|
*/
|
|
997
|
-
export function
|
|
985
|
+
export function getSketchedSliceIds(milestoneId) {
|
|
998
986
|
if (!currentDb)
|
|
999
|
-
return;
|
|
987
|
+
return [];
|
|
1000
988
|
const rows = currentDb.prepare(`SELECT id FROM slices WHERE milestone_id = :mid AND is_sketch = 1`).all({ ":mid": milestoneId });
|
|
1001
|
-
|
|
1002
|
-
if (hasPlanFile(row.id)) {
|
|
1003
|
-
setSliceSketchFlag(milestoneId, row.id, false);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
989
|
+
return rows.map((r) => r.id);
|
|
1006
990
|
}
|
|
1007
991
|
export function upsertSlicePlanning(milestoneId, sliceId, planning) {
|
|
1008
992
|
if (!currentDb)
|