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
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* globals or AutoContext dependency.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
11
10
|
import { parseUnitId } from "./unit-id.js";
|
|
12
11
|
import { MILESTONE_ID_RE } from "./milestone-ids.js";
|
|
13
12
|
import { appendEvent } from "./workflow-events.js";
|
|
@@ -20,15 +19,6 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
20
19
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
21
20
|
import { readIntegrationBranch } from "./git-service.js";
|
|
22
21
|
import { isClosedStatus } from "./status-guards.js";
|
|
23
|
-
import {
|
|
24
|
-
nativeConflictFiles,
|
|
25
|
-
nativeCommit,
|
|
26
|
-
nativeCheckoutTheirs,
|
|
27
|
-
nativeAddPaths,
|
|
28
|
-
nativeMergeAbort,
|
|
29
|
-
nativeRebaseAbort,
|
|
30
|
-
nativeResetHard,
|
|
31
|
-
} from "./native-git-bridge.js";
|
|
32
22
|
import {
|
|
33
23
|
resolveSlicePath,
|
|
34
24
|
resolveSliceFile,
|
|
@@ -46,7 +36,6 @@ import {
|
|
|
46
36
|
mkdirSync,
|
|
47
37
|
readFileSync,
|
|
48
38
|
writeFileSync,
|
|
49
|
-
unlinkSync,
|
|
50
39
|
} from "node:fs";
|
|
51
40
|
import { execFileSync } from "node:child_process";
|
|
52
41
|
import { dirname, join } from "node:path";
|
|
@@ -1001,205 +990,14 @@ export function writeBlockerPlaceholder(
|
|
|
1001
990
|
}
|
|
1002
991
|
|
|
1003
992
|
// ─── Merge State Reconciliation ───────────────────────────────────────────────
|
|
993
|
+
// Body relocated to state-reconciliation/drift/merge-state.ts (ADR-017 #5701).
|
|
994
|
+
// Re-exported here for backward compatibility with existing call sites:
|
|
995
|
+
// auto.ts, auto/loop-deps.ts, tests/integration/auto-recovery.test.ts.
|
|
1004
996
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
function abortAndResetMerge(
|
|
1010
|
-
basePath: string,
|
|
1011
|
-
hasMergeHead: boolean,
|
|
1012
|
-
squashMsgPath: string,
|
|
1013
|
-
): void {
|
|
1014
|
-
if (hasMergeHead) {
|
|
1015
|
-
try {
|
|
1016
|
-
nativeMergeAbort(basePath);
|
|
1017
|
-
} catch (err) {
|
|
1018
|
-
/* best-effort */
|
|
1019
|
-
logWarning("recovery", `git merge-abort failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1020
|
-
}
|
|
1021
|
-
} else if (squashMsgPath) {
|
|
1022
|
-
try {
|
|
1023
|
-
unlinkSync(squashMsgPath);
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
/* best-effort */
|
|
1026
|
-
logWarning("recovery", `file unlink failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
try {
|
|
1030
|
-
nativeResetHard(basePath);
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
/* best-effort */
|
|
1033
|
-
logError("recovery", `git reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
export type MergeReconcileResult = "clean" | "reconciled" | "blocked";
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* Detect and abort other in-progress git operations left behind by a SIGKILL'd
|
|
1041
|
-
* worker (rebase, cherry-pick, revert). Without this, a killed worker mid-rebase
|
|
1042
|
-
* leaves `.git/rebase-merge/` or `.git/CHERRY_PICK_HEAD` and the worktree is
|
|
1043
|
-
* wedged until the user manually runs the matching `--abort`.
|
|
1044
|
-
*
|
|
1045
|
-
* Called before merge-state reconciliation because these states block any
|
|
1046
|
-
* subsequent merge/commit operation. (Issue #4980 HIGH-7)
|
|
1047
|
-
*/
|
|
1048
|
-
function reconcileOtherInProgressGitOps(
|
|
1049
|
-
basePath: string,
|
|
1050
|
-
ctx: ExtensionContext,
|
|
1051
|
-
): "clean" | "reconciled" | "blocked" {
|
|
1052
|
-
const gitDir = join(basePath, ".git");
|
|
1053
|
-
const states: Array<{
|
|
1054
|
-
label: string;
|
|
1055
|
-
indicators: string[];
|
|
1056
|
-
abort: () => void;
|
|
1057
|
-
}> = [
|
|
1058
|
-
{
|
|
1059
|
-
label: "rebase",
|
|
1060
|
-
indicators: [join(gitDir, "rebase-merge"), join(gitDir, "rebase-apply")],
|
|
1061
|
-
abort: () => nativeRebaseAbort(basePath),
|
|
1062
|
-
},
|
|
1063
|
-
{
|
|
1064
|
-
label: "cherry-pick",
|
|
1065
|
-
indicators: [join(gitDir, "CHERRY_PICK_HEAD")],
|
|
1066
|
-
abort: () => {
|
|
1067
|
-
// No native helper; fall back to git CLI.
|
|
1068
|
-
try {
|
|
1069
|
-
execFileSync("git", ["cherry-pick", "--abort"], {
|
|
1070
|
-
cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8",
|
|
1071
|
-
});
|
|
1072
|
-
} catch (err) { logWarning("recovery", `cherry-pick --abort failed: ${getErrorMessage(err)}`); }
|
|
1073
|
-
},
|
|
1074
|
-
},
|
|
1075
|
-
{
|
|
1076
|
-
label: "revert",
|
|
1077
|
-
indicators: [join(gitDir, "REVERT_HEAD")],
|
|
1078
|
-
abort: () => {
|
|
1079
|
-
try {
|
|
1080
|
-
execFileSync("git", ["revert", "--abort"], {
|
|
1081
|
-
cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8",
|
|
1082
|
-
});
|
|
1083
|
-
} catch (err) { logWarning("recovery", `revert --abort failed: ${getErrorMessage(err)}`); }
|
|
1084
|
-
},
|
|
1085
|
-
},
|
|
1086
|
-
];
|
|
1087
|
-
|
|
1088
|
-
let reconciled = false;
|
|
1089
|
-
for (const s of states) {
|
|
1090
|
-
const present = s.indicators.some((p) => existsSync(p));
|
|
1091
|
-
if (!present) continue;
|
|
1092
|
-
try {
|
|
1093
|
-
s.abort();
|
|
1094
|
-
ctx.ui.notify(
|
|
1095
|
-
`Detected leftover ${s.label} state from prior session — aborted.`,
|
|
1096
|
-
"warning",
|
|
1097
|
-
);
|
|
1098
|
-
reconciled = true;
|
|
1099
|
-
} catch (err) {
|
|
1100
|
-
logError("recovery", `${s.label} abort failed: ${getErrorMessage(err)}`);
|
|
1101
|
-
ctx.ui.notify(
|
|
1102
|
-
`Detected leftover ${s.label} state but auto-abort failed. ` +
|
|
1103
|
-
`Run \`git ${s.label} --abort\` manually before retrying.`,
|
|
1104
|
-
"error",
|
|
1105
|
-
);
|
|
1106
|
-
return "blocked";
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return reconciled ? "reconciled" : "clean";
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
/**
|
|
1113
|
-
* Detect leftover merge state from a prior session and reconcile it.
|
|
1114
|
-
* If MERGE_HEAD or SQUASH_MSG exists, check whether conflicts are resolved.
|
|
1115
|
-
* If resolved: finalize the commit. If only .gsd conflicts remain: auto-resolve.
|
|
1116
|
-
* If code conflicts remain: fail safe without modifying the worktree.
|
|
1117
|
-
*/
|
|
1118
|
-
export function reconcileMergeState(
|
|
1119
|
-
basePath: string,
|
|
1120
|
-
ctx: ExtensionContext,
|
|
1121
|
-
): MergeReconcileResult {
|
|
1122
|
-
// First, abort any rebase/cherry-pick/revert left over from a SIGKILL'd
|
|
1123
|
-
// worker. Doing this before the merge-state check unblocks any merge that
|
|
1124
|
-
// would otherwise refuse with "you have unfinished operation". (HIGH-7)
|
|
1125
|
-
const otherOpsResult = reconcileOtherInProgressGitOps(basePath, ctx);
|
|
1126
|
-
if (otherOpsResult === "blocked") return "blocked";
|
|
1127
|
-
|
|
1128
|
-
const mergeHeadPath = join(basePath, ".git", "MERGE_HEAD");
|
|
1129
|
-
const squashMsgPath = join(basePath, ".git", "SQUASH_MSG");
|
|
1130
|
-
const hasMergeHead = existsSync(mergeHeadPath);
|
|
1131
|
-
const hasSquashMsg = existsSync(squashMsgPath);
|
|
1132
|
-
if (!hasMergeHead && !hasSquashMsg) {
|
|
1133
|
-
// If we cleaned up another op type, return "reconciled" so the caller
|
|
1134
|
-
// re-derives state from a known-good baseline.
|
|
1135
|
-
return otherOpsResult === "reconciled" ? "reconciled" : "clean";
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
const conflictedFiles = nativeConflictFiles(basePath);
|
|
1139
|
-
if (conflictedFiles.length === 0) {
|
|
1140
|
-
// All conflicts resolved — finalize the merge/squash commit
|
|
1141
|
-
try {
|
|
1142
|
-
const commitSha = nativeCommit(basePath, "chore(gsd): reconcile merge state");
|
|
1143
|
-
if (commitSha) {
|
|
1144
|
-
const mode = hasMergeHead ? "merge" : "squash commit";
|
|
1145
|
-
ctx.ui.notify(`Finalized leftover ${mode} from prior session.`, "info");
|
|
1146
|
-
} else {
|
|
1147
|
-
ctx.ui.notify("No new commit needed for leftover merge/squash state — already committed.", "info");
|
|
1148
|
-
}
|
|
1149
|
-
} catch (err) {
|
|
1150
|
-
const errorMessage = getErrorMessage(err);
|
|
1151
|
-
ctx.ui.notify(`Failed to finalize leftover merge/squash commit: ${errorMessage}`, "error");
|
|
1152
|
-
return "blocked";
|
|
1153
|
-
}
|
|
1154
|
-
} else {
|
|
1155
|
-
// Still conflicted — try auto-resolving .gsd/ state file conflicts (#530)
|
|
1156
|
-
const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/"));
|
|
1157
|
-
const codeConflicts = conflictedFiles.filter((f) => !f.startsWith(".gsd/"));
|
|
1158
|
-
|
|
1159
|
-
if (gsdConflicts.length > 0 && codeConflicts.length === 0) {
|
|
1160
|
-
// All conflicts are in .gsd/ state files — auto-resolve by accepting theirs
|
|
1161
|
-
let resolved = true;
|
|
1162
|
-
try {
|
|
1163
|
-
nativeCheckoutTheirs(basePath, gsdConflicts);
|
|
1164
|
-
nativeAddPaths(basePath, gsdConflicts);
|
|
1165
|
-
} catch (e) {
|
|
1166
|
-
logError("recovery", `auto-resolve .gsd/ conflicts failed: ${(e as Error).message}`);
|
|
1167
|
-
resolved = false;
|
|
1168
|
-
}
|
|
1169
|
-
if (resolved) {
|
|
1170
|
-
try {
|
|
1171
|
-
nativeCommit(
|
|
1172
|
-
basePath,
|
|
1173
|
-
"chore: auto-resolve .gsd/ state file conflicts",
|
|
1174
|
-
);
|
|
1175
|
-
ctx.ui.notify(
|
|
1176
|
-
`Auto-resolved ${gsdConflicts.length} .gsd/ state file conflict(s) from prior merge.`,
|
|
1177
|
-
"info",
|
|
1178
|
-
);
|
|
1179
|
-
} catch (e) {
|
|
1180
|
-
logError("recovery", `auto-commit .gsd/ conflict resolution failed: ${(e as Error).message}`);
|
|
1181
|
-
resolved = false;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
if (!resolved) {
|
|
1185
|
-
abortAndResetMerge(basePath, hasMergeHead, squashMsgPath);
|
|
1186
|
-
ctx.ui.notify(
|
|
1187
|
-
"Detected leftover merge state — auto-resolve failed, cleaned up. Re-deriving state.",
|
|
1188
|
-
"warning",
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
} else {
|
|
1192
|
-
// Code conflicts present — fail safe and preserve any manual resolution
|
|
1193
|
-
// work instead of discarding it with merge --abort/reset --hard.
|
|
1194
|
-
ctx.ui.notify(
|
|
1195
|
-
"Detected leftover merge state with unresolved code conflicts. Auto-mode will pause without modifying the worktree so manual conflict resolution is preserved.",
|
|
1196
|
-
"error",
|
|
1197
|
-
);
|
|
1198
|
-
return "blocked";
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
return "reconciled";
|
|
1202
|
-
}
|
|
997
|
+
export {
|
|
998
|
+
reconcileMergeState,
|
|
999
|
+
type MergeReconcileResult,
|
|
1000
|
+
} from "./state-reconciliation/drift/merge-state.js";
|
|
1203
1001
|
|
|
1204
1002
|
// ─── Loop Remediation ─────────────────────────────────────────────────────────
|
|
1205
1003
|
|
|
@@ -1812,16 +1812,20 @@ export function createWiredAutoOrchestrationModule(
|
|
|
1812
1812
|
stateReconciliation: {
|
|
1813
1813
|
async reconcileBeforeDispatch() {
|
|
1814
1814
|
const result = await reconcileBeforeDispatch(dispatchBasePath);
|
|
1815
|
-
if (
|
|
1815
|
+
if (result.blockers.length > 0) {
|
|
1816
1816
|
return {
|
|
1817
1817
|
ok: false,
|
|
1818
|
-
reason: result.
|
|
1818
|
+
reason: result.blockers[0],
|
|
1819
1819
|
stateSnapshot: result.stateSnapshot,
|
|
1820
1820
|
};
|
|
1821
1821
|
}
|
|
1822
|
+
const repairedKinds = result.repaired.map((d) => d.kind);
|
|
1822
1823
|
return {
|
|
1823
1824
|
ok: true,
|
|
1824
|
-
reason:
|
|
1825
|
+
reason:
|
|
1826
|
+
repairedKinds.length > 0
|
|
1827
|
+
? `repaired: ${repairedKinds.join(", ")}`
|
|
1828
|
+
: "clean",
|
|
1825
1829
|
stateSnapshot: result.stateSnapshot,
|
|
1826
1830
|
};
|
|
1827
1831
|
},
|
|
@@ -153,8 +153,7 @@ export function isBareClaudeCodeStreamAbortPlaceholder(lastMsg: unknown): boolea
|
|
|
153
153
|
* Claude Code abort markers are intentionally ignored when the abort fires
|
|
154
154
|
* while the session-switch is in flight: the abort is the expected side-effect
|
|
155
155
|
* of the transition, not a user signal. Other branches (genuine `stopReason
|
|
156
|
-
* === "aborted"` with
|
|
157
|
-
* behavior.
|
|
156
|
+
* === "aborted"` with explicit errorMessage) preserve the prior behavior.
|
|
158
157
|
*/
|
|
159
158
|
export function _handleSessionSwitchAgentEnd(
|
|
160
159
|
lastMsg: unknown,
|
|
@@ -178,10 +177,8 @@ export function _handleSessionSwitchAgentEnd(
|
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
if (m.stopReason === "aborted") {
|
|
181
|
-
const content = m.content;
|
|
182
|
-
const hasEmptyContent = Array.isArray(content) && content.length === 0;
|
|
183
180
|
const hasErrorMessage = !!m.errorMessage;
|
|
184
|
-
if (
|
|
181
|
+
if (hasErrorMessage) {
|
|
185
182
|
resolveCancelled(_buildAbortedPauseContext(m as { errorMessage?: unknown }));
|
|
186
183
|
}
|
|
187
184
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { formatEligibilityReport } from "../../parallel-eligibility.js";
|
|
15
15
|
import { formatMergeResults, mergeAllCompleted, mergeCompletedMilestone } from "../../parallel-merge.js";
|
|
16
16
|
import { loadEffectiveGSDPreferences, resolveParallelConfig } from "../../preferences.js";
|
|
17
|
+
import { reconcileBeforeSpawn } from "../../state-reconciliation.js";
|
|
17
18
|
import { projectRoot } from "../context.js";
|
|
18
19
|
function emitParallelMessage(pi: ExtensionAPI, content: string): void {
|
|
19
20
|
pi.sendMessage({ customType: "gsd-parallel", content, display: true });
|
|
@@ -40,6 +41,17 @@ export async function handleParallelCommand(trimmed: string, _ctx: ExtensionComm
|
|
|
40
41
|
emitParallelMessage(pi, `${report}\n\nNo milestones are eligible for parallel execution.`);
|
|
41
42
|
return true;
|
|
42
43
|
}
|
|
44
|
+
// ADR-017 #5707: reconcile before spawning so workers don't independently
|
|
45
|
+
// race on the same drift. Failures abort the spawn with an actionable
|
|
46
|
+
// user-visible message.
|
|
47
|
+
const gate = await reconcileBeforeSpawn(root);
|
|
48
|
+
if (!gate.ok) {
|
|
49
|
+
emitParallelMessage(
|
|
50
|
+
pi,
|
|
51
|
+
`${report}\n\nParallel orchestration aborted before spawn — ${gate.reason}`,
|
|
52
|
+
);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
43
55
|
const result = await startParallel(
|
|
44
56
|
root,
|
|
45
57
|
candidates.eligible.map((candidate) => candidate.milestoneId),
|
|
@@ -1136,33 +1136,17 @@ export function setSliceSketchFlag(milestoneId: string, sliceId: string, isSketc
|
|
|
1136
1136
|
}
|
|
1137
1137
|
|
|
1138
1138
|
/**
|
|
1139
|
-
* ADR-
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
* to keep path logic in one place — do not hand-roll the path inside the callback.
|
|
1144
|
-
*
|
|
1145
|
-
* Recovers from two scenarios:
|
|
1146
|
-
* 1. Crash between `gsd_plan_slice` write and the sketch flag flip.
|
|
1147
|
-
* 2. Flag-OFF downgrade path: when `progressive_planning` is off, the dispatch
|
|
1148
|
-
* rule routes sketch slices to plan-slice, which writes PLAN.md but leaves
|
|
1149
|
-
* `is_sketch=1` — the next state derivation auto-heals it to 0 here.
|
|
1150
|
-
*
|
|
1151
|
-
* Not aggressive in practice: PLAN.md is only written via the DB-backed
|
|
1152
|
-
* `gsd_plan_slice` tool (which also inserts tasks), so a "stale PLAN.md with
|
|
1153
|
-
* is_sketch=1" is extremely unlikely to indicate anything other than the two
|
|
1154
|
-
* recovery scenarios above.
|
|
1139
|
+
* ADR-017 raw primitive: returns slice IDs in a milestone whose is_sketch flag
|
|
1140
|
+
* is still 1. The stale-sketch-flag drift handler at
|
|
1141
|
+
* `state-reconciliation/drift/sketch-flag.ts` composes this with PLAN.md
|
|
1142
|
+
* existence checks to detect drift, then writes via `setSliceSketchFlag`.
|
|
1155
1143
|
*/
|
|
1156
|
-
export function
|
|
1157
|
-
if (!currentDb) return;
|
|
1144
|
+
export function getSketchedSliceIds(milestoneId: string): string[] {
|
|
1145
|
+
if (!currentDb) return [];
|
|
1158
1146
|
const rows = currentDb.prepare(
|
|
1159
1147
|
`SELECT id FROM slices WHERE milestone_id = :mid AND is_sketch = 1`,
|
|
1160
1148
|
).all({ ":mid": milestoneId }) as Array<{ id: string }>;
|
|
1161
|
-
|
|
1162
|
-
if (hasPlanFile(row.id)) {
|
|
1163
|
-
setSliceSketchFlag(milestoneId, row.id, false);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1149
|
+
return rows.map((r) => r.id);
|
|
1166
1150
|
}
|
|
1167
1151
|
|
|
1168
1152
|
export function upsertSlicePlanning(milestoneId: string, sliceId: string, planning: Partial<SlicePlanningRecord>): void {
|
|
@@ -920,101 +920,10 @@ export function detectStaleRenders(basePath: string): StaleEntry[] {
|
|
|
920
920
|
}
|
|
921
921
|
|
|
922
922
|
// ─── Stale Repair ─────────────────────────────────────────────────────────
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
* For each stale entry, calls the appropriate render function:
|
|
928
|
-
* - Roadmap checkbox mismatches → renderRoadmapCheckboxes()
|
|
929
|
-
* - Plan checkbox mismatches → renderPlanCheckboxes()
|
|
930
|
-
* - Missing task summaries → renderTaskSummary()
|
|
931
|
-
* - Missing slice summaries/UATs → renderSliceSummary()
|
|
932
|
-
*
|
|
933
|
-
* Idempotent: calling twice with no DB changes produces zero repairs on the second call.
|
|
934
|
-
*
|
|
935
|
-
* @returns the number of files repaired
|
|
936
|
-
*/
|
|
937
|
-
export async function repairStaleRenders(basePath: string): Promise<number> {
|
|
938
|
-
const staleEntries = detectStaleRenders(basePath);
|
|
939
|
-
if (staleEntries.length === 0) return 0;
|
|
940
|
-
|
|
941
|
-
// Deduplicate: a single roadmap/plan file might appear multiple times
|
|
942
|
-
// (once per mismatched checkbox). We only need to re-render it once.
|
|
943
|
-
const repairedPaths = new Set<string>();
|
|
944
|
-
let repairCount = 0;
|
|
945
|
-
|
|
946
|
-
for (const entry of staleEntries) {
|
|
947
|
-
if (repairedPaths.has(entry.path)) continue;
|
|
948
|
-
// Normalize path separators for cross-platform regex matching
|
|
949
|
-
const normPath = entry.path.replace(/\\/g, "/");
|
|
950
|
-
|
|
951
|
-
try {
|
|
952
|
-
// Determine repair action from the reason
|
|
953
|
-
if (entry.reason.includes("in roadmap")) {
|
|
954
|
-
// Roadmap checkbox mismatch — extract milestone ID from path
|
|
955
|
-
const milestoneMatch = normPath.match(/milestones\/([^/]+)\//);
|
|
956
|
-
if (milestoneMatch) {
|
|
957
|
-
const ok = await renderRoadmapCheckboxes(basePath, milestoneMatch[1]);
|
|
958
|
-
if (ok) {
|
|
959
|
-
repairedPaths.add(entry.path);
|
|
960
|
-
repairCount++;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
} else if (entry.reason.includes("in plan")) {
|
|
964
|
-
// Plan checkbox mismatch — extract milestone + slice IDs from path
|
|
965
|
-
const pathMatch = normPath.match(/milestones\/([^/]+)\/slices\/([^/]+)\//);
|
|
966
|
-
if (pathMatch) {
|
|
967
|
-
const ok = await renderPlanCheckboxes(basePath, pathMatch[1], pathMatch[2]);
|
|
968
|
-
if (ok) {
|
|
969
|
-
repairedPaths.add(entry.path);
|
|
970
|
-
repairCount++;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
} else if (entry.reason.includes("SUMMARY.md missing") && entry.reason.match(/^T\d+/)) {
|
|
974
|
-
// Missing task summary — extract IDs from path
|
|
975
|
-
const pathMatch = normPath.match(/milestones\/([^/]+)\/slices\/([^/]+)\/tasks\//);
|
|
976
|
-
const taskMatch = entry.reason.match(/^(T\d+)/);
|
|
977
|
-
if (pathMatch && taskMatch) {
|
|
978
|
-
const ok = await renderTaskSummary(basePath, pathMatch[1], pathMatch[2], taskMatch[1]);
|
|
979
|
-
if (ok) {
|
|
980
|
-
repairedPaths.add(entry.path);
|
|
981
|
-
repairCount++;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
} else if (entry.reason.includes("SUMMARY.md missing") && entry.reason.match(/^S\d+/)) {
|
|
985
|
-
// Missing slice summary — extract IDs from path
|
|
986
|
-
const pathMatch = normPath.match(/milestones\/([^/]+)\/slices\/([^/]+)\//);
|
|
987
|
-
if (pathMatch) {
|
|
988
|
-
const ok = await renderSliceSummary(basePath, pathMatch[1], pathMatch[2]);
|
|
989
|
-
if (ok) {
|
|
990
|
-
repairedPaths.add(entry.path);
|
|
991
|
-
repairCount++;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
} else if (entry.reason.includes("UAT.md missing")) {
|
|
995
|
-
// Missing slice UAT — renderSliceSummary handles both SUMMARY + UAT
|
|
996
|
-
const pathMatch = normPath.match(/milestones\/([^/]+)\/slices\/([^/]+)\//);
|
|
997
|
-
if (pathMatch) {
|
|
998
|
-
const ok = await renderSliceSummary(basePath, pathMatch[1], pathMatch[2]);
|
|
999
|
-
if (ok) {
|
|
1000
|
-
repairedPaths.add(entry.path);
|
|
1001
|
-
repairCount++;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
} catch (err) {
|
|
1006
|
-
logWarning("renderer", `repair failed for ${entry.path}: ${(err as Error).message}`);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
if (repairCount > 0) {
|
|
1011
|
-
process.stderr.write(
|
|
1012
|
-
`markdown-renderer: repaired ${repairCount} stale render(s)\n`,
|
|
1013
|
-
);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
return repairCount;
|
|
1017
|
-
}
|
|
923
|
+
// Body relocated to state-reconciliation/drift/stale-render.ts (ADR-017 #5702).
|
|
924
|
+
// detectStaleRenders above stays as a useful diagnostic primitive; the
|
|
925
|
+
// drift handler composes it with the per-reason renderer dispatch and the
|
|
926
|
+
// reconcileBeforeDispatch lifecycle.
|
|
1018
927
|
|
|
1019
928
|
// ─── Replan & Assessment Renderers ────────────────────────────────────────
|
|
1020
929
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// File Purpose: ADR-015 Recovery Classification module for runtime failure taxonomy.
|
|
3
3
|
|
|
4
4
|
import { classifyError, isTransient, type ErrorClass } from "./error-classifier.js";
|
|
5
|
+
import { ReconciliationFailedError } from "./state-reconciliation.js";
|
|
5
6
|
|
|
6
7
|
export type RecoveryFailureKind =
|
|
7
8
|
| "tool-schema"
|
|
@@ -9,6 +10,7 @@ export type RecoveryFailureKind =
|
|
|
9
10
|
| "stale-worker"
|
|
10
11
|
| "worktree-invalid"
|
|
11
12
|
| "verification-drift"
|
|
13
|
+
| "reconciliation-drift"
|
|
12
14
|
| "provider"
|
|
13
15
|
| "runtime-unknown";
|
|
14
16
|
|
|
@@ -33,7 +35,13 @@ export interface RecoveryClassification {
|
|
|
33
35
|
|
|
34
36
|
export function classifyFailure(input: RecoveryClassificationInput): RecoveryClassification {
|
|
35
37
|
const message = errorMessage(input.error);
|
|
36
|
-
|
|
38
|
+
// ADR-017: ReconciliationFailedError is a typed throw from the State
|
|
39
|
+
// Reconciliation Module. Recognize it by class regardless of caller-supplied
|
|
40
|
+
// failureKind so the taxonomy stays consistent.
|
|
41
|
+
const failureKind =
|
|
42
|
+
input.error instanceof ReconciliationFailedError
|
|
43
|
+
? "reconciliation-drift"
|
|
44
|
+
: input.failureKind ?? inferFailureKind(message);
|
|
37
45
|
|
|
38
46
|
switch (failureKind) {
|
|
39
47
|
case "tool-schema":
|
|
@@ -76,6 +84,15 @@ export function classifyFailure(input: RecoveryClassificationInput): RecoveryCla
|
|
|
76
84
|
exitReason: "verification-drift",
|
|
77
85
|
remediation: "Inspect the verification artifact and reconcile the state snapshot before resuming.",
|
|
78
86
|
};
|
|
87
|
+
case "reconciliation-drift":
|
|
88
|
+
return {
|
|
89
|
+
failureKind,
|
|
90
|
+
action: "escalate",
|
|
91
|
+
reason: `Reconciliation drift${unitSuffix(input)}: ${message}`,
|
|
92
|
+
exitReason: "reconciliation-drift",
|
|
93
|
+
remediation:
|
|
94
|
+
"Inspect the persistent or repair-failed drift kinds reported by the State Reconciliation Module before resuming.",
|
|
95
|
+
};
|
|
79
96
|
case "provider": {
|
|
80
97
|
const providerClass = classifyError(message, input.retryAfterMs);
|
|
81
98
|
return {
|
|
@@ -597,6 +597,47 @@ export function isSessionLockProcessAlive(data: SessionLockData): boolean {
|
|
|
597
597
|
return isPidAlive(data.pid);
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
/**
|
|
601
|
+
* ADR-017 raw primitive: remove orphaned lock artifacts (lock dir + lock file)
|
|
602
|
+
* when the recorded PID is dead or no metadata is present. Mirrors the
|
|
603
|
+
* pre-flight cleanup logic in acquireSessionLock so the stale-worker drift
|
|
604
|
+
* handler can clear the orphan proactively without going through the full
|
|
605
|
+
* acquire path. No-op when the lock is held by an alive process.
|
|
606
|
+
*
|
|
607
|
+
* Returns true when artifacts were removed (drift was present).
|
|
608
|
+
*/
|
|
609
|
+
export function removeStaleSessionLock(basePath: string): boolean {
|
|
610
|
+
const lp = lockPath(basePath);
|
|
611
|
+
const gsdDir = gsdRoot(basePath);
|
|
612
|
+
const lockTarget = effectiveLockTarget(gsdDir);
|
|
613
|
+
const lockDir = lockTarget + ".lock";
|
|
614
|
+
|
|
615
|
+
const existingData = readExistingLockData(lp);
|
|
616
|
+
const isOrphan =
|
|
617
|
+
!existingData ||
|
|
618
|
+
(typeof existingData.pid === "number" && !isPidAlive(existingData.pid));
|
|
619
|
+
if (!isOrphan) return false;
|
|
620
|
+
|
|
621
|
+
let removed = false;
|
|
622
|
+
if (existsSync(lockDir)) {
|
|
623
|
+
try {
|
|
624
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
625
|
+
removed = true;
|
|
626
|
+
} catch {
|
|
627
|
+
/* best-effort */
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (existsSync(lp)) {
|
|
631
|
+
try {
|
|
632
|
+
unlinkSync(lp);
|
|
633
|
+
removed = true;
|
|
634
|
+
} catch {
|
|
635
|
+
/* best-effort */
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return removed;
|
|
639
|
+
}
|
|
640
|
+
|
|
600
641
|
/**
|
|
601
642
|
* Returns true if we currently hold a session lock for the given path.
|
|
602
643
|
*/
|