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
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { deriveState } from "./state.js";
13
13
  import { parseUnitId } from "./unit-id.js";
14
+ import { assessInterruptedSession, readPausedSessionMetadata, } from "./interrupted-session.js";
14
15
  import { getManifestStatus } from "./files.js";
15
16
  export { inlinePriorMilestoneSummary } from "./files.js";
16
17
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
@@ -18,7 +19,7 @@ import { gsdRoot, resolveMilestoneFile, resolveMilestonePath, resolveDir, milest
18
19
  import { invalidateAllCaches } from "./cache.js";
19
20
  import { clearActivityLogState } from "./activity-log.js";
20
21
  import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
21
- import { writeLock, clearLock, readCrashLock, isLockProcessAlive, } from "./crash-recovery.js";
22
+ import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, } from "./crash-recovery.js";
22
23
  import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
23
24
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
24
25
  import { sendDesktopNotification } from "./notifications.js";
@@ -34,7 +35,8 @@ import { clearSkillSnapshot } from "./skill-discovery.js";
34
35
  import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
35
36
  import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
36
37
  import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
37
- import { setLogBasePath, logWarning } from "./workflow-logger.js";
38
+ import { logWarning } from "./workflow-logger.js";
39
+ import { homedir } from "node:os";
38
40
  import { join } from "node:path";
39
41
  import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
40
42
  import { atomicWriteSync } from "./atomic-write.js";
@@ -665,6 +667,8 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
665
667
  stepMode: s.stepMode,
666
668
  pausedAt: new Date().toISOString(),
667
669
  sessionFile: s.pausedSessionFile,
670
+ unitType: s.currentUnit?.type ?? undefined,
671
+ unitId: s.currentUnit?.id ?? undefined,
668
672
  activeEngineId: s.activeEngineId,
669
673
  activeRunDir: s.activeRunDir,
670
674
  autoStartTime: s.autoStartTime,
@@ -853,38 +857,54 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
853
857
  return;
854
858
  }
855
859
  const requestedStepMode = options?.step ?? false;
860
+ const interruptedAssessment = options?.interrupted ?? null;
856
861
  // Escape stale worktree cwd from a previous milestone (#608).
857
862
  base = escapeStaleWorktree(base);
863
+ const freshStartAssessment = interruptedAssessment
864
+ ?? await assessInterruptedSession(base);
865
+ if (freshStartAssessment.classification === "running") {
866
+ const pid = freshStartAssessment.lock?.pid;
867
+ ctx.ui.notify(pid
868
+ ? `Another auto-mode session (PID ${pid}) appears to be running.\nStop it with \`kill ${pid}\` before starting a new session.`
869
+ : "Another auto-mode session appears to be running.", "error");
870
+ return;
871
+ }
858
872
  // If resuming from paused state, just re-activate and dispatch next unit.
859
873
  // Check persisted paused-session first (#1383) — survives /exit.
