gsd-pi 2.32.0-dev.3d7932c → 2.32.0-dev.d9c9e0c
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/extensions/gsd/auto-dispatch.ts +40 -12
- package/dist/resources/extensions/gsd/auto.ts +85 -160
- package/dist/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
- package/dist/resources/extensions/gsd/tests/loop-regression.test.ts +839 -0
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +40 -12
- package/src/resources/extensions/gsd/auto.ts +85 -160
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +839 -0
|
@@ -65,6 +65,28 @@ export function resetRewriteCircuitBreaker(): void {
|
|
|
65
65
|
rewriteAttemptCount = 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Guard for accessing activeSlice/activeTask in dispatch rules.
|
|
70
|
+
* Returns a stop action if the expected ref is null (corrupt state).
|
|
71
|
+
*/
|
|
72
|
+
function requireSlice(state: GSDState): { sid: string; sTitle: string } | DispatchAction {
|
|
73
|
+
if (!state.activeSlice) {
|
|
74
|
+
return { action: "stop", reason: `Phase "${state.phase}" but no active slice — run /gsd doctor.`, level: "error" };
|
|
75
|
+
}
|
|
76
|
+
return { sid: state.activeSlice.id, sTitle: state.activeSlice.title };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function requireTask(state: GSDState): { sid: string; sTitle: string; tid: string; tTitle: string } | DispatchAction {
|
|
80
|
+
if (!state.activeSlice || !state.activeTask) {
|
|
81
|
+
return { action: "stop", reason: `Phase "${state.phase}" but no active slice/task — run /gsd doctor.`, level: "error" };
|
|
82
|
+
}
|
|
83
|
+
return { sid: state.activeSlice.id, sTitle: state.activeSlice.title, tid: state.activeTask.id, tTitle: state.activeTask.title };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isStopAction(v: unknown): v is DispatchAction {
|
|
87
|
+
return typeof v === "object" && v !== null && "action" in v;
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
// ─── Rules ────────────────────────────────────────────────────────────────
|
|
69
91
|
|
|
70
92
|
const DISPATCH_RULES: DispatchRule[] = [
|
|
@@ -93,8 +115,9 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
93
115
|
name: "summarizing → complete-slice",
|
|
94
116
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
95
117
|
if (state.phase !== "summarizing") return null;
|
|
96
|
-
const
|
|
97
|
-
|
|
118
|
+
const sliceRef = requireSlice(state);
|
|
119
|
+
if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
|
|
120
|
+
const { sid, sTitle } = sliceRef;
|
|
98
121
|
return {
|
|
99
122
|
action: "dispatch",
|
|
100
123
|
unitType: "complete-slice",
|
|
@@ -222,8 +245,9 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
222
245
|
if (state.phase !== "planning") return null;
|
|
223
246
|
// Phase skip: skip research when preference or profile says so
|
|
224
247
|
if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research) return null;
|
|
225
|
-
const
|
|
226
|
-
|
|
248
|
+
const sliceRef = requireSlice(state);
|
|
249
|
+
if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
|
|
250
|
+
const { sid, sTitle } = sliceRef;
|
|
227
251
|
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
228
252
|
if (researchFile) return null; // has research, fall through
|
|
229
253
|
// Skip slice research for S01 when milestone research already exists —
|
|
@@ -242,8 +266,9 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
242
266
|
name: "planning → plan-slice",
|
|
243
267
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
244
268
|
if (state.phase !== "planning") return null;
|
|
245
|
-
const
|
|
246
|
-
|
|
269
|
+
const sliceRef = requireSlice(state);
|
|
270
|
+
if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
|
|
271
|
+
const { sid, sTitle } = sliceRef;
|
|
247
272
|
return {
|
|
248
273
|
action: "dispatch",
|
|
249
274
|
unitType: "plan-slice",
|
|
@@ -256,8 +281,9 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
256
281
|
name: "replanning-slice → replan-slice",
|
|
257
282
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
258
283
|
if (state.phase !== "replanning-slice") return null;
|
|
259
|
-
const
|
|
260
|
-
|
|
284
|
+
const sliceRef = requireSlice(state);
|
|
285
|
+
if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
|
|
286
|
+
const { sid, sTitle } = sliceRef;
|
|
261
287
|
return {
|
|
262
288
|
action: "dispatch",
|
|
263
289
|
unitType: "replan-slice",
|
|
@@ -270,8 +296,9 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
270
296
|
name: "executing → execute-task (recover missing task plan → plan-slice)",
|
|
271
297
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
272
298
|
if (state.phase !== "executing" || !state.activeTask) return null;
|
|
273
|
-
const
|
|
274
|
-
|
|
299
|
+
const sliceRef = requireSlice(state);
|
|
300
|
+
if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
|
|
301
|
+
const { sid, sTitle } = sliceRef;
|
|
275
302
|
const tid = state.activeTask.id;
|
|
276
303
|
|
|
277
304
|
// Guard: if the slice plan exists but the individual task plan files are
|
|
@@ -296,8 +323,9 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
296
323
|
name: "executing → execute-task",
|
|
297
324
|
match: async ({ state, mid, basePath }) => {
|
|
298
325
|
if (state.phase !== "executing" || !state.activeTask) return null;
|
|
299
|
-
const
|
|
300
|
-
|
|
326
|
+
const sliceRef = requireSlice(state);
|
|
327
|
+
if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
|
|
328
|
+
const { sid, sTitle } = sliceRef;
|
|
301
329
|
const tid = state.activeTask.id;
|
|
302
330
|
const tTitle = state.activeTask.title;
|
|
303
331
|
|
|
@@ -215,52 +215,7 @@ export function shouldUseWorktreeIsolation(): boolean {
|
|
|
215
215
|
return true; // default: worktree
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
/** Pending verification retry — set when gate fails with retries remaining, consumed by dispatchNextUnit */
|
|
221
|
-
|
|
222
|
-
/** Verification retry count per unitId — separate from s.unitDispatchCount which tracks artifact-missing retries */
|
|
223
|
-
|
|
224
|
-
/** Session file path captured at pause — used to synthesize recovery briefing on resume */
|
|
225
|
-
|
|
226
|
-
/** Dashboard tracking */
|
|
227
|
-
|
|
228
|
-
/** Track dynamic routing decision for the current unit (for metrics) */
|
|
229
|
-
|
|
230
|
-
/** Queue of quick-task captures awaiting dispatch after triage resolution */
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Model captured at auto-mode start. Used to prevent model bleed between
|
|
234
|
-
* concurrent GSD instances sharing the same global settings.json (#650).
|
|
235
|
-
* When preferences don't specify a model for a unit type, this ensures
|
|
236
|
-
* the session's original model is re-applied instead of reading from
|
|
237
|
-
* the shared global settings (which another instance may have overwritten).
|
|
238
|
-
*/
|
|
239
|
-
|
|
240
|
-
/** Track current milestone to detect transitions */
|
|
241
|
-
|
|
242
|
-
/** Model the user had selected before auto-mode started */
|
|
243
|
-
|
|
244
|
-
/** Progress-aware timeout supervision */
|
|
245
|
-
|
|
246
|
-
/** Context-pressure continue-here monitor — fires once when context usage >= 70% */
|
|
247
|
-
|
|
248
|
-
/** Dispatch gap watchdog — detects when the state machine stalls between units.
|
|
249
|
-
* After handleAgentEnd completes, if auto-mode is still active but no new unit
|
|
250
|
-
* has been dispatched (sendMessage not called), this timer fires to force a
|
|
251
|
-
* re-evaluation. Covers the case where dispatchNextUnit silently fails or
|
|
252
|
-
* an unhandled error kills the dispatch chain. */
|
|
253
|
-
|
|
254
|
-
/** Prompt character measurement for token savings analysis (R051). */
|
|
255
|
-
|
|
256
|
-
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Tool calls currently being executed — prevents false idle detection during long-running tools.
|
|
260
|
-
* Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
|
|
261
|
-
* running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
|
|
262
|
-
*/
|
|
263
|
-
|
|
218
|
+
// All mutable state lives in AutoSession (auto/session.ts) — see encapsulation invariant above.
|
|
264
219
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
265
220
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
266
221
|
s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
|
|
@@ -406,6 +361,79 @@ function buildSnapshotOpts(unitType: string, unitId: string): { continueHereFire
|
|
|
406
361
|
};
|
|
407
362
|
}
|
|
408
363
|
|
|
364
|
+
// ─── Extracted Merge Helper ───────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Attempt to merge the current milestone branch to main.
|
|
368
|
+
* Handles both worktree and branch isolation modes with a single code path.
|
|
369
|
+
* Returns true if merge succeeded, false on error (non-fatal, logged).
|
|
370
|
+
*
|
|
371
|
+
* Extracted from 4 duplicate merge blocks in dispatchNextUnit to eliminate
|
|
372
|
+
* the bug factory where fixing one copy didn't fix the others (#1308).
|
|
373
|
+
*/
|
|
374
|
+
function tryMergeMilestone(ctx: ExtensionContext, milestoneId: string, mode: "transition" | "complete"): boolean {
|
|
375
|
+
const isolationMode = getIsolationMode();
|
|
376
|
+
|
|
377
|
+
// Worktree merge path
|
|
378
|
+
if (isInAutoWorktree(s.basePath) && s.originalBasePath) {
|
|
379
|
+
try {
|
|
380
|
+
const roadmapPath = resolveMilestoneFile(s.originalBasePath, milestoneId, "ROADMAP");
|
|
381
|
+
if (!roadmapPath) {
|
|
382
|
+
teardownAutoWorktree(s.originalBasePath, milestoneId);
|
|
383
|
+
ctx.ui.notify(`Exited worktree for ${milestoneId} (no roadmap for merge).`, "info");
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
387
|
+
const mergeResult = mergeMilestoneToMain(s.originalBasePath, milestoneId, roadmapContent);
|
|
388
|
+
s.basePath = s.originalBasePath;
|
|
389
|
+
s.gitService = createGitService(s.basePath);
|
|
390
|
+
ctx.ui.notify(
|
|
391
|
+
`Milestone ${milestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
392
|
+
"info",
|
|
393
|
+
);
|
|
394
|
+
return true;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
ctx.ui.notify(
|
|
397
|
+
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
398
|
+
"warning",
|
|
399
|
+
);
|
|
400
|
+
if (s.originalBasePath) {
|
|
401
|
+
s.basePath = s.originalBasePath;
|
|
402
|
+
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Branch-mode merge path
|
|
409
|
+
if (isolationMode === "branch") {
|
|
410
|
+
try {
|
|
411
|
+
const currentBranch = getCurrentBranch(s.basePath);
|
|
412
|
+
const milestoneBranch = autoWorktreeBranch(milestoneId);
|
|
413
|
+
if (currentBranch === milestoneBranch) {
|
|
414
|
+
const roadmapPath = resolveMilestoneFile(s.basePath, milestoneId, "ROADMAP");
|
|
415
|
+
if (roadmapPath) {
|
|
416
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
417
|
+
const mergeResult = mergeMilestoneToMain(s.basePath, milestoneId, roadmapContent);
|
|
418
|
+
s.gitService = createGitService(s.basePath);
|
|
419
|
+
ctx.ui.notify(
|
|
420
|
+
`Milestone ${milestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
421
|
+
"info",
|
|
422
|
+
);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
ctx.ui.notify(
|
|
428
|
+
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
|
429
|
+
"warning",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
409
437
|
/**
|
|
410
438
|
* Start a watchdog that fires if no new unit is dispatched within DISPATCH_GAP_TIMEOUT_MS
|
|
411
439
|
* after handleAgentEnd completes. This catches the case where the dispatch chain silently
|
|
@@ -1107,32 +1135,14 @@ async function dispatchNextUnit(
|
|
|
1107
1135
|
} catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
|
|
1108
1136
|
|
|
1109
1137
|
// ── Worktree lifecycle on milestone transition (#616) ──
|
|
1110
|
-
if (isInAutoWorktree(s.basePath)
|
|
1111
|
-
|
|
1112
|
-
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1113
|
-
if (roadmapPath) {
|
|
1114
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1115
|
-
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1116
|
-
ctx.ui.notify(
|
|
1117
|
-
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1118
|
-
"info",
|
|
1119
|
-
);
|
|
1120
|
-
} else {
|
|
1121
|
-
teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId);
|
|
1122
|
-
ctx.ui.notify(`Exited worktree for ${ s.currentMilestoneId } (no roadmap for merge).`, "info");
|
|
1123
|
-
}
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
ctx.ui.notify(
|
|
1126
|
-
`Milestone merge failed during transition: ${getErrorMessage(err)}`,
|
|
1127
|
-
"warning",
|
|
1128
|
-
);
|
|
1129
|
-
if (s.originalBasePath) {
|
|
1130
|
-
try { process.chdir(s.originalBasePath); } catch { /* best-effort */ }
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1138
|
+
if ((isInAutoWorktree(s.basePath) || getIsolationMode() === "branch") && shouldUseWorktreeIsolation()) {
|
|
1139
|
+
tryMergeMilestone(ctx, s.currentMilestoneId, "transition");
|
|
1133
1140
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1141
|
+
// Reset to project root and re-derive state for the new milestone
|
|
1142
|
+
if (s.originalBasePath) {
|
|
1143
|
+
s.basePath = s.originalBasePath;
|
|
1144
|
+
s.gitService = createGitService(s.basePath);
|
|
1145
|
+
}
|
|
1136
1146
|
invalidateAllCaches();
|
|
1137
1147
|
|
|
1138
1148
|
state = await deriveState(s.basePath);
|
|
@@ -1177,51 +1187,8 @@ async function dispatchNextUnit(
|
|
|
1177
1187
|
const incomplete = (state.registry ?? []).filter(m => m.status !== "complete" && m.status !== "parked");
|
|
1178
1188
|
if (incomplete.length === 0) {
|
|
1179
1189
|
// Genuinely all complete (parked milestones excluded) — merge milestone branch to main before stopping (#962)
|
|
1180
|
-
if (s.currentMilestoneId
|
|
1181
|
-
|
|
1182
|
-
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1183
|
-
if (roadmapPath) {
|
|
1184
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1185
|
-
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1186
|
-
s.basePath = s.originalBasePath;
|
|
1187
|
-
s.gitService = createGitService(s.basePath);
|
|
1188
|
-
ctx.ui.notify(
|
|
1189
|
-
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1190
|
-
"info",
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
} catch (err) {
|
|
1194
|
-
ctx.ui.notify(
|
|
1195
|
-
`Milestone merge failed: ${getErrorMessage(err)}`,
|
|
1196
|
-
"warning",
|
|
1197
|
-
);
|
|
1198
|
-
if (s.originalBasePath) {
|
|
1199
|
-
s.basePath = s.originalBasePath;
|
|
1200
|
-
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() === "branch") {
|
|
1204
|
-
try {
|
|
1205
|
-
const currentBranch = getCurrentBranch(s.basePath);
|
|
1206
|
-
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
1207
|
-
if (currentBranch === milestoneBranch) {
|
|
1208
|
-
const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
|
|
1209
|
-
if (roadmapPath) {
|
|
1210
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1211
|
-
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1212
|
-
s.gitService = createGitService(s.basePath);
|
|
1213
|
-
ctx.ui.notify(
|
|
1214
|
-
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1215
|
-
"info",
|
|
1216
|
-
);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
} catch (err) {
|
|
1220
|
-
ctx.ui.notify(
|
|
1221
|
-
`Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
|
|
1222
|
-
"warning",
|
|
1223
|
-
);
|
|
1224
|
-
}
|
|
1190
|
+
if (s.currentMilestoneId) {
|
|
1191
|
+
tryMergeMilestone(ctx, s.currentMilestoneId, "complete");
|
|
1225
1192
|
}
|
|
1226
1193
|
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
1227
1194
|
await stopAuto(ctx, pi, "All milestones complete");
|
|
@@ -1280,50 +1247,8 @@ async function dispatchNextUnit(
|
|
|
1280
1247
|
s.completedKeySet.clear();
|
|
1281
1248
|
} catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
|
|
1282
1249
|
// ── Milestone merge ──
|
|
1283
|
-
if (s.currentMilestoneId
|
|
1284
|
-
|
|
1285
|
-
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
|
1286
|
-
if (!roadmapPath) throw new GSDError(GSD_ARTIFACT_MISSING, `Cannot resolve ROADMAP file for milestone ${ s.currentMilestoneId }`);
|
|
1287
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1288
|
-
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1289
|
-
s.basePath = s.originalBasePath;
|
|
1290
|
-
s.gitService = createGitService(s.basePath);
|
|
1291
|
-
ctx.ui.notify(
|
|
1292
|
-
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1293
|
-
"info",
|
|
1294
|
-
);
|
|
1295
|
-
} catch (err) {
|
|
1296
|
-
ctx.ui.notify(
|
|
1297
|
-
`Milestone merge failed: ${getErrorMessage(err)}`,
|
|
1298
|
-
"warning",
|
|
1299
|
-
);
|
|
1300
|
-
if (s.originalBasePath) {
|
|
1301
|
-
s.basePath = s.originalBasePath;
|
|
1302
|
-
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() === "branch") {
|
|
1306
|
-
try {
|
|
1307
|
-
const currentBranch = getCurrentBranch(s.basePath);
|
|
1308
|
-
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
1309
|
-
if (currentBranch === milestoneBranch) {
|
|
1310
|
-
const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
|
|
1311
|
-
if (roadmapPath) {
|
|
1312
|
-
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1313
|
-
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1314
|
-
s.gitService = createGitService(s.basePath);
|
|
1315
|
-
ctx.ui.notify(
|
|
1316
|
-
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1317
|
-
"info",
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
} catch (err) {
|
|
1322
|
-
ctx.ui.notify(
|
|
1323
|
-
`Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
|
|
1324
|
-
"warning",
|
|
1325
|
-
);
|
|
1326
|
-
}
|
|
1250
|
+
if (s.currentMilestoneId) {
|
|
1251
|
+
tryMergeMilestone(ctx, s.currentMilestoneId, "complete");
|
|
1327
1252
|
}
|
|
1328
1253
|
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
1329
1254
|
await stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
@@ -70,31 +70,34 @@ test("auto.ts 'all milestones complete' path merges before stopping (#962)", ()
|
|
|
70
70
|
const incompleteIdx = autoSrc.indexOf("incomplete.length === 0");
|
|
71
71
|
assert.ok(incompleteIdx > -1, "auto.ts should have 'incomplete.length === 0' check");
|
|
72
72
|
|
|
73
|
-
// The merge call must appear BETWEEN the incomplete check and the stopAuto call
|
|
74
|
-
//
|
|
73
|
+
// The merge call must appear BETWEEN the incomplete check and the stopAuto call.
|
|
74
|
+
// After the #1308 refactor, the merge is delegated to tryMergeMilestone.
|
|
75
75
|
const blockAfterIncomplete = autoSrc.slice(incompleteIdx, incompleteIdx + 3000);
|
|
76
76
|
|
|
77
77
|
assert.ok(
|
|
78
|
-
blockAfterIncomplete.includes("
|
|
79
|
-
"auto.ts should call
|
|
78
|
+
blockAfterIncomplete.includes("tryMergeMilestone"),
|
|
79
|
+
"auto.ts should call tryMergeMilestone in the 'all milestones complete' path",
|
|
80
80
|
);
|
|
81
81
|
|
|
82
82
|
// The merge should come before stopAuto in this block
|
|
83
|
-
const mergePos = blockAfterIncomplete.indexOf("
|
|
83
|
+
const mergePos = blockAfterIncomplete.indexOf("tryMergeMilestone");
|
|
84
84
|
const stopPos = blockAfterIncomplete.indexOf("stopAuto");
|
|
85
85
|
assert.ok(
|
|
86
86
|
mergePos < stopPos,
|
|
87
|
-
"
|
|
87
|
+
"tryMergeMilestone should be called before stopAuto in the 'all complete' path",
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
-
//
|
|
90
|
+
// Verify tryMergeMilestone handles both worktree and branch isolation
|
|
91
|
+
const helperIdx = autoSrc.indexOf("function tryMergeMilestone");
|
|
92
|
+
assert.ok(helperIdx > -1, "tryMergeMilestone helper should exist");
|
|
93
|
+
const helperBlock = autoSrc.slice(helperIdx, helperIdx + 2000);
|
|
91
94
|
assert.ok(
|
|
92
|
-
|
|
93
|
-
"should check isInAutoWorktree for worktree mode",
|
|
95
|
+
helperBlock.includes("isInAutoWorktree"),
|
|
96
|
+
"tryMergeMilestone should check isInAutoWorktree for worktree mode",
|
|
94
97
|
);
|
|
95
98
|
assert.ok(
|
|
96
|
-
|
|
97
|
-
"should check
|
|
99
|
+
helperBlock.includes("getIsolationMode") || helperBlock.includes("isolationMode"),
|
|
100
|
+
"tryMergeMilestone should check isolation mode for branch mode",
|
|
98
101
|
);
|
|
99
102
|
});
|
|
100
103
|
|