gsd-pi 2.67.0-dev.5399650 → 2.67.0-dev.6fc2289

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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -70
  3. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-start.js +16 -30
  6. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  7. package/dist/resources/extensions/gsd/auto.js +94 -59
  8. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  9. package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
  10. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
  11. package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
  12. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
  13. package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
  14. package/dist/resources/extensions/gsd/doctor.js +8 -4
  15. package/dist/resources/extensions/gsd/guided-flow.js +40 -31
  16. package/dist/resources/extensions/gsd/init-wizard.js +15 -12
  17. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  18. package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
  19. package/dist/resources/extensions/gsd/workflow-mcp.js +64 -24
  20. package/dist/web/standalone/.next/BUILD_ID +1 -1
  21. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  22. package/dist/web/standalone/.next/build-manifest.json +3 -3
  23. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  24. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  25. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  52. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  55. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  56. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  57. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  58. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  59. package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  60. package/package.json +1 -1
  61. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  62. package/packages/mcp-server/dist/workflow-tools.js +10 -4
  63. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  64. package/packages/mcp-server/src/workflow-tools.ts +13 -2
  65. package/packages/pi-agent-core/dist/agent-loop.js +14 -6
  66. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  67. package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
  68. package/packages/pi-agent-core/src/agent-loop.ts +20 -6
  69. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  70. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  72. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  74. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  76. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  78. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  80. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -12
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  101. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  102. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  103. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
  104. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
  105. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
  106. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  107. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -15
  108. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +190 -93
  109. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +89 -116
  110. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  111. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  112. package/src/resources/extensions/gsd/auto-start.ts +23 -55
  113. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  114. package/src/resources/extensions/gsd/auto.ts +104 -63
  115. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  116. package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
  117. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
  118. package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
  119. package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
  120. package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
  121. package/src/resources/extensions/gsd/doctor.ts +9 -5
  122. package/src/resources/extensions/gsd/guided-flow.ts +42 -36
  123. package/src/resources/extensions/gsd/init-wizard.ts +17 -11
  124. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  125. package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
  126. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  127. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  128. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  129. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  130. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  131. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  132. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  133. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
  134. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
  135. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  136. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  137. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  138. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
  139. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
  140. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  141. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +178 -17
  142. package/src/resources/extensions/gsd/workflow-mcp.ts +76 -23
  143. package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
  144. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
  145. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
  147. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
  148. package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
  149. package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +0 -121
  150. /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → yh2vT27L1E6PChb_C1N_F}/_buildManifest.js +0 -0
  151. /package/dist/web/standalone/.next/static/{6_QPFhgX0DQnDhhquheRc → yh2vT27L1E6PChb_C1N_F}/_ssgManifest.js +0 -0