860
874
  if (!s.paused) {
861
875
  try {
876
+ const meta = freshStartAssessment.pausedSession ?? readPausedSessionMetadata(base);
862
877
  const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
863
- if (existsSync(pausedPath)) {
864
- const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
865
- if (meta.activeEngineId && meta.activeEngineId !== "dev") {
866
- // Custom workflow resume — restore engine state
867
- s.activeEngineId = meta.activeEngineId;
868
- s.activeRunDir = meta.activeRunDir ?? null;
869
- s.originalBasePath = meta.originalBasePath || base;
870
- s.stepMode = meta.stepMode ?? requestedStepMode;
871
- s.autoStartTime = meta.autoStartTime || Date.now();
872
- s.paused = true;
873
- // Don't delete pause file yet — defer until lock is acquired.
874
- // If lock fails, the file must survive for retry.
875
- s.pausedSessionFile = pausedPath;
876
- ctx.ui.notify(`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`, "info");
878
+ if (meta?.activeEngineId && meta.activeEngineId !== "dev") {
879
+ // Custom workflow resume — restore engine state
880
+ s.activeEngineId = meta.activeEngineId;
881
+ s.activeRunDir = meta.activeRunDir ?? null;
882
+ s.originalBasePath = meta.originalBasePath || base;
883
+ s.stepMode = meta.stepMode ?? requestedStepMode;
884
+ s.autoStartTime = meta.autoStartTime || Date.now();
885
+ s.paused = true;
886
+ try {
887
+ unlinkSync(pausedPath);
877
888
  }
878
- else if (meta.milestoneId) {
889
+ catch (e) {
890
+ logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
891
+ }
892
+ ctx.ui.notify(`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`, "info");
893
+ }
894
+ else if (meta?.milestoneId) {
895
+ const shouldResumePausedSession = freshStartAssessment.classification === "recoverable"
896
+ && (freshStartAssessment.hasResumableDiskState
897
+ || !!freshStartAssessment.recoveryPrompt
898
+ || !!freshStartAssessment.lock);
899
+ if (shouldResumePausedSession) {
879
900
  // Validate the milestone still exists and isn't already complete (#1664).
880
901
  const mDir = resolveMilestonePath(base, meta.milestoneId);
881
902
  const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
882
903
  if (!mDir || summaryFile) {
883
- // Stale milestone — clean up and fall through to fresh bootstrap
884
904
  try {
885
905
  unlinkSync(pausedPath);
886
906
  }
887
- catch (err) { /* non-fatal */
907
+ catch (err) {
888
908
  logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
889
909
  }
890
910
  ctx.ui.notify(`Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`, "info");
@@ -893,12 +913,26 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
893
913
  s.currentMilestoneId = meta.milestoneId;
894
914
  s.originalBasePath = meta.originalBasePath || base;
895
915
  s.stepMode = meta.stepMode ?? requestedStepMode;
916
+ s.pausedSessionFile = meta.sessionFile ?? null;
917
+ s.pausedUnitType = meta.unitType ?? null;
918
+ s.pausedUnitId = meta.unitId ?? null;
896
919
  s.autoStartTime = meta.autoStartTime || Date.now();
897
920
  s.paused = true;
898
- // Don't delete pause file yet — defer until lock is acquired.
899
- // If lock fails, the file must survive for retry.
900
- s.pausedSessionFile = pausedPath;
901
- ctx.ui.notify(`Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`, "info");
921
+ try {
922
+ unlinkSync(pausedPath);
923
+ }
924
+ catch (e) {
925
+ logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
926
+ }
927
+ ctx.ui.notify(`Resuming paused session for ${meta.milestoneId}${meta.worktreePath && existsSync(meta.worktreePath) ? ` (worktree)` : ""}.`, "info");
928
+ }
929
+ }
930
+ else if (existsSync(pausedPath)) {
931
+ try {
932
+ unlinkSync(pausedPath);
933
+ }
934
+ catch (e) {
935
+ logWarning("session", `stale pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
902
936
  }
903
937
  }
904
938
  }
@@ -907,6 +941,30 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
907
941
  // Malformed or missing — proceed with fresh bootstrap
908
942
  logWarning("session", `paused-session restore failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
909
943
  }
944
+ // Guard against zero/missing autoStartTime after resume (#3585)
945
+ if (!s.autoStartTime || s.autoStartTime <= 0)
946
+ s.autoStartTime = Date.now();
947
+ }
948
+ if (!s.paused) {
949
+ s.stepMode = requestedStepMode;
950
+ }
951
+ if (freshStartAssessment.lock) {
952
+ clearLock(base);
953
+ }
954
+ if (!s.paused) {
955
+ s.pendingCrashRecovery =
956
+ freshStartAssessment.classification === "recoverable"
957
+ ? freshStartAssessment.recoveryPrompt
958
+ : null;
959
+ if (freshStartAssessment.classification === "recoverable" && freshStartAssessment.lock) {
960
+ const info = formatCrashInfo(freshStartAssessment.lock);
961
+ if (freshStartAssessment.recoveryToolCallCount > 0) {
962
+ ctx.ui.notify(`${info}\nRecovered ${freshStartAssessment.recoveryToolCallCount} tool calls from crashed session. Resuming with full context.`, "warning");
963
+ }
964
+ else if (freshStartAssessment.hasResumableDiskState) {
965
+ ctx.ui.notify(`${info}\nResuming from disk state.`, "warning");
966
+ }
967
+ }
910
968
  }
911
969
  if (s.paused) {
912
970
  const resumeLock = acquireSessionLock(base);
@@ -931,29 +989,19 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
931
989
  s.active = true;
932
990
  s.verbose = verboseMode;
933
991
  s.stepMode = requestedStepMode;
934
- // Preserve the original cmdCtx (ExtensionCommandContext with newSession)
935
- // when resuming from a provider-error pause. The resume callback receives
936
- // an ExtensionContext (from the agent_end hook) which lacks newSession —
937
- // using it would crash runUnit with "newSession is not a function".
938
- // Only override if the new ctx actually has newSession (user-initiated resume).
939
- if ("newSession" in ctx && typeof ctx.newSession === "function") {
940
- s.cmdCtx = ctx;
941
- }
942
- else if (!s.cmdCtx) {
943
- // No saved cmdCtx — this shouldn't happen, but handle gracefully
944
- s.cmdCtx = ctx;
945
- }
946
- // else: keep existing s.cmdCtx which has the real newSession
992
+ s.cmdCtx = ctx;
947
993
  s.basePath = base;
948
- setLogBasePath(base);
949
- if (!s.autoStartTime || s.autoStartTime <= 0)
950
- s.autoStartTime = Date.now();
951
994
  s.unitDispatchCount.clear();
952
995
  s.unitLifetimeDispatches.clear();
953
996
  if (!getLedger())
954
997
  initMetrics(base);
955
998
  if (s.currentMilestoneId)
956
999
  setActiveMilestoneId(base, s.currentMilestoneId);
1000
+ // Re-register health level notification callback lost across process restart
1001
+ setLevelChangeCallback((_from, to, summary) => {
1002
+ const level = to === "red" ? "error" : to === "yellow" ? "warning" : "info";
1003
+ ctx.ui.notify(summary, level);
1004
+ });
957
1005
  // ── Auto-worktree: re-enter worktree on resume ──
958
1006
  if (s.currentMilestoneId &&
959
1007
  shouldUseWorktreeIsolation() &&
@@ -970,6 +1018,11 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
970
1018
  ctx.ui.setFooter(hideFooter);
971
1019
  ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
972
1020
  restoreHookState(s.basePath);
1021
+ // Re-sync managed resources on resume so long-lived auto sessions pick up
1022
+ // bundled extension updates before resume-time verification/state logic runs.
1023
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(process.env.GSD_HOME || homedir(), ".gsd", "agent");
1024
+ const { initResources } = await import("../../../" + "resource-loader.js");
1025
+ initResources(agentDir);
973
1026
  // Open the project DB before rebuild/derive so resume uses DB-backed
974
1027
  // state instead of falling back to stale markdown parsing (#2940).
975
1028
  await openProjectDbIfPresent(s.basePath);
@@ -996,7 +1049,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
996
1049
  invalidateAllCaches();
997
1050
  if (s.pausedSessionFile) {
998
1051
  const activityDir = join(gsdRoot(s.basePath), "activity");
999
- const recovery = synthesizeCrashRecovery(s.basePath, s.currentUnit?.type ?? "unknown", s.currentUnit?.id ?? "unknown", s.pausedSessionFile ?? undefined, activityDir);
1052
+ const recovery = synthesizeCrashRecovery(s.basePath, s.currentUnit?.type ?? s.pausedUnitType ?? "unknown", s.currentUnit?.id ?? s.pausedUnitId ?? "unknown", s.pausedSessionFile ?? undefined, activityDir);
1000
1053
  if (recovery && recovery.trace.toolCallCount > 0) {
1001
1054
  s.pendingCrashRecovery = recovery.prompt;
1002
1055
  ctx.ui.notify(`Recovered ${recovery.trace.toolCallCount} tool calls from paused session. Resuming with context.`, "info");
@@ -1018,7 +1071,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1018
1071
  lockBase,
1019
1072
  buildResolver,
1020
1073
  };
1021
- const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
1074
+ const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps, freshStartAssessment);
1022
1075
  if (!ready)
1023
1076
  return;
1024
1077
  captureProjectRootEnv(s.originalBasePath || s.basePath);
@@ -1101,24 +1154,6 @@ function ensurePreconditions(unitType, unitId, base, state) {
1101
1154
  }
1102
1155
  }
1103
1156
  }
1104
- // ─── Diagnostics ──────────────────────────────────────────────────────────────
1105
- /** Build recovery context from module state for recoverTimedOutUnit */
1106
- function buildRecoveryContext() {
1107
- return {
1108
- basePath: s.basePath,
1109
- verbose: s.verbose,
1110
- currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(),
1111
- unitRecoveryCount: s.unitRecoveryCount,
1112
- };
1113
- }
1114
- /**
1115
- * Test-only: expose skip-loop state for unit tests.
1116
- * Not part of the public API.
1117
- */
1118
- /**
1119
- * Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
1120
- * Used for manual hook triggers via /gsd run-hook.
1121
- */
1122
1157
  export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, triggerUnitId, hookPrompt, hookModel, targetBasePath) {
1123
1158
  if (!s.active) {
1124
1159
  s.active = true;
@@ -141,7 +141,7 @@ export async function buildBeforeAgentStartResult(event, ctx) {
141
141
  warnDeprecatedAgentInstructions();
142
142
  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
143
143
  // Re-inject forensics context on follow-up turns (#2941)
144
- const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd()) : null;
144
+ const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd(), event.prompt) : null;
145
145
  const worktreeBlock = buildWorktreeContextBlock();
146
146
  const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
147
147
  stopContextTimer({
@@ -425,7 +425,7 @@ function oneLine(text) {
425
425
  * Check for an active forensics session and return the prompt content
426
426
  * so it can be re-injected on follow-up turns.
427
427
  */
428
- function buildForensicsContextInjection(basePath) {
428
+ export function buildForensicsContextInjection(basePath, prompt) {
429
429
  const marker = readForensicsMarker(basePath);
430
430
  if (!marker)
431
431
  return null;
@@ -435,6 +435,11 @@ function buildForensicsContextInjection(basePath) {
435
435
  clearForensicsMarker(basePath);
436
436
  return null;
437
437
  }
438
+ const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
439
+ if (trimmed && !RESUME_INTENT_PATTERNS.test(trimmed)) {
440
+ clearForensicsMarker(basePath);
441
+ return null;
442
+ }
438
443
  return marker.promptContent;
439
444
  }
440
445
  /**
@@ -58,7 +58,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
58
58
  { cmd: "templates", desc: "List available workflow templates" },
59
59
  { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
60
60
  { cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" },
61
- { cmd: "mcp", desc: "MCP server status and connectivity check (status, check <server>)" },
61
+ { cmd: "mcp", desc: "MCP server status, connectivity, and local config bootstrap (status, check, init)" },
62
62
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
63
63
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
64
64
  { cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
@@ -188,6 +188,7 @@ const NESTED_COMPLETIONS = {
188
188
  mcp: [
189
189
  { cmd: "status", desc: "Show all MCP server statuses (default)" },
190
190
  { cmd: "check", desc: "Detailed status for a specific server" },
191
+ { cmd: "init", desc: "Write .mcp.json for the local GSD workflow MCP server" },
191
192
  ],
192
193
  doctor: [
193
194
  { cmd: "fix", desc: "Auto-fix detected issues" },
@@ -55,7 +55,7 @@ export function showHelp(ctx) {
55
55
  " /gsd hooks Show post-unit hook configuration",
56
56
  " /gsd extensions Manage extensions [list|enable|disable|info]",
57
57
  " /gsd fast Toggle OpenAI service tier [on|off|flex|status]",
58
- " /gsd mcp MCP server status and connectivity [status|check <server>]",
58
+ " /gsd mcp MCP server status and connectivity [status|check <server>|init [dir]]",
59
59
  "",
60
60
  "MAINTENANCE",
61
61
  " /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",
@@ -7,9 +7,26 @@
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
  import { existsSync, readFileSync } from "node:fs";
12
- import { join } from "node:path";
13
+ import { join, resolve } from "node:path";
14
+ import { ensureProjectWorkflowMcpConfig } from "./mcp-project-config.js";
15
+ export function formatMcpInitResult(status, configPath, targetPath) {
16
+ const summary = status === "created"
17
+ ? "Created project MCP config."
18
+ : status === "updated"
19
+ ? "Updated project MCP config."
20
+ : "Project MCP config is already up to date.";
21
+ return [
22
+ summary,
23
+ "",
24
+ `Project: ${targetPath}`,
25
+ `Config: ${configPath}`,
26
+ "",
27
+ "Claude Code can now load the GSD workflow MCP server from this folder.",
28
+ ].join("\n");
29
+ }
13
30
  function readMcpConfigs() {
14
31
  const servers = [];
15
32
  const seen = new Set();
@@ -61,6 +78,7 @@ export function formatMcpStatusReport(servers) {
61
78
  "No MCP servers configured.",
62
79
  "",
63
80
  "Add servers to .mcp.json or .gsd/mcp.json to enable MCP integrations.",
81
+ "Tip: run /gsd mcp init . to write the local GSD workflow MCP config.",
64
82
  "See: https://modelcontextprotocol.io/quickstart",
65
83
  ].join("\n");
66
84
  }
@@ -109,11 +127,28 @@ export function formatMcpServerDetail(server) {
109
127
  * Handle `/gsd mcp [status|check <server>]`.
110
128
  */
111
129
  export async function handleMcpStatus(args, ctx) {
112
- const trimmed = args.trim().toLowerCase();
130
+ const trimmed = args.trim();
131
+ const lowered = trimmed.toLowerCase();
113
132
  const configs = readMcpConfigs();
133
+ // /gsd mcp init [dir]
134
+ if (!lowered || lowered === "status") {
135
+ // handled below
136
+ }
137
+ else if (lowered === "init" || lowered.startsWith("init ")) {
138
+ const rawPath = trimmed.slice("init".length).trim();
139
+ const targetPath = resolve(rawPath || ".");
140
+ try {
141
+ const result = ensureProjectWorkflowMcpConfig(targetPath);
142
+ ctx.ui.notify(formatMcpInitResult(result.status, result.configPath, targetPath), "info");
143
+ }
144
+ catch (err) {
145
+ ctx.ui.notify(`Failed to prepare MCP config for ${targetPath}: ${err instanceof Error ? err.message : String(err)}`, "error");
146
+ }
147
+ return;
148
+ }
114
149
  // /gsd mcp check <server>
115
- if (trimmed.startsWith("check ")) {
116
- const serverName = args.trim().slice("check ".length).trim();
150
+ if (lowered.startsWith("check ")) {
151
+ const serverName = trimmed.slice("check ".length).trim();
117
152
  const config = configs.find((c) => c.name === serverName);
118
153
  if (!config) {
119
154
  const available = configs.map((c) => c.name).join(", ") || "(none)";
@@ -149,7 +184,7 @@ export async function handleMcpStatus(args, ctx) {
149
184
  return;
150
185
  }
151
186
  // /gsd mcp or /gsd mcp status
152
- if (!trimmed || trimmed === "status") {
187
+ if (!lowered || lowered === "status") {
153
188
  // Build status for each server
154
189
  const statuses = [];
155
190
  for (const config of configs) {
@@ -181,7 +216,8 @@ export async function handleMcpStatus(args, ctx) {
181
216
  return;
182
217
  }
183
218
  // Unknown subcommand
184
- ctx.ui.notify("Usage: /gsd mcp [status|check <server>]\n\n" +
219
+ ctx.ui.notify("Usage: /gsd mcp [status|check <server>|init [dir]]\n\n" +
185
220
  " status Show all MCP server statuses (default)\n" +
186
- " check <server> Detailed status for a specific server", "warning");
221
+ " check <server> Detailed status for a specific server\n" +
222
+ " init [dir] Write .mcp.json for the local GSD workflow MCP server", "warning");
187
223
  }
@@ -8,7 +8,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
8
8
  import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
9
9
  import { abortAndReset } from "./git-self-heal.js";
10
10
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
11
- import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
11
+ import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
12
12
  import { getAllWorktreeHealth } from "./worktree-health.js";
13
13
  import { loadEffectiveGSDPreferences } from "./preferences.js";
14
14
  /**
@@ -380,19 +380,19 @@ export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix,
380
380
  code: "stale_uncommitted_changes",
381
381
  scope: "project",
382
382
  unitId: "project",
383
- message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting uncommitted changes.`,
383
+ message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
384
384
  fixable: true,
385
385
  });
386
386
  if (shouldFix("stale_uncommitted_changes")) {
387
387
  try {
388
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
388
+ nativeAddTracked(basePath);
389
389
  const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
390
390
  const result = nativeCommit(basePath, commitMsg);
391
391
  if (result) {
392
392
  fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
393
393
  }
394
394
  else {
395
- fixesApplied.push("gsd snapshot skipped — nothing to commit after staging changes");
395
+ fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
396
396
  }
397
397
  }
398
398
  catch {
@@ -20,8 +20,8 @@ import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.j
20
20
  import { abortAndReset } from "./git-self-heal.js";
21
21
  import { rebuildState } from "./doctor.js";
22
22
  import { deriveState } from "./state.js";
23
- import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch } from "./git-service.js";
24
- import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
23
+ import { resolveMilestoneIntegrationBranch } from "./git-service.js";
24
+ import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
25
25
  import { loadEffectiveGSDPreferences } from "./preferences.js";
26
26
  import { runEnvironmentChecks } from "./doctor-environment.js";
27
27
  /** In-memory health history for the current auto-mode session. */
@@ -247,7 +247,7 @@ export async function preDispatchHealthGate(basePath) {
247
247
  if (minutesSinceCommit >= thresholdMinutes) {
248
248
  const mins = Math.floor(minutesSinceCommit);
249
249
  try {
250
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
250
+ nativeAddTracked(basePath);
251
251
  const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
252
252
  const result = nativeCommit(basePath, commitMsg);
253
253
  if (result) {
@@ -7,6 +7,7 @@ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSl
7
7
  import { deriveState, isMilestoneComplete } from "./state.js";
8
8
  import { invalidateAllCaches } from "./cache.js";
9
9
  import { loadEffectiveGSDPreferences } from "./preferences.js";
10
+ import { isClosedStatus } from "./status-guards.js";
10
11
  import { GLOBAL_STATE_CODES } from "./doctor-types.js";
11
12
  import { checkGitHealth, checkRuntimeHealth, checkGlobalHealth, checkEngineHealth } from "./doctor-checks.js";
12
13
  import { checkEnvironmentHealth } from "./doctor-environment.js";
@@ -443,8 +444,9 @@ export async function runGSDDoctor(basePath, options) {
443
444
  slices = dbSlices.map(s => ({
444
445
  id: s.id,
445
446
  title: s.title,
446
- done: s.status === "complete",
447
+ done: isClosedStatus(s.status),
447
448
  pending: s.status === "pending",
449
+ skipped: s.status === "skipped",
448
450
  risk: (s.risk || "medium"),
449
451
  depends: s.depends,
450
452
  demo: s.demo,
@@ -541,8 +543,9 @@ export async function runGSDDoctor(basePath, options) {
541
543
  const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
542
544
  if (!slicePath) {
543
545
  // Pending slices haven't been planned yet — directories are created
544
- // lazily by ensurePreconditions() at dispatch time. Skip them.
545
- if (slice.pending)
546
+ // lazily by ensurePreconditions() at dispatch time. Skipped slices are
547
+ // intentionally allowed to remain summary-less and directory-less.
548
+ if (slice.pending || slice.skipped)
546
549
  continue;
547
550
  const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
548
551
  issues.push({
@@ -566,7 +569,8 @@ export async function runGSDDoctor(basePath, options) {
566
569
  const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
567
570
  if (!tasksDir) {
568
571
  // Pending slices haven't been planned yet — tasks/ is created on demand.
569
- if (slice.pending)
572
+ // Skipped slices may legitimately never create tasks/.
573
+ if (slice.pending || slice.skipped)
570
574
  continue;
571
575
  issues.push({
572
576
  severity: slice.done ? "warning" : "error",
@@ -14,7 +14,8 @@ import { buildSkillActivationBlock } from "./auto-prompts.js";
14
14
  import { deriveState } from "./state.js";
15
15
  import { invalidateAllCaches } from "./cache.js";
16
16
  import { startAuto } from "./auto.js";
17
- import { readCrashLock, clearLock, formatCrashInfo } from "./crash-recovery.js";
17
+ import { clearLock } from "./crash-recovery.js";
18
+ import { assessInterruptedSession, formatInterruptedSessionRunningMessage, formatInterruptedSessionSummary, } from "./interrupted-session.js";
18
19
  import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
19
20
  import { resolveExpectedArtifactPath } from "./auto.js";
20
21
  import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, } from "./paths.js";
@@ -167,14 +168,9 @@ export function checkAutoStartAfterDiscuss() {
167
168
  }
168
169
  // Gate 4: Discussion manifest process verification (multi-milestone only)
169
170
  // The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
170
- // If the project is multi-milestone, the manifest is required. When it is
171
- // missing, fail closed instead of assuming the discussion finished.
171
+ // When it exists, validate it before auto-starting. Project history alone is
172
+ // not a reliable signal for the current discussion mode.
172
173
  const manifestPath = join(gsdRoot(basePath), "DISCUSSION-MANIFEST.json");
173
- const requiresManifest = projectIds.length > 1 || findMilestoneIds(basePath).length > 1;
174
- if (requiresManifest && !existsSync(manifestPath)) {
175
- ctx.ui.notify("Multi-milestone discussion manifest is missing. Auto-start will remain paused until the manifest is written.", "warning");
176
- return false;
177
- }
178
174
  if (existsSync(manifestPath)) {
179
175
  try {
180
176
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -1102,33 +1098,46 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1102
1098
  untrackRuntimeFiles(basePath);
1103
1099
  // ── Self-heal stale runtime records from crashed auto-mode sessions ──
1104
1100
  selfHealRuntimeRecords(basePath, ctx);
1105
- // Check for crash from previous auto-mode session.
1106
- // Skip if the lock was written by the current process — acquireSessionLock()
1107
- // writes to the same file, so we'd always false-positive (#1398).
1108
- const crashLock = readCrashLock(basePath);
1109
- if (crashLock && crashLock.pid !== process.pid) {
1101
+ const interrupted = await assessInterruptedSession(basePath);
1102
+ if (interrupted.classification === "running") {
1103
+ ctx.ui.notify(formatInterruptedSessionRunningMessage(interrupted), "error");
1104
+ return;
1105
+ }
1106
+ if (interrupted.classification === "stale") {
1110
1107
  clearLock(basePath);
1111
- // Bootstrap crash with zero completed units = no work was lost.
1112
- // Auto-discard instead of prompting the user — this commonly happens
1113
- // when the user exits during init wizard or discuss phase before any
1114
- // real auto-mode work begins.
1115
- const isBootstrapCrash = crashLock.unitType === "starting"
1116
- && crashLock.unitId === "bootstrap";
1117
- if (!isBootstrapCrash) {
1118
- const resume = await showNextAction(ctx, {
1119
- title: "GSD — Interrupted Session Detected",
1120
- summary: [formatCrashInfo(crashLock)],
1121
- actions: [
1122
- { id: "resume", label: "Resume with /gsd auto", description: "Pick up where it left off", recommended: true },
1123
- { id: "continue", label: "Continue manually", description: "Open the wizard as normal" },
1124
- ],
1125
- });
1126
- if (resume === "resume") {
1127
- await startAuto(ctx, pi, basePath, false);
1128
- return;
1108
+ if (interrupted.pausedSession) {
1109
+ try {
1110
+ unlinkSync(join(gsdRoot(basePath), "runtime", "paused-session.json"));
1129
1111
  }
1112
+ catch (e) {
1113
+ logWarning("guided", `stale pause file cleanup failed: ${e.message}`, { file: "guided-flow.ts" });
1114
+ }
1115
+ }
1116
+ }
1117
+ else if (interrupted.classification === "recoverable") {
1118
+ if (interrupted.lock)
1119
+ clearLock(basePath);
1120
+ const resumeLabel = interrupted.pausedSession?.stepMode
1121
+ ? "Resume with /gsd next"
1122
+ : "Resume with /gsd auto";
1123
+ const resume = await showNextAction(ctx, {
1124
+ title: "GSD — Interrupted Session Detected",
1125
+ summary: formatInterruptedSessionSummary(interrupted),
1126
+ actions: [
1127
+ { id: "resume", label: resumeLabel, description: "Pick up where it left off", recommended: true },
1128
+ { id: "continue", label: "Continue manually", description: "Open the wizard as normal" },
1129
+ ],
1130
+ });
1131
+ if (resume === "resume") {
1132
+ await startAuto(ctx, pi, basePath, false, {
1133
+ interrupted,
1134
+ step: interrupted.pausedSession?.stepMode ?? false,
1135
+ });
1136
+ return;
1130
1137
  }
1131
1138
  }
1139
+ // Always derive from the project root — the assessment may have derived
1140
+ // state from a worktree path that was cleaned up in the stale branch above.
1132
1141
  const state = await deriveState(basePath);
1133
1142
  // Rebuild STATE.md from derived state before any dispatch (#3475).
1134
1143
  try {