@@ -19,6 +19,11 @@ import type {
19
19
  import { deriveState } from "./state.js";
20
20
  import { parseUnitId } from "./unit-id.js";
21
21
  import type { GSDState } from "./types.js";
22
+ import {
23
+ assessInterruptedSession,
24
+ readPausedSessionMetadata,
25
+ type InterruptedSessionAssessment,
26
+ } from "./interrupted-session.js";
22
27
  import { getManifestStatus } from "./files.js";
23
28
  export { inlinePriorMilestoneSummary } from "./files.js";
24
29
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
@@ -46,6 +51,7 @@ import {
46
51
  clearLock,
47
52
  readCrashLock,
48
53
  isLockProcessAlive,
54
+ formatCrashInfo,
49
55
  } from "./crash-recovery.js";
50
56
  import {
51
57
  acquireSessionLock,
@@ -118,6 +124,7 @@ import {
118
124
  formatTokenCount,
119
125
  } from "./metrics.js";
120
126
  import { setLogBasePath, logWarning, logError } from "./workflow-logger.js";
127
+ import { homedir } from "node:os";
121
128
  import { join } from "node:path";
122
129
  import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
123
130
  import { atomicWriteSync } from "./atomic-write.js";
@@ -920,6 +927,8 @@ export async function pauseAuto(
920
927
  stepMode: s.stepMode,
921
928
  pausedAt: new Date().toISOString(),
922
929
  sessionFile: s.pausedSessionFile,
930
+ unitType: s.currentUnit?.type ?? undefined,
931
+ unitId: s.currentUnit?.id ?? undefined,
923
932
  activeEngineId: s.activeEngineId,
924
933
  activeRunDir: s.activeRunDir,
925
934
  autoStartTime: s.autoStartTime,
@@ -1141,7 +1150,10 @@ export async function startAuto(
1141
1150
  pi: ExtensionAPI,
1142
1151
  base: string,
1143
1152
  verboseMode: boolean,
1144
- options?: { step?: boolean },
1153
+ options?: {
1154
+ step?: boolean;
1155
+ interrupted?: InterruptedSessionAssessment;
1156
+ },
1145
1157
  ): Promise<void> {
1146
1158
  if (s.active) {
1147
1159
  debugLog("startAuto", { phase: "already-active", skipping: true });
@@ -1149,41 +1161,60 @@ export async function startAuto(
1149
1161
  }
1150
1162
 
1151
1163
  const requestedStepMode = options?.step ?? false;
1164
+ const interruptedAssessment = options?.interrupted ?? null;
1152
1165
 
1153
1166
  // Escape stale worktree cwd from a previous milestone (#608).
1154
1167
  base = escapeStaleWorktree(base);
1155
1168
 
1169
+ const freshStartAssessment = interruptedAssessment
1170
+ ?? await assessInterruptedSession(base);
1171
+
1172
+ if (freshStartAssessment.classification === "running") {
1173
+ const pid = freshStartAssessment.lock?.pid;
1174
+ ctx.ui.notify(
1175
+ pid
1176
+ ? `Another auto-mode session (PID ${pid}) appears to be running.\nStop it with \`kill ${pid}\` before starting a new session.`
1177
+ : "Another auto-mode session appears to be running.",
1178
+ "error",
1179
+ );
1180
+ return;
1181
+ }
1182
+
1156
1183
  // If resuming from paused state, just re-activate and dispatch next unit.
1157
1184
  // Check persisted paused-session first (#1383) — survives /exit.
1158
1185
  if (!s.paused) {
1159
1186
  try {
1187
+ const meta = freshStartAssessment.pausedSession ?? readPausedSessionMetadata(base);
1160
1188
  const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
1161
- if (existsSync(pausedPath)) {
1162
- const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
1163
- if (meta.activeEngineId && meta.activeEngineId !== "dev") {
1164
- // Custom workflow resume — restore engine state
1165
- s.activeEngineId = meta.activeEngineId;
1166
- s.activeRunDir = meta.activeRunDir ?? null;
1167
- s.originalBasePath = meta.originalBasePath || base;
1168
- s.stepMode = meta.stepMode ?? requestedStepMode;
1169
- s.autoStartTime = meta.autoStartTime || Date.now();
1170
- s.paused = true;
1171
- // Don't delete pause file yet defer until lock is acquired.
1172
- // If lock fails, the file must survive for retry.
1173
- s.pausedSessionFile = pausedPath;
1174
- ctx.ui.notify(
1175
- `Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`,
1176
- "info",
1189
+ if (meta?.activeEngineId && meta.activeEngineId !== "dev") {
1190
+ // Custom workflow resume — restore engine state
1191
+ s.activeEngineId = meta.activeEngineId;
1192
+ s.activeRunDir = meta.activeRunDir ?? null;
1193
+ s.originalBasePath = meta.originalBasePath || base;
1194
+ s.stepMode = meta.stepMode ?? requestedStepMode;
1195
+ s.autoStartTime = meta.autoStartTime || Date.now();
1196
+ s.paused = true;
1197
+ try { unlinkSync(pausedPath); } catch (e) { logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" }); }
1198
+ ctx.ui.notify(
1199
+ `Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`,
1200
+ "info",
1201
+ );
1202
+ } else if (meta?.milestoneId) {
1203
+ const shouldResumePausedSession =
1204
+ freshStartAssessment.classification === "recoverable"
1205
+ && (
1206
+ freshStartAssessment.hasResumableDiskState
1207
+ || !!freshStartAssessment.recoveryPrompt
1208
+ || !!freshStartAssessment.lock
1177
1209
  );
1178
- } else if (meta.milestoneId) {
1210
+ if (shouldResumePausedSession) {
1179
1211
  // Validate the milestone still exists and isn't already complete (#1664).
1180
1212
  const mDir = resolveMilestonePath(base, meta.milestoneId);
1181
1213
  const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
1182
1214
  if (!mDir || summaryFile) {
1183
- // Stale milestone clean up and fall through to fresh bootstrap
1184
- try { unlinkSync(pausedPath); } catch (err) { /* non-fatal */
1185
- logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1186
- }
1215
+ try { unlinkSync(pausedPath); } catch (err) {
1216
+ logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1217
+ }
1187
1218
  ctx.ui.notify(
1188
1219
  `Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`,
1189
1220
  "info",
@@ -1192,22 +1223,54 @@ export async function startAuto(
1192
1223
  s.currentMilestoneId = meta.milestoneId;
1193
1224
  s.originalBasePath = meta.originalBasePath || base;
1194
1225
  s.stepMode = meta.stepMode ?? requestedStepMode;
1226
+ s.pausedSessionFile = meta.sessionFile ?? null;
1227
+ s.pausedUnitType = meta.unitType ?? null;
1228
+ s.pausedUnitId = meta.unitId ?? null;
1195
1229
  s.autoStartTime = meta.autoStartTime || Date.now();
1196
1230
  s.paused = true;
1197
- // Don't delete pause file yet defer until lock is acquired.
1198
- // If lock fails, the file must survive for retry.
1199
- s.pausedSessionFile = pausedPath;
1231
+ try { unlinkSync(pausedPath); } catch (e) { logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" }); }
1200
1232
  ctx.ui.notify(
1201
- `Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`,
1233
+ `Resuming paused session for ${meta.milestoneId}${meta.worktreePath && existsSync(meta.worktreePath) ? ` (worktree)` : ""}.`,
1202
1234
  "info",
1203
1235
  );
1204
1236
  }
1237
+ } else if (existsSync(pausedPath)) {
1238
+ try { unlinkSync(pausedPath); } catch (e) { logWarning("session", `stale pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" }); }
1205
1239
  }
1206
1240
  }
1207
1241
  } catch (err) {
1208
1242
  // Malformed or missing — proceed with fresh bootstrap
1209
1243
  logWarning("session", `paused-session restore failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1210
1244
  }
1245
+ // Guard against zero/missing autoStartTime after resume (#3585)
1246
+ if (!s.autoStartTime || s.autoStartTime <= 0) s.autoStartTime = Date.now();
1247
+ }
1248
+
1249
+ if (!s.paused) {
1250
+ s.stepMode = requestedStepMode;
1251
+ }
1252
+
1253
+ if (freshStartAssessment.lock) {
1254
+ clearLock(base);
1255
+ }
1256
+
1257
+ if (!s.paused) {
1258
+ s.pendingCrashRecovery =
1259
+ freshStartAssessment.classification === "recoverable"
1260
+ ? freshStartAssessment.recoveryPrompt
1261
+ : null;
1262
+
1263
+ if (freshStartAssessment.classification === "recoverable" && freshStartAssessment.lock) {
1264
+ const info = formatCrashInfo(freshStartAssessment.lock);
1265
+ if (freshStartAssessment.recoveryToolCallCount > 0) {
1266
+ ctx.ui.notify(
1267
+ `${info}\nRecovered ${freshStartAssessment.recoveryToolCallCount} tool calls from crashed session. Resuming with full context.`,
1268
+ "warning",
1269
+ );
1270
+ } else if (freshStartAssessment.hasResumableDiskState) {
1271
+ ctx.ui.notify(`${info}\nResuming from disk state.`, "warning");
1272
+ }
1273
+ }
1211
1274
  }
1212
1275
 
1213
1276
  if (s.paused) {
@@ -1232,26 +1295,19 @@ export async function startAuto(
1232
1295
  s.active = true;
1233
1296
  s.verbose = verboseMode;
1234
1297
  s.stepMode = requestedStepMode;
1235
- // Preserve the original cmdCtx (ExtensionCommandContext with newSession)
1236
- // when resuming from a provider-error pause. The resume callback receives
1237
- // an ExtensionContext (from the agent_end hook) which lacks newSession —
1238
- // using it would crash runUnit with "newSession is not a function".
1239
- // Only override if the new ctx actually has newSession (user-initiated resume).
1240
- if ("newSession" in ctx && typeof (ctx as any).newSession === "function") {
1241
- s.cmdCtx = ctx;
1242
- } else if (!s.cmdCtx) {
1243
- // No saved cmdCtx — this shouldn't happen, but handle gracefully
1244
- s.cmdCtx = ctx as ExtensionCommandContext;
1245
- }
1246
- // else: keep existing s.cmdCtx which has the real newSession
1298
+ s.cmdCtx = ctx;
1247
1299
  s.basePath = base;
1248
- setLogBasePath(base);
1249
- if (!s.autoStartTime || s.autoStartTime <= 0) s.autoStartTime = Date.now();
1250
1300
  s.unitDispatchCount.clear();
1251
1301
  s.unitLifetimeDispatches.clear();
1252
1302
  if (!getLedger()) initMetrics(base);
1253
1303
  if (s.currentMilestoneId) setActiveMilestoneId(base, s.currentMilestoneId);
1254
1304
 
1305
+ // Re-register health level notification callback lost across process restart
1306
+ setLevelChangeCallback((_from, to, summary) => {
1307
+ const level = to === "red" ? "error" : to === "yellow" ? "warning" : "info";
1308
+ ctx.ui.notify(summary, level as "info" | "warning" | "error");
1309
+ });
1310
+
1255
1311
  // ── Auto-worktree: re-enter worktree on resume ──
1256
1312
  if (
1257
1313
  s.currentMilestoneId &&
@@ -1275,6 +1331,11 @@ export async function startAuto(
1275
1331
  "info",
1276
1332
  );
1277
1333
  restoreHookState(s.basePath);
1334
+ // Re-sync managed resources on resume so long-lived auto sessions pick up
1335
+ // bundled extension updates before resume-time verification/state logic runs.
1336
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(process.env.GSD_HOME || homedir(), ".gsd", "agent");
1337
+ const { initResources } = await import("../../../" + "resource-loader.js");
1338
+ initResources(agentDir);
1278
1339
  // Open the project DB before rebuild/derive so resume uses DB-backed
1279
1340
  // state instead of falling back to stale markdown parsing (#2940).
1280
1341
  await openProjectDbIfPresent(s.basePath);
@@ -1305,8 +1366,8 @@ export async function startAuto(
1305
1366
  const activityDir = join(gsdRoot(s.basePath), "activity");
1306
1367
  const recovery = synthesizeCrashRecovery(
1307
1368
  s.basePath,
1308
- s.currentUnit?.type ?? "unknown",
1309
- s.currentUnit?.id ?? "unknown",
1369
+ s.currentUnit?.type ?? s.pausedUnitType ?? "unknown",
1370
+ s.currentUnit?.id ?? s.pausedUnitId ?? "unknown",
1310
1371
  s.pausedSessionFile ?? undefined,
1311
1372
  activityDir,
1312
1373
  );
@@ -1354,6 +1415,7 @@ export async function startAuto(
1354
1415
  verboseMode,
1355
1416
  requestedStepMode,
1356
1417
  bootstrapDeps,
1418
+ freshStartAssessment,
1357
1419
  );
1358
1420
  if (!ready) return;
1359
1421
 
@@ -1467,27 +1529,6 @@ function ensurePreconditions(
1467
1529
  }
1468
1530
  }
1469
1531
 
1470
- // ─── Diagnostics ──────────────────────────────────────────────────────────────
1471
-
1472
- /** Build recovery context from module state for recoverTimedOutUnit */
1473
- function buildRecoveryContext(): import("./auto-timeout-recovery.js").RecoveryContext {
1474
- return {
1475
- basePath: s.basePath,
1476
- verbose: s.verbose,
1477
- currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(),
1478
- unitRecoveryCount: s.unitRecoveryCount,
1479
- };
1480
- }
1481
-
1482
- /**
1483
- * Test-only: expose skip-loop state for unit tests.
1484
- * Not part of the public API.
1485
- */
1486
-
1487
- /**
1488
- * Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
1489
- * Used for manual hook triggers via /gsd run-hook.
1490
- */
1491
1532
  export async function dispatchHookUnit(
1492
1533
  ctx: ExtensionContext,
1493
1534
  pi: ExtensionAPI,
@@ -168,7 +168,7 @@ export async function buildBeforeAgentStartResult(
168
168
  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
169
169
 
170
170
  // Re-inject forensics context on follow-up turns (#2941)
171
- const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd()) : null;
171
+ const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd(), event.prompt) : null;
172
172
 
173
173
  const worktreeBlock = buildWorktreeContextBlock();
174
174
  const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
@@ -481,7 +481,7 @@ function oneLine(text: string): string {
481
481
  * Check for an active forensics session and return the prompt content
482
482
  * so it can be re-injected on follow-up turns.
483
483
  */
484
- function buildForensicsContextInjection(basePath: string): string | null {
484
+ export function buildForensicsContextInjection(basePath: string, prompt: string): string | null {
485
485
  const marker = readForensicsMarker(basePath);
486
486
  if (!marker) return null;
487
487
 
@@ -492,6 +492,12 @@ function buildForensicsContextInjection(basePath: string): string | null {
492
492
  return null;
493
493
  }
494
494
 
495
+ const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
496
+ if (trimmed && !RESUME_INTENT_PATTERNS.test(trimmed)) {
497
+ clearForensicsMarker(basePath);
498
+ return null;
499
+ }
500
+
495
501
  return marker.promptContent;
496
502
  }
497
503
 
@@ -70,7 +70,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
70
70
  { cmd: "templates", desc: "List available workflow templates" },
71
71
  { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
72
72
  { cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" },
73
- { cmd: "mcp", desc: "MCP server status and connectivity check (status, check <server>)" },
73
+ { cmd: "mcp", desc: "MCP server status, connectivity, and local config bootstrap (status, check, init)" },
74
74
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
75
75
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
76
76
  { cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
@@ -201,6 +201,7 @@ const NESTED_COMPLETIONS: CompletionMap = {
201
201
  mcp: [
202
202
  { cmd: "status", desc: "Show all MCP server statuses (default)" },
203
203
  { cmd: "check", desc: "Detailed status for a specific server" },
204
+ { cmd: "init", desc: "Write .mcp.json for the local GSD workflow MCP server" },
204
205
  ],
205
206
  doctor: [
206
207
  { cmd: "fix", desc: "Auto-fix detected issues" },
@@ -60,7 +60,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
60
60
  " /gsd hooks Show post-unit hook configuration",
61
61
  " /gsd extensions Manage extensions [list|enable|disable|info]",
62
62
  " /gsd fast Toggle OpenAI service tier [on|off|flex|status]",
63
- " /gsd mcp MCP server status and connectivity [status|check <server>]",
63
+ " /gsd mcp MCP server status and connectivity [status|check <server>|init [dir]]",
64
64
  "",
65
65
  "MAINTENANCE",
66
66
  " /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",
@@ -7,12 +7,15 @@
7
7
  * /gsd mcp — Overview of all servers (alias: /gsd mcp status)
8
8
  * /gsd mcp status — Same as bare /gsd mcp
9
9
  * /gsd mcp check <srv> — Detailed status for a specific server
10
+ * /gsd mcp init [dir] — Write project-local GSD workflow MCP config
10
11
  */
11
12
 
12
13
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
13
14
 
14
15
  import { existsSync, readFileSync } from "node:fs";
15
- import { join } from "node:path";
16
+ import { join, resolve } from "node:path";
17
+
18
+ import { ensureProjectWorkflowMcpConfig } from "./mcp-project-config.js";
16
19
 
17
20
  // ─── Types ──────────────────────────────────────────────────────────────────
18
21
 
@@ -28,6 +31,28 @@ export interface McpServerDetail extends McpServerStatus {
28
31
  tools: string[];
29
32
  }
30
33
 
34
+ export function formatMcpInitResult(
35
+ status: "created" | "updated" | "unchanged",
36
+ configPath: string,
37
+ targetPath: string,
38
+ ): string {
39
+ const summary =
40
+ status === "created"
41
+ ? "Created project MCP config."
42
+ : status === "updated"
43
+ ? "Updated project MCP config."
44
+ : "Project MCP config is already up to date.";
45
+
46
+ return [
47
+ summary,
48
+ "",
49
+ `Project: ${targetPath}`,
50
+ `Config: ${configPath}`,
51
+ "",
52
+ "Claude Code can now load the GSD workflow MCP server from this folder.",
53
+ ].join("\n");
54
+ }
55
+
31
56
  // ─── Config reader (standalone — does not import mcp-client internals) ──────
32
57
 
33
58
  interface McpServerRawConfig {
@@ -94,6 +119,7 @@ export function formatMcpStatusReport(servers: McpServerStatus[]): string {
94
119
  "No MCP servers configured.",
95
120
  "",
96
121
  "Add servers to .mcp.json or .gsd/mcp.json to enable MCP integrations.",
122
+ "Tip: run /gsd mcp init . to write the local GSD workflow MCP config.",
97
123
  "See: https://modelcontextprotocol.io/quickstart",
98
124
  ].join("\n");
99
125
  }
@@ -153,12 +179,31 @@ export async function handleMcpStatus(
153
179
  args: string,
154
180
  ctx: ExtensionCommandContext,
155
181
  ): Promise<void> {
156
- const trimmed = args.trim().toLowerCase();
182
+ const trimmed = args.trim();
183
+ const lowered = trimmed.toLowerCase();
157
184
  const configs = readMcpConfigs();
158
185
 
186
+ // /gsd mcp init [dir]
187
+ if (!lowered || lowered === "status") {
188
+ // handled below
189
+ } else if (lowered === "init" || lowered.startsWith("init ")) {
190
+ const rawPath = trimmed.slice("init".length).trim();
191
+ const targetPath = resolve(rawPath || ".");
192
+ try {
193
+ const result = ensureProjectWorkflowMcpConfig(targetPath);
194
+ ctx.ui.notify(formatMcpInitResult(result.status, result.configPath, targetPath), "info");
195
+ } catch (err) {
196
+ ctx.ui.notify(
197
+ `Failed to prepare MCP config for ${targetPath}: ${err instanceof Error ? err.message : String(err)}`,
198
+ "error",
199
+ );
200
+ }
201
+ return;
202
+ }
203
+
159
204
  // /gsd mcp check <server>
160
- if (trimmed.startsWith("check ")) {
161
- const serverName = args.trim().slice("check ".length).trim();
205
+ if (lowered.startsWith("check ")) {
206
+ const serverName = trimmed.slice("check ".length).trim();
162
207
  const config = configs.find((c) => c.name === serverName);
163
208
  if (!config) {
164
209
  const available = configs.map((c) => c.name).join(", ") || "(none)";
@@ -202,7 +247,7 @@ export async function handleMcpStatus(
202
247
  }
203
248
 
204
249
  // /gsd mcp or /gsd mcp status
205
- if (!trimmed || trimmed === "status") {
250
+ if (!lowered || lowered === "status") {
206
251
  // Build status for each server
207
252
  const statuses: McpServerStatus[] = [];
208
253
 
@@ -239,9 +284,10 @@ export async function handleMcpStatus(
239
284
 
240
285
  // Unknown subcommand
241
286
  ctx.ui.notify(
242
- "Usage: /gsd mcp [status|check <server>]\n\n" +
287
+ "Usage: /gsd mcp [status|check <server>|init [dir]]\n\n" +
243
288
  " status Show all MCP server statuses (default)\n" +
244
- " check <server> Detailed status for a specific server",
289
+ " check <server> Detailed status for a specific server\n" +
290
+ " init [dir] Write .mcp.json for the local GSD workflow MCP server",
245
291
  "warning",
246
292
  );
247
293
  }
@@ -10,7 +10,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
10
10
  import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
11
11
  import { abortAndReset } from "./git-self-heal.js";
12
12
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
13
- import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
13
+ import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
14
14
  import { getAllWorktreeHealth } from "./worktree-health.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
 
@@ -386,19 +386,19 @@ export async function checkGitHealth(
386
386
  code: "stale_uncommitted_changes",
387
387
  scope: "project",
388
388
  unitId: "project",
389
- message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting uncommitted changes.`,
389
+ message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
390
390
  fixable: true,
391
391
  });
392
392
 
393
393
  if (shouldFix("stale_uncommitted_changes")) {
394
394
  try {
395
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
395
+ nativeAddTracked(basePath);
396
396
  const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
397
397
  const result = nativeCommit(basePath, commitMsg);
398
398
  if (result) {
399
399
  fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
400
400
  } else {
401
- fixesApplied.push("gsd snapshot skipped — nothing to commit after staging changes");
401
+ fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
402
402
  }
403
403
  } catch {
404
404
  fixesApplied.push("failed to create gsd snapshot commit");
@@ -21,8 +21,8 @@ import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.j
21
21
  import { abortAndReset } from "./git-self-heal.js";
22
22
  import { rebuildState } from "./doctor.js";
23
23
  import { deriveState } from "./state.js";
24
- import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch } from "./git-service.js";
25
- import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
24
+ import { resolveMilestoneIntegrationBranch } from "./git-service.js";
25
+ import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
26
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
27
27
  import { runEnvironmentChecks } from "./doctor-environment.js";
28
28
 
@@ -312,7 +312,7 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
312
312
  if (minutesSinceCommit >= thresholdMinutes) {
313
313
  const mins = Math.floor(minutesSinceCommit);
314
314
  try {
315
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
315
+ nativeAddTracked(basePath);
316
316
  const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
317
317
  const result = nativeCommit(basePath, commitMsg);
318
318
  if (result) {
@@ -8,6 +8,7 @@ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSl
8
8
  import { deriveState, isMilestoneComplete } from "./state.js";
9
9
  import { invalidateAllCaches } from "./cache.js";
10
10
  import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
11
+ import { isClosedStatus } from "./status-guards.js";
11
12
 
12
13
  import type { DoctorIssue, DoctorIssueCode, DoctorReport } from "./doctor-types.js";
13
14
  import { GLOBAL_STATE_CODES } from "./doctor-types.js";
@@ -474,15 +475,16 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
474
475
  if (!roadmapContent) continue;
475
476
 
476
477
  // Normalize slices: prefer DB, fall back to parser
477
- type NormSlice = RoadmapSliceEntry & { pending?: boolean };
478
+ type NormSlice = RoadmapSliceEntry & { pending?: boolean; skipped?: boolean };
478
479
  let slices: NormSlice[];
479
480
  if (isDbAvailable()) {
480
481
  const dbSlices = getMilestoneSlices(milestoneId);
481
482
  slices = dbSlices.map(s => ({
482
483
  id: s.id,
483
484
  title: s.title,
484
- done: s.status === "complete",
485
+ done: isClosedStatus(s.status),
485
486
  pending: s.status === "pending",
487
+ skipped: s.status === "skipped",
486
488
  risk: (s.risk || "medium") as RoadmapSliceEntry["risk"],
487
489
  depends: s.depends,
488
490
  demo: s.demo,
@@ -578,8 +580,9 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
578
580
  const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
579
581
  if (!slicePath) {
580
582
  // Pending slices haven't been planned yet — directories are created
581
- // lazily by ensurePreconditions() at dispatch time. Skip them.
582
- if (slice.pending) continue;
583
+ // lazily by ensurePreconditions() at dispatch time. Skipped slices are
584
+ // intentionally allowed to remain summary-less and directory-less.
585
+ if (slice.pending || slice.skipped) continue;
583
586
  const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
584
587
  issues.push({
585
588
  severity: slice.done ? "warning" : "error",
@@ -603,7 +606,8 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
603
606
  const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
604
607
  if (!tasksDir) {
605
608
  // Pending slices haven't been planned yet — tasks/ is created on demand.
606
- if (slice.pending) continue;
609
+ // Skipped slices may legitimately never create tasks/.
610
+ if (slice.pending || slice.skipped) continue;
607
611
  issues.push({
608
612
  severity: slice.done ? "warning" : "error",
609
613
  code: "missing_tasks_dir",
@@ -16,7 +16,12 @@ import { buildSkillActivationBlock } from "./auto-prompts.js";
16
16
  import { deriveState } from "./state.js";
17
17
  import { invalidateAllCaches } from "./cache.js";
18
18
  import { startAuto } from "./auto.js";
19
- import { readCrashLock, clearLock, formatCrashInfo } from "./crash-recovery.js";
19
+ import { clearLock } from "./crash-recovery.js";
20
+ import {
21
+ assessInterruptedSession,
22
+ formatInterruptedSessionRunningMessage,
23
+ formatInterruptedSessionSummary,
24
+ } from "./interrupted-session.js";
20
25
  import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
21
26
  import { resolveExpectedArtifactPath } from "./auto.js";
22
27
  import {
@@ -215,17 +220,9 @@ export function checkAutoStartAfterDiscuss(): boolean {
215
220
 
216
221
  // Gate 4: Discussion manifest process verification (multi-milestone only)
217
222
  // The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
218
- // If the project is multi-milestone, the manifest is required. When it is
219
- // missing, fail closed instead of assuming the discussion finished.
223
+ // When it exists, validate it before auto-starting. Project history alone is
224
+ // not a reliable signal for the current discussion mode.
220
225
  const manifestPath = join(gsdRoot(basePath), "DISCUSSION-MANIFEST.json");
221
- const requiresManifest = projectIds.length > 1 || findMilestoneIds(basePath).length > 1;
222
- if (requiresManifest && !existsSync(manifestPath)) {
223
- ctx.ui.notify(
224
- "Multi-milestone discussion manifest is missing. Auto-start will remain paused until the manifest is written.",
225
- "warning",
226
- );
227
- return false;
228
- }
229
226
  if (existsSync(manifestPath)) {
230
227
  try {
231
228
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -1322,36 +1319,45 @@ export async function showSmartEntry(
1322
1319
  // ── Self-heal stale runtime records from crashed auto-mode sessions ──
1323
1320
  selfHealRuntimeRecords(basePath, ctx);
1324
1321
 
1325
- // Check for crash from previous auto-mode session.
1326
- // Skip if the lock was written by the current process — acquireSessionLock()
1327
- // writes to the same file, so we'd always false-positive (#1398).
1328
- const crashLock = readCrashLock(basePath);
1329
- if (crashLock && crashLock.pid !== process.pid) {
1330
- clearLock(basePath);
1322
+ const interrupted = await assessInterruptedSession(basePath);
1323
+ if (interrupted.classification === "running") {
1324
+ ctx.ui.notify(formatInterruptedSessionRunningMessage(interrupted), "error");
1325
+ return;
1326
+ }
1331
1327
 
1332
- // Bootstrap crash with zero completed units = no work was lost.
1333
- // Auto-discard instead of prompting the user — this commonly happens
1334
- // when the user exits during init wizard or discuss phase before any
1335
- // real auto-mode work begins.
1336
- const isBootstrapCrash = crashLock.unitType === "starting"
1337
- && crashLock.unitId === "bootstrap";
1338
-
1339
- if (!isBootstrapCrash) {
1340
- const resume = await showNextAction(ctx, {
1341
- title: "GSD — Interrupted Session Detected",
1342
- summary: [formatCrashInfo(crashLock)],
1343
- actions: [
1344
- { id: "resume", label: "Resume with /gsd auto", description: "Pick up where it left off", recommended: true },
1345
- { id: "continue", label: "Continue manually", description: "Open the wizard as normal" },
1346
- ],
1347
- });
1348
- if (resume === "resume") {
1349
- await startAuto(ctx, pi, basePath, false);
1350
- return;
1328
+ if (interrupted.classification === "stale") {
1329
+ clearLock(basePath);
1330
+ if (interrupted.pausedSession) {
1331
+ try {
1332
+ unlinkSync(join(gsdRoot(basePath), "runtime", "paused-session.json"));
1333
+ } catch (e) {
1334
+ logWarning("guided", `stale pause file cleanup failed: ${(e as Error).message}`, { file: "guided-flow.ts" });
1351
1335
  }
1352
1336
  }
1337
+ } else if (interrupted.classification === "recoverable") {
1338
+ if (interrupted.lock) clearLock(basePath);
1339
+ const resumeLabel = interrupted.pausedSession?.stepMode
1340
+ ? "Resume with /gsd next"
1341
+ : "Resume with /gsd auto";
1342
+ const resume = await showNextAction(ctx, {
1343
+ title: "GSD — Interrupted Session Detected",
1344
+ summary: formatInterruptedSessionSummary(interrupted),
1345
+ actions: [
1346
+ { id: "resume", label: resumeLabel, description: "Pick up where it left off", recommended: true },
1347
+ { id: "continue", label: "Continue manually", description: "Open the wizard as normal" },
1348
+ ],
1349
+ });
1350
+ if (resume === "resume") {
1351
+ await startAuto(ctx, pi, basePath, false, {
1352
+ interrupted,
1353
+ step: interrupted.pausedSession?.stepMode ?? false,
1354
+ });
1355
+ return;
1356
+ }
1353
1357
  }
1354
1358
 
1359
+ // Always derive from the project root — the assessment may have derived
1360
+ // state from a worktree path that was cleaned up in the stale branch above.
1355
1361
  const state = await deriveState(basePath);
1356
1362
 
1357
1363
  // Rebuild STATE.md from derived state before any dispatch (#3475